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, */ createFakeElement() { const isRTL = document.documentElement.getAttribute('dir') === 'rtl'; 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; return this.fakeElem; } /** * Get's the value of fakeElem, * and makes a selection on it. */ selectFake() { const fakeElem = this.createFakeElement(); this.fakeHandlerCallback = () => this.removeFake(); this.fakeHandler = this.container.addEventListener('click', this.fakeHandlerCallback) || true; this.container.appendChild(fakeElem); this.selectedText = select(fakeElem); this.copyText(); this.removeFake(); } /** * 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(); } document.activeElement.blur(); 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(); } } export default ClipboardAction;