Compare commits

..

1 Commits

Author SHA1 Message Date
Zeno Rocha
0a0de9fc01 Release v1.5.3 2015-10-28 13:06:53 -07:00
59 changed files with 1296 additions and 22276 deletions

View File

@@ -1,11 +0,0 @@
{
"presets": [
[
"@babel/env",
{
"forceAllTransforms": true,
"modules": false
}
]
]
}

6
.banner Normal file
View File

@@ -0,0 +1,6 @@
/*!
* clipboard.js v<%= pkg.version %>
* https://zenorocha.github.io/clipboard.js
*
* Licensed MIT © Zeno Rocha
*/

View File

@@ -7,7 +7,7 @@ root = true
[*]
# Change these settings to your own preference
indent_style = space
indent_size = 2
indent_size = 4
# We recommend you to keep these unchanged
end_of_line = lf

View File

@@ -1,12 +0,0 @@
# Ignore artifacts:
dist
lib
npm-debug.log
bower_components
node_modules
yarn-error.log
yarn.lock
src/*.ts
/*.js

View File

@@ -1,24 +0,0 @@
{
"env": {
"browser": true,
"es2021": true,
"mocha": true
},
"extends": ["airbnb-base", "plugin:prettier/recommended"],
"parserOptions": {
"ecmaVersion": 12,
"sourceType": "module"
},
"plugins": ["prettier"],
"rules": {
"prettier/prettier": "error",
"prefer-const": "off",
"camelcase": "off",
"no-underscore-dangle": "off",
"consistent-return": "off",
/* Remove the necessity to use this on classes */
"class-methods-use-this": "off",
/* Enable variables declarations from shadowing variables declared in the outer scope */
"no-shadow": "off"
}
}

View File

