mirror of
https://github.com/zenorocha/clipboard.js.git
synced 2023-08-10 21:12:48 +03:00

blur() results in a loss/reset of keyboard focus, meaning keyboard users usually get thrown right back to the start of the page. Instead, this moves focus back to the trigger (which had the focus when the trigger was activated). As with the proposed fix in https://github.com/zenorocha/clipboard.js/pull/418 this obviously results in the focus styles (like the default outline, unless suppressed) being applied to the trigger (see the related PR for rationale and future fix using `:focus-ring`)
205 lines
5.6 KiB
JavaScript
205 lines
5.6 KiB
JavaScript
import select from 'select';
|
|
|
|
/**
|
|
* Inner class which performs selection from either `text` or `target`
|
|
* properties and then executes copy or cut operations.
|
|
*/
|
|
class ClipboardAction {
|
|
/**
|
|
* @param {Object} options
|
|
*/
|
|
constructor(options) {
|
|
this.resolveOptions(options);
|
|
this.initSelection();
|
|
}
|
|
|
|
/**
|
|
* Defines base properties passed from constructor.
|
|
* @param {Object} options
|
|
*/
|
|
resolveOptions(options = {}) {
|
|
this.action = options.action;
|
|
this.container = options.container;
|
|
this.emitter = options.emitter;
|
|
this.target = options.target;
|
|
this.text = options.text;
|
|
this.trigger = options.trigger;
|
|
|
|
this.selectedText = '';
|
|
}
|
|
|
|
/**
|
|
* Decides which selection strategy is going to be applied based
|
|
* on the existence of `text` and `target` properties.
|
|
*/
|
|
initSelection() {
|
|
if (this.text) {
|
|
this.selectFake();
|
|
}
|
|
else if (this.target) {
|
|
this.selectTarget();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Creates a fake textarea element, sets its value from `text` property,
|
|
* and makes a selection on it.
|
|
*/
|
|
selectFake() {
|
|
const isRTL = document.documentElement.getAttribute('dir') == 'rtl';
|
|
|
|
this.removeFake();
|
|
|
|
this.fakeHandlerCallback = () => this.removeFake();
|
|
this.fakeHandler = this.container.addEventListener('click', this.fakeHandlerCallback) || true;
|
|
|
|
this.fakeElem = document.createElement('textarea');
|
|
// Prevent zooming on iOS
|
|
this.fakeElem.style.fontSize = '12pt';
|
|
// Reset box model
|
|
this.fakeElem.style.border = '0';
|
|
this.fakeElem.style.padding = '0';
|
|
this.fakeElem.style.margin = '0';
|
|
// Move element out of screen horizontally
|
|
this.fakeElem.style.position = 'absolute';
|
|
this.fakeElem.style[ isRTL ? 'right' : 'left' ] = '-9999px';
|
|
// Move element to the same position vertically
|
|
let yPosition = window.pageYOffset || document.documentElement.scrollTop;
|
|
this.fakeElem.style.top = `${yPosition}px`;
|
|
|
|
this.fakeElem.setAttribute('readonly', '');
|
|
this.fakeElem.value = this.text;
|
|
|
|
this.container.appendChild(this.fakeElem);
|
|
|
|
this.selectedText = select(this.fakeElem);
|
|
this.copyText();
|
|
}
|
|
|
|
/**
|
|
* Only removes the fake element after another click event, that way
|
|
* a user can hit `Ctrl+C` to copy because selection still exists.
|
|
*/
|
|
removeFake() {
|
|
if (this.fakeHandler) {
|
|
this.container.removeEventListener('click', this.fakeHandlerCallback);
|
|
this.fakeHandler = null;
|
|
this.fakeHandlerCallback = null;
|
|
}
|
|
|
|
if (this.fakeElem) {
|
|
this.container.removeChild(this.fakeElem);
|
|
this.fakeElem = null;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Selects the content from element passed on `target` property.
|
|
*/
|
|
selectTarget() {
|
|
this.selectedText = select(this.target);
|
|
this.copyText();
|
|
}
|
|
|
|
/**
|
|
* Executes the copy operation based on the current selection.
|
|
*/
|
|
copyText() {
|
|
let succeeded;
|
|
|
|
try {
|
|
succeeded = document.execCommand(this.action);
|
|
}
|
|
catch (err) {
|
|
succeeded = false;
|
|
}
|
|
|
|
this.handleResult(succeeded);
|
|
}
|
|
|
|
/**
|
|
* Fires an event based on the copy operation result.
|
|
* @param {Boolean} succeeded
|
|
*/
|
|
handleResult(succeeded) {
|
|
this.emitter.emit(succeeded ? 'success' : 'error', {
|
|
action: this.action,
|
|
text: this.selectedText,
|
|
trigger: this.trigger,
|
|
clearSelection: this.clearSelection.bind(this)
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Moves focus away from `target` and back to the trigger, removes current selection.
|
|
*/
|
|
clearSelection() {
|
|
if (this.trigger) {
|
|
this.trigger.focus();
|
|
}
|
|
|
|
window.getSelection().removeAllRanges();
|
|
}
|
|
|
|
/**
|
|
* Sets the `action` to be performed which can be either 'copy' or 'cut'.
|
|
* @param {String} action
|
|
*/
|
|
set action(action = 'copy') {
|
|
this._action = action;
|
|
|
|
if (this._action !== 'copy' && this._action !== 'cut') {
|
|
throw new Error('Invalid "action" value, use either "copy" or "cut"');
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Gets the `action` property.
|
|
* @return {String}
|
|
*/
|
|
get action() {
|
|
return this._action;
|
|
}
|
|
|
|
/**
|
|
* Sets the `target` property using an element
|
|
* that will be have its content copied.
|
|
* @param {Element} target
|
|
*/
|
|
set target(target) {
|
|
if (target !== undefined) {
|
|
if (target && typeof target === 'object' && target.nodeType === 1) {
|
|
if (this.action === 'copy' && target.hasAttribute('disabled')) {
|
|
throw new Error('Invalid "target" attribute. Please use "readonly" instead of "disabled" attribute');
|
|
}
|
|
|
|
if (this.action === 'cut' && (target.hasAttribute('readonly') || target.hasAttribute('disabled'))) {
|
|
throw new Error('Invalid "target" attribute. You can\'t cut text from elements with "readonly" or "disabled" attributes');
|
|
}
|
|
|
|
this._target = target;
|
|
}
|
|
else {
|
|
throw new Error('Invalid "target" value, use a valid Element');
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Gets the `target` property.
|
|
* @return {String|HTMLElement}
|
|
*/
|
|
get target() {
|
|
return this._target;
|
|
}
|
|
|
|
/**
|
|
* Destroy lifecycle.
|
|
*/
|
|
destroy() {
|
|
this.removeFake();
|
|
}
|
|
}
|
|
|
|
module.exports = ClipboardAction;
|