├── .travis.yml
├── .gitignore
├── .babelrc
├── package.js
├── bower.json
├── .github
├── issue_template.md
└── stale.yml
├── .editorconfig
├── composer.json
├── demo
├── target-div.html
├── function-text.html
├── target-textarea.html
├── target-input.html
├── constructor-node.html
├── function-target.html
├── constructor-selector.html
└── constructor-nodelist.html
├── karma.conf.js
├── LICENSE
├── package.json
├── webpack.config.js
├── contributing.md
├── src
├── clipboard.js
└── clipboard-action.js
├── test
├── clipboard.js
└── clipboard-action.js
├── readme.md
└── dist
├── clipboard.min.js
└── clipboard.js
/.travis.yml:
--------------------------------------------------------------------------------
1 | sudo: false
2 | language: node_js
3 | node_js:
4 | - stable
5 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | lib
2 | npm-debug.log
3 | bower_components
4 | node_modules
5 | yarn-error.log
6 | yarn.lock
7 |
--------------------------------------------------------------------------------
/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "presets": [
3 | [
4 | "env",
5 | {
6 | "targets": {
7 | "uglify": true
8 | },
9 | "modules": false
10 | }
11 | ]
12 | ]
13 | }
14 |
--------------------------------------------------------------------------------
/package.js:
--------------------------------------------------------------------------------
1 | // Package metadata for Meteor.js.
2 |
3 | Package.describe({
4 | name: "zenorocha:clipboard",
5 | summary: "Modern copy to clipboard. No Flash. Just 3kb.",
6 | version: "2.0.6",
7 | git: "https://github.com/zenorocha/clipboard.js"
8 | });
9 |
10 | Package.onUse(function(api) {
11 | api.addFiles("dist/clipboard.js", "client");
12 | });
13 |
--------------------------------------------------------------------------------
/bower.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "clipboard",
3 | "version": "2.0.6",
4 | "description": "Modern copy to clipboard. No Flash. Just 3kb",
5 | "license": "MIT",
6 | "main": "dist/clipboard.js",
7 | "ignore": [
8 | "/.*/",
9 | "/demo/",
10 | "/test/",
11 | "/.*",
12 | "/bower.json",
13 | "/karma.conf.js",
14 | "/src",
15 | "/lib"
16 | ],
17 | "keywords": [
18 | "clipboard",
19 | "copy",
20 | "cut"
21 | ]
22 | }
23 |
--------------------------------------------------------------------------------
/.github/issue_template.md:
--------------------------------------------------------------------------------
1 | ### Minimal example
2 |
3 | > Fork this [JSFiddle](https://jsfiddle.net/zenorocha/5kk0eysw/) and reproduce your issue.
4 |
5 | ### Expected behaviour
6 |
7 | I thought that by going to the page '...' and pressing the button '...' then '...' would happen.
8 |
9 | ### Actual behaviour
10 |
11 | Instead of '...', what I saw was that '...' happened instead.
12 |
13 | ### Browsers affected
14 |
15 | I tested on all major browsers and only IE 11 does not work.
16 |
--------------------------------------------------------------------------------
/.editorconfig:
--------------------------------------------------------------------------------
1 | # EditorConfig helps developers define and maintain consistent
2 | # coding styles between different editors and IDEs
3 | # http://editorconfig.org
4 |
5 | root = true
6 |
7 | [*]
8 | # Change these settings to your own preference
9 | indent_style = space
10 | indent_size = 4
11 |
12 | # We recommend you to keep these unchanged
13 | end_of_line = lf
14 | charset = utf-8
15 | trim_trailing_whitespace = true
16 | insert_final_newline = true
17 |
18 | [*.md]
19 | trim_trailing_whitespace = false
20 |
21 | [{package.json,bower.json}]
22 | indent_size = 2
23 |
--------------------------------------------------------------------------------
/composer.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "zenorocha/clipboardjs",
3 | "description": "Modern copy to clipboard. No Flash. Just 3kb gzipped https://clipboardjs.com",
4 | "type": "component",
5 | "homepage": "https://clipboardjs.com/",
6 | "authors": [
7 | {
8 | "name": "Zeno Rocha",
9 | "homepage": "http://zenorocha.com/"
10 | }
11 | ],
12 | "require": {
13 | "oomphinc/composer-installers-extender": "*"
14 | },
15 | "extra": {
16 | "component": {
17 | "scripts": [
18 | "dist/clipboard.js"
19 | ],
20 | "files": [
21 | "dist/clipboard.min.js"
22 | ]
23 | }
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/.github/stale.yml:
--------------------------------------------------------------------------------
1 | # Number of days of inactivity before an issue becomes stale
2 | daysUntilStale: 60
3 |
4 | # Number of days of inactivity before a stale issue is closed
5 | daysUntilClose: 7
6 |
7 | # Issues with these labels will never be considered stale
8 | exemptLabels:
9 | - pinned
10 |
11 | # Label to use when marking an issue as stale
12 | staleLabel: stale
13 |
14 | # Comment to post when marking an issue as stale. Set to `false` to disable
15 | markComment: >
16 | This issue has been automatically marked as stale because it has not had
17 | recent activity. It will be closed if no further activity occurs. Thank you
18 | for your contributions.
19 |
20 | # Comment to post when closing a stale issue. Set to `false` to disable
21 | closeComment: false
22 |
--------------------------------------------------------------------------------
/demo/target-div.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | target-div
6 |
7 |
8 |
9 |
10 | hello
11 |
12 |
13 |
14 |
15 |
16 |
17 |
28 |
29 |
30 |
--------------------------------------------------------------------------------
/karma.conf.js:
--------------------------------------------------------------------------------
1 | var webpackConfig = require('./webpack.config.js');
2 |
3 | module.exports = function (karma) {
4 | karma.set({
5 | plugins: ['karma-webpack', 'karma-chai', 'karma-sinon', 'karma-mocha', 'karma-chrome-launcher'],
6 |
7 | frameworks: ['chai', 'sinon', 'mocha'],
8 |
9 | files: [
10 | 'src/**/*.js',
11 | 'test/**/*.js',
12 | ],
13 |
14 | preprocessors: {
15 | 'src/**/*.js': ['webpack'],
16 | 'test/**/*.js': ['webpack']
17 | },
18 |
19 | webpack: {
20 | module: webpackConfig.module,
21 | plugins: webpackConfig.plugins
22 | },
23 |
24 | webpackMiddleware: {
25 | stats: 'errors-only'
26 | },
27 |
28 | browsers: ['ChromeHeadless']
29 | });
30 | };
31 |
--------------------------------------------------------------------------------
/demo/function-text.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | function-text
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
31 |
32 |
33 |
--------------------------------------------------------------------------------
/demo/target-textarea.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | target-textarea
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
28 |
29 |
30 |
--------------------------------------------------------------------------------
/demo/target-input.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | target-input
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
28 |
29 |
30 |
--------------------------------------------------------------------------------
/demo/constructor-node.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | constructor-node
6 |
7 |
8 |
9 |
10 |
11 | Copy
12 |
13 |
14 |
15 |
16 |
17 |
18 |
30 |
31 |
32 |
--------------------------------------------------------------------------------
/demo/function-target.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | function-target
6 |
7 |
8 |
9 |
10 |
11 | hello
12 |
13 |
14 |
15 |
16 |
17 |
32 |
33 |
34 |
--------------------------------------------------------------------------------
/demo/constructor-selector.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | constructor-selector
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
29 |
30 |
31 |
--------------------------------------------------------------------------------
/demo/constructor-nodelist.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | constructor-nodelist
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
30 |
31 |
32 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) Zeno Rocha
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "clipboard",
3 | "version": "2.0.6",
4 | "description": "Modern copy to clipboard. No Flash. Just 2kb",
5 | "repository": "zenorocha/clipboard.js",
6 | "license": "MIT",
7 | "main": "dist/clipboard.js",
8 | "keywords": [
9 | "clipboard",
10 | "copy",
11 | "cut"
12 | ],
13 | "dependencies": {
14 | "good-listener": "^1.2.2",
15 | "select": "^1.1.2",
16 | "tiny-emitter": "^2.0.0"
17 | },
18 | "devDependencies": {
19 | "babel-core": "^6.26.0",
20 | "babel-loader": "^7.1.4",
21 | "babel-preset-env": "^1.7.0",
22 | "chai": "^4.2.0",
23 | "cross-env": "^5.2.0",
24 | "karma": "^3.1.1",
25 | "karma-chai": "^0.1.0",
26 | "karma-mocha": "^1.2.0",
27 | "karma-chrome-launcher": "^2.2.0",
28 | "karma-sinon": "^1.0.4",
29 | "karma-webpack": "^3.0.5",
30 | "mocha": "^5.2.0",
31 | "sinon": "^7.1.1",
32 | "uglifyjs-webpack-plugin": "^2.0.1",
33 | "webpack": "^4.5.0",
34 | "webpack-cli": "^3.1.2"
35 | },
36 | "scripts": {
37 | "build": "npm run build-debug && npm run build-min",
38 | "build-debug": "webpack",
39 | "build-min": "cross-env NODE_ENV=production webpack",
40 | "build-watch": "webpack --watch",
41 | "test": "karma start --single-run",
42 | "prepublish": "npm run build"
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/webpack.config.js:
--------------------------------------------------------------------------------
1 | const pkg = require('./package.json');
2 | const path = require('path');
3 | const webpack = require('webpack');
4 | const UglifyJSPlugin = require('uglifyjs-webpack-plugin');
5 |
6 | const production = process.env.NODE_ENV === 'production' || false;
7 |
8 | const banner = `clipboard.js v${pkg.version}
9 | https://clipboardjs.com/
10 |
11 | Licensed MIT © Zeno Rocha`;
12 |
13 | module.exports = {
14 | entry: './src/clipboard.js',
15 | mode: 'production',
16 | output: {
17 | filename: production ? 'clipboard.min.js' : 'clipboard.js',
18 | path: path.resolve(__dirname, 'dist'),
19 | library: 'ClipboardJS',
20 | globalObject: 'this',
21 | libraryExport: 'default',
22 | libraryTarget: 'umd'
23 | },
24 | module: {
25 | rules: [
26 | {test: /\.js$/, exclude: /node_modules/, loader: 'babel-loader'}
27 | ]
28 | },
29 | optimization: {
30 | minimize: production,
31 | minimizer: [
32 | new UglifyJSPlugin({
33 | parallel: require('os').cpus().length,
34 | uglifyOptions: {
35 | ie8: false,
36 | keep_fnames: false,
37 | output: {
38 | beautify: false,
39 | comments: (node, {value, type}) => type == 'comment2' && value.startsWith('!')
40 | }
41 | }
42 | })
43 | ]
44 | },
45 | plugins: [new webpack.BannerPlugin({ banner })]
46 | };
47 |
--------------------------------------------------------------------------------
/contributing.md:
--------------------------------------------------------------------------------
1 | # Contributing guide
2 |
3 | Want to contribute to Clipboard.js? Awesome!
4 | There are many ways you can contribute, see below.
5 |
6 | ## Opening issues
7 |
8 | Open an issue to report bugs or to propose new features.
9 |
10 | - Reporting bugs: describe the bug as clearly as you can, including steps to reproduce, what happened and what you were expecting to happen. Also include browser version, OS and other related software's (npm, Node.js, etc) versions when applicable.
11 |
12 | - Proposing features: explain the proposed feature, what it should do, why it is useful, how users should use it. Give us as much info as possible so it will be easier to discuss, access and implement the proposed feature. When you're unsure about a certain aspect of the feature, feel free to leave it open for others to discuss and find an appropriate solution.
13 |
14 | ## Proposing pull requests
15 |
16 | Pull requests are very welcome. Note that if you are going to propose drastic changes, be sure to open an issue for discussion first, to make sure that your PR will be accepted before you spend effort coding it.
17 |
18 | Fork the Clipboard.js repository, clone it locally and create a branch for your proposed bug fix or new feature. Avoid working directly on the master branch.
19 |
20 | Implement your bug fix or feature, write tests to cover it and make sure all tests are passing (run a final `npm test` to make sure everything is correct). Then commit your changes, push your bug fix/feature branch to the origin (your forked repo) and open a pull request to the upstream (the repository you originally forked)'s master branch.
21 |
22 | ## Documentation
23 |
24 | 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.
25 |
26 | ## Known issues
27 | If you're using npm@3 you'll probably face some issues related to peerDependencies.
28 | https://github.com/npm/npm/issues/9204
29 |
--------------------------------------------------------------------------------
/src/clipboard.js:
--------------------------------------------------------------------------------
1 | import ClipboardAction from './clipboard-action';
2 | import Emitter from 'tiny-emitter';
3 | import listen from 'good-listener';
4 |
5 | /**
6 | * Base class which takes one or more elements, adds event listeners to them,
7 | * and instantiates a new `ClipboardAction` on each click.
8 | */
9 | class Clipboard extends Emitter {
10 | /**
11 | * @param {String|HTMLElement|HTMLCollection|NodeList} trigger
12 | * @param {Object} options
13 | */
14 | constructor(trigger, options) {
15 | super();
16 |
17 | this.resolveOptions(options);
18 | this.listenClick(trigger);
19 | }
20 |
21 | /**
22 | * Defines if attributes would be resolved using internal setter functions
23 | * or custom functions that were passed in the constructor.
24 | * @param {Object} options
25 | */
26 | resolveOptions(options = {}) {
27 | this.action = (typeof options.action === 'function') ? options.action : this.defaultAction;
28 | this.target = (typeof options.target === 'function') ? options.target : this.defaultTarget;
29 | this.text = (typeof options.text === 'function') ? options.text : this.defaultText;
30 | this.container = (typeof options.container === 'object') ? options.container : document.body;
31 | }
32 |
33 | /**
34 | * Adds a click event listener to the passed trigger.
35 | * @param {String|HTMLElement|HTMLCollection|NodeList} trigger
36 | */
37 | listenClick(trigger) {
38 | this.listener = listen(trigger, 'click', (e) => this.onClick(e));
39 | }
40 |
41 | /**
42 | * Defines a new `ClipboardAction` on each click event.
43 | * @param {Event} e
44 | */
45 | onClick(e) {
46 | const trigger = e.delegateTarget || e.currentTarget;
47 |
48 | if (this.clipboardAction) {
49 | this.clipboardAction = null;
50 | }
51 |
52 | this.clipboardAction = new ClipboardAction({
53 | action : this.action(trigger),
54 | target : this.target(trigger),
55 | text : this.text(trigger),
56 | container : this.container,
57 | trigger : trigger,
58 | emitter : this
59 | });
60 | }
61 |
62 | /**
63 | * Default `action` lookup function.
64 | * @param {Element} trigger
65 | */
66 | defaultAction(trigger) {
67 | return getAttributeValue('action', trigger);
68 | }
69 |
70 | /**
71 | * Default `target` lookup function.
72 | * @param {Element} trigger
73 | */
74 | defaultTarget(trigger) {
75 | const selector = getAttributeValue('target', trigger);
76 |
77 | if (selector) {
78 | return document.querySelector(selector);
79 | }
80 | }
81 |
82 | /**
83 | * Returns the support of the given action, or all actions if no action is
84 | * given.
85 | * @param {String} [action]
86 | */
87 | static isSupported(action = ['copy', 'cut']) {
88 | const actions = (typeof action === 'string') ? [action] : action;
89 | let support = !!document.queryCommandSupported;
90 |
91 | actions.forEach((action) => {
92 | support = support && !!document.queryCommandSupported(action);
93 | });
94 |
95 | return support;
96 | }
97 |
98 | /**
99 | * Default `text` lookup function.
100 | * @param {Element} trigger
101 | */
102 | defaultText(trigger) {
103 | return getAttributeValue('text', trigger);
104 | }
105 |
106 | /**
107 | * Destroy lifecycle.
108 | */
109 | destroy() {
110 | this.listener.destroy();
111 |
112 | if (this.clipboardAction) {
113 | this.clipboardAction.destroy();
114 | this.clipboardAction = null;
115 | }
116 | }
117 | }
118 |
119 |
120 | /**
121 | * Helper function to retrieve attribute value.
122 | * @param {String} suffix
123 | * @param {Element} element
124 | */
125 | function getAttributeValue(suffix, element) {
126 | const attribute = `data-clipboard-${suffix}`;
127 |
128 | if (!element.hasAttribute(attribute)) {
129 | return;
130 | }
131 |
132 | return element.getAttribute(attribute);
133 | }
134 |
135 | export default Clipboard;
136 |
--------------------------------------------------------------------------------
/test/clipboard.js:
--------------------------------------------------------------------------------
1 | import Clipboard from '../src/clipboard';
2 | import ClipboardAction from '../src/clipboard-action';
3 | import listen from 'good-listener';
4 |
5 | describe('Clipboard', () => {
6 | before(() => {
7 | global.button = document.createElement('button');
8 | global.button.setAttribute('class', 'btn');
9 | global.button.setAttribute('data-clipboard-text', 'foo');
10 | document.body.appendChild(global.button);
11 |
12 | global.span = document.createElement('span');
13 | global.span.innerHTML = 'bar';
14 |
15 | global.button.appendChild(span);
16 |
17 | global.event = {
18 | target: global.button,
19 | currentTarget: global.button
20 | };
21 | });
22 |
23 | after(() => {
24 | document.body.innerHTML = '';
25 | });
26 |
27 | describe('#resolveOptions', () => {
28 | before(() => {
29 | global.fn = () => {};
30 | });
31 |
32 | it('should set action as a function', () => {
33 | let clipboard = new Clipboard('.btn', {
34 | action: global.fn
35 | });
36 |
37 | assert.equal(global.fn, clipboard.action);
38 | });
39 |
40 | it('should set target as a function', () => {
41 | let clipboard = new Clipboard('.btn', {
42 | target: global.fn
43 | });
44 |
45 | assert.equal(global.fn, clipboard.target);
46 | });
47 |
48 | it('should set text as a function', () => {
49 | let clipboard = new Clipboard('.btn', {
50 | text: global.fn
51 | });
52 |
53 | assert.equal(global.fn, clipboard.text);
54 | });
55 |
56 | it('should set container as an object', () => {
57 | let clipboard = new Clipboard('.btn', {
58 | container: document.body
59 | });
60 |
61 | assert.equal(document.body, clipboard.container);
62 | });
63 |
64 | it('should set container as body by default', () => {
65 | let clipboard = new Clipboard('.btn');
66 |
67 | assert.equal(document.body, clipboard.container);
68 | });
69 | });
70 |
71 | describe('#listenClick', () => {
72 | it('should add a click event listener to the passed selector', () => {
73 | let clipboard = new Clipboard('.btn');
74 | assert.isObject(clipboard.listener);
75 | });
76 | });
77 |
78 | describe('#onClick', () => {
79 | it('should create a new instance of ClipboardAction', () => {
80 | let clipboard = new Clipboard('.btn');
81 |
82 | clipboard.onClick(global.event);
83 | assert.instanceOf(clipboard.clipboardAction, ClipboardAction);
84 | });
85 |
86 | it('should use an event\'s currentTarget when not equal to target', () => {
87 | let clipboard = new Clipboard('.btn');
88 | let bubbledEvent = { target: global.span, currentTarget: global.button };
89 |
90 | clipboard.onClick(bubbledEvent);
91 | assert.instanceOf(clipboard.clipboardAction, ClipboardAction);
92 | });
93 |
94 | it('should throw an exception when target is invalid', done => {
95 | try {
96 | const clipboard = new Clipboard('.btn', {
97 | target() {
98 | return null;
99 | }
100 | });
101 |
102 | clipboard.onClick(global.event);
103 | }
104 | catch(e) {
105 | assert.equal(e.message, 'Invalid "target" value, use a valid Element');
106 | done();
107 | }
108 | });
109 | });
110 |
111 | describe('#static isSupported', () => {
112 | it('should return the support of the given action', () => {
113 | assert.equal(Clipboard.isSupported('copy'), true);
114 | assert.equal(Clipboard.isSupported('cut'), true);
115 | });
116 |
117 | it('should return the support of the cut and copy actions', () => {
118 | assert.equal(Clipboard.isSupported(), true);
119 | });
120 | });
121 |
122 | describe('#destroy', () => {
123 | it('should destroy an existing instance of ClipboardAction', () => {
124 | let clipboard = new Clipboard('.btn');
125 |
126 | clipboard.onClick(global.event);
127 | clipboard.destroy();
128 |
129 | assert.equal(clipboard.clipboardAction, null);
130 | });
131 | });
132 | });
133 |
--------------------------------------------------------------------------------
/src/clipboard-action.js:
--------------------------------------------------------------------------------
1 | import select from 'select';
2 |
3 | /**
4 | * Inner class which performs selection from either `text` or `target`
5 | * properties and then executes copy or cut operations.
6 | */
7 | class ClipboardAction {
8 | /**
9 | * @param {Object} options
10 | */
11 | constructor(options) {
12 | this.resolveOptions(options);
13 | this.initSelection();
14 | }
15 |
16 | /**
17 | * Defines base properties passed from constructor.
18 | * @param {Object} options
19 | */
20 | resolveOptions(options = {}) {
21 | this.action = options.action;
22 | this.container = options.container;
23 | this.emitter = options.emitter;
24 | this.target = options.target;
25 | this.text = options.text;
26 | this.trigger = options.trigger;
27 |
28 | this.selectedText = '';
29 | }
30 |
31 | /**
32 | * Decides which selection strategy is going to be applied based
33 | * on the existence of `text` and `target` properties.
34 | */
35 | initSelection() {
36 | if (this.text) {
37 | this.selectFake();
38 | }
39 | else if (this.target) {
40 | this.selectTarget();
41 | }
42 | }
43 |
44 | /**
45 | * Creates a fake textarea element, sets its value from `text` property,
46 | * and makes a selection on it.
47 | */
48 | selectFake() {
49 | const isRTL = document.documentElement.getAttribute('dir') == 'rtl';
50 |
51 | this.removeFake();
52 |
53 | this.fakeHandlerCallback = () => this.removeFake();
54 | this.fakeHandler = this.container.addEventListener('click', this.fakeHandlerCallback) || true;
55 |
56 | this.fakeElem = document.createElement('textarea');
57 | // Prevent zooming on iOS
58 | this.fakeElem.style.fontSize = '12pt';
59 | // Reset box model
60 | this.fakeElem.style.border = '0';
61 | this.fakeElem.style.padding = '0';
62 | this.fakeElem.style.margin = '0';
63 | // Move element out of screen horizontally
64 | this.fakeElem.style.position = 'absolute';
65 | this.fakeElem.style[ isRTL ? 'right' : 'left' ] = '-9999px';
66 | // Move element to the same position vertically
67 | let yPosition = window.pageYOffset || document.documentElement.scrollTop;
68 | this.fakeElem.style.top = `${yPosition}px`;
69 |
70 | this.fakeElem.setAttribute('readonly', '');
71 | this.fakeElem.value = this.text;
72 |
73 | this.container.appendChild(this.fakeElem);
74 |
75 | this.selectedText = select(this.fakeElem);
76 | this.copyText();
77 | }
78 |
79 | /**
80 | * Only removes the fake element after another click event, that way
81 | * a user can hit `Ctrl+C` to copy because selection still exists.
82 | */
83 | removeFake() {
84 | if (this.fakeHandler) {
85 | this.container.removeEventListener('click', this.fakeHandlerCallback);
86 | this.fakeHandler = null;
87 | this.fakeHandlerCallback = null;
88 | }
89 |
90 | if (this.fakeElem) {
91 | this.container.removeChild(this.fakeElem);
92 | this.fakeElem = null;
93 | }
94 | }
95 |
96 | /**
97 | * Selects the content from element passed on `target` property.
98 | */
99 | selectTarget() {
100 | this.selectedText = select(this.target);
101 | this.copyText();
102 | }
103 |
104 | /**
105 | * Executes the copy operation based on the current selection.
106 | */
107 | copyText() {
108 | let succeeded;
109 |
110 | try {
111 | succeeded = document.execCommand(this.action);
112 | }
113 | catch (err) {
114 | succeeded = false;
115 | }
116 |
117 | this.handleResult(succeeded);
118 | }
119 |
120 | /**
121 | * Fires an event based on the copy operation result.
122 | * @param {Boolean} succeeded
123 | */
124 | handleResult(succeeded) {
125 | this.emitter.emit(succeeded ? 'success' : 'error', {
126 | action: this.action,
127 | text: this.selectedText,
128 | trigger: this.trigger,
129 | clearSelection: this.clearSelection.bind(this)
130 | });
131 | }
132 |
133 | /**
134 | * Moves focus away from `target` and back to the trigger, removes current selection.
135 | */
136 | clearSelection() {
137 | if (this.trigger) {
138 | this.trigger.focus();
139 | }
140 | document.activeElement.blur();
141 | window.getSelection().removeAllRanges();
142 | }
143 |
144 | /**
145 | * Sets the `action` to be performed which can be either 'copy' or 'cut'.
146 | * @param {String} action
147 | */
148 | set action(action = 'copy') {
149 | this._action = action;
150 |
151 | if (this._action !== 'copy' && this._action !== 'cut') {
152 | throw new Error('Invalid "action" value, use either "copy" or "cut"');
153 | }
154 | }
155 |
156 | /**
157 | * Gets the `action` property.
158 | * @return {String}
159 | */
160 | get action() {
161 | return this._action;
162 | }
163 |
164 | /**
165 | * Sets the `target` property using an element
166 | * that will be have its content copied.
167 | * @param {Element} target
168 | */
169 | set target(target) {
170 | if (target !== undefined) {
171 | if (target && typeof target === 'object' && target.nodeType === 1) {
172 | if (this.action === 'copy' && target.hasAttribute('disabled')) {
173 | throw new Error('Invalid "target" attribute. Please use "readonly" instead of "disabled" attribute');
174 | }
175 |
176 | if (this.action === 'cut' && (target.hasAttribute('readonly') || target.hasAttribute('disabled'))) {
177 | throw new Error('Invalid "target" attribute. You can\'t cut text from elements with "readonly" or "disabled" attributes');
178 | }
179 |
180 | this._target = target;
181 | }
182 | else {
183 | throw new Error('Invalid "target" value, use a valid Element');
184 | }
185 | }
186 | }
187 |
188 | /**
189 | * Gets the `target` property.
190 | * @return {String|HTMLElement}
191 | */
192 | get target() {
193 | return this._target;
194 | }
195 |
196 | /**
197 | * Destroy lifecycle.
198 | */
199 | destroy() {
200 | this.removeFake();
201 | }
202 | }
203 |
204 | export default ClipboardAction;
205 |
--------------------------------------------------------------------------------
/test/clipboard-action.js:
--------------------------------------------------------------------------------
1 | import ClipboardAction from '../src/clipboard-action';
2 | import Emitter from 'tiny-emitter';
3 |
4 | describe('ClipboardAction', () => {
5 | before(() => {
6 | global.input = document.createElement('input');
7 | global.input.setAttribute('id', 'input');
8 | global.input.setAttribute('value', 'abc');
9 | document.body.appendChild(global.input);
10 |
11 | global.paragraph = document.createElement('p');
12 | global.paragraph.setAttribute('id', 'paragraph');
13 | global.paragraph.textContent = 'abc';
14 | document.body.appendChild(global.paragraph);
15 | });
16 |
17 | after(() => {
18 | document.body.innerHTML = '';
19 | });
20 |
21 | describe('#resolveOptions', () => {
22 | it('should set base properties', () => {
23 | let clip = new ClipboardAction({
24 | emitter: new Emitter(),
25 | container: document.body,
26 | text: 'foo'
27 | });
28 |
29 | assert.property(clip, 'action');
30 | assert.property(clip, 'container');
31 | assert.property(clip, 'emitter');
32 | assert.property(clip, 'target');
33 | assert.property(clip, 'text');
34 | assert.property(clip, 'trigger');
35 | assert.property(clip, 'selectedText');
36 | });
37 | });
38 |
39 | describe('#initSelection', () => {
40 | it('should set the position right style property', done => {
41 | // Set document direction
42 | document.documentElement.setAttribute('dir', 'rtl');
43 |
44 | let clip = new ClipboardAction({
45 | emitter: new Emitter(),
46 | container: document.body,
47 | text: 'foo'
48 | });
49 |
50 | assert.equal(clip.fakeElem.style.right, '-9999px');
51 | done();
52 | });
53 | });
54 |
55 | describe('#set action', () => {
56 | it('should throw an error since "action" is invalid', done => {
57 | try {
58 | new ClipboardAction({
59 | text: 'foo',
60 | action: 'paste'
61 | });
62 | }
63 | catch(e) {
64 | assert.equal(e.message, 'Invalid "action" value, use either "copy" or "cut"');
65 | done();
66 | }
67 | });
68 | });
69 |
70 | describe('#set target', () => {
71 | it('should throw an error since "target" do not match any element', done => {
72 | try {
73 | new ClipboardAction({
74 | target: document.querySelector('#foo')
75 | });
76 | }
77 | catch(e) {
78 | assert.equal(e.message, 'Invalid "target" value, use a valid Element');
79 | done();
80 | }
81 | });
82 | });
83 |
84 | describe('#selectText', () => {
85 | it('should create a fake element and select its value', () => {
86 | let clip = new ClipboardAction({
87 | emitter: new Emitter(),
88 | container: document.body,
89 | text: 'blah'
90 | });
91 |
92 | assert.equal(clip.selectedText, clip.fakeElem.value);
93 | });
94 | });
95 |
96 | describe('#removeFake', () => {
97 | it('should remove a temporary fake element', () => {
98 | let clip = new ClipboardAction({
99 | emitter: new Emitter(),
100 | container: document.body,
101 | text: 'blah'
102 | });
103 |
104 | clip.removeFake();
105 |
106 | assert.equal(clip.fakeElem, null);
107 | });
108 | });
109 |
110 | describe('#selectTarget', () => {
111 | it('should select text from editable element', () => {
112 | let clip = new ClipboardAction({
113 | emitter: new Emitter(),
114 | container: document.body,
115 | target: document.querySelector('#input')
116 | });
117 |
118 | assert.equal(clip.selectedText, clip.target.value);
119 | });
120 |
121 | it('should select text from non-editable element', () => {
122 | let clip = new ClipboardAction({
123 | emitter: new Emitter(),
124 | container: document.body,
125 | target: document.querySelector('#paragraph')
126 | });
127 |
128 | assert.equal(clip.selectedText, clip.target.textContent);
129 | });
130 | });
131 |
132 | describe('#copyText', () => {
133 | before(() => {
134 | global.stub = sinon.stub(document, 'execCommand');
135 | });
136 |
137 | after(() => {
138 | global.stub.restore();
139 | });
140 |
141 | it('should fire a success event on browsers that support copy command', done => {
142 | global.stub.returns(true);
143 |
144 | let emitter = new Emitter();
145 |
146 | emitter.on('success', () => {
147 | done();
148 | });
149 |
150 | let clip = new ClipboardAction({
151 | emitter,
152 | target: document.querySelector('#input')
153 | });
154 | });
155 |
156 | it('should fire an error event on browsers that support copy command', done => {
157 | global.stub.returns(false);
158 |
159 | let emitter = new Emitter();
160 |
161 | emitter.on('error', () => {
162 | done();
163 | });
164 |
165 | let clip = new ClipboardAction({
166 | emitter,
167 | target: document.querySelector('#input')
168 | });
169 | });
170 | });
171 |
172 | describe('#handleResult', () => {
173 | it('should fire a success event with certain properties', done => {
174 | let clip = new ClipboardAction({
175 | emitter: new Emitter(),
176 | container: document.body,
177 | target: document.querySelector('#input')
178 | });
179 |
180 | clip.emitter.on('success', (e) => {
181 | assert.property(e, 'action');
182 | assert.property(e, 'text');
183 | assert.property(e, 'trigger');
184 | assert.property(e, 'clearSelection');
185 |
186 | done();
187 | });
188 |
189 | clip.handleResult(true);
190 | });
191 |
192 | it('should fire a error event with certain properties', done => {
193 | let clip = new ClipboardAction({
194 | emitter: new Emitter(),
195 | container: document.body,
196 | target: document.querySelector('#input')
197 | });
198 |
199 | clip.emitter.on('error', (e) => {
200 | assert.property(e, 'action');
201 | assert.property(e, 'trigger');
202 | assert.property(e, 'clearSelection');
203 |
204 | done();
205 | });
206 |
207 | clip.handleResult(false);
208 | });
209 | });
210 |
211 | describe('#clearSelection', () => {
212 | it('should remove focus from target and text selection', () => {
213 | let clip = new ClipboardAction({
214 | emitter: new Emitter(),
215 | container: document.body,
216 | target: document.querySelector('#input')
217 | });
218 |
219 | clip.clearSelection();
220 |
221 | let selectedElem = document.activeElement;
222 | let selectedText = window.getSelection().toString();
223 |
224 | assert.equal(selectedElem, document.body);
225 | assert.equal(selectedText, '');
226 | });
227 | });
228 |
229 | describe('#destroy', () => {
230 | it('should destroy an existing fake element', () => {
231 | let clip = new ClipboardAction({
232 | emitter: new Emitter(),
233 | container: document.body,
234 | text: 'blah'
235 | });
236 |
237 | clip.selectFake();
238 | clip.destroy();
239 |
240 | assert.equal(clip.fakeElem, null);
241 | });
242 | });
243 | });
244 |
--------------------------------------------------------------------------------
/readme.md:
--------------------------------------------------------------------------------
1 | # clipboard.js
2 |
3 | [](https://travis-ci.org/zenorocha/clipboard.js)
4 | 
5 |
6 | > Modern copy to clipboard. No Flash. Just 3kb gzipped.
7 |
8 |
9 |
10 | ## Why
11 |
12 | Copying text to the clipboard shouldn't be hard. It shouldn't require dozens of steps to configure or hundreds of KBs to load. But most of all, it shouldn't depend on Flash or any bloated framework.
13 |
14 | That's why clipboard.js exists.
15 |
16 | ## Install
17 |
18 | You can get it on npm.
19 |
20 | ```
21 | npm install clipboard --save
22 | ```
23 |
24 | Or if you're not into package management, just [download a ZIP](https://github.com/zenorocha/clipboard.js/archive/master.zip) file.
25 |
26 | ## Setup
27 |
28 | First, include the script located on the `dist` folder or load it from [a third-party CDN provider](https://github.com/zenorocha/clipboard.js/wiki/CDN-Providers).
29 |
30 | ```html
31 |
32 | ```
33 |
34 | 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).
35 |
36 | ```js
37 | new ClipboardJS('.btn');
38 | ```
39 |
40 | 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.
41 |
42 | For this reason we use [event delegation](https://stackoverflow.com/questions/1687296/what-is-dom-event-delegation) which replaces multiple event listeners with just a single listener. After all, [#perfmatters](https://twitter.com/hashtag/perfmatters).
43 |
44 | # Usage
45 |
46 | We're living a _declarative renaissance_, that's why we decided to take advantage of [HTML5 data attributes](https://developer.mozilla.org/en-US/docs/Web/Guide/HTML/Using_data_attributes) for better usability.
47 |
48 | ### Copy text from another element
49 |
50 | A pretty common use case is to copy content from another element. You can do that by adding a `data-clipboard-target` attribute in your trigger element.
51 |
52 | The value you include on this attribute needs to match another's element selector.
53 |
54 |
55 |
56 | ```html
57 |
58 |
59 |
60 |
61 |
64 | ```
65 |
66 | ### Cut text from another element
67 |
68 | Additionally, you can define a `data-clipboard-action` attribute to specify if you want to either `copy` or `cut` content.
69 |
70 | If you omit this attribute, `copy` will be used by default.
71 |
72 |
73 |
74 | ```html
75 |
76 |
77 |
78 |
79 |
82 | ```
83 |
84 | As you may expect, the `cut` action only works on `` or `