@@ -1,57 +0,0 @@
---
name: 🐛 Bug Report
about: Submit a bug report to help us improve
labels: 'bug, needs triage'
---
<!--
! PLEASE HELP US HELP YOU !
Bugs are fixed faster if you include:
- a repro repository to inspect the code
- an url to see the problem live
-->
## 🐛 Bug Report
> Fork this [JSFiddle](https://jsfiddle.net/zenorocha/5kk0eysw/) and reproduce your issue.
(A clear and concise description of what the issue is.)
### Have you read the [Contributing Guidelines on issues](https://github.com/zenorocha/clipboard.js/blob/master/contributing.md)?
(Write your answer here.)
### Expected Behaviour
<!--
How did you expect your project to behave?
Its fine if youre not sure your understanding is correct.
Write down what you thought would happen.
-->
I thought that by going to the page '...' and pressing the button '...' then '...' would happen.
_Tip: Try to use screenshots, gifs, videos, always remember people better understand with a visual way._
### Actual Behaviour
Instead of '...', what I saw was that '...' happened instead.
### To Reproduce
(Write your steps such as:)
1. Step 1...
1. Step 2...
1. Step 3...
### Browsers Affected
I tested on all major browsers and only IE 11 does not work.
### Operational System
(Place here your Operational System.)

View File

@@ -1,13 +0,0 @@
---
name: 📚 Documentation
about: Report an issue related to documentation
labels: 'documentation, needs triage'
---
## 📚 Documentation
(A clear and concise description of what the issue is.)
### Have you read the [Contributing Guidelines on issues](https://github.com/zenorocha/clipboard.js/blob/master/contributing.md)?
(Write your answer here.)

View File

@@ -1,26 +0,0 @@
---
name: 💥 Proposal
about: Propose a non-trivial change to Clipboard.js
labels: 'proposal, needs triage'
---
## 💥 Proposal
**Is your feature request related to a problem? Please describe**
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
**Describe the solution you'd like**
A clear and concise description of what you want to happen.
**Describe alternatives you've considered**
A clear and concise description of any alternative solutions or features you've considered.
**Are you able to assist to bring the feature to reality?**
no | yes, I can...
**Additional context**
Add any other context or screenshots about the feature request here.
### Have you read the [Contributing Guidelines on issues](https://github.com/zenorocha/clipboard.js/blob/master/contributing.md)?
(Write your answer here.)

View File

@@ -1,35 +0,0 @@
<!--
Please make sure to read the Pull Request Guidelines:
https://github.com/zenorocha/clipboard.js/blob/master/contributing.md#proposing-pull-requests
-->
<!-- PULL REQUEST TEMPLATE -->
<!-- (Update "[ ]" to "[x]" to check a box) -->
**What kind of change does this PR introduce?** (check at least one)
- [ ] Bugfix
- [ ] Feature
- [ ] Code style update
- [ ] Refactor
- [ ] Build-related changes
- [ ] Other, please describe:
**Does this PR introduce a breaking change?** (check one)
- [ ] Yes
- [ ] No
If yes, please describe the impact and migration path for existing applications:
**The PR fulfills these requirements:**
- [ ] It's submitted to the `dev` branch for v2.x (or to a previous version branch), _not_ the `master` branch
- [ ] When resolving a specific issue, it's referenced in the PR's title (e.g. `fix #xxx[,#xxx]`, where "xxx" is the issue number)
- [ ] New/updated tests are included
If adding a **new feature**, the PR's description includes:
- [ ] A convincing reason for adding this feature (to avoid wasting your time, it's best to open a suggestion issue first and wait for approval before working on it)
**Other information:**

21
.github/stale.yml vendored
View File

@@ -1,21 +0,0 @@
# Number of days of inactivity before an issue becomes stale
daysUntilStale: 60
# Number of days of inactivity before a stale issue is closed
daysUntilClose: 7
# Issues with these labels will never be considered stale
exemptLabels:
- pinned
# Label to use when marking an issue as stale
staleLabel: stale
# Comment to post when marking an issue as stale. Set to `false` to disable
markComment: >
This issue has been automatically marked as stale because it has not had
recent activity. It will be closed if no further activity occurs. Thank you
for your contributions.
# Comment to post when closing a stale issue. Set to `false` to disable
closeComment: false

View File

@@ -1,47 +0,0 @@
# This workflow will run tests using node and then publish a package to GitHub Packages when a release is created
# For more information see: https://help.github.com/actions/language-and-framework-guides/publishing-nodejs-packages
name: publish
on:
release:
types: [created]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/setup-node@v1
with:
node-version: 14
- run: npm ci
- run: npm test
publish-npm:
needs: build
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/setup-node@v1
with:
node-version: 14
registry-url: https://registry.npmjs.org/
- run: npm ci
- run: npm publish
env:
NODE_AUTH_TOKEN: ${{secrets.npm_token}}
publish-gpr:
needs: build
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/setup-node@v1
with:
node-version: 14
registry-url: https://npm.pkg.github.com/
- run: npm ci
- run: npm publish
env:
NODE_AUTH_TOKEN: ${{secrets.GITHUB_TOKEN}}

View File

@@ -1,34 +0,0 @@
# This workflow will do a clean install of node dependencies, build the source code and run tests across different versions of node
# For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions
name: build
on:
push:
branches: [master]
pull_request:
branches: [master]
env:
FORCE_COLOR: 2
jobs:
build:
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [10.x, 12.x, 14.x, 15.x]
# See supported Node.js release schedule at https://nodejs.org/en/about/releases/
# For now is not possible to target LTS verssions =/ check progress here https://github.com/actions/setup-node/issues/26
steps:
- uses: actions/checkout@v2
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v2
with:
node-version: ${{ matrix.node-version }}
- run: npm ci
- run: npm run build --if-present
- run: npm run lint
- run: npm test

3
.gitignore vendored
View File

@@ -2,6 +2,3 @@ lib
npm-debug.log
bower_components
node_modules
yarn-error.log
yarn.lock
.DS_Store

1
.husky/.gitignore vendored
View File

@@ -1 +0,0 @@
_

View File

@@ -1,4 +0,0 @@
#!/bin/sh
. "$(dirname "$0")/_/husky.sh"
npx --no-install lint-staged

7
.npmignore Normal file
View File

@@ -0,0 +1,7 @@
/.*/
/demo/
/test/
/.*
/bower.json
/karma.conf.js
/src

1
.nvmrc
View File

@@ -1 +0,0 @@
14

View File

@@ -1,9 +0,0 @@
# Ignore artifacts:
dist
lib
npm-debug.log
bower_components
node_modules
yarn-error.log
yarn.lock

View File

@@ -1,9 +0,0 @@
{
"printWidth": 80,
"tabWidth": 2,
"semi": true,
"singleQuote": true,
"trailingComma": "es5",
"bracketSpacing": true,
"arrowParens": "always"
}

4
.travis.yml Normal file
View File

@@ -0,0 +1,4 @@
sudo: false
language: node_js
node_js:
- stable

21
LICENSE
View File

@@ -1,21 +0,0 @@
MIT License
Copyright (c) Zeno Rocha
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@@ -1,7 +1,7 @@
{
"name": "clipboard",
"version": "2.0.9",
"description": "Modern copy to clipboard. No Flash. Just 3kb",
"version": "1.5.3",
"description": "Modern copy to clipboard. No Flash. Just 2kb",
"license": "MIT",
"main": "dist/clipboard.js",
"ignore": [
@@ -14,5 +14,9 @@
"/src",
"/lib"
],
"keywords": ["clipboard", "copy", "cut"]
"keywords": [
"clipboard",
"copy",
"cut"
]
}

View File

@@ -1,25 +0,0 @@
{
"name": "zenorocha/clipboardjs",
"description": "Modern copy to clipboard. No Flash. Just 3kb gzipped https://clipboardjs.com",
"type": "component",
"homepage": "https://clipboardjs.com/",
"authors": [
{
"name": "Zeno Rocha",
"homepage": "http://zenorocha.com/"
}
],
"require": {
"oomphinc/composer-installers-extender": "*"
},
"extra": {
"component": {
"scripts": [
"dist/clipboard.js"
],
"files": [
"dist/clipboard.min.js"
]
}
}
}

View File

@@ -24,6 +24,5 @@ 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

View File

@@ -1,31 +1,28 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<head>
<meta charset="UTF-8">
<title>constructor-node</title>
<meta name="viewport" content="width=device-width, initial-scale=1" />
</head>
<body>
</head>
<body>
<!-- 1. Define some markup -->
<div id="btn" data-clipboard-text="1">
<span>Copy</span>
</div>
<button id="btn" data-clipboard-text="1">Copy</button>
<!-- 2. Include library -->
<script src="../dist/clipboard.min.js"></script>
<!-- 3. Instantiate clipboard by passing a HTML element -->
<script>
var btn = document.getElementById('btn');
var clipboard = new ClipboardJS(btn);
var btn = document.getElementById('btn');
var clipboard = new Clipboard(btn);
clipboard.on('success', function (e) {
clipboard.on('success', function(e) {
console.log(e);
});
});
clipboard.on('error', function (e) {
clipboard.on('error', function(e) {
console.log(e);
});
});
</script>
</body>
</body>
</html>

View File

@@ -1,11 +1,10 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<head>
<meta charset="UTF-8">
<title>constructor-nodelist</title>
<meta name="viewport" content="width=device-width, initial-scale=1" />
</head>
<body>
</head>
<body>
<!-- 1. Define some markup -->
<button data-clipboard-text="1">Copy</button>
<button data-clipboard-text="2">Copy</button>
@@ -16,16 +15,16 @@
<!-- 3. Instantiate clipboard by passing a list of HTML elements -->
<script>
var btns = document.querySelectorAll('button');
var clipboard = new ClipboardJS(btns);
var btns = document.querySelectorAll('button');
var clipboard = new Clipboard(btns);
clipboard.on('success', function (e) {
clipboard.on('success', function(e) {
console.log(e);
});
});
clipboard.on('error', function (e) {
clipboard.on('error', function(e) {
console.log(e);
});
});
</script>
</body>
</body>
</html>

View File

@@ -1,11 +1,10 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<head>
<meta charset="UTF-8">
<title>constructor-selector</title>
<meta name="viewport" content="width=device-width, initial-scale=1" />
</head>
<body>
</head>
<body>
<!-- 1. Define some markup -->
<button class="btn" data-clipboard-text="1">Copy</button>
<button class="btn" data-clipboard-text="2">Copy</button>
@@ -16,15 +15,15 @@
<!-- 3. Instantiate clipboard by passing a string selector -->
<script>
var clipboard = new ClipboardJS('.btn');
var clipboard = new Clipboard('.btn');
clipboard.on('success', function (e) {
clipboard.on('success', function(e) {
console.log(e);
});
});
clipboard.on('error', function (e) {
clipboard.on('error', function(e) {
console.log(e);
});
});
</script>
</body>
</body>
</html>

View File

@@ -1,11 +1,10 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<head>
<meta charset="UTF-8">
<title>function-target</title>
<meta name="viewport" content="width=device-width, initial-scale=1" />
</head>
<body>
</head>
<body>
<!-- 1. Define some markup -->
<button class="btn">Copy</button>
<div>hello</div>
@@ -15,19 +14,19 @@
<!-- 3. Instantiate clipboard -->
<script>
var clipboard = new ClipboardJS('.btn', {
target: function () {
return document.querySelector('div');
},
});
var clipboard = new Clipboard('.btn', {
target: function() {
return document.querySelector('div');
}
});
clipboard.on('success', function (e) {
clipboard.on('success', function(e) {
console.log(e);
});
});
clipboard.on('error', function (e) {
clipboard.on('error', function(e) {
console.log(e);
});
});
</script>
</body>
</body>
</html>

View File

@@ -1,11 +1,10 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<head>
<meta charset="UTF-8">
<title>function-text</title>
<meta name="viewport" content="width=device-width, initial-scale=1" />
</head>
<body>
</head>
<body>
<!-- 1. Define some markup -->
<button class="btn">Copy</button>
@@ -14,19 +13,19 @@
<!-- 3. Instantiate clipboard -->
<script>
var clipboard = new ClipboardJS('.btn', {
text: function () {
return 'to be or not to be';
},
});
var clipboard = new Clipboard('.btn', {
text: function() {
return 'to be or not to be';
}
});
clipboard.on('success', function (e) {
clipboard.on('success', function(e) {
console.log(e);
});
});
clipboard.on('error', function (e) {
clipboard.on('error', function(e) {
console.log(e);
});
});
</script>
</body>
</body>
</html>

View File

@@ -1,35 +1,28 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<head>
<meta charset="UTF-8">
<title>target-div</title>
<meta name="viewport" content="width=device-width, initial-scale=1" />
</head>
<body>
</head>
<body>
<!-- 1. Define some markup -->
<div>hello</div>
<button
class="btn"
data-clipboard-action="copy"
data-clipboard-target="div"
>
Copy
</button>
<button class="btn" data-clipboard-action="copy" data-clipboard-target="div">Copy</button>
<!-- 2. Include library -->
<script src="../dist/clipboard.min.js"></script>
<!-- 3. Instantiate clipboard -->
<script>
var clipboard = new ClipboardJS('.btn');
var clipboard = new Clipboard('.btn');
clipboard.on('success', function (e) {
clipboard.on('success', function(e) {
console.log(e);
});
});
clipboard.on('error', function (e) {
clipboard.on('error', function(e) {
console.log(e);
});
});
</script>
</body>
</body>
</html>

View File

@@ -1,35 +1,28 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<head>
<meta charset="UTF-8">
<title>target-input</title>
<meta name="viewport" content="width=device-width, initial-scale=1" />
</head>
<body>
</head>
<body>
<!-- 1. Define some markup -->
<input id="foo" type="text" value="hello" />
<button
class="btn"
data-clipboard-action="copy"
data-clipboard-target="#foo"
>
Copy
</button>
<input id="foo" type="text" value="hello">
<button class="btn" data-clipboard-action="copy" data-clipboard-target="#foo">Copy</button>
<!-- 2. Include library -->
<script src="../dist/clipboard.min.js"></script>
<!-- 3. Instantiate clipboard -->
<script>
var clipboard = new ClipboardJS('.btn');
var clipboard = new Clipboard('.btn');
clipboard.on('success', function (e) {
clipboard.on('success', function(e) {
console.log(e);
});
});
clipboard.on('error', function (e) {
clipboard.on('error', function(e) {
console.log(e);
});
});
</script>
</body>
</body>
</html>

View File

@@ -1,28 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>target-programmatic-copy</title>
<meta name="viewport" content="width=device-width, initial-scale=1" />
</head>
<body>
<!-- 1. Define some markup -->
<textarea id="bar">hello</textarea>
<button id="btn">
Copy
</button>
<!-- 2. Include library -->
<script src="../dist/clipboard.min.js"></script>
<!-- 3. Instantiate clipboard -->
<script>
var btn = document.querySelector('#btn');
btn.addEventListener('click', () => {
const textCopied = ClipboardJS.copy(document.querySelector('#bar'));
console.log('copied!', textCopied);
})
</script>
</body>
</html>

View File

@@ -1,28 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>target-programmatic-cut</title>
<meta name="viewport" content="width=device-width, initial-scale=1" />
</head>
<body>
<!-- 1. Define some markup -->
<textarea id="bar">hello</textarea>
<button id="btn">
Cut
</button>
<!-- 2. Include library -->
<script src="../dist/clipboard.min.js"></script>
<!-- 3. Instantiate clipboard -->
<script>
var btn = document.querySelector('#btn');
btn.addEventListener('click', () => {
const textCut = ClipboardJS.cut(document.querySelector('#bar'));
console.log('cut!', textCut);
})
</script>
</body>
</html>

View File

@@ -1,35 +1,28 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<head>
<meta charset="UTF-8">
<title>target-textarea</title>
<meta name="viewport" content="width=device-width, initial-scale=1" />
</head>
<body>
</head>
<body>
<!-- 1. Define some markup -->
<textarea id="bar">hello</textarea>
<button
class="btn"
data-clipboard-action="cut"
data-clipboard-target="#bar"
>
Cut
</button>
<button class="btn" data-clipboard-action="cut" data-clipboard-target="#bar">Cut</button>
<!-- 2. Include library -->
<script src="../dist/clipboard.min.js"></script>
<!-- 3. Instantiate clipboard -->
<script>
var clipboard = new ClipboardJS('.btn');
var clipboard = new Clipboard('.btn');
clipboard.on('success', function (e) {
clipboard.on('success', function(e) {
console.log(e);
});
});
clipboard.on('error', function (e) {
clipboard.on('error', function(e) {
console.log(e);
});
});
</script>
</body>
</body>
</html>

View File

@@ -1,27 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>text-programmatic-copy</title>
<meta name="viewport" content="width=device-width, initial-scale=1" />
</head>
<body>
<!-- 1. Define some markup -->
<button id="btn">
Copy
</button>
<!-- 2. Include library -->
<script src="../dist/clipboard.min.js"></script>
<!-- 3. Instantiate clipboard -->
<script>
var btn = document.querySelector('#btn');
btn.addEventListener('click', () => {
const textCopied = ClipboardJS.copy('123');
console.log('copied!', textCopied);
})
</script>
</body>
</html>

1063
dist/clipboard.js vendored

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

View File

@@ -1,36 +1,27 @@
var webpackConfig = require('./webpack.config.js');
module.exports = function(karma) {
karma.set({
plugins: ['karma-browserify', 'karma-chai', 'karma-sinon', 'karma-mocha', 'karma-phantomjs-launcher'],
module.exports = function (karma) {
karma.set({
plugins: [
'karma-webpack',
'karma-chai',
'karma-sinon',
'karma-mocha',
'karma-chrome-launcher',
],
frameworks: ['browserify', 'chai', 'sinon', 'mocha'],
frameworks: ['chai', 'sinon', 'mocha', 'webpack'],
files: [
'src/**/*.js',
'test/**/*.js',
'./node_modules/phantomjs-polyfill/bind-polyfill.js'
],
files: [
{ pattern: 'src/**/*.js', watched: false },
{ pattern: 'test/**/*.js', watched: false },
],
exclude: ['test/module-systems.js'],
preprocessors: {
'src/**/*.js': ['webpack'],
'test/**/*.js': ['webpack'],
},
preprocessors: {
'src/**/*.js' : ['browserify'],
'test/**/*.js': ['browserify']
},
webpack: {
module: webpackConfig.module,
plugins: webpackConfig.plugins,
},
browserify: {
debug: true,
transform: ['babelify']
},
webpackMiddleware: {
stats: 'errors-only',
},
browsers: ['ChromeHeadless'],
});
};
browsers: ['PhantomJS']
});
}

20143
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,12 +1,12 @@
// Package metadata for Meteor.js.
Package.describe({
name: 'zenorocha:clipboard',
summary: 'Modern copy to clipboard. No Flash. Just 3kb.',
version: '2.0.9',
git: 'https://github.com/zenorocha/clipboard.js',
name: "zenorocha:clipboard",
summary: "Modern copy to clipboard. No Flash. Just 2kb.",
version: "1.5.1",
git: "https://github.com/zenorocha/clipboard.js"
});
Package.onUse(function (api) {
api.addFiles('dist/clipboard.js', 'client');
Package.onUse(function(api) {
api.addFiles("dist/clipboard.min.js", "client");
});

View File

@@ -1,63 +1,42 @@
{
"name": "clipboard",
"version": "2.0.9",
"version": "1.5.3",
"description": "Modern copy to clipboard. No Flash. Just 2kb",
"homepage": "https://clipboardjs.com",
"repository": "zenorocha/clipboard.js",
"license": "MIT",
"main": "dist/clipboard.js",
"types": "src/clipboard.d.ts",
"main": "lib/clipboard.js",
"keywords": [
"clipboard",
"copy",
"cut"
],
"dependencies": {
"good-listener": "^1.2.2",
"select": "^1.1.2",
"tiny-emitter": "^2.0.0"
"good-listener": "^1.1.2",
"select": "^1.0.4",
"tiny-emitter": "^1.0.0"
},
"devDependencies": {
"@babel/core": "^7.12.10",
"@babel/preset-env": "^7.12.11",
"babel-loader": "^8.2.2",
"chai": "^4.2.0",
"cross-env": "^7.0.3",
"eslint": "^7.20.0",
"eslint-config-airbnb-base": "^14.2.1",
"eslint-config-prettier": "^7.2.0",
"eslint-plugin-import": "^2.22.1",
"eslint-plugin-prettier": "^3.3.1",
"husky": "^5.0.9",
"karma": "^6.0.0",
"babel": "^5.8.29",
"babelify": "^6.3.0",
"browserify": "^11.2.0",
"karma": "^0.13.10",
"karma-browserify": "^4.4.0",
"karma-chai": "^0.1.0",
"karma-chrome-launcher": "^3.1.0",
"karma-mocha": "^2.0.1",
"karma-mocha": "^0.2.0",
"karma-phantomjs-launcher": "^0.2.1",
"karma-sinon": "^1.0.4",
"karma-webpack": "^5.0.0-alpha.5",
"lint-staged": "^10.5.3",
"mocha": "^8.2.1",
"prettier": "2.2.1",
"sinon": "^9.2.3",
"tsd": "^0.7.2",
"uglifyjs-webpack-plugin": "^2.2.0",
"webpack": "^5.15.0",
"webpack-cli": "^4.4.0"
"mocha": "^2.3.3",
"phantomjs-polyfill": "0.0.1",
"uglify-js": "^2.4.24",
"watchify": "^3.4.0",
"bannerify": "Vekat/bannerify#feature-option"
},
"scripts": {
"test:types": "tsd",
"build": "npm run build-debug && npm run build-min",
"build-debug": "webpack",
"build-min": "cross-env NODE_ENV=production webpack",
"build-watch": "webpack --watch",
"build-debug": "browserify src/clipboard.js -s Clipboard -t [babelify --loose all] -p [bannerify --file .banner ] -o dist/clipboard.js",
"build-min": "uglifyjs dist/clipboard.js --comments '/!/' -m screw_ie8=true -c screw_ie8=true,unused=false -o dist/clipboard.min.js",
"build-watch": "watchify src/clipboard.js -s Clipboard -t [babelify --loose all] -o dist/clipboard.js -v",
"test": "karma start --single-run",
"prepublish": "npm run build",
"lint": "eslint --ext .js src/"
},
"lint-staged": {
"*.{js,css,md}": [
"prettier --write",
"eslint --fix"
]
"prepublish": "babel src --out-dir lib --loose all"
}
}

111
readme.md
View File

@@ -1,11 +1,11 @@
# clipboard.js
![Build Status](https://github.com/zenorocha/clipboard.js/workflows/build/badge.svg)
[![Build Status](http://img.shields.io/travis/zenorocha/clipboard.js/master.svg?style=flat)](https://travis-ci.org/zenorocha/clipboard.js)
![Killing Flash](https://img.shields.io/badge/killing-flash-brightgreen.svg?style=flat)
> Modern copy to clipboard. No Flash. Just 3kb gzipped.
> Modern copy to clipboard. No Flash. Just 2kb
<a href="https://clipboardjs.com/"><img width="728" src="https://cloud.githubusercontent.com/assets/398893/16165747/a0f6fc46-349a-11e6-8c9b-c5fd58d9099c.png" alt="Demo"></a>
<a href="http://clipboardjs.com/"><img width="728" src="https://cloud.githubusercontent.com/assets/398893/9983535/5ab0a950-5fb4-11e5-9602-e73c0b661883.jpg" alt="Demo"></a>
## Why
@@ -21,7 +21,13 @@ You can get it on npm.
npm install clipboard --save
```
Or if you're not into package management, just [download a ZIP](https://github.com/zenorocha/clipboard.js/archive/master.zip) file.
Or bower, too.
```
bower install clipboard --save
```
If you're not into package management, just [download a ZIP](https://github.com/zenorocha/clipboard.js/archive/master.zip) file.
## Setup
@@ -34,12 +40,12 @@ 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 Clipboard('.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.
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).
For this reason we use [event delegation](http://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).
# Usage
@@ -51,15 +57,15 @@ A pretty common use case is to copy content from another element. You can do tha
The value you include on this attribute needs to match another's element selector.
<a href="https://clipboardjs.com/#example-target"><img width="473" alt="example-2" src="https://cloud.githubusercontent.com/assets/398893/9983467/a4946aaa-5fb1-11e5-9780-f09fcd7ca6c8.png"></a>
<a href="http://clipboardjs.com/#example-target"><img width="473" alt="example-2" src="https://cloud.githubusercontent.com/assets/398893/9983467/a4946aaa-5fb1-11e5-9780-f09fcd7ca6c8.png"></a>
```html
<!-- Target -->
<input id="foo" value="https://github.com/zenorocha/clipboard.js.git" />
<input id="foo" value="https://github.com/zenorocha/clipboard.js.git">
<!-- Trigger -->
<button class="btn" data-clipboard-target="#foo">
<img src="assets/clippy.svg" alt="Copy to clipboard" />
<img src="assets/clippy.svg" alt="Copy to clipboard">
</button>
```
@@ -69,7 +75,7 @@ Additionally, you can define a `data-clipboard-action` attribute to specify if y
If you omit this attribute, `copy` will be used by default.
<a href="https://clipboardjs.com/#example-action"><img width="473" alt="example-3" src="https://cloud.githubusercontent.com/assets/398893/10000358/7df57b9c-6050-11e5-9cd1-fbc51d2fd0a7.png"></a>
<a href="http://clipboardjs.com/#example-action"><img width="473" alt="example-3" src="https://cloud.githubusercontent.com/assets/398893/10000358/7df57b9c-6050-11e5-9cd1-fbc51d2fd0a7.png"></a>
```html
<!-- Target -->
@@ -77,7 +83,7 @@ If you omit this attribute, `copy` will be used by default.
<!-- Trigger -->
<button class="btn" data-clipboard-action="cut" data-clipboard-target="#bar">
Cut to clipboard
Cut to clipboard
</button>
```
@@ -87,15 +93,12 @@ As you may expect, the `cut` action only works on `<input>` or `<textarea>` elem
Truth is, you don't even need another element to copy its content from. You can just include a `data-clipboard-text` attribute in your trigger element.
<a href="https://clipboardjs.com/#example-text"><img width="147" alt="example-1" src="https://cloud.githubusercontent.com/assets/398893/10000347/6e16cf8c-6050-11e5-9883-1c5681f9ec45.png"></a>
<a href="http://clipboardjs.com/#example-text"><img width="147" alt="example-1" src="https://cloud.githubusercontent.com/assets/398893/10000347/6e16cf8c-6050-11e5-9883-1c5681f9ec45.png"></a>
```html
<!-- Trigger -->
<button
class="btn"
data-clipboard-text="Just because you can doesn't mean you should — clipboard.js"
>
Copy to clipboard
<button class="btn" data-clipboard-text="Just because you can doesn't mean you should — clipboard.js">
Copy to clipboard
</button>
```
@@ -106,29 +109,23 @@ 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 Clipboard('.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);
});
```
For a live demonstration, go to this [site](https://clipboardjs.com/) and open your console.
## Tooltips
Each application has different design needs, that's why clipboard.js does not include any CSS or built-in tooltip solution.
The tooltips you see on the [demo site](https://clipboardjs.com/) were built using [GitHub's Primer](https://primer.style/css/components/tooltips). You may want to check that out if you're looking for a similar look and feel.
For a live demonstration, open this [site](http://clipboardjs.com/) and just your console :)
## Advanced Options
@@ -137,56 +134,44 @@ 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 Clipboard('.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 Clipboard('.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.
Also, with 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
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 Clipboard('.btn');
clipboard.destroy();
```
## Browser Support
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.
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 second one is supported in the following browsers.
| <img src="https://clipboardjs.com/assets/images/chrome.png" width="48px" height="48px" alt="Chrome logo"> | <img src="https://clipboardjs.com/assets/images/edge.png" width="48px" height="48px" alt="Edge logo"> | <img src="https://clipboardjs.com/assets/images/firefox.png" width="48px" height="48px" alt="Firefox logo"> | <img src="https://clipboardjs.com/assets/images/ie.png" width="48px" height="48px" alt="Internet Explorer logo"> | <img src="https://clipboardjs.com/assets/images/opera.png" width="48px" height="48px" alt="Opera logo"> | <img src="https://clipboardjs.com/assets/images/safari.png" width="48px" height="48px" alt="Safari logo"> |
| :-------------------------------------------------------------------------------------------------------: | :---------------------------------------------------------------------------------------------------: | :---------------------------------------------------------------------------------------------------------: | :--------------------------------------------------------------------------------------------------------------: | :-----------------------------------------------------------------------------------------------------: | :-------------------------------------------------------------------------------------------------------: |
| 42+ ✔ | 12+ ✔ | 41+ ✔ | 9+ ✔ | 29+ ✔ | 10+ ✔ |
| <img src="http://clipboardjs.com/assets/images/chrome.png" width="48px" height="48px" alt="Chrome logo"> | <img src="http://clipboardjs.com/assets/images/firefox.png" width="48px" height="48px" alt="Firefox logo"> | <img src="http://clipboardjs.com/assets/images/ie.png" width="48px" height="48px" alt="Internet Explorer logo"> | <img src="http://clipboardjs.com/assets/images/opera.png" width="48px" height="48px" alt="Opera logo"> | <img src="http://clipboardjs.com/assets/images/safari.png" width="48px" height="48px" alt="Safari logo"> |
|:---:|:---:|:---:|:---:|:---:|
| 42+ ✔ | 41+ ✔ | 9+ ✔ | 29+ ✔ | Nope ✘ |
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.
Although copy/cut operations with [execCommand](https://developer.mozilla.org/en-US/docs/Web/API/Document/execCommand) aren't supported on Safari yet (including mobile), it gracefully degrades because [Selection](https://developer.mozilla.org/en-US/docs/Web/API/Selection) is supported.
You can also check if clipboard.js is supported or not by running `ClipboardJS.isSupported()`, that way you can hide copy/cut buttons from the UI.
That means you can 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.
## 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._
Install for [Chrome](https://chrome.google.com/webstore/detail/codecopy/fkbfebkcoelajmhanocgppanfoojcdmg) and [Firefox](https://addons.mozilla.org/en-US/firefox/addon/codecopy/).
For a live demonstration, open this [site](http://clipboardjs.com) on Safari.
## License
[MIT License](https://zenorocha.mit-license.org/) © Zeno Rocha
[MIT License](http://zenorocha.mit-license.org/) © Zeno Rocha

View File

@@ -1,29 +0,0 @@
import select from 'select';
import command from '../common/command';
import createFakeElement from '../common/create-fake-element';
/**
* Copy action wrapper.
* @param {String|HTMLElement} target
* @param {Object} options
* @return {String}
*/
const ClipboardActionCopy = (
target,
options = { container: document.body }
) => {
let selectedText = '';
if (typeof target === 'string') {
const fakeElement = createFakeElement(target);
options.container.appendChild(fakeElement);
selectedText = select(fakeElement);
command('copy');
fakeElement.remove();
} else {
selectedText = select(target);
command('copy');
}
return selectedText;
};
export default ClipboardActionCopy;

View File

@@ -1,15 +0,0 @@
import select from 'select';
import command from '../common/command';
/**
* Cut action wrapper.
* @param {String|HTMLElement} target
* @return {String}
*/
const ClipboardActionCut = (target) => {
const selectedText = select(target);
command('cut');
return selectedText;
};
export default ClipboardActionCut;

View File

@@ -1,53 +0,0 @@
import ClipboardActionCut from './cut';
import ClipboardActionCopy from './copy';
/**
* Inner function which performs selection from either `text` or `target`
* properties and then executes copy or cut operations.
* @param {Object} options
*/
const ClipboardActionDefault = (options = {}) => {
// Defines base properties passed from constructor.
const { action = 'copy', container, target, text } = options;
// Sets the `action` to be performed which can be either 'copy' or 'cut'.
if (action !== 'copy' && action !== 'cut') {
throw new Error('Invalid "action" value, use either "copy" or "cut"');
}
// Sets the `target` property using an element that will be have its content copied.
if (target !== undefined) {
if (target && typeof target === 'object' && target.nodeType === 1) {
if (action === 'copy' && target.hasAttribute('disabled')) {
throw new Error(
'Invalid "target" attribute. Please use "readonly" instead of "disabled" attribute'
);
}
if (
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'
);
}
} else {
throw new Error('Invalid "target" value, use a valid Element');
}
}
// Define selection strategy based on `text` property.
if (text) {
return ClipboardActionCopy(text, { container });
}
// Defines which selection strategy based on `target` property.
if (target) {
return action === 'cut'
? ClipboardActionCut(target)
: ClipboardActionCopy(target, { container });
}
};
export default ClipboardActionDefault;

196
src/clipboard-action.js Normal file
View File

@@ -0,0 +1,196 @@
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.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.target) {
throw new Error('Multiple attributes declared, use either "target" or "text"');
}
else if (this.text) {
this.selectFake();
}
else if (this.target) {
this.selectTarget();
}
else {
throw new Error('Missing required attributes, use either "target" or "text"');
}
}
/**
* Creates a fake textarea element, sets its value from `text` property,
* and makes a selection on it.
*/
selectFake() {
this.removeFake();
this.fakeHandler = document.body.addEventListener('click', () => this.removeFake());
this.fakeElem = document.createElement('textarea');
this.fakeElem.style.position = 'absolute';
this.fakeElem.style.left = '-9999px';
this.fakeElem.style.top = (window.pageYOffset || document.documentElement.scrollTop) + 'px';
this.fakeElem.setAttribute('readonly', '');
this.fakeElem.value = this.text;
document.body.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) {
document.body.removeEventListener('click');
this.fakeHandler = null;
}
if (this.fakeElem) {
document.body.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) {
if (succeeded) {
this.emitter.emit('success', {
action: this.action,
text: this.selectedText,
trigger: this.trigger,
clearSelection: this.clearSelection.bind(this)
});
}
else {
this.emitter.emit('error', {
action: this.action,
trigger: this.trigger,
clearSelection: this.clearSelection.bind(this)
});
}
}
/**
* Removes current selection and focus from `target` element.
*/
clearSelection() {
if (this.target) {
this.target.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) {
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;

91
src/clipboard.d.ts vendored
View File

@@ -1,91 +0,0 @@
/// <reference lib="dom"/>
type Action = 'cut' | 'copy';
type Response = 'success' | 'error';
type CopyActionOptions = {
container?: Element;
};
/**
* Base class which takes one or more elements, adds event listeners to them,
* and instantiates a new `ClipboardAction` on each click.
*/
declare class ClipboardJS {
constructor(
selector: string | Element | NodeListOf<Element>,
options?: ClipboardJS.Options
);
/**
* Subscribes to events that indicate the result of a copy/cut operation.
* @param type Event type ('success' or 'error').
* @param handler Callback function.
*/
on(type: Response, handler: (e: ClipboardJS.Event) => void): this;
on(type: string, handler: (...args: any[]) => void): this;
/**
* Clears all event bindings.
*/
destroy(): void;
/**
* Checks if clipboard.js is supported
*/
static isSupported(): boolean;
/**
* Fires a copy action
*/
static copy(target: string | Element, options: CopyActionOptions): string;
/**
* Fires a cut action
*/
static cut(target: string | Element): string;
}
declare namespace ClipboardJS {
interface Options {
/**
* Overwrites default command ('cut' or 'copy').
* @param elem Current element
*/
action?(elem: Element): Action;
/**
* Overwrites default target input element.
* @param elem Current element
* @returns <input> element to use.
*/
target?(elem: Element): Element;
/**
* Returns the explicit text to copy.
* @param elem Current element
* @returns Text to be copied.
*/
text?(elem: Element): string;
/**
* 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.
*/
container?: Element;
}
interface Event {
action: string;
text: string;
trigger: Element;
clearSelection(): void;
}
}
export = ClipboardJS;
export as namespace ClipboardJS;

View File

@@ -1,8 +1,101 @@
import ClipboardAction from './clipboard-action';
import Emitter from 'tiny-emitter';
import listen from 'good-listener';
import ClipboardActionDefault from './actions/default';
import ClipboardActionCut from './actions/cut';
import ClipboardActionCopy from './actions/copy';
/**
* 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();
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;
}
/**
* 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) {
if (this.clipboardAction) {
this.clipboardAction = null;
}
this.clipboardAction = new ClipboardAction({
action : this.action(e.target),
target : this.target(e.target),
text : this.text(e.target),
trigger : e.target,
emitter : this
});
}
/**
* Default `action` lookup function.
* @param {Element} trigger
*/
defaultAction(trigger) {
return getAttributeValue('action', trigger);
}
/**
* Default `target` lookup function.
* @param {Element} trigger
*/
defaultTarget(trigger) {
let selector = getAttributeValue('target', trigger);
if (selector) {
return document.querySelector(selector);
}
}
/**
* 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.
@@ -10,156 +103,13 @@ import ClipboardActionCopy from './actions/copy';
* @param {Element} element
*/
function getAttributeValue(suffix, element) {
const attribute = `data-clipboard-${suffix}`;
let attribute = `data-clipboard-${suffix}`;
if (!element.hasAttribute(attribute)) {
return;
}
return element.getAttribute(attribute);
}
/**
* 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();
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;
const selectedText = ClipboardActionDefault({
action: this.action(trigger),
container: this.container,
target: this.target(trigger),
text: this.text(trigger),
});
// Fires an event based on the copy operation result.
this.emit(selectedText ? 'success' : 'error', {
action: this.action,
text: selectedText,
trigger,
clearSelection() {
if (trigger) {
trigger.focus();
}
document.activeElement.blur();
window.getSelection().removeAllRanges();
},
});
}
/**
* 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);
if (!element.hasAttribute(attribute)) {
return;
}
}
/**
* Allow fire programmatically a copy action
* @param {String|HTMLElement} target
* @param {Object} options
* @returns Text copied.
*/
static copy(target, options = { container: document.body }) {
return ClipboardActionCopy(target, options);
}
/**
* Allow fire programmatically a cut action
* @param {String|HTMLElement} target
* @returns Text cutted.
*/
static cut(target) {
return ClipboardActionCut(target);
}
/**
* 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();
}
return element.getAttribute(attribute);
}
export default Clipboard;

View File

@@ -1,4 +0,0 @@
import { expectType } from 'tsd';
import * as Clipboard from './clipboard';
expectType<Clipboard>(new Clipboard('.btn'));

View File

@@ -1,12 +0,0 @@
/**
* Executes a given operation type.
* @param {String} type
* @return {Boolean}
*/
export default function command(type) {
try {
return document.execCommand(type);
} catch (err) {
return false;
}
}

View File

@@ -1,26 +0,0 @@
/**
* Creates a fake textarea element with a value.
* @param {String} value
* @return {HTMLElement}
*/
export default function createFakeElement(value) {
const isRTL = document.documentElement.getAttribute('dir') === 'rtl';
const fakeElement = document.createElement('textarea');
// Prevent zooming on iOS
fakeElement.style.fontSize = '12pt';
// Reset box model
fakeElement.style.border = '0';
fakeElement.style.padding = '0';
fakeElement.style.margin = '0';
// Move element out of screen horizontally
fakeElement.style.position = 'absolute';
fakeElement.style[isRTL ? 'right' : 'left'] = '-9999px';
// Move element to the same position vertically
let yPosition = window.pageYOffset || document.documentElement.scrollTop;
fakeElement.style.top = `${yPosition}px`;
fakeElement.setAttribute('readonly', '');
fakeElement.value = value;
return fakeElement;
}

View File

@@ -1,55 +0,0 @@
import ClipboardActionCopy from '../../src/actions/copy';
describe('ClipboardActionCopy', () => {
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('#selectText', () => {
it('should select its value based on input target', () => {
const selectedText = ClipboardActionCopy(
document.querySelector('#input'),
{
container: document.body,
}
);
assert.equal(selectedText, document.querySelector('#input').value);
});
it('should select its value based on element target', () => {
const selectedText = ClipboardActionCopy(
document.querySelector('#paragraph'),
{
container: document.body,
}
);
assert.equal(
selectedText,
document.querySelector('#paragraph').textContent
);
});
it('should select its value based on text', () => {
const text = 'abc';
const selectedText = ClipboardActionCopy(text, {
container: document.body,
});
assert.equal(selectedText, text);
});
});
});

View File

@@ -1,32 +0,0 @@
import ClipboardActionCut from '../../src/actions/cut';
describe('ClipboardActionCut', () => {
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('#selectText', () => {
it('should select its value', () => {
const selectedText = ClipboardActionCut(
document.querySelector('#input'),
{
container: document.body,
}
);
assert.equal(selectedText, document.querySelector('#input').value);
});
});
});

View File

@@ -1,80 +0,0 @@
import ClipboardActionDefault from '../../src/actions/default';
describe('ClipboardActionDefault', () => {
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', () => {
const selectedText = ClipboardActionDefault({
container: document.body,
text: 'foo',
});
assert.equal(selectedText, 'foo');
});
});
describe('#set action', () => {
it('should throw an error since "action" is invalid', (done) => {
try {
let clip = ClipboardActionDefault({
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 {
let clip = ClipboardActionDefault({
target: document.querySelector('#foo'),
});
} catch (e) {
assert.equal(e.message, 'Invalid "target" value, use a valid Element');
done();
}
});
});
describe('#selectedText', () => {
it('should select text from editable element', () => {
const selectedText = ClipboardActionDefault({
container: document.body,
target: document.querySelector('#input'),
});
assert.equal(selectedText, 'abc');
});
it('should select text from non-editable element', () => {
const selectedText = ClipboardActionDefault({
container: document.body,
target: document.querySelector('#paragraph'),
});
assert.equal(selectedText, 'abc');
});
});
});

242
test/clipboard-action.js Normal file
View File

@@ -0,0 +1,242 @@
import ClipboardAction from '../src/clipboard-action';
import Emitter from 'tiny-emitter';
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(),
text: 'foo'
});
assert.property(clip, 'action');
assert.property(clip, 'emitter');
assert.property(clip, 'target');
assert.property(clip, 'text');
assert.property(clip, 'trigger');
assert.property(clip, 'selectedText');
});
});
describe('#initSelection', () => {
it('should throw an error since both "text" and "target" were passed', done => {
try {
new ClipboardAction({
text: 'foo',
target: document.querySelector('#input')
});
}
catch(e) {
assert.equal(e.message, 'Multiple attributes declared, use either "target" or "text"');
done();
}
});
it('should throw an error since neither "text" nor "target" were passed', done => {
try {
new ClipboardAction();
}
catch(e) {
assert.equal(e.message, 'Missing required attributes, use either "target" or "text"');
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(),
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(),
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(),
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(),
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: 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: emitter,
target: document.querySelector('#input')
});
});
});
describe('#handleResult', () => {
it('should fire a success event with certain properties', done => {
let clip = new ClipboardAction({
emitter: new Emitter(),
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(),
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(),
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(),
text: 'blah'
});
clip.selectFake();
clip.destroy();
assert.equal(clip.fakeElem, null);
});
});
});

View File

@@ -1,191 +1,93 @@
import Clipboard from '../src/clipboard';
import ClipboardAction from '../src/clipboard-action';
import listen from 'good-listener';
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.fn = () => {};
global.button = document.createElement('button');
global.button.setAttribute('class', 'btn');
global.button.setAttribute('data-clipboard-text', 'foo');
document.body.appendChild(global.button);
global.event = {
target: global.button
};
});
it('should set action as a function', () => {
let clipboard = new Clipboard('.btn', {
action: global.fn,
});
assert.equal(global.fn, clipboard.action);
after(() => {
document.body.innerHTML = '';
});
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);
});
});
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 init when called', (done) => {
let clipboard = new Clipboard('.btn');
clipboard.on('success', () => {
done();
});
clipboard.onClick(global.event);
});
it("should use an event's currentTarget when not equal to target", (done) => {
let clipboard = new Clipboard('.btn');
let bubbledEvent = {
target: global.span,
currentTarget: global.button,
};
clipboard.on('success', () => {
done();
});
clipboard.onClick(bubbledEvent);
});
it('should throw an exception when target is invalid', (done) => {
try {
const clipboard = new Clipboard('.btn', {
target() {
return null;
},
describe('#resolveOptions', () => {
before(() => {
global.fn = function() {};
});
clipboard.onClick(global.event);
} catch (e) {
assert.equal(e.message, 'Invalid "target" value, use a valid Element');
done();
}
});
});
it('should set action as a function', () => {
let clipboard = new Clipboard('.btn', {
action: global.fn
});
describe('#static isSupported', () => {
it('should return the support of the given action', () => {
assert.equal(Clipboard.isSupported('copy'), true);
assert.equal(Clipboard.isSupported('cut'), true);
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 return the support of the cut and copy actions', () => {
assert.equal(Clipboard.isSupported(), true);
});
});
describe('#static copy', () => {
it('should copy in an programatic way based on text', () => {
assert.equal(Clipboard.copy('lorem'), 'lorem');
describe('#listenClick', () => {
it('should add a click event listener to the passed selector', () => {
let clipboard = new Clipboard('.btn');
assert.isObject(clipboard.listener);
});
});
it('should copy in an programatic way based on target', () => {
assert.equal(Clipboard.copy(document.querySelector('span')), 'bar');
describe('#onClick', () => {
it('should create a new instance of ClipboardAction', () => {
let clipboard = new Clipboard('.btn');
clipboard.onClick(global.event);
assert.instanceOf(clipboard.clipboardAction, ClipboardAction);
});
it('should throws exception target', done => {
try {
var clipboard = new Clipboard('.btn', {
target: function() {
return null;
}
});
clipboard.onClick(global.event);
}
catch(e) {
assert.equal(e.message, 'Invalid "target" value, use a valid Element');
done();
}
});
});
});
describe('#static cut', () => {
it('should cut in an programatic way based on text', () => {
assert.equal(Clipboard.cut(document.querySelector('span')), 'bar');
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);
});
});
});
describe('#destroy', () => {
it('should destroy an existing instance of ClipboardActionDefault', () => {
let clipboard = new Clipboard('.btn');
clipboard.onClick(global.event);
clipboard.destroy();
assert.equal(clipboard.clipboardAction, null);
});
});
describe('#events', () => {
it('should fire a success event with certain properties', (done) => {
let clipboard = new Clipboard('.btn');
clipboard.on('success', (e) => {
assert.property(e, 'action');
assert.property(e, 'text');
assert.property(e, 'trigger');
assert.property(e, 'clearSelection');
done();
});
clipboard.onClick(global.event);
});
});
describe('#clearSelection', () => {
it('should remove focus from target and text selection', (done) => {
let clipboard = new Clipboard('.btn');
clipboard.on('success', (e) => {
let selectedElem = document.activeElement;
let selectedText = window.getSelection().toString();
e.clearSelection();
assert.equal(selectedElem, document.body);
assert.equal(selectedText, '');
done();
});
clipboard.onClick(global.event);
});
});
});

View File

@@ -1,49 +0,0 @@
import select from 'select';
import command from '../../src/common/command';
describe('#command', () => {
before(() => {
global.stub = sinon.stub(document, 'execCommand');
global.input = document.createElement('input');
global.input.setAttribute('id', 'input');
global.input.setAttribute('value', 'abc');
document.body.appendChild(global.input);
});
after(() => {
global.stub.restore();
document.body.innerHTML = '';
});
it('should execute cut', (done) => {
global.stub.returns(true);
select(document.querySelector('#input'));
assert.isTrue(command('cut'));
done();
});
it('should execute copy', (done) => {
global.stub.returns(true);
select(document.querySelector('#input'));
assert.isTrue(command('copy'));
done();
});
it('should not execute copy', (done) => {
global.stub.returns(false);
select(document.querySelector('#input'));
assert.isFalse(command('copy'));
done();
});
it('should not execute cut', (done) => {
global.stub.returns(false);
select(document.querySelector('#input'));
assert.isFalse(command('cut'));
done();
});
});

View File

@@ -1,13 +0,0 @@
import createFakeElement from '../../src/common/create-fake-element';
describe('createFakeElement', () => {
it('should define a fake element and set the position right style property', (done) => {
// Set document direction
document.documentElement.setAttribute('dir', 'rtl');
const el = createFakeElement(document.body);
assert.equal(el.style.right, '-9999px');
done();
});
});

View File

@@ -1,46 +0,0 @@
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 banner = `clipboard.js v${pkg.version}
https://clipboardjs.com/
Licensed MIT © Zeno Rocha`;
module.exports = {
entry: './src/clipboard.js',
mode: 'production',
target: ['web', 'es5'],
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 })],
};