kopia lustrzana https://github.com/wagtail/wagtail
Add ClipboardController to allow copying to the clipboard (Stimulus)
rodzic
d585fa48dc
commit
bb33e7c508
|
@ -0,0 +1,207 @@
|
|||
import { Application } from '@hotwired/stimulus';
|
||||
import { ClipboardController } from './ClipboardController';
|
||||
|
||||
jest.useFakeTimers();
|
||||
|
||||
describe('ClipboardController', () => {
|
||||
const handleEvent = jest.fn();
|
||||
|
||||
document.addEventListener('w-clipboard:copy', handleEvent);
|
||||
document.addEventListener('w-clipboard:copied', handleEvent);
|
||||
document.addEventListener('w-clipboard:error', handleEvent);
|
||||
|
||||
let app;
|
||||
const writeText = jest.fn(() => Promise.resolve());
|
||||
|
||||
const setup = async (
|
||||
html = `
|
||||
<div id="container" data-controller="w-clipboard" data-action="some-event->w-clipboard#copy">
|
||||
<input type="text id="content" data-w-clipboard-target="value" value="copy me to the clipboard" />
|
||||
<button type="button" data-action="w-clipboard#copy">Copy</button>
|
||||
</div>
|
||||
`,
|
||||
) => {
|
||||
document.body.innerHTML = `<main>${html}</main>`;
|
||||
|
||||
app = Application.start();
|
||||
app.register('w-clipboard', ClipboardController);
|
||||
|
||||
await Promise.resolve();
|
||||
};
|
||||
|
||||
afterEach(() => {
|
||||
app?.stop();
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
describe('when the clipboard is not available', () => {
|
||||
it('should not have the clipboard available', () => {
|
||||
expect(window.navigator.clipboard).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should send an error event if the copy method is called', async () => {
|
||||
await setup();
|
||||
|
||||
expect(handleEvent).not.toHaveBeenCalled();
|
||||
|
||||
document.querySelector('button').click();
|
||||
|
||||
await jest.runAllTimersAsync();
|
||||
|
||||
expect(handleEvent).toHaveBeenLastCalledWith(
|
||||
expect.objectContaining({
|
||||
type: 'w-clipboard:error',
|
||||
detail: { clear: true, type: 'error' },
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when the clipboard is available', () => {
|
||||
beforeAll(() => {
|
||||
Object.defineProperty(window.navigator, 'clipboard', {
|
||||
writable: true,
|
||||
value: { writeText },
|
||||
});
|
||||
});
|
||||
|
||||
it('should have the clipboard available', () => {
|
||||
expect(window.navigator.clipboard).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should copy the content from the value target with the copy method', async () => {
|
||||
await setup();
|
||||
|
||||
expect(handleEvent).not.toHaveBeenCalled();
|
||||
expect(writeText).not.toHaveBeenCalled();
|
||||
|
||||
document.querySelector('button').click();
|
||||
|
||||
expect(handleEvent).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ type: 'w-clipboard:copy' }),
|
||||
);
|
||||
|
||||
await jest.runAllTimersAsync();
|
||||
|
||||
expect(writeText).toHaveBeenCalledWith('copy me to the clipboard');
|
||||
expect(handleEvent).toHaveBeenLastCalledWith(
|
||||
expect.objectContaining({
|
||||
type: 'w-clipboard:copied',
|
||||
detail: { clear: true, type: 'success' },
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('should support a way to block the copy method with event listeners', async () => {
|
||||
document.addEventListener(
|
||||
'w-clipboard:copy',
|
||||
(event) => event.preventDefault(),
|
||||
{ once: true },
|
||||
);
|
||||
|
||||
await setup();
|
||||
|
||||
expect(handleEvent).not.toHaveBeenCalled();
|
||||
expect(writeText).not.toHaveBeenCalled();
|
||||
|
||||
document.querySelector('button').click();
|
||||
|
||||
expect(handleEvent).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ type: 'w-clipboard:copy' }),
|
||||
);
|
||||
|
||||
await jest.runAllTimersAsync();
|
||||
|
||||
expect(writeText).not.toHaveBeenCalled();
|
||||
expect(handleEvent).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('should support a custom dispatched event with the value to copy', async () => {
|
||||
expect(handleEvent).not.toHaveBeenCalled();
|
||||
expect(writeText).not.toHaveBeenCalled();
|
||||
|
||||
await setup();
|
||||
|
||||
document.getElementById('container').dispatchEvent(
|
||||
new CustomEvent('some-event', {
|
||||
detail: { value: 'copy me from the event' },
|
||||
}),
|
||||
);
|
||||
|
||||
expect(handleEvent).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ type: 'w-clipboard:copy' }),
|
||||
);
|
||||
|
||||
await jest.runAllTimersAsync();
|
||||
|
||||
expect(writeText).toHaveBeenCalledWith('copy me from the event');
|
||||
expect(handleEvent).toHaveBeenLastCalledWith(
|
||||
expect.objectContaining({
|
||||
type: 'w-clipboard:copied',
|
||||
detail: { clear: true, type: 'success' },
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('should support a custom action params to provide the value to copy', async () => {
|
||||
expect(handleEvent).not.toHaveBeenCalled();
|
||||
expect(writeText).not.toHaveBeenCalled();
|
||||
|
||||
await setup(`
|
||||
<div id="container" data-controller="w-clipboard">
|
||||
<button type="button" data-action="w-clipboard#copy" data-w-clipboard-value-param="Copy me instead!">Copy</button>
|
||||
</div>
|
||||
`);
|
||||
|
||||
document.querySelector('button').click();
|
||||
|
||||
expect(handleEvent).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ type: 'w-clipboard:copy' }),
|
||||
);
|
||||
|
||||
await jest.runAllTimersAsync();
|
||||
|
||||
expect(writeText).toHaveBeenCalledWith('Copy me instead!');
|
||||
expect(handleEvent).toHaveBeenLastCalledWith(
|
||||
expect.objectContaining({
|
||||
type: 'w-clipboard:copied',
|
||||
detail: { clear: true, type: 'success' },
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('should support falling back to the controlled element value if no other value is provided', async () => {
|
||||
expect(handleEvent).not.toHaveBeenCalled();
|
||||
expect(writeText).not.toHaveBeenCalled();
|
||||
|
||||
await setup(`
|
||||
<textarea id="container" data-controller="w-clipboard" data-action="custom-event->w-clipboard#copy">
|
||||
Copy the content inside the controlled element.
|
||||
</textarea>
|
||||
`);
|
||||
|
||||
document
|
||||
.querySelector('textarea')
|
||||
.dispatchEvent(new CustomEvent('custom-event'));
|
||||
|
||||
expect(handleEvent).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ type: 'w-clipboard:copy' }),
|
||||
);
|
||||
|
||||
await jest.runAllTimersAsync();
|
||||
|
||||
expect(writeText).toHaveBeenCalledWith(
|
||||
expect.stringContaining(
|
||||
'Copy the content inside the controlled element.',
|
||||
),
|
||||
);
|
||||
|
||||
expect(handleEvent).toHaveBeenLastCalledWith(
|
||||
expect.objectContaining({
|
||||
type: 'w-clipboard:copied',
|
||||
detail: { clear: true, type: 'success' },
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,61 @@
|
|||
import { Controller } from '@hotwired/stimulus';
|
||||
|
||||
type CopyOptions = {
|
||||
/** Custom supplied value to copy to the clipboard. */
|
||||
value?: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* Adds the ability for an element to copy the value from a target to the clipboard.
|
||||
*
|
||||
* @example
|
||||
* ```html
|
||||
* <div data-controller="w-clipboard">
|
||||
* <input type="text" value="Hello World" data-w-clipboard-target="value" />
|
||||
* <button type="button" data-action="w-clipboard#copy">Copy</button>
|
||||
* </div>
|
||||
* ```
|
||||
*/
|
||||
export class ClipboardController extends Controller<HTMLElement> {
|
||||
static targets = ['value'];
|
||||
|
||||
declare readonly hasValueTarget: boolean;
|
||||
declare readonly valueTarget:
|
||||
| HTMLInputElement
|
||||
| HTMLTextAreaElement
|
||||
| HTMLSelectElement;
|
||||
|
||||
/**
|
||||
* Copies the value from either the Custom Event detail, Stimulus action params or
|
||||
* the value target to the clipboard. If no value is found, nothing happens.
|
||||
* If the clipboard is not available an error event is dispatched and it will
|
||||
* intentionally fail silently.
|
||||
*/
|
||||
copy(event: CustomEvent<CopyOptions> & { params?: CopyOptions }) {
|
||||
const {
|
||||
value = this.hasValueTarget
|
||||
? this.valueTarget.value
|
||||
: (this.element as HTMLInputElement).value || null,
|
||||
} = { ...event.detail, ...event.params };
|
||||
|
||||
if (!value) return;
|
||||
|
||||
const copyEvent = this.dispatch('copy');
|
||||
|
||||
if (copyEvent.defaultPrevented) return;
|
||||
|
||||
new Promise((resolve, reject) => {
|
||||
if (navigator.clipboard) {
|
||||
navigator.clipboard.writeText(value).then(resolve, reject);
|
||||
} else {
|
||||
reject();
|
||||
}
|
||||
})
|
||||
.then(() =>
|
||||
this.dispatch('copied', { detail: { clear: true, type: 'success' } }),
|
||||
)
|
||||
.catch(() =>
|
||||
this.dispatch('error', { detail: { clear: true, type: 'error' } }),
|
||||
);
|
||||
}
|
||||
}
|
|
@ -4,6 +4,7 @@ import type { Definition } from '@hotwired/stimulus';
|
|||
import { ActionController } from './ActionController';
|
||||
import { AutosizeController } from './AutosizeController';
|
||||
import { BulkController } from './BulkController';
|
||||
import { ClipboardController } from './ClipboardController';
|
||||
import { CloneController } from './CloneController';
|
||||
import { CountController } from './CountController';
|
||||
import { DialogController } from './DialogController';
|
||||
|
@ -30,6 +31,7 @@ export const coreControllerDefinitions: Definition[] = [
|
|||
{ controllerConstructor: ActionController, identifier: 'w-action' },
|
||||
{ controllerConstructor: AutosizeController, identifier: 'w-autosize' },
|
||||
{ controllerConstructor: BulkController, identifier: 'w-bulk' },
|
||||
{ controllerConstructor: ClipboardController, identifier: 'w-clipboard' },
|
||||
{ controllerConstructor: CloneController, identifier: 'w-clone' },
|
||||
{ controllerConstructor: CloneController, identifier: 'w-messages' },
|
||||
{ controllerConstructor: CountController, identifier: 'w-count' },
|
||||
|
|
Ładowanie…
Reference in New Issue