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:
Andrew Fontaine 2019-06-06 09:36:17 +00:00 committed by Filipa Lacerda
parent 7468ed5fd2
commit 8bea9eeddf
4 changed files with 180 additions and 0 deletions

View file

@ -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>

View file

@ -0,0 +1,5 @@
---
title: Add a New Copy Button That Works in Modals
merge_request: 28676
author:
type: added

View file

@ -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.

View file

@ -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');
});
});
});