From 2660565b61cf26e3598732180088cc8594601c40 Mon Sep 17 00:00:00 2001 From: vitormalencar Date: Wed, 20 Jan 2021 16:55:03 +0100 Subject: [PATCH] update code style --- .editorconfig | 2 +- bower.json | 6 +- contributing.md | 1 + demo/constructor-node.html | 26 +- demo/constructor-nodelist.html | 24 +- demo/constructor-selector.html | 22 +- demo/function-target.html | 30 +-- demo/function-text.html | 30 +-- demo/target-div.html | 30 ++- demo/target-input.html | 32 ++- demo/target-textarea.html | 30 ++- karma.conf.js | 45 ++-- package.js | 4 +- readme.md | 61 ++--- src/clipboard-action.js | 362 ++++++++++++++------------- src/clipboard.js | 227 +++++++++-------- test/clipboard-action.js | 445 +++++++++++++++++---------------- test/clipboard.js | 226 ++++++++--------- webpack.config.js | 73 +++--- 19 files changed, 856 insertions(+), 820 deletions(-) diff --git a/.editorconfig b/.editorconfig index 0f1d01b..202ee21 100644 --- a/.editorconfig +++ b/.editorconfig @@ -7,7 +7,7 @@ root = true [*] # Change these settings to your own preference indent_style = space -indent_size = 4 +indent_size = 2 # We recommend you to keep these unchanged end_of_line = lf diff --git a/bower.json b/bower.json index 63d834c..280af67 100644 --- a/bower.json +++ b/bower.json @@ -14,9 +14,5 @@ "/src", "/lib" ], - "keywords": [ - "clipboard", - "copy", - "cut" - ] + "keywords": ["clipboard", "copy", "cut"] } diff --git a/contributing.md b/contributing.md index 9ab2c8f..9146adc 100644 --- a/contributing.md +++ b/contributing.md @@ -24,5 +24,6 @@ Implement your bug fix or feature, write tests to cover it and make sure all tes Documentation is extremely important and takes a fair deal of time and effort to write and keep updated. Please submit any and all improvements you can make to the repository's docs. ## Known issues + If you're using npm@3 you'll probably face some issues related to peerDependencies. https://github.com/npm/npm/issues/9204 diff --git a/demo/constructor-node.html b/demo/constructor-node.html index 44b0059..057e40b 100644 --- a/demo/constructor-node.html +++ b/demo/constructor-node.html @@ -1,14 +1,14 @@ - - + + constructor-node - - - + + +
- Copy + Copy
@@ -16,16 +16,16 @@ - + diff --git a/demo/constructor-nodelist.html b/demo/constructor-nodelist.html index ece98c6..93568b0 100644 --- a/demo/constructor-nodelist.html +++ b/demo/constructor-nodelist.html @@ -1,11 +1,11 @@ - - + + constructor-nodelist - - - + + + @@ -16,16 +16,16 @@ - + diff --git a/demo/constructor-selector.html b/demo/constructor-selector.html index 7a5f8b1..7b90991 100644 --- a/demo/constructor-selector.html +++ b/demo/constructor-selector.html @@ -1,11 +1,11 @@ - - + + constructor-selector - - - + + + @@ -16,15 +16,15 @@ - + diff --git a/demo/function-target.html b/demo/function-target.html index a1aa191..4271fca 100644 --- a/demo/function-target.html +++ b/demo/function-target.html @@ -1,11 +1,11 @@ - - + + function-target - - - + + +
hello
@@ -15,19 +15,19 @@ - + diff --git a/demo/function-text.html b/demo/function-text.html index 9134aad..c0acaf1 100644 --- a/demo/function-text.html +++ b/demo/function-text.html @@ -1,11 +1,11 @@ - - + + function-text - - - + + + @@ -14,19 +14,19 @@ - + diff --git a/demo/target-div.html b/demo/target-div.html index 8ced2f2..a2c1a9e 100644 --- a/demo/target-div.html +++ b/demo/target-div.html @@ -1,29 +1,35 @@ - - + + target-div - - - + + +
hello
- + - + diff --git a/demo/target-input.html b/demo/target-input.html index b13eeed..4d07829 100644 --- a/demo/target-input.html +++ b/demo/target-input.html @@ -1,29 +1,35 @@ - - + + target-input - - - + + + - - + + - + diff --git a/demo/target-textarea.html b/demo/target-textarea.html index d42cc8c..47c1096 100644 --- a/demo/target-textarea.html +++ b/demo/target-textarea.html @@ -1,29 +1,35 @@ - - + + target-textarea - - - + + + - + - + diff --git a/karma.conf.js b/karma.conf.js index 93af9e8..600a4cb 100644 --- a/karma.conf.js +++ b/karma.conf.js @@ -1,30 +1,33 @@ -var webpackConfig = require('./webpack.config.js'); +var webpackConfig = require("./webpack.config.js"); module.exports = function (karma) { - karma.set({ - plugins: ['karma-webpack', 'karma-chai', 'karma-sinon', 'karma-mocha', 'karma-chrome-launcher'], + karma.set({ + plugins: [ + "karma-webpack", + "karma-chai", + "karma-sinon", + "karma-mocha", + "karma-chrome-launcher", + ], - frameworks: ['chai', 'sinon', 'mocha'], + frameworks: ["chai", "sinon", "mocha"], - files: [ - 'src/**/*.js', - 'test/**/*.js', - ], + files: ["src/**/*.js", "test/**/*.js"], - preprocessors: { - 'src/**/*.js': ['webpack'], - 'test/**/*.js': ['webpack'] - }, + preprocessors: { + "src/**/*.js": ["webpack"], + "test/**/*.js": ["webpack"], + }, - webpack: { - module: webpackConfig.module, - plugins: webpackConfig.plugins - }, + webpack: { + module: webpackConfig.module, + plugins: webpackConfig.plugins, + }, - webpackMiddleware: { - stats: 'errors-only' - }, + webpackMiddleware: { + stats: "errors-only", + }, - browsers: ['ChromeHeadless'] - }); + browsers: ["ChromeHeadless"], + }); }; diff --git a/package.js b/package.js index d720b15..307e90a 100644 --- a/package.js +++ b/package.js @@ -4,9 +4,9 @@ Package.describe({ name: "zenorocha:clipboard", summary: "Modern copy to clipboard. No Flash. Just 3kb.", version: "2.0.6", - git: "https://github.com/zenorocha/clipboard.js" + git: "https://github.com/zenorocha/clipboard.js", }); -Package.onUse(function(api) { +Package.onUse(function (api) { api.addFiles("dist/clipboard.js", "client"); }); diff --git a/readme.md b/readme.md index ec2cb8d..c27ef70 100644 --- a/readme.md +++ b/readme.md @@ -34,7 +34,7 @@ First, include the script located on the `dist` folder or load it from [a third- Now, you need to instantiate it by [passing a DOM selector](https://github.com/zenorocha/clipboard.js/blob/master/demo/constructor-selector.html#L18), [HTML element](https://github.com/zenorocha/clipboard.js/blob/master/demo/constructor-node.html#L16-L17), or [list of HTML elements](https://github.com/zenorocha/clipboard.js/blob/master/demo/constructor-nodelist.html#L18-L19). ```js -new ClipboardJS('.btn'); +new ClipboardJS(".btn"); ``` Internally, we need to fetch all elements that matches with your selector and attach event listeners for each one. But guess what? If you have hundreds of matches, this operation can consume a lot of memory. @@ -55,11 +55,11 @@ The value you include on this attribute needs to match another's element selecto ```html - + ``` @@ -77,7 +77,7 @@ If you omit this attribute, `copy` will be used by default. ``` @@ -91,8 +91,11 @@ Truth is, you don't even need another element to copy its content from. You can ```html - ``` @@ -103,19 +106,19 @@ There are cases where you'd like to show some user feedback or capture what has That's why we fire custom events such as `success` and `error` for you to listen and implement your custom logic. ```js -var clipboard = new ClipboardJS('.btn'); +var clipboard = new ClipboardJS(".btn"); -clipboard.on('success', function(e) { - console.info('Action:', e.action); - console.info('Text:', e.text); - console.info('Trigger:', e.trigger); +clipboard.on("success", function (e) { + console.info("Action:", e.action); + console.info("Text:", e.text); + console.info("Trigger:", e.trigger); - e.clearSelection(); + e.clearSelection(); }); -clipboard.on('error', function(e) { - console.error('Action:', e.action); - console.error('Trigger:', e.trigger); +clipboard.on("error", function (e) { + console.error("Action:", e.action); + console.error("Trigger:", e.trigger); }); ``` @@ -134,35 +137,35 @@ If you don't want to modify your HTML, there's a pretty handy imperative API for For instance, if you want to dynamically set a `target`, you'll need to return a Node. ```js -new ClipboardJS('.btn', { - target: function(trigger) { - return trigger.nextElementSibling; - } +new ClipboardJS(".btn", { + target: function (trigger) { + return trigger.nextElementSibling; + }, }); ``` If you want to dynamically set a `text`, you'll return a String. ```js -new ClipboardJS('.btn', { - text: function(trigger) { - return trigger.getAttribute('aria-label'); - } +new ClipboardJS(".btn", { + text: function (trigger) { + return trigger.getAttribute("aria-label"); + }, }); ``` For use in Bootstrap Modals or with any other library that changes the focus you'll want to set the focused element as the `container` value. ```js -new ClipboardJS('.btn', { - container: document.getElementById('modal') +new ClipboardJS(".btn", { + container: document.getElementById("modal"), }); ``` Also, if you are working with single page apps, you may want to manage the lifecycle of the DOM more precisely. Here's how you clean up the events and objects that we create. ```js -var clipboard = new ClipboardJS('.btn'); +var clipboard = new ClipboardJS(".btn"); clipboard.destroy(); ``` @@ -171,8 +174,8 @@ clipboard.destroy(); This library relies on both [Selection](https://developer.mozilla.org/en-US/docs/Web/API/Selection) and [execCommand](https://developer.mozilla.org/en-US/docs/Web/API/Document/execCommand) APIs. The first one is [supported by all browsers](https://caniuse.com/#search=selection) while the second one is supported in the following browsers. | Chrome logo | Edge logo | Firefox logo | Internet Explorer logo | Opera logo | Safari logo | -|:---:|:---:|:---:|:---:|:---:|:---:| -| 42+ ✔ | 12+ ✔ | 41+ ✔ | 9+ ✔ | 29+ ✔ | 10+ ✔ | +| :-------------------------------------------------------------------------------------------------------: | :---------------------------------------------------------------------------------------------------: | :---------------------------------------------------------------------------------------------------------: | :--------------------------------------------------------------------------------------------------------------: | :-----------------------------------------------------------------------------------------------------: | :-------------------------------------------------------------------------------------------------------: | +| 42+ ✔ | 12+ ✔ | 41+ ✔ | 9+ ✔ | 29+ ✔ | 10+ ✔ | The good news is that clipboard.js gracefully degrades if you need to support older browsers. All you have to do is show a tooltip saying `Copied!` when `success` event is called and `Press Ctrl+C to copy` when `error` event is called because the text is already selected. @@ -180,7 +183,7 @@ You can also check if clipboard.js is supported or not by running `ClipboardJS.i ## Bonus -A browser extension that adds a "copy to clipboard" button to every code block on *GitHub, MDN, Gist, StackOverflow, StackExchange, npm, and even Medium.* +A browser extension that adds a "copy to clipboard" button to every code block on _GitHub, MDN, Gist, StackOverflow, StackExchange, npm, and even Medium._ Install for [Chrome](https://chrome.google.com/webstore/detail/codecopy/fkbfebkcoelajmhanocgppanfoojcdmg) and [Firefox](https://addons.mozilla.org/en-US/firefox/addon/codecopy/). diff --git a/src/clipboard-action.js b/src/clipboard-action.js index fb298e7..f91bac7 100644 --- a/src/clipboard-action.js +++ b/src/clipboard-action.js @@ -1,204 +1,210 @@ -import select from 'select'; +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(); + /** + * @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; } - /** - * 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; + if (this.fakeElem) { + this.container.removeChild(this.fakeElem); + this.fakeElem = null; + } + } - this.selectedText = ''; + /** + * 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; } - /** - * 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(); - } + 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(); + } - /** - * 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'; + /** + * Sets the `action` to be performed which can be either 'copy' or 'cut'. + * @param {String} action + */ + set action(action = "copy") { + this._action = action; - 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(); + if (this._action !== "copy" && this._action !== "cut") { + throw new Error('Invalid "action" value, use either "copy" or "cut"'); } + } - /** - * 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; + /** + * 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.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; + 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.handleResult(succeeded); + this._target = target; + } else { + throw new Error('Invalid "target" value, use a valid Element'); + } } + } - /** - * 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) - }); - } + /** + * Gets the `target` property. + * @return {String|HTMLElement} + */ + get target() { + return this._target; + } - /** - * 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(); - } + /** + * Destroy lifecycle. + */ + destroy() { + this.removeFake(); + } } export default ClipboardAction; diff --git a/src/clipboard.js b/src/clipboard.js index 02ebede..29a5fb6 100644 --- a/src/clipboard.js +++ b/src/clipboard.js @@ -1,135 +1,142 @@ -import ClipboardAction from './clipboard-action'; -import Emitter from 'tiny-emitter'; -import listen from 'good-listener'; +import ClipboardAction from "./clipboard-action"; +import Emitter from "tiny-emitter"; +import listen from "good-listener"; /** * Base class which takes one or more elements, adds event listeners to them, * and instantiates a new `ClipboardAction` on each click. */ class Clipboard extends Emitter { - /** - * @param {String|HTMLElement|HTMLCollection|NodeList} trigger - * @param {Object} options - */ - constructor(trigger, options) { - super(); + /** + * @param {String|HTMLElement|HTMLCollection|NodeList} trigger + * @param {Object} options + */ + constructor(trigger, options) { + super(); - this.resolveOptions(options); - this.listenClick(trigger); + this.resolveOptions(options); + this.listenClick(trigger); + } + + /** + * Defines if attributes would be resolved using internal setter functions + * or custom functions that were passed in the constructor. + * @param {Object} options + */ + resolveOptions(options = {}) { + this.action = + typeof options.action === "function" + ? options.action + : this.defaultAction; + this.target = + typeof options.target === "function" + ? options.target + : this.defaultTarget; + this.text = + typeof options.text === "function" ? options.text : this.defaultText; + this.container = + typeof options.container === "object" ? options.container : document.body; + } + + /** + * Adds a click event listener to the passed trigger. + * @param {String|HTMLElement|HTMLCollection|NodeList} trigger + */ + listenClick(trigger) { + this.listener = listen(trigger, "click", (e) => this.onClick(e)); + } + + /** + * Defines a new `ClipboardAction` on each click event. + * @param {Event} e + */ + onClick(e) { + const trigger = e.delegateTarget || e.currentTarget; + + if (this.clipboardAction) { + this.clipboardAction = null; } - /** - * Defines if attributes would be resolved using internal setter functions - * or custom functions that were passed in the constructor. - * @param {Object} options - */ - resolveOptions(options = {}) { - this.action = (typeof options.action === 'function') ? options.action : this.defaultAction; - this.target = (typeof options.target === 'function') ? options.target : this.defaultTarget; - this.text = (typeof options.text === 'function') ? options.text : this.defaultText; - this.container = (typeof options.container === 'object') ? options.container : document.body; + this.clipboardAction = new ClipboardAction({ + action: this.action(trigger), + target: this.target(trigger), + text: this.text(trigger), + container: this.container, + trigger: trigger, + emitter: this, + }); + } + + /** + * Default `action` lookup function. + * @param {Element} trigger + */ + defaultAction(trigger) { + return getAttributeValue("action", trigger); + } + + /** + * Default `target` lookup function. + * @param {Element} trigger + */ + defaultTarget(trigger) { + const selector = getAttributeValue("target", trigger); + + if (selector) { + return document.querySelector(selector); } + } - /** - * Adds a click event listener to the passed trigger. - * @param {String|HTMLElement|HTMLCollection|NodeList} trigger - */ - listenClick(trigger) { - this.listener = listen(trigger, 'click', (e) => this.onClick(e)); - } - - /** - * Defines a new `ClipboardAction` on each click event. - * @param {Event} e - */ - onClick(e) { - const trigger = e.delegateTarget || e.currentTarget; - - if (this.clipboardAction) { - this.clipboardAction = null; - } - - this.clipboardAction = new ClipboardAction({ - action : this.action(trigger), - target : this.target(trigger), - text : this.text(trigger), - container : this.container, - trigger : trigger, - emitter : this - }); - } - - /** - * Default `action` lookup function. - * @param {Element} trigger - */ - defaultAction(trigger) { - return getAttributeValue('action', trigger); - } - - /** - * Default `target` lookup function. - * @param {Element} trigger - */ - defaultTarget(trigger) { - const selector = getAttributeValue('target', trigger); - - if (selector) { - return document.querySelector(selector); - } - } - - /** - * Returns the support of the given action, or all actions if no action is - * given. - * @param {String} [action] - */ - static isSupported(action = ['copy', 'cut']) { - const actions = (typeof action === 'string') ? [action] : action; - let support = !!document.queryCommandSupported; - - actions.forEach((action) => { - support = support && !!document.queryCommandSupported(action); - }); - - return support; - } - - /** - * Default `text` lookup function. - * @param {Element} trigger - */ - defaultText(trigger) { - return getAttributeValue('text', trigger); - } - - /** - * Destroy lifecycle. - */ - destroy() { - this.listener.destroy(); - - if (this.clipboardAction) { - this.clipboardAction.destroy(); - this.clipboardAction = null; - } + /** + * Returns the support of the given action, or all actions if no action is + * given. + * @param {String} [action] + */ + static isSupported(action = ["copy", "cut"]) { + const actions = typeof action === "string" ? [action] : action; + let support = !!document.queryCommandSupported; + + actions.forEach((action) => { + support = support && !!document.queryCommandSupported(action); + }); + + return support; + } + + /** + * Default `text` lookup function. + * @param {Element} trigger + */ + defaultText(trigger) { + return getAttributeValue("text", trigger); + } + + /** + * Destroy lifecycle. + */ + destroy() { + this.listener.destroy(); + + if (this.clipboardAction) { + this.clipboardAction.destroy(); + this.clipboardAction = null; } + } } - /** * Helper function to retrieve attribute value. * @param {String} suffix * @param {Element} element */ function getAttributeValue(suffix, element) { - const attribute = `data-clipboard-${suffix}`; + const attribute = `data-clipboard-${suffix}`; - if (!element.hasAttribute(attribute)) { - return; - } + if (!element.hasAttribute(attribute)) { + return; + } - return element.getAttribute(attribute); + return element.getAttribute(attribute); } export default Clipboard; diff --git a/test/clipboard-action.js b/test/clipboard-action.js index 8a3133c..f33aa80 100644 --- a/test/clipboard-action.js +++ b/test/clipboard-action.js @@ -1,243 +1,244 @@ -import ClipboardAction from '../src/clipboard-action'; -import Emitter from 'tiny-emitter'; +import ClipboardAction from "../src/clipboard-action"; +import Emitter from "tiny-emitter"; -describe('ClipboardAction', () => { +describe("ClipboardAction", () => { + before(() => { + global.input = document.createElement("input"); + global.input.setAttribute("id", "input"); + global.input.setAttribute("value", "abc"); + document.body.appendChild(global.input); + + global.paragraph = document.createElement("p"); + global.paragraph.setAttribute("id", "paragraph"); + global.paragraph.textContent = "abc"; + document.body.appendChild(global.paragraph); + }); + + after(() => { + document.body.innerHTML = ""; + }); + + describe("#resolveOptions", () => { + it("should set base properties", () => { + let clip = new ClipboardAction({ + emitter: new Emitter(), + container: document.body, + text: "foo", + }); + + assert.property(clip, "action"); + assert.property(clip, "container"); + assert.property(clip, "emitter"); + assert.property(clip, "target"); + assert.property(clip, "text"); + assert.property(clip, "trigger"); + assert.property(clip, "selectedText"); + }); + }); + + describe("#initSelection", () => { + it("should set the position right style property", (done) => { + // Set document direction + document.documentElement.setAttribute("dir", "rtl"); + + let clip = new ClipboardAction({ + emitter: new Emitter(), + container: document.body, + text: "foo", + }); + + assert.equal(clip.fakeElem.style.right, "-9999px"); + done(); + }); + }); + + describe("#set action", () => { + it('should throw an error since "action" is invalid', (done) => { + try { + new ClipboardAction({ + text: "foo", + action: "paste", + }); + } catch (e) { + assert.equal( + e.message, + 'Invalid "action" value, use either "copy" or "cut"' + ); + done(); + } + }); + }); + + describe("#set target", () => { + it('should throw an error since "target" do not match any element', (done) => { + try { + new ClipboardAction({ + target: document.querySelector("#foo"), + }); + } catch (e) { + assert.equal(e.message, 'Invalid "target" value, use a valid Element'); + done(); + } + }); + }); + + describe("#selectText", () => { + it("should create a fake element and select its value", () => { + let clip = new ClipboardAction({ + emitter: new Emitter(), + container: document.body, + text: "blah", + }); + + assert.equal(clip.selectedText, clip.fakeElem.value); + }); + }); + + describe("#removeFake", () => { + it("should remove a temporary fake element", () => { + let clip = new ClipboardAction({ + emitter: new Emitter(), + container: document.body, + text: "blah", + }); + + clip.removeFake(); + + assert.equal(clip.fakeElem, null); + }); + }); + + describe("#selectTarget", () => { + it("should select text from editable element", () => { + let clip = new ClipboardAction({ + emitter: new Emitter(), + container: document.body, + target: document.querySelector("#input"), + }); + + assert.equal(clip.selectedText, clip.target.value); + }); + + it("should select text from non-editable element", () => { + let clip = new ClipboardAction({ + emitter: new Emitter(), + container: document.body, + target: document.querySelector("#paragraph"), + }); + + assert.equal(clip.selectedText, clip.target.textContent); + }); + }); + + describe("#copyText", () => { before(() => { - global.input = document.createElement('input'); - global.input.setAttribute('id', 'input'); - global.input.setAttribute('value', 'abc'); - document.body.appendChild(global.input); - - global.paragraph = document.createElement('p'); - global.paragraph.setAttribute('id', 'paragraph'); - global.paragraph.textContent = 'abc'; - document.body.appendChild(global.paragraph); + global.stub = sinon.stub(document, "execCommand"); }); after(() => { - document.body.innerHTML = ''; + global.stub.restore(); }); - describe('#resolveOptions', () => { - it('should set base properties', () => { - let clip = new ClipboardAction({ - emitter: new Emitter(), - container: document.body, - text: 'foo' - }); + it("should fire a success event on browsers that support copy command", (done) => { + global.stub.returns(true); - assert.property(clip, 'action'); - assert.property(clip, 'container'); - assert.property(clip, 'emitter'); - assert.property(clip, 'target'); - assert.property(clip, 'text'); - assert.property(clip, 'trigger'); - assert.property(clip, 'selectedText'); - }); + let emitter = new Emitter(); + + emitter.on("success", () => { + done(); + }); + + let clip = new ClipboardAction({ + emitter, + target: document.querySelector("#input"), + }); }); - describe('#initSelection', () => { - it('should set the position right style property', done => { - // Set document direction - document.documentElement.setAttribute('dir', 'rtl'); + it("should fire an error event on browsers that support copy command", (done) => { + global.stub.returns(false); - let clip = new ClipboardAction({ - emitter: new Emitter(), - container: document.body, - text: 'foo' - }); + let emitter = new Emitter(); - assert.equal(clip.fakeElem.style.right, '-9999px'); - done(); - }); + emitter.on("error", () => { + done(); + }); + + let clip = new ClipboardAction({ + emitter, + target: document.querySelector("#input"), + }); + }); + }); + + describe("#handleResult", () => { + it("should fire a success event with certain properties", (done) => { + let clip = new ClipboardAction({ + emitter: new Emitter(), + container: document.body, + target: document.querySelector("#input"), + }); + + clip.emitter.on("success", (e) => { + assert.property(e, "action"); + assert.property(e, "text"); + assert.property(e, "trigger"); + assert.property(e, "clearSelection"); + + done(); + }); + + clip.handleResult(true); }); - describe('#set action', () => { - it('should throw an error since "action" is invalid', done => { - try { - new ClipboardAction({ - text: 'foo', - action: 'paste' - }); - } - catch(e) { - assert.equal(e.message, 'Invalid "action" value, use either "copy" or "cut"'); - done(); - } - }); + it("should fire a error event with certain properties", (done) => { + let clip = new ClipboardAction({ + emitter: new Emitter(), + container: document.body, + target: document.querySelector("#input"), + }); + + clip.emitter.on("error", (e) => { + assert.property(e, "action"); + assert.property(e, "trigger"); + assert.property(e, "clearSelection"); + + done(); + }); + + clip.handleResult(false); }); + }); - describe('#set target', () => { - it('should throw an error since "target" do not match any element', done => { - try { - new ClipboardAction({ - target: document.querySelector('#foo') - }); - } - catch(e) { - assert.equal(e.message, 'Invalid "target" value, use a valid Element'); - done(); - } - }); + describe("#clearSelection", () => { + it("should remove focus from target and text selection", () => { + let clip = new ClipboardAction({ + emitter: new Emitter(), + container: document.body, + target: document.querySelector("#input"), + }); + + clip.clearSelection(); + + let selectedElem = document.activeElement; + let selectedText = window.getSelection().toString(); + + assert.equal(selectedElem, document.body); + assert.equal(selectedText, ""); }); + }); - describe('#selectText', () => { - it('should create a fake element and select its value', () => { - let clip = new ClipboardAction({ - emitter: new Emitter(), - container: document.body, - text: 'blah' - }); + describe("#destroy", () => { + it("should destroy an existing fake element", () => { + let clip = new ClipboardAction({ + emitter: new Emitter(), + container: document.body, + text: "blah", + }); - assert.equal(clip.selectedText, clip.fakeElem.value); - }); - }); - - describe('#removeFake', () => { - it('should remove a temporary fake element', () => { - let clip = new ClipboardAction({ - emitter: new Emitter(), - container: document.body, - text: 'blah' - }); - - clip.removeFake(); - - assert.equal(clip.fakeElem, null); - }); - }); - - describe('#selectTarget', () => { - it('should select text from editable element', () => { - let clip = new ClipboardAction({ - emitter: new Emitter(), - container: document.body, - target: document.querySelector('#input') - }); - - assert.equal(clip.selectedText, clip.target.value); - }); - - it('should select text from non-editable element', () => { - let clip = new ClipboardAction({ - emitter: new Emitter(), - container: document.body, - target: document.querySelector('#paragraph') - }); - - assert.equal(clip.selectedText, clip.target.textContent); - }); - }); - - describe('#copyText', () => { - before(() => { - global.stub = sinon.stub(document, 'execCommand'); - }); - - after(() => { - global.stub.restore(); - }); - - it('should fire a success event on browsers that support copy command', done => { - global.stub.returns(true); - - let emitter = new Emitter(); - - emitter.on('success', () => { - done(); - }); - - let clip = new ClipboardAction({ - emitter, - target: document.querySelector('#input') - }); - }); - - it('should fire an error event on browsers that support copy command', done => { - global.stub.returns(false); - - let emitter = new Emitter(); - - emitter.on('error', () => { - done(); - }); - - let clip = new ClipboardAction({ - emitter, - target: document.querySelector('#input') - }); - }); - }); - - describe('#handleResult', () => { - it('should fire a success event with certain properties', done => { - let clip = new ClipboardAction({ - emitter: new Emitter(), - container: document.body, - target: document.querySelector('#input') - }); - - clip.emitter.on('success', (e) => { - assert.property(e, 'action'); - assert.property(e, 'text'); - assert.property(e, 'trigger'); - assert.property(e, 'clearSelection'); - - done(); - }); - - clip.handleResult(true); - }); - - it('should fire a error event with certain properties', done => { - let clip = new ClipboardAction({ - emitter: new Emitter(), - container: document.body, - target: document.querySelector('#input') - }); - - clip.emitter.on('error', (e) => { - assert.property(e, 'action'); - assert.property(e, 'trigger'); - assert.property(e, 'clearSelection'); - - done(); - }); - - clip.handleResult(false); - }); - }); - - describe('#clearSelection', () => { - it('should remove focus from target and text selection', () => { - let clip = new ClipboardAction({ - emitter: new Emitter(), - container: document.body, - target: document.querySelector('#input') - }); - - clip.clearSelection(); - - let selectedElem = document.activeElement; - let selectedText = window.getSelection().toString(); - - assert.equal(selectedElem, document.body); - assert.equal(selectedText, ''); - }); - }); - - describe('#destroy', () => { - it('should destroy an existing fake element', () => { - let clip = new ClipboardAction({ - emitter: new Emitter(), - container: document.body, - text: 'blah' - }); - - clip.selectFake(); - clip.destroy(); - - assert.equal(clip.fakeElem, null); - }); + clip.selectFake(); + clip.destroy(); + + assert.equal(clip.fakeElem, null); }); + }); }); diff --git a/test/clipboard.js b/test/clipboard.js index 7a3b235..c2646c6 100644 --- a/test/clipboard.js +++ b/test/clipboard.js @@ -1,132 +1,134 @@ -import Clipboard from '../src/clipboard'; -import ClipboardAction from '../src/clipboard-action'; -import listen from 'good-listener'; +import Clipboard from "../src/clipboard"; +import ClipboardAction from "../src/clipboard-action"; +import listen from "good-listener"; -describe('Clipboard', () => { +describe("Clipboard", () => { + before(() => { + global.button = document.createElement("button"); + global.button.setAttribute("class", "btn"); + global.button.setAttribute("data-clipboard-text", "foo"); + document.body.appendChild(global.button); + + global.span = document.createElement("span"); + global.span.innerHTML = "bar"; + + global.button.appendChild(span); + + global.event = { + target: global.button, + currentTarget: global.button, + }; + }); + + after(() => { + document.body.innerHTML = ""; + }); + + describe("#resolveOptions", () => { before(() => { - global.button = document.createElement('button'); - global.button.setAttribute('class', 'btn'); - global.button.setAttribute('data-clipboard-text', 'foo'); - document.body.appendChild(global.button); - - global.span = document.createElement('span'); - global.span.innerHTML = 'bar'; - - global.button.appendChild(span); - - global.event = { - target: global.button, - currentTarget: global.button - }; + global.fn = () => {}; }); - after(() => { - document.body.innerHTML = ''; + it("should set action as a function", () => { + let clipboard = new Clipboard(".btn", { + action: global.fn, + }); + + assert.equal(global.fn, clipboard.action); }); - describe('#resolveOptions', () => { - before(() => { - global.fn = () => {}; - }); + it("should set target as a function", () => { + let clipboard = new Clipboard(".btn", { + target: global.fn, + }); - it('should set action as a function', () => { - let clipboard = new Clipboard('.btn', { - action: global.fn - }); - - assert.equal(global.fn, clipboard.action); - }); - - it('should set target as a function', () => { - let clipboard = new Clipboard('.btn', { - target: global.fn - }); - - assert.equal(global.fn, clipboard.target); - }); - - it('should set text as a function', () => { - let clipboard = new Clipboard('.btn', { - text: global.fn - }); - - assert.equal(global.fn, clipboard.text); - }); - - it('should set container as an object', () => { - let clipboard = new Clipboard('.btn', { - container: document.body - }); - - assert.equal(document.body, clipboard.container); - }); - - it('should set container as body by default', () => { - let clipboard = new Clipboard('.btn'); - - assert.equal(document.body, clipboard.container); - }); + assert.equal(global.fn, clipboard.target); }); - describe('#listenClick', () => { - it('should add a click event listener to the passed selector', () => { - let clipboard = new Clipboard('.btn'); - assert.isObject(clipboard.listener); - }); + it("should set text as a function", () => { + let clipboard = new Clipboard(".btn", { + text: global.fn, + }); + + assert.equal(global.fn, clipboard.text); }); - describe('#onClick', () => { - it('should create a new instance of ClipboardAction', () => { - let clipboard = new Clipboard('.btn'); + it("should set container as an object", () => { + let clipboard = new Clipboard(".btn", { + container: document.body, + }); - clipboard.onClick(global.event); - assert.instanceOf(clipboard.clipboardAction, ClipboardAction); - }); - - it('should use an event\'s currentTarget when not equal to target', () => { - let clipboard = new Clipboard('.btn'); - let bubbledEvent = { target: global.span, currentTarget: global.button }; - - clipboard.onClick(bubbledEvent); - assert.instanceOf(clipboard.clipboardAction, ClipboardAction); - }); - - it('should throw an exception when target is invalid', done => { - try { - const clipboard = new Clipboard('.btn', { - target() { - return null; - } - }); - - clipboard.onClick(global.event); - } - catch(e) { - assert.equal(e.message, 'Invalid "target" value, use a valid Element'); - done(); - } - }); + assert.equal(document.body, clipboard.container); }); - describe('#static isSupported', () => { - it('should return the support of the given action', () => { - assert.equal(Clipboard.isSupported('copy'), true); - assert.equal(Clipboard.isSupported('cut'), true); - }); + it("should set container as body by default", () => { + let clipboard = new Clipboard(".btn"); - it('should return the support of the cut and copy actions', () => { - assert.equal(Clipboard.isSupported(), true); - }); + assert.equal(document.body, clipboard.container); + }); + }); + + describe("#listenClick", () => { + it("should add a click event listener to the passed selector", () => { + let clipboard = new Clipboard(".btn"); + assert.isObject(clipboard.listener); + }); + }); + + describe("#onClick", () => { + it("should create a new instance of ClipboardAction", () => { + let clipboard = new Clipboard(".btn"); + + clipboard.onClick(global.event); + assert.instanceOf(clipboard.clipboardAction, ClipboardAction); }); - describe('#destroy', () => { - it('should destroy an existing instance of ClipboardAction', () => { - let clipboard = new Clipboard('.btn'); + it("should use an event's currentTarget when not equal to target", () => { + let clipboard = new Clipboard(".btn"); + let bubbledEvent = { + target: global.span, + currentTarget: global.button, + }; - clipboard.onClick(global.event); - clipboard.destroy(); - - assert.equal(clipboard.clipboardAction, null); - }); + clipboard.onClick(bubbledEvent); + assert.instanceOf(clipboard.clipboardAction, ClipboardAction); }); + + it("should throw an exception when target is invalid", (done) => { + try { + const clipboard = new Clipboard(".btn", { + target() { + return null; + }, + }); + + clipboard.onClick(global.event); + } catch (e) { + assert.equal(e.message, 'Invalid "target" value, use a valid Element'); + done(); + } + }); + }); + + describe("#static isSupported", () => { + it("should return the support of the given action", () => { + assert.equal(Clipboard.isSupported("copy"), true); + assert.equal(Clipboard.isSupported("cut"), true); + }); + + it("should return the support of the cut and copy actions", () => { + assert.equal(Clipboard.isSupported(), true); + }); + }); + + describe("#destroy", () => { + it("should destroy an existing instance of ClipboardAction", () => { + let clipboard = new Clipboard(".btn"); + + clipboard.onClick(global.event); + clipboard.destroy(); + + assert.equal(clipboard.clipboardAction, null); + }); + }); }); diff --git a/webpack.config.js b/webpack.config.js index 39a858d..3b2691b 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -1,9 +1,9 @@ -const pkg = require('./package.json'); -const path = require('path'); -const webpack = require('webpack'); -const UglifyJSPlugin = require('uglifyjs-webpack-plugin'); +const pkg = require("./package.json"); +const path = require("path"); +const webpack = require("webpack"); +const UglifyJSPlugin = require("uglifyjs-webpack-plugin"); -const production = process.env.NODE_ENV === 'production' || false; +const production = process.env.NODE_ENV === "production" || false; const banner = `clipboard.js v${pkg.version} https://clipboardjs.com/ @@ -11,36 +11,35 @@ https://clipboardjs.com/ Licensed MIT © Zeno Rocha`; module.exports = { - entry: './src/clipboard.js', - mode: 'production', - output: { - filename: production ? 'clipboard.min.js' : 'clipboard.js', - path: path.resolve(__dirname, 'dist'), - library: 'ClipboardJS', - globalObject: 'this', - libraryExport: 'default', - libraryTarget: 'umd' - }, - module: { - rules: [ - {test: /\.js$/, exclude: /node_modules/, loader: 'babel-loader'} - ] - }, - optimization: { - minimize: production, - minimizer: [ - new UglifyJSPlugin({ - parallel: require('os').cpus().length, - uglifyOptions: { - ie8: false, - keep_fnames: false, - output: { - beautify: false, - comments: (node, {value, type}) => type == 'comment2' && value.startsWith('!') - } - } - }) - ] - }, - plugins: [new webpack.BannerPlugin({ banner })] + entry: "./src/clipboard.js", + mode: "production", + output: { + filename: production ? "clipboard.min.js" : "clipboard.js", + path: path.resolve(__dirname, "dist"), + library: "ClipboardJS", + globalObject: "this", + libraryExport: "default", + libraryTarget: "umd", + }, + module: { + rules: [{ test: /\.js$/, exclude: /node_modules/, loader: "babel-loader" }], + }, + optimization: { + minimize: production, + minimizer: [ + new UglifyJSPlugin({ + parallel: require("os").cpus().length, + uglifyOptions: { + ie8: false, + keep_fnames: false, + output: { + beautify: false, + comments: (node, { value, type }) => + type == "comment2" && value.startsWith("!"), + }, + }, + }), + ], + }, + plugins: [new webpack.BannerPlugin({ banner })], };