Add a New Copy Button That Works in Modals
This copy button manages a local instance of the Clipboard plugin specific to it, which means it is created/destroyed on the creation/destruction of the component. This allows it to work well in gitlab-ui modals, as the event listeners are bound on creation of the button. It also allows for bindings to the `container` option of the Clipboard plugin, which allows it to work within the focus trap set by bootstrap's modals.
This commit is contained in:
parent
7468ed5fd2
commit
8bea9eeddf
4 changed files with 180 additions and 0 deletions
|
@ -0,0 +1,121 @@
|
|||
<script>
|
||||
import $ from 'jquery';
|
||||
import { GlButton, GlTooltipDirective } from '@gitlab/ui';
|
||||
import { __ } from '~/locale';
|
||||
import Icon from '~/vue_shared/components/icon.vue';
|
||||
import Clipboard from 'clipboard';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
GlButton,
|
||||
Icon,
|
||||
},
|
||||
|
||||
directives: {
|
||||
GlTooltip: GlTooltipDirective,
|
||||
},
|
||||
|
||||
props: {
|
||||
text: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: '',
|
||||
},
|
||||
container: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: '',
|
||||
},
|
||||
modalId: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: '',
|
||||
},
|
||||
target: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: '',
|
||||
},
|
||||
title: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
tooltipPlacement: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: 'top',
|
||||
},
|
||||
tooltipContainer: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: null,
|
||||
},
|
||||
},
|
||||
|
||||
copySuccessText: __('Copied'),
|
||||
|
||||
computed: {
|
||||
modalDomId() {
|
||||
return this.modalId ? `#${this.modalId}` : '';
|
||||
},
|
||||
},
|
||||
|
||||
mounted() {
|
||||
this.$nextTick(() => {
|
||||
this.clipboard = new Clipboard(this.$el, {
|
||||
container:
|
||||
document.querySelector(`${this.modalDomId} div.modal-content`) ||
|
||||
document.getElementById(this.container) ||
|
||||
document.body,
|
||||
});
|
||||
this.clipboard
|
||||
.on('success', e => {
|
||||
this.updateTooltip(e.trigger);
|
||||
this.$emit('success', e);
|
||||
// Clear the selection and blur the trigger so it loses its border
|
||||
e.clearSelection();
|
||||
$(e.trigger).blur();
|
||||
})
|
||||
.on('error', e => this.$emit('error', e));
|
||||
});
|
||||
},
|
||||
|
||||
destroyed() {
|
||||
if (this.clipboard) {
|
||||
this.clipboard.destroy();
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
updateTooltip(target) {
|
||||
const $target = $(target);
|
||||
const originalTitle = $target.data('originalTitle');
|
||||
|
||||
if ($target.tooltip) {
|
||||
/**
|
||||
* The original tooltip will continue staying there unless we remove it by hand.
|
||||
* $target.tooltip('hide') isn't working.
|
||||
*/
|
||||
$('.tooltip').remove();
|
||||
$target.attr('title', this.$options.copySuccessText);
|
||||
$target.tooltip('_fixTitle');
|
||||
$target.tooltip('show');
|
||||
$target.attr('title', originalTitle);
|
||||
$target.tooltip('_fixTitle');
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
<template>
|
||||
<gl-button
|
||||
v-gl-tooltip="{ placement: tooltipPlacement, container: tooltipContainer }"
|
||||
:data-clipboard-target="target"
|
||||
:data-clipboard-text="text"
|
||||
:title="title"
|
||||
>
|
||||
<slot>
|
||||
<icon name="duplicate" />
|
||||
</slot>
|
||||
</gl-button>
|
||||
</template>
|
5
changelogs/unreleased/copy-button-in-modals.yml
Normal file
5
changelogs/unreleased/copy-button-in-modals.yml
Normal file
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Add a New Copy Button That Works in Modals
|
||||
merge_request: 28676
|
||||
author:
|
||||
type: added
|
|
@ -25,3 +25,17 @@ document.body.dataset.page
|
|||
```
|
||||
|
||||
Find here the [source code setting the attribute](https://gitlab.com/gitlab-org/gitlab-ce/blob/cc5095edfce2b4d4083a4fb1cdc7c0a1898b9921/app/views/layouts/application.html.haml#L4).
|
||||
|
||||
### `modal_copy_button` vs `clipboard_button`
|
||||
|
||||
The `clipboard_button` uses the `copy_to_clipboard.js` behaviour, which is
|
||||
initialized on page load, so if there are vue-based clipboard buttons that
|
||||
don't exist at page load (such as ones in a `GlModal`), they do not have the
|
||||
click handlers associated with the clipboard package.
|
||||
|
||||
`modal_copy_button` was added that manages an instance of the
|
||||
[`clipboard` plugin](https://www.npmjs.com/package/clipboard) specific to
|
||||
the instance of that component, which means that clipboard events are
|
||||
bound on mounting and destroyed when the button is, mitigating the above
|
||||
issue. It also has bindings to a particular container or modal ID
|
||||
available, to work with the focus trap created by our GlModal.
|
||||
|
|
|
@ -0,0 +1,40 @@
|
|||
import Vue from 'vue';
|
||||
import { shallowMount } from '@vue/test-utils';
|
||||
import modalCopyButton from '~/vue_shared/components/modal_copy_button.vue';
|
||||
|
||||
describe('modal copy button', () => {
|
||||
const Component = Vue.extend(modalCopyButton);
|
||||
let wrapper;
|
||||
|
||||
afterEach(() => {
|
||||
wrapper.destroy();
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
wrapper = shallowMount(Component, {
|
||||
propsData: {
|
||||
text: 'copy me',
|
||||
title: 'Copy this value into Clipboard!',
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
describe('clipboard', () => {
|
||||
it('should fire a `success` event on click', () => {
|
||||
document.execCommand = jest.fn(() => true);
|
||||
window.getSelection = jest.fn(() => ({
|
||||
toString: jest.fn(() => 'test'),
|
||||
removeAllRanges: jest.fn(),
|
||||
}));
|
||||
wrapper.trigger('click');
|
||||
expect(wrapper.emitted().success).not.toBeEmpty();
|
||||
expect(document.execCommand).toHaveBeenCalledWith('copy');
|
||||
});
|
||||
it("should propagate the clipboard error event if execCommand doesn't work", () => {
|
||||
document.execCommand = jest.fn(() => false);
|
||||
wrapper.trigger('click');
|
||||
expect(wrapper.emitted().error).not.toBeEmpty();
|
||||
expect(document.execCommand).toHaveBeenCalledWith('copy');
|
||||
});
|
||||
});
|
||||
});
|
Loading…
Reference in a new issue