Update v1.0.6

This commit is contained in:
Bhanu Slathia
2016-02-16 23:22:09 +05:30
parent 62d04a0372
commit c710c20b9e
7620 changed files with 244752 additions and 1070312 deletions

View File

@@ -0,0 +1,4 @@
node_modules
coverage
npm-debug.log
bin/*

View File

@@ -0,0 +1,62 @@
{
/* Enforcing options */
"bitwise": true,
"camelcase": true,
"curly": true,
"eqeqeq": true,
"es3": true,
"es5": false,
"forin": true,
"freeze": true,
"immed": true,
"indent": 2,
"latedef": false, /* IDEAL: `true` */
"newcap": true,
"noarg": true,
"noempty": true,
"nonbsp": true,
"nonew": true,
"plusplus": false,
"quotmark": "double",
"undef": false, /* IDEAL: `true` */
"unused": false, /* IDEAL: `true` */
"strict": false, /* IDEAL: `true` */
"trailing": true,
"maxparams": 4,
"maxdepth": 5,
"maxstatements": 25,
"maxlen": false, /* IDEAL: 120? */
/* Relaxing options */
"asi": false,
"boss": false,
"debug": false,
"eqnull": true,
"esnext": false,
"evil": false,
"expr": false,
"funcscope": false,
"gcl": false,
"globalstrict": false,
"iterator": false,
"lastsemic": false,
"laxbreak": false,
"laxcomma": false,
"loopfunc": false,
"maxerr": 50,
"moz": false,
"multistr": false,
"notypeof": false,
"proto": false,
"scripturl": false,
"smarttabs": false,
"shadow": false,
"sub": false,
"supernew": false,
"validthis": false,
"noyield": false,
/* Environments */
"browser": true
}

View File

@@ -0,0 +1,3 @@
docs/
test/
.DS_Store

View File

@@ -0,0 +1,3 @@
docs
src
test

View File

@@ -0,0 +1,5 @@
language: node_js
node_js:
- "0.10"
before_script:
- npm install -g grunt-cli

View File

@@ -0,0 +1,29 @@
Contributing
==============
If you find an issue, submitting a pull request is always better than a bug report! Please fork and submit your code fixes.
If you want to build some new features, we have a [roadmap.md](docs/roadmap.md) of features we want. You can add features you want there, or just code the feature and send a pull request.
### Cloning
```sh
$ git clone https://github.com/zeroclipboard/zeroclipboard.git
$ cd zeroclipboard/
$ npm install -g grunt-cli
$ npm install
$ grunt
```
### Developing
```sh
$ npm install
$ grunt
```
### Testing
```sh
$ grunt test
```

View File

@@ -0,0 +1,332 @@
/*jshint -W106 */
/*jshint node:true, maxstatements: false, maxlen: false */
var os = require("os");
var path = require("path");
var loadGruntTasks = require("load-grunt-tasks");
module.exports = function(grunt) {
"use strict";
// Load necessary tasks
loadGruntTasks(grunt);
// Metadata
var pkg = grunt.file.readJSON("package.json");
// Make a temp dir for Flash compilation
var tmpDir = os.tmpdir ? os.tmpdir() : os.tmpDir();
var flashTmpDir = path.join(tmpDir, "zcflash");
// Shared configuration
var localPort = 7320; // "ZERO"
// Project configuration.
var config = {
// Task configuration
jshint: {
options: {
jshintrc: true
},
gruntfile: ["Gruntfile.js"],
component: ["index.js"],
js: ["src/js/**/*.js", "!src/js/start.js", "!src/js/end.js"],
test: ["test/**/*.js"],
dist: ["dist/*.js", "!dist/*.min.js"]
},
flexpmd: {
flash: {
src: [flashTmpDir]
}
},
clean: {
dist: ["ZeroClipboard.*", "dist/ZeroClipboard.*"],
flash: {
options: {
// Force is required when trying to clean outside of the project dir
force: true
},
src: [flashTmpDir]
},
meta: ["bower.json", "composer.json", "LICENSE"],
coveralls: ["tmp/", "coverage/"]
},
concat: {
options: {
stripBanners: false,
process: {
data: pkg
}
},
core: {
src: [
"src/meta/source-banner.tmpl",
"src/js/start.js",
"src/js/shared/state.js",
"src/js/shared/private.js",
"src/js/core/state.js",
"src/js/core/private.js",
"src/js/core/api.js",
"src/js/end.js"
],
dest: "dist/ZeroClipboard.Core.js"
},
client: {
src: [
"src/meta/source-banner.tmpl",
"src/js/start.js",
"src/js/shared/state.js",
"src/js/shared/private.js",
"src/js/core/state.js",
"src/js/core/private.js",
"src/js/core/api.js",
"src/js/client/state.js",
"src/js/client/private.js",
"src/js/client/api.js",
"src/js/end.js"
],
dest: "dist/ZeroClipboard.js"
},
flash: {
files: [
{
src: [
"src/meta/source-banner.tmpl",
"src/flash/ZeroClipboard.as"
],
dest: path.join(flashTmpDir, "ZeroClipboard.as")
},
{
src: [
"src/meta/source-banner.tmpl",
"src/flash/ClipboardInjector.as"
],
dest: path.join(flashTmpDir, "ClipboardInjector.as")
},
{
src: [
"src/meta/source-banner.tmpl",
"src/flash/JsProxy.as"
],
dest: path.join(flashTmpDir, "JsProxy.as")
},
{
src: [
"src/meta/source-banner.tmpl",
"src/flash/XssUtils.as"
],
dest: path.join(flashTmpDir, "XssUtils.as")
}
]
}
},
uglify: {
options: {
report: "min"
},
js: {
options: {
preserveComments: function(node, comment) {
return comment &&
comment.type === "comment2" &&
/^(!|\*|\*!)\r?\n/.test(comment.value);
},
beautify: {
beautify: true,
// `indent_level` requires jshint -W106
indent_level: 2
},
mangle: false,
compress: false
},
files: [
{
src: ["<%= concat.core.dest %>"],
dest: "<%= concat.core.dest %>"
},
{
src: ["<%= concat.client.dest %>"],
dest: "<%= concat.client.dest %>"
}
]
},
minjs: {
options: {
preserveComments: function(node, comment) {
return comment &&
comment.type === "comment2" &&
/^(!|\*!)\r?\n/.test(comment.value);
},
sourceMap: true,
sourceMapName: function(dest) {
return dest.replace(".min.js", ".min.map");
},
// Bundles the contents of "`src`" into the "`dest`.map" source map file. This way,
// consumers only need to host the "*.min.js" and "*.min.map" files rather than
// needing to host all three files: "*.js", "*.min.js", and "*.min.map".
sourceMapIncludeSources: true
},
files: [
{
src: ["<%= concat.core.dest %>"],
dest: "dist/ZeroClipboard.Core.min.js"
},
{
src: ["<%= concat.client.dest %>"],
dest: "dist/ZeroClipboard.min.js"
}
]
}
},
mxmlc: {
options: {
rawConfig: "-target-player=11.0.0 -static-link-runtime-shared-libraries=true"
},
swf: {
files: {
"dist/ZeroClipboard.swf": ["<%= concat.flash.files[0].dest %>"]
}
}
},
template: {
options: {
data: pkg
},
bower: {
files: {
"bower.json": ["src/meta/bower.json.tmpl"]
}
},
composer: {
files: {
"composer.json": ["src/meta/composer.json.tmpl"]
}
},
LICENSE: {
files: {
"LICENSE": ["src/meta/LICENSE.tmpl"]
}
}
},
chmod: {
options: {
mode: "444"
},
dist: ["dist/ZeroClipboard.*"],
meta: ["bower.json", "composer.json", "LICENSE"]
},
connect: {
server: {
options: {
port: localPort
}
}
},
qunit: {
file: [
"test/shared/private.tests.js.html",
"test/core/private.tests.js.html",
"test/core/api.tests.js.html",
"test/client/private.tests.js.html",
"test/client/api.tests.js.html",
"test/built/ZeroClipboard.Core.tests.js.html",
"test/built/ZeroClipboard.tests.js.html"
//"test/**/*.tests.js.html"
],
http: {
options: {
urls:
grunt.file.expand([
"test/shared/private.tests.js.html",
"test/core/private.tests.js.html",
"test/core/api.tests.js.html",
"test/client/private.tests.js.html",
"test/client/api.tests.js.html",
"test/built/ZeroClipboard.Core.tests.js.html",
"test/built/ZeroClipboard.tests.js.html"
//"test/**/*.tests.js.html"
]).map(function(testPage) {
return "http://localhost:" + localPort + "/" + testPage + "?noglobals=true";
})
}
},
coveralls: {
options: {
"--web-security": false,
timeout: 10000,
coverage: {
baseUrl: ".",
src: [
"src/js/**/*.js",
"!src/js/start.js",
"!src/js/end.js",
"dist/*.js",
"!dist/*.min.js"
],
instrumentedFiles: "tmp",
htmlReport: "coverage/html",
lcovReport: "coverage/lcov",
statementsThresholdPct: 60.0,
disposeCollector: true
},
urls:
grunt.file.expand([
"test/shared/private.tests.js.html",
"test/core/private.tests.js.html",
"test/core/api.tests.js.html",
"test/client/private.tests.js.html",
"test/client/api.tests.js.html",
"test/built/ZeroClipboard.Core.tests.js.html",
"test/built/ZeroClipboard.tests.js.html"
//"test/**/*.tests.js.html"
]).map(function(testPage) {
return "http://localhost:" + localPort + "/" + testPage + "?noglobals=true";
})
}
}
},
coveralls: {
options: {
force: true
},
all: {
src: "<%= qunit.coveralls.options.coverage.lcovReport %>/lcov.info"
}
},
watch: {
options: {
spawn: false
},
gruntfile: {
files: "<%= jshint.Gruntfile %>",
tasks: ["jshint:Gruntfile"]
},
js: {
files: "<%= jshint.js %>",
tasks: ["jshint:js", "unittest"]
},
test: {
files: "<%= jshint.test %>",
tasks: ["jshint:test", "unittest"]
}
}
};
grunt.initConfig(config);
// Task aliases and chains
grunt.registerTask("jshint-prebuild", ["jshint:gruntfile", "jshint:component", "jshint:js", "jshint:test"]);
grunt.registerTask("prep-flash", ["clean:flash", "concat:flash"]);
grunt.registerTask("validate", ["jshint-prebuild", "prep-flash", "flexpmd"]);
grunt.registerTask("build", ["clean", "concat", "jshint:dist", "uglify", "mxmlc", "template", "chmod"]);
grunt.registerTask("build-travis", ["clean", "concat", "jshint:dist", "mxmlc", "chmod:dist"]);
grunt.registerTask("test", ["connect", "qunit:file", "qunit:http"]);
// Default task
grunt.registerTask("default", ["validate", "build", "test"]);
// Travis CI task
grunt.registerTask("travis", ["validate", "build-travis", "test", "qunit:coveralls", "coveralls"]);
};

View File

@@ -0,0 +1,8 @@
The MIT License (MIT)
Copyright (c) 2014 Jon Rohan, James M. Greene
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

@@ -0,0 +1,91 @@
### WARNING
**This `master` branch contains the `v2.x` codebase for ZeroClipboard! For the `v1.x` codebase, see the [`1.x-master`](https://github.com/zeroclipboard/zeroclipboard/tree/1.x-master) branch instead.**
# ZeroClipboard
[![GitHub Latest Release](https://badge.fury.io/gh/zeroclipboard%2Fzeroclipboard.png)](https://github.com/zeroclipboard/zeroclipboard) [![Build Status](https://secure.travis-ci.org/zeroclipboard/zeroclipboard.png?branch=master)](https://travis-ci.org/zeroclipboard/zeroclipboard) [![Coverage Status](https://coveralls.io/repos/zeroclipboard/zeroclipboard/badge.png?branch=master)](https://coveralls.io/r/zeroclipboard/zeroclipboard?branch=master) [![Dependency Status](https://david-dm.org/zeroclipboard/zeroclipboard.png?theme=shields.io)](https://david-dm.org/zeroclipboard/zeroclipboard) [![Dev Dependency Status](https://david-dm.org/zeroclipboard/zeroclipboard/dev-status.png?theme=shields.io)](https://david-dm.org/zeroclipboard/zeroclipboard#info=devDependencies)
The ZeroClipboard library provides an easy way to copy text to the clipboard using an invisible [Adobe Flash](http://en.wikipedia.org/wiki/Adobe_Flash) movie and a [JavaScript](http://en.wikipedia.org/wiki/JavaScript) interface. The "Zero" signifies that the library is invisible and the user interface is left entirely up to you.
This is achieved by automatically floating the invisible movie on top of a [DOM](http://en.wikipedia.org/wiki/Document_Object_Model) element of your choice. Standard mouse events are even propagated out to your DOM element, so you can still have rollover and mousedown effects.
## Limitations
### User Interaction Required
Due to browser and Flash security restrictions, this clipboard injection can _**ONLY**_ occur when
the user clicks on the invisible Flash movie. A simulated `click` event from JavaScript will not
suffice as this would enable [clipboard poisoning](http://www.computerworld.com/s/article/9117268/Adobe_patches_Flash_clickjacking_and_clipboard_poisoning_bugs).
### Other Limitations
For a complete list of limitations, see [docs/instructions.md#limitations](docs/instructions.md#limitations).
## Simple Example
```html
<html>
<body>
<button id="copy-button" data-clipboard-text="Copy Me!" title="Click to copy me.">Copy to Clipboard</button>
<script src="ZeroClipboard.js"></script>
<script src="main.js"></script>
</body>
</html>
```
```js
// main.js
var client = new ZeroClipboard( document.getElementById("copy-button") );
client.on( "ready", function( readyEvent ) {
// alert( "ZeroClipboard SWF is ready!" );
client.on( "aftercopy", function( event ) {
// `this` === `client`
// `event.target` === the element that was clicked
event.target.style.display = "none";
alert("Copied text to clipboard: " + event.data["text/plain"] );
} );
} );
```
See [docs/instructions.md](docs/instructions.md) for more advanced options in using the library on your site.
See [docs/api/ZeroClipboard.md](docs/api/ZeroClipboard.md) for the complete API documentation.
Here is a working [test page](http://zeroclipboard.org/#demo) where you can try out ZeroClipboard in your browser.
## Testing ZeroClipboard Locally
To test the page [demo page](http://zeroclipboard.org/#demo) locally, clone the [website repo](https://github.com/zeroclipboard/zeroclipboard.org).
## Support
This library is fully compatible with Flash Player 11.0.0 and above, which requires
that the clipboard copy operation be initiated by a user click event inside the
Flash movie. This is achieved by automatically floating the invisible movie on top
of a [DOM](http://en.wikipedia.org/wiki/Document_Object_Model) element of your
choice. Standard mouse events are even propagated out to your DOM element, so you
can still have rollover and mousedown effects with just a _little_ extra effort.
ZeroClipboard `v2.x` is expected to work in IE9+ and all of the evergreen browsers.
## Contributing
see [CONTRIBUTING.md](CONTRIBUTING.md)
## Releases
Starting with version [1.1.7](https://github.com/zeroclipboard/zeroclipboard/releases/tag/v1.1.7), ZeroClipboard uses [semantic versioning](http://semver.org/).
see [releases](https://github.com/zeroclipboard/zeroclipboard/releases)
## Roadmap
see [roadmap.md](docs/roadmap.md)

View File

@@ -0,0 +1,17 @@
{
"name": "zeroclipboard",
"description": "The ZeroClipboard library provides an easy way to copy text to the clipboard using an invisible Adobe Flash movie and a JavaScript interface.",
"version": "2.1.6",
"main": ["./dist/ZeroClipboard.js", "./dist/ZeroClipboard.swf"],
"keywords": ["flash","clipboard","copy","cut","paste","zclip","clip","clippy"],
"license": "https://github.com/zeroclipboard/zeroclipboard/blob/master/LICENSE",
"authors": [{"name":"Jon Rohan","url":"http://jonrohan.me/"},{"name":"James M. Greene","email":"james.m.greene@gmail.com","url":"http://jamesgreene.net/"}],
"homepage": "http://zeroclipboard.org/",
"repository": {"type":"git","url":"https://github.com/zeroclipboard/zeroclipboard.git"},
"location": "git://github.com/zeroclipboard/zeroclipboard.git",
"ignore": [
"*",
"!/bower.json",
"!/dist/**"
]
}

View File

@@ -0,0 +1,14 @@
{
"name": "zeroclipboard/zeroclipboard",
"description": "The ZeroClipboard library provides an easy way to copy text to the clipboard using an invisible Adobe Flash movie and a JavaScript interface.",
"version": "2.1.6",
"type": "library",
"keywords": ["flash","clipboard","copy","cut","paste","zclip","clip","clippy"],
"license": "MIT",
"authors": [{"name":"Jon Rohan","homepage":"http://jonrohan.me/","role":"Developer"},{"name":"James M. Greene","email":"james.m.greene@gmail.com","homepage":"http://jamesgreene.net/","role":"Developer"}],
"homepage": "http://zeroclipboard.org/",
"support": {
"source": "https://github.com/zeroclipboard/zeroclipboard.git",
"issues": "https://github.com/zeroclipboard/zeroclipboard/issues"
}
}

View File

@@ -0,0 +1,18 @@
$(document).ready(function() {
ZeroClipboard.config( { swfPath: './scripts/zeroclipboard/dist/ZeroClipboard.swf' } );
var client = new ZeroClipboard( document.getElementById("copy-button") );
client.on( "ready", function( readyEvent ) {
// alert( "ZeroClipboard SWF is ready!" );
client.on( "aftercopy", function( event ) {
// `this` === `client`
// `event.target` === the element that was clicked
// event.target.style.display = "none";
//alert("Copied text to clipboard: " + event.data["text/plain"] );
} );
} );
});

View File

@@ -0,0 +1,70 @@
{
/* Enforcing options */
"bitwise": true,
"camelcase": true,
"curly": true,
"eqeqeq": true,
"es3": true,
"es5": false,
"forin": true,
"freeze": true,
"immed": true,
"indent": 2,
"latedef": true,
"newcap": true,
"noarg": true,
"noempty": true,
"nonbsp": true,
"nonew": true,
"plusplus": false,
"quotmark": "double",
"undef": true,
"unused": true,
"strict": true,
"trailing": true,
"maxparams": 4,
"maxdepth": 5,
"maxstatements": false,
"maxlen": false, /* IDEAL: 120? */
/* Relaxing options */
"asi": false,
"boss": false,
"debug": false,
"eqnull": true,
"esnext": false,
"evil": false,
"expr": false,
"funcscope": false,
"gcl": false,
"globalstrict": false,
"iterator": false,
"lastsemic": false,
"laxbreak": false,
"laxcomma": false,
"loopfunc": false,
"maxerr": 50,
"moz": false,
"multistr": false,
"notypeof": false,
"proto": false,
"scripturl": false,
"smarttabs": false,
"shadow": false,
"sub": false,
"supernew": false,
"validthis": false,
"noyield": false,
/* Environments */
"browser": true,
/* Global variables */
"globals": {
/* AMD */
"define": false,
/* CommonJS */
"module": false
}
}

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

Binary file not shown.

View File

@@ -0,0 +1,716 @@
# ZeroClipboard.Core API
This documents details the **ZeroClipboard.Core** API, including various types of properties, methods, and events. **ZeroClipboard.Core** is primarily intended for use in wrapping ZeroClipboard in 3rd party plugins, e.g. [jquery.zeroclipboard](https://github.com/zeroclipboard/jquery.zeroclipboard).
## Static
### Static Properties
#### `ZeroClipboard.version`
_[`String`]_ The version of the ZeroClipboard library being used, e.g. `"2.0.0"`.
### Static Methods
#### `ZeroClipboard.config(...)`
```js
var config = ZeroClipboard.config();
```
_[`Object`]_ Get a copy of the active configuration for ZeroClipboard.
```js
var swfPath = ZeroClipboard.config("swfPath");
```
_[`*`]_ Get a copy of the actively configured value for this configuration property for ZeroClipboard.
```js
var config = ZeroClipboard.config({
forceHandCursor: true
});
```
_[`Object`]_ Set the active configuration for ZeroClipboard. Returns a copy of the updated active configuration. For complete details about what can be configured, see [**Configuration Options** below](#configuration-options).
#### `ZeroClipboard.create()`
_[`undefined`]_ Create the Flash bridge SWF object.
#### `ZeroClipboard.destroy()`
_[`undefined`]_ Emit the [`"destroy"`](#destroy) event, remove all event handlers, and destroy the Flash bridge.
#### `ZeroClipboard.setData(...)`
```js
ZeroClipboard.setData("text/plain", "Blah");
```
_[`undefined`]_ Set the pending `data` of type `format` for clipboard injection.
```js
ZeroClipboard.setData({
"text/plain": "Blah",
"text/html": "<b>Blah</b>"
});
```
_[`undefined`]_ Set the pending `data` of various formats for clipboard injection.
#### `ZeroClipboard.clearData(...)`
```js
ZeroClipboard.clearData("text/plain");
```
_[`undefined`]_ Clear the pending data of type `format` for clipboard injection.
```js
ZeroClipboard.clearData();
```
_[`undefined`]_ Clear the pending data of ALL formats for clipboard injection.
#### `ZeroClipboard.getData(...)`
```js
var text = ZeroClipboard.getData("text/plain");
```
_[`String`]_ Get the pending data of type `format` for clipboard injection.
```js
var dataObj = ZeroClipboard.getData();
```
_[`Object`]_ Get a copy of the pending data of ALL formats for clipboard injection.
#### `ZeroClipboard.focus(...)`
#### `ZeroClipboard.activate(...)`
```js
ZeroClipboard.focus(document.getElementById("d_clip_button"));
```
_[`undefined`]_ Focus/"activate" the provided element by moving the Flash SWF object in front of it. **NOTE:** The preferred method to use is `focus` but the alias `activate` is available for backward compatibility's sake.
#### `ZeroClipboard.blur()`
#### `ZeroClipboard.deactivate()`
_[`undefined`]_ Blur/"deactivate" the currently focused/"activated" element, moving the Flash SWF object off the screen. **NOTE:** The preferred method to use is `blur` but the alias `deactivate` is available for backward compatibility's sake.
#### `ZeroClipboard.activeElement()`
```js
var el = document.getElementById("d_clip_button");
ZeroClipboard.focus(el);
var activeEl = ZeroClipboard.activeElement(); // activeEl === el
```
_[`HTMLElement` or `null`]_ Return the currently "activated" element that the Flash SWF object is in front of it.
#### `ZeroClipboard.state()`
_[`Object`]_ Diagnostic method that describes the state of the browser, Flash Player, and ZeroClipboard.
#### `ZeroClipboard.isFlashUnavailable()`
_[`Boolean`]_ Indicates if Flash Player is **definitely** unusable (disabled, outdated, unavailable, or deactivated). _**IMPORTANT:**_ This method should be considered private.
#### `ZeroClipboard.on(...)`
```js
var listenerFn = function(e) { var ZeroClipboard = this; /* ... */ };
ZeroClipboard.on("ready", listenerFn);
var listenerObj = {
handleEvent: function(e) { var listenerObj = this; /* ... */ }
};
ZeroClipboard.on("error", listenerObj);
```
_[`undefined`]_ Add a `listener` function/object for an `eventType`.
```js
ZeroClipboard.on("ready error", function(e) { /* ... */ });
```
_[`undefined`]_ Add a `listener` function/object for multiple `eventType`s.
```js
ZeroClipboard.on({
"ready": function(e) { /* ... */ },
"error": function(e) { /* ... */ }
});
```
_[`undefined`]_ Add a set of `eventType` to `listener` function/object mappings.
#### `ZeroClipboard.off(...)`
```js
ZeroClipboard.off("ready", listenerFn);
ZeroClipboard.off("error", listenerObj);
```
_[`undefined`]_ Remove a `listener` function/object for an `eventType`.
```js
ZeroClipboard.off("ready error", listenerFn);
```
_[`undefined`]_ Remove a `listener` function/object for multiple `eventType`s.
```js
ZeroClipboard.off({
"ready": readyListenerFn,
"error": errorListenerFn
});
```
_[`undefined`]_ Remove a set of `eventType` to `listener` function/object mappings.
```js
ZeroClipboard.off("ready");
```
_[`undefined`]_ Remove ALL listener functions/objects for an `eventType`.
```js
ZeroClipboard.off();
```
_[`undefined`]_ Remove ALL listener functions/objects for ALL registered event types.
#### `ZeroClipboard.emit(...)`
```js
ZeroClipboard.emit("ready");
ZeroClipboard.emit({
type: "error",
name: "flash-disabled"
});
var pendingCopyData = ZeroClipboard.emit("copy");
```
_[`undefined`, or a Flash-friendly data Object for the `"copy"` event]_ Dispatch an event to all
registered listeners. The emission of some types of events will result in side effects.
#### `ZeroClipboard.handlers()`
```js
var listeners = ZeroClipboard.handlers("ready");
```
_[`Array`]_ Retrieves a copy of the registered listener functions/objects for the given `eventType`.
```js
var listeners = ZeroClipboard.handlers();
```
_[`Object`]_ Retrieves a copy of the map of registered listener functions/objects for ALL event types.
### Static Events
#### `"ready"`
The `ready` event is fired when the Flash SWF completes loading and is ready for action. Please
note that you need to set most configuration options [with [`ZeroClipboard.config(...)`](#zeroclipboardconfig)]
before `ZeroClipboard.create()` is invoked.
```js
ZeroClipboard.on("ready", function(e) {
/*
e = {
type: "ready",
message: "Flash communication is established",
target: null,
relatedTarget: null,
currentTarget: flashSwfObjectRef,
version: "11.2.202",
timeStamp: Date.now()
};
*/
});
```
#### `"beforecopy"`
On `click`, the Flash object will fire off a `beforecopy` event. This event is generally only
used for "UI prepartion" if you want to alter anything before the `copy` event fires.
**IMPORTANT:** Handlers of this event are expected to operate synchronously if they intend to be
finished before the "copy" event is triggered.
```js
ZeroClipboard.on("beforecopy", function(e) {
/*
e = {
type: "beforecopy",
target: currentlyActivatedElementOrNull,
relatedTarget: dataClipboardElementTargetOfCurrentlyActivatedElementOrNull,
currentTarget: flashSwfObjectRef,
timeStamp: Date.now()
};
*/
});
```
#### `"copy"`
On `click` (and after the `beforecopy` event), the Flash object will fire off a `copy` event. If
the HTML object has `data-clipboard-text` or `data-clipboard-target`, then ZeroClipboard will take
care of getting an initial set of data. It will then invoke any `copy` event handlers, in which you
can call `event.clipboardData.setData` to set the text, which will complete the loop.
**IMPORTANT:** If a handler of this event intends to modify the pending data for clipboard
injection, it _MUST_ operate synchronously in order to maintain the temporarily elevated
permissions granted by the user's `click` event. The most common "gotcha" for this restriction is
if someone wants to make an asynchronous XMLHttpRequest in response to the `copy` event to get the
data to inject &mdash; this won't work; make it a _synchronous_ XMLHttpRequest instead, or do the
work in advance before the `copy` event is fired.
```js
ZeroClipboard.on("copy", function(e) {
/*
e = {
type: "copy",
target: currentlyActivatedElementOrNull,
relatedTarget: dataClipboardElementTargetOfCurrentlyActivatedElementOrNull,
currentTarget: flashSwfObjectRef,
timeStamp: Date.now(),
clipboardData: {
setData: ZeroClipboard.setData,
clearData: ZeroClipboard.clearData
}
};
*/
});
```
#### `"aftercopy"`
The `aftercopy` event is fired when the text is copied [or failed to copy] to the clipboard.
```js
ZeroClipboard.on("aftercopy", function(e) {
/*
e = {
type: "aftercopy",
target: currentlyActivatedElementOrNull,
relatedTarget: dataClipboardElementTargetOfCurrentlyActivatedElementOrNull,
currentTarget: flashSwfObjectRef,
timeStamp: Date.now(),
success: {
"text/plain": true,
"text/html": true,
"application/rtf": false
},
data: {
"text/plain": "Blah",
"text/html": "<b>Blah</b>",
"application/rtf": "{\\rtf1\\ansi\n{\\b Blah}}"
}
};
*/
});
```
#### `"destroy"`
The `destroy` event is fired when `ZeroClipboard.destroy()` is invoked.
**IMPORTANT:** Handlers of this event are expected to operate synchronously if they intend to be
finished before the destruction is complete.
```js
ZeroClipboard.on("destroy", function(e) {
/*
e = {
type: "destroy",
target: null,
relatedTarget: null,
currentTarget: flashSwfObjectRef,
timeStamp: Date.now(),
success: {
"text/plain": true,
"text/html": true,
"application/rtf": false
},
data: {
"text/plain": "Blah",
"text/html": "<b>Blah</b>",
"application/rtf": "{\\rtf1\\ansi\n{\\b Blah}}"
}
};
*/
});
```
#### `"error"`
The `error` event is fired under a number of conditions, which will be detailed as sub-sections below.
Some consumers may not consider all `error` types to be critical, and thus ZeroClipboard does not take it upon
itself to implode by calling `ZeroClipboard.destroy()` under error conditions. However, many consumers may
want to do just that.
##### `error[name = "flash-disabled"]`
This type of `error` event fires when Flash Player is either not installed or not enabled in the browser.
```js
ZeroClipboard.on("error", function(e) {
/*
e = {
type: "error",
name: "flash-disabled",
messsage: "Flash is disabled or not installed",
target: null,
relatedTarget: null,
currentTarget: flashSwfObjectRef,
timeStamp: Date.now(),
minimumVersion: "11.0.0"
};
*/
});
```
##### `error[name = "flash-outdated"]`
This type of `error` event fires when Flash Player is installed in the browser but the version is too old
for ZeroClipboard. ZeroClipboard requires Flash Player 11.0.0 or above.
```js
ZeroClipboard.on("error", function(e) {
/*
e = {
type: "error",
name: "flash-outdated",
messsage: "Flash is too outdated to support ZeroClipboard",
target: null,
relatedTarget: null,
currentTarget: flashSwfObjectRef,
timeStamp: Date.now(),
minimumVersion: "11.0.0",
version: "10.3.183"
};
*/
});
```
##### `error[name = "flash-unavailable"]`
This type of `error` event fires when the browser's installation of Flash Player cannot communicate bidirectionally with JavaScript.
```js
ZeroClipboard.on("error", function(e) {
/*
e = {
type: "error",
name: "flash-unavailable",
messsage: "Flash is unable to communicate bidirectionally with JavaScript",
target: null,
relatedTarget: null,
currentTarget: flashSwfObjectRef,
timeStamp: Date.now(),
minimumVersion: "11.0.0",
version: "11.2.202"
};
*/
});
```
##### `error[name = "flash-deactivated"]`
This type of `error` event fires when the browser's installation of Flash Player is either too old
for the browser [but _not_ too old for ZeroClipboard] or if Flash objects are configured as
click-to-play and the user does not authorize it within `_globalConfig.flashLoadTimeout`
milliseconds or does not authorize it at all.
```js
ZeroClipboard.on("error", function(e) {
/*
e = {
type: "error",
name: "flash-deactivated",
messsage: "Flash is too outdated for your browser and/or is configured as click-to-activate",
target: null,
relatedTarget: null,
currentTarget: flashSwfObjectRef,
timeStamp: Date.now(),
minimumVersion: "11.0.0",
version: "11.2.202"
};
*/
});
```
##### `error[name = "flash-overdue"]`
This type of `error` event fires when the SWF loads successfully but takes longer than
`_globalConfig.flashLoadTimeout` milliseconds to do so. This would likely be caused by
one of the following situations:
1. Too short of a `_globalConfig.flashLoadTimeout` duration configured
2. Network latency
3. The user's installation of Flash is configured as click-to-play but then authorized
by the user too late such that the SWF does not finish loading before the timeout
period has expired (or it may have expired before they authorized it at all).
The appropriate response to this event is left up to the consumer. For instance, if they
chose to invoke `ZeroClipboard.destroy()` in response to the earlier
`error[name = "flash-deactivated"]` event but then receive this `error[name = "flash-overdue"]`
event, they may choose to "restart" their process and construct new ZeroClipboard client instances,
or they may choose to just log the error to their server so they can consider increasing the
allowed timeout duration in the future.
This may be especially important for SPA or PJAX-based applications to consider as their users
may remain on a single page for an extended period of time during which they _possibly_ could
have enjoyed an improved experience if ZeroClipboard had been "restarted" after an initial hiccup.
```js
ZeroClipboard.on("error", function(e) {
/*
e = {
type: "error",
name: "flash-overdue",
messsage: "Flash communication was established but NOT within the acceptable time limit",
target: null,
relatedTarget: null,
currentTarget: flashSwfObjectRef,
timeStamp: Date.now(),
minimumVersion: "11.0.0",
version: "11.2.202"
};
*/
});
```
## Configuration Options
These are default values for the global configurations options. You should generally update these _before_ you create your clients.
```js
var _globalConfig = {
// SWF URL, relative to the page. Default value will be "ZeroClipboard.swf"
// under the same path as the ZeroClipboard JS file.
swfPath: _swfPath,
// SWF inbound scripting policy: page domains that the SWF should trust.
// (single string, or array of strings)
trustedDomains: window.location.host ? [window.location.host] : [],
// Include a "noCache" query parameter on requests for the SWF.
cacheBust: true,
// Enable use of the fancy "Desktop" clipboard, even on Linux where it is
// known to suck.
forceEnhancedClipboard: false,
// How many milliseconds to wait for the Flash SWF to load and respond before assuming that
// Flash is deactivated (e.g. click-to-play) in the user's browser. If you don't care about
// how long it takes to load the SWF, you can set this to `null`.
flashLoadTimeout: 30000,
// Setting this to `false` would allow users to handle calling `ZeroClipboard.focus(...);`
// themselves instead of relying on our per-element `mouseover` handler.
autoActivate: true,
// Bubble synthetic events in JavaScript after they are received by the Flash object.
bubbleEvents: true,
// Sets the ID of the `div` encapsulating the Flash object.
// Value is validated against the [HTML4 spec for `ID` tokens][valid_ids].
containerId: "global-zeroclipboard-html-bridge",
// Sets the class of the `div` encapsulating the Flash object.
containerClass: "global-zeroclipboard-container",
// Sets the ID and name of the Flash `object` element.
// Value is validated against the [HTML4 spec for `ID` and `Name` tokens][valid_ids].
swfObjectId: "global-zeroclipboard-flash-bridge",
// The class used to indicate that a clipped element is being hovered over.
hoverClass: "zeroclipboard-is-hover",
// The class used to indicate that a clipped element is active (is being clicked).
activeClass: "zeroclipboard-is-active",
// Forcibly set the hand cursor ("pointer") for all clipped elements.
// IMPORTANT: This configuration value CAN be modified while a SWF is actively embedded.
forceHandCursor: false,
// Sets the title of the `div` encapsulating the Flash object.
// IMPORTANT: This configuration value CAN be modified while a SWF is actively embedded.
title: null,
// The z-index used by the Flash object.
// Max value (32-bit): 2147483647.
// IMPORTANT: This configuration value CAN be modified while a SWF is actively embedded.
zIndex: 999999999
};
```
You can override the defaults by making calls like `ZeroClipboard.config({ swfPath: "new/path" });`
before you create any clients.
### SWF Inbound Scripting Access: The `trustedDomains` option
This allows other SWF files and HTML pages from the allowed domains to access/call publicly
exposed ActionScript code, e.g. functions shared via `ExternalInterface.addCallback`. In other
words, it controls the SWF inbound scripting access.
If your ZeroClipboard SWF is served from a different origin/domain than your page, you need to tell
the SWF that it's OK to trust your page. The default value of `[window.location.host]` is almost
_**always**_ what you will want unless you specifically want the SWF to communicate with pages from
other domains (e.g. in `iframe`s or child windows).
For more information about trusted domains, consult the [_official Flash documentation for `flash.system.Security.allowDomain(...)`_](http://help.adobe.com/en_US/FlashPlatform/reference/actionscript/3/flash/system/Security.html#allowDomain\(\)).
### SWF Outbound Scripting Access
The `allowScriptAccess` parameter (for Flash embedding markup) allows the SWF file to access/call
JavaScript/HTML functionality of HTML pages on allowed domains, e.g. invoking functions via
`ExternalInterface.call`. In other words, it controls the SWF outbound scripting access.
As of version `v2.0.0-alpha.2`, the `allowScriptAccess` configuration option no longer exists. The
appropriate value will be determined immediately before the Flash object is embedded on the page.
The value is based on a relationship between the current domain (`window.location.host`) and the
value of the `trustedDomains` configuration option.
For more information about `allowScriptAccess`, consult the [_official Flash documentation_](http://helpx.adobe.com/flash/kb/control-access-scripts-host-web.html).
### Cross-Protocol Limitations
ZeroClipboard was intentionally configured to _not_ allow the SWF to be served from a secure domain (HTTPS) but scripted by an insecure domain (HTTP).
If you find yourself in this situation (as in [Issue #170](https://github.com/zeroclipboard/zeroclipboard/issues/170)), please consider the following options:
1. Serve the SWF over HTTP instead of HTTPS. If the page's protocol can vary (e.g. authorized/unauthorized, staging/production, etc.), you should include add the SWF with a relative protocol (`//s3.amazonaws.com/blah/ZeroClipboard.swf`) instead of an absolute protocol (`https://s3.amazonaws.com/blah/ZeroClipboard.swf`).
2. Serve the page over HTTPS instead of HTTP. If the page's protocol can vary, see the note on the previous option (1).
3. Update ZeroClipboard's ActionScript codebase to call the [`allowInsecureDomain`](http://help.adobe.com/en_US/FlashPlatform/reference/actionscript/3/flash/system/Security.html#allowInsecureDomain\(\)) method, then recompile the SWF with your custom changes.
### `file://` Protocol Limitations
If you want to host a page locally on the `file://` protocol, you must specifically configure
ZeroClipboard to trust ALL domains for SWF interaction via a wildcard. This configuration must be
set _before_ creating ZeroClipboard client instances as a typical consumer, or before calling
`ZeroClipboard.create()` in a 3rd party wrapper:
```js
ZeroClipboard.config({ trustedDomains: ["*"] });
```
This wildcard configuration should _**NOT**_ be used in environments hosted over HTTP/HTTPS.
## Extending `ZeroClipboard`
For developers who want to wrap ZeroClipboard into a 3rd party plugin
(e.g. [jquery.zeroclipboard](https://github.com/zeroclipboard/jquery.zeroclipboard)), here
are the important extension points:
### Constructor
Although the root `ZeroClipboard` function itself is actually a constructor, it also contains a
particular hook that checks for the existence of a `ZeroClipboard._createClient` static function
and invokes it with `this` (the freshly created `ZeroClipboard` instance) as the context and passes
along all provided arguments to the constructor function, e.g.:
```js
var counterId = 0;
ZeroClipboard._createClient = function(elements, otherStuff, etc) {
this.id = "" + (counterId++);
/* ... */
};
var $elementsToOperateOn = $(".clip_button");
var client = new ZeroClipboard($elementsToOperateOn);
```
### Prototype Chain
Using the `ZeroClipboard` constructor will allow you to also extend the underlying prototype with
new instance-based methods, e.g.:
```js
ZeroClipboard.prototype.clientEmitOrSomeOtherOperationToInvoke = function(e) {
e.client = this;
};
```
### Eventing
Most clients will want to listen for some or all of the `ZeroClipboard.Core` events, and some
clients will even want to regurgitate the same events to their own client-based listeners. To
make the latter easier, `ZeroClipboard.Core` will also allow you to add a listener to an
`eventType` of `"*"`, e.g.:
```js
ZeroClipboard._createClient = function() {
var client = this;
ZeroClipboard.on("*", function(e) {
client.clientEmitOrSomeOtherOperationToInvoke(e);
});
};
```
### Static Extension
The `ZeroClipboard.Core` API is composed of static methods stored as properties of the
root `ZeroClipboard` function. As such, additional static methods can be added as desired, if there
is any actual benefit to doing so, e.g.:
```js
ZeroClipboard.log = function() {
if (typeof console !== "undefined" && console.log) {
console.log.apply(console, Array.prototype.slice.call(arguments, 0));
}
};
```

View File

@@ -0,0 +1,892 @@
# ZeroClipboard API
This documents details the ZeroClipboard API, including various types of properties, methods, and events.
## Static
### Static Properties
#### `ZeroClipboard.version`
_[`String`]_ The version of the ZeroClipboard library being used, e.g. `"2.0.0"`.
### Static Methods
#### `ZeroClipboard.config(...)`
```js
var config = ZeroClipboard.config();
```
_[`Object`]_ Get a copy of the active configuration for ZeroClipboard.
```js
var swfPath = ZeroClipboard.config("swfPath");
```
_[`*`]_ Get a copy of the actively configured value for this configuration property for ZeroClipboard.
```js
var config = ZeroClipboard.config({
forceHandCursor: true
});
```
_[`Object`]_ Set the active configuration for ZeroClipboard. Returns a copy of the updated active configuration. For complete details about what can be configured, see [**Configuration Options** below](#configuration-options).
#### `ZeroClipboard.create()`
_[`undefined`]_ Create the Flash bridge SWF object. _**IMPORTANT:**_ This method should be considered private.
#### `ZeroClipboard.destroy()`
_[`undefined`]_ Emit the [`"destroy"`](#destroy) event, remove all event handlers, and destroy the Flash bridge.
#### `ZeroClipboard.setData(...)`
```js
ZeroClipboard.setData("text/plain", "Blah");
```
_[`undefined`]_ Set the pending `data` of type `format` for clipboard injection.
```js
ZeroClipboard.setData({
"text/plain": "Blah",
"text/html": "<b>Blah</b>"
});
```
_[`undefined`]_ Set the pending `data` of various formats for clipboard injection.
#### `ZeroClipboard.clearData(...)`
```js
ZeroClipboard.clearData("text/plain");
```
_[`undefined`]_ Clear the pending data of type `format` for clipboard injection.
```js
ZeroClipboard.clearData();
```
_[`undefined`]_ Clear the pending data of ALL formats for clipboard injection.
#### `ZeroClipboard.getData(...)`
```js
var text = ZeroClipboard.getData("text/plain");
```
_[`String`]_ Get the pending data of type `format` for clipboard injection.
```js
var dataObj = ZeroClipboard.getData();
```
_[`Object`]_ Get a copy of the pending data of ALL formats for clipboard injection.
#### `ZeroClipboard.focus(...)`
#### `ZeroClipboard.activate(...)`
```js
ZeroClipboard.focus(document.getElementById("d_clip_button"));
```
_[`undefined`]_ Focus/"activate" the provided element by moving the Flash SWF object in front of it. **NOTE:** The preferred method to use is `focus` but the alias `activate` is available for backward compatibility's sake.
#### `ZeroClipboard.blur()`
#### `ZeroClipboard.deactivate()`
_[`undefined`]_ Blur/"deactivate" the currently focused/"activated" element, moving the Flash SWF object off the screen. **NOTE:** The preferred method to use is `blur` but the alias `deactivate` is available for backward compatibility's sake.
#### `ZeroClipboard.activeElement()`
```js
var el = document.getElementById("d_clip_button");
ZeroClipboard.focus(el);
var activeEl = ZeroClipboard.activeElement(); // activeEl === el
```
_[`HTMLElement` or `null`]_ Return the currently "activated" element that the Flash SWF object is in front of it.
#### `ZeroClipboard.state()`
_[`Object`]_ Diagnostic method that describes the state of the browser, Flash Player, and ZeroClipboard.
#### `ZeroClipboard.isFlashUnavailable()`
_[`Boolean`]_ Indicates if Flash Player is **definitely** unusable (disabled, outdated, unavailable, or deactivated). _**IMPORTANT:**_ This method should be considered private.
#### `ZeroClipboard.on(...)`
```js
var listenerFn = function(e) { var ZeroClipboard = this; /* ... */ };
ZeroClipboard.on("ready", listenerFn);
var listenerObj = {
handleEvent: function(e) { var listenerObj = this; /* ... */ }
};
ZeroClipboard.on("error", listenerObj);
```
_[`undefined`]_ Add a `listener` function/object for an `eventType`.
```js
ZeroClipboard.on("ready error", function(e) { /* ... */ });
```
_[`undefined`]_ Add a `listener` function/object for multiple `eventType`s.
```js
ZeroClipboard.on({
"ready": function(e) { /* ... */ },
"error": function(e) { /* ... */ }
});
```
_[`undefined`]_ Add a set of `eventType` to `listener` function/object mappings.
#### `ZeroClipboard.off(...)`
```js
ZeroClipboard.off("ready", listenerFn);
ZeroClipboard.off("error", listenerObj);
```
_[`undefined`]_ Remove a `listener` function/object for an `eventType`.
```js
ZeroClipboard.off("ready error", listenerFn);
```
_[`undefined`]_ Remove a `listener` function/object for multiple `eventType`s.
```js
ZeroClipboard.off({
"ready": readyListenerFn,
"error": errorListenerFn
});
```
_[`undefined`]_ Remove a set of `eventType` to `listener` function/object mappings.
```js
ZeroClipboard.off("ready");
```
_[`undefined`]_ Remove ALL listener functions/objects for an `eventType`.
```js
ZeroClipboard.off();
```
_[`undefined`]_ Remove ALL listener functions/objects for ALL registered event types.
#### `ZeroClipboard.emit(...)`
```js
ZeroClipboard.emit("ready");
ZeroClipboard.emit({
type: "error",
name: "flash-disabled"
});
var pendingCopyData = ZeroClipboard.emit("copy");
```
_[`undefined`, or a Flash-friendly data `Object` for the `"copy"` event]_ Dispatch an event to all
registered listeners. The emission of some types of events will result in side effects.
#### `ZeroClipboard.handlers(...)`
```js
var listeners = ZeroClipboard.handlers("ready");
```
_[`Array`]_ Retrieves a copy of the registered listener functions/objects for the given `eventType`.
```js
var listeners = ZeroClipboard.handlers();
```
_[`Object`]_ Retrieves a copy of the map of registered listener functions/objects for ALL event types.
### Static Events
#### `"ready"`
The `ready` event is fired when the Flash SWF completes loading and is ready for action. Please
note that you need to set most configuration options [with [`ZeroClipboard.config(...)`](#zeroclipboardconfig)]
before `ZeroClipboard.create()` is invoked.
```js
ZeroClipboard.on("ready", function(e) {
/*
e = {
type: "ready",
message: "Flash communication is established",
target: null,
relatedTarget: null,
currentTarget: flashSwfObjectRef,
version: "11.2.202",
timeStamp: Date.now()
};
*/
});
```
#### `"beforecopy"`
On `click`, the Flash object will fire off a `beforecopy` event. This event is generally only
used for "UI prepartion" if you want to alter anything before the `copy` event fires.
**IMPORTANT:** Handlers of this event are expected to operate synchronously if they intend to be
finished before the "copy" event is triggered.
```js
ZeroClipboard.on("beforecopy", function(e) {
/*
e = {
type: "beforecopy",
target: currentlyActivatedElementOrNull,
relatedTarget: dataClipboardElementTargetOfCurrentlyActivatedElementOrNull,
currentTarget: flashSwfObjectRef,
timeStamp: Date.now()
};
*/
});
```
#### `"copy"`
On `click` (and after the `beforecopy` event), the Flash object will fire off a `copy` event. If
the HTML object has `data-clipboard-text` or `data-clipboard-target`, then ZeroClipboard will take
care of getting an initial set of data. It will then invoke any `copy` event handlers, in which you
can call `event.clipboardData.setData` to set the text, which will complete the loop.
**IMPORTANT:** If a handler of this event intends to modify the pending data for clipboard
injection, it _MUST_ operate synchronously in order to maintain the temporarily elevated
permissions granted by the user's `click` event. The most common "gotcha" for this restriction is
if someone wants to make an asynchronous XMLHttpRequest in response to the `copy` event to get the
data to inject &mdash; this won't work; make it a _synchronous_ XMLHttpRequest instead, or do the
work in advance before the `copy` event is fired.
```js
ZeroClipboard.on("copy", function(e) {
/*
e = {
type: "copy",
target: currentlyActivatedElementOrNull,
relatedTarget: dataClipboardElementTargetOfCurrentlyActivatedElementOrNull,
currentTarget: flashSwfObjectRef,
timeStamp: Date.now(),
clipboardData: {
setData: ZeroClipboard.setData,
clearData: ZeroClipboard.clearData
}
};
*/
});
```
#### `"aftercopy"`
The `aftercopy` event is fired when the text is copied [or failed to copy] to the clipboard.
```js
ZeroClipboard.on("aftercopy", function(e) {
/*
e = {
type: "aftercopy",
target: currentlyActivatedElementOrNull,
relatedTarget: dataClipboardElementTargetOfCurrentlyActivatedElementOrNull,
currentTarget: flashSwfObjectRef,
timeStamp: Date.now(),
success: {
"text/plain": true,
"text/html": true,
"application/rtf": false
},
data: {
"text/plain": "Blah",
"text/html": "<b>Blah</b>",
"application/rtf": "{\\rtf1\\ansi\n{\\b Blah}}"
}
};
*/
});
```
#### `"destroy"`
The `destroy` event is fired when `ZeroClipboard.destroy()` is invoked.
**IMPORTANT:** Handlers of this event are expected to operate synchronously if they intend to be
finished before the destruction is complete.
```js
ZeroClipboard.on("destroy", function(e) {
/*
e = {
type: "destroy",
target: null,
relatedTarget: null,
currentTarget: flashSwfObjectRef,
timeStamp: Date.now(),
success: {
"text/plain": true,
"text/html": true,
"application/rtf": false
},
data: {
"text/plain": "Blah",
"text/html": "<b>Blah</b>",
"application/rtf": "{\\rtf1\\ansi\n{\\b Blah}}"
}
};
*/
});
```
#### `"error"`
The `error` event is fired under a number of conditions, which will be detailed as sub-sections below.
Some consumers may not consider all `error` types to be critical, and thus ZeroClipboard does not take it upon
itself to implode by calling `ZeroClipboard.destroy()` under error conditions. However, many consumers may
want to do just that.
##### `error[name = "flash-disabled"]`
This type of `error` event fires when Flash Player is either not installed or not enabled in the browser.
```js
ZeroClipboard.on("error", function(e) {
/*
e = {
type: "error",
name: "flash-disabled",
messsage: "Flash is disabled or not installed",
target: null,
relatedTarget: null,
currentTarget: flashSwfObjectRef,
timeStamp: Date.now(),
minimumVersion: "11.0.0"
};
*/
});
```
##### `error[name = "flash-outdated"]`
This type of `error` event fires when Flash Player is installed in the browser but the version is too old
for ZeroClipboard. ZeroClipboard requires Flash Player 11.0.0 or above.
```js
ZeroClipboard.on("error", function(e) {
/*
e = {
type: "error",
name: "flash-outdated",
messsage: "Flash is too outdated to support ZeroClipboard",
target: null,
relatedTarget: null,
currentTarget: flashSwfObjectRef,
timeStamp: Date.now(),
minimumVersion: "11.0.0",
version: "10.3.183"
};
*/
});
```
##### `error[name = "flash-unavailable"]`
This type of `error` event fires when the browser's installation of Flash Player cannot communicate bidirectionally with JavaScript.
```js
ZeroClipboard.on("error", function(e) {
/*
e = {
type: "error",
name: "flash-unavailable",
messsage: "Flash is unable to communicate bidirectionally with JavaScript",
target: null,
relatedTarget: null,
currentTarget: flashSwfObjectRef,
timeStamp: Date.now(),
minimumVersion: "11.0.0",
version: "11.2.202"
};
*/
});
```
##### `error[name = "flash-deactivated"]`
This type of `error` event fires when the browser's installation of Flash Player is either too old
for the browser [but _not_ too old for ZeroClipboard] or if Flash objects are configured as
click-to-play and the user does not authorize it within `_globalConfig.flashLoadTimeout`
milliseconds or does not authorize it at all.
```js
ZeroClipboard.on("error", function(e) {
/*
e = {
type: "error",
name: "flash-deactivated",
messsage: "Flash is too outdated for your browser and/or is configured as click-to-activate",
target: null,
relatedTarget: null,
currentTarget: flashSwfObjectRef,
timeStamp: Date.now(),
minimumVersion: "11.0.0",
version: "11.2.202"
};
*/
});
```
##### `error[name = "flash-overdue"]`
This type of `error` event fires when the SWF loads successfully but takes longer than
`_globalConfig.flashLoadTimeout` milliseconds to do so. This would likely be caused by
one of the following situations:
1. Too short of a `_globalConfig.flashLoadTimeout` duration configured
2. Network latency
3. The user's installation of Flash is configured as click-to-play but then authorized
by the user too late such that the SWF does not finish loading before the timeout
period has expired (or it may have expired before they authorized it at all).
The appropriate response to this event is left up to the consumer. For instance, if they
chose to invoke `ZeroClipboard.destroy()` in response to the earlier
`error[name = "flash-deactivated"]` event but then receive this `error[name = "flash-overdue"]`
event, they may choose to "restart" their process and construct new ZeroClipboard client instances,
or they may choose to just log the error to their server so they can consider increasing the
allowed timeout duration in the future.
This may be especially important for SPA or PJAX-based applications to consider as their users
may remain on a single page for an extended period of time during which they _possibly_ could
have enjoyed an improved experience if ZeroClipboard had been "restarted" after an initial hiccup.
```js
ZeroClipboard.on("error", function(e) {
/*
e = {
type: "error",
name: "flash-overdue",
messsage: "Flash communication was established but NOT within the acceptable time limit",
target: null,
relatedTarget: null,
currentTarget: flashSwfObjectRef,
timeStamp: Date.now(),
minimumVersion: "11.0.0",
version: "11.2.202"
};
*/
});
```
## Instance
The following properties and methods are accessible via a `ZeroClipboard` client instance, e.g.
```js
var clippedEl = document.getElementById("d_clip_button");
var client = new ZeroClipboard(clippedEl);
```
### Instance Properties
#### `client.id`
_[`String`]_ A unique identifier for this ZeroClipboard client instance.
### Instance Methods
#### `client.destroy()`
_[`undefined`]_ Remove all event handlers and unclip all clipped elements.
#### `client.setText(...)`
```js
client.setText("Blah");
```
_[`this`]_ Set the pending `data` of type `"text/plain"` for clipboard injection.
#### `client.setHtml(...)`
```js
client.setHtml("<b>Blah</b>");
```
_[`this`]_ Set the pending `data` of type `"text/html"` for clipboard injection.
#### `client.setRichText(...)`
```js
client.setRichText("{\\rtf1\\ansi\n{\\b Blah}}");
```
_[`this`]_ Set the pending `data` of type `"application/rtf"` for clipboard injection.
#### `client.setData(...)`
```js
client.setData("text/plain", "Blah");
```
_[`this`]_ Set the pending `data` of type `format` for clipboard injection.
```js
client.setData({
"text/plain": "Blah",
"text/html": "<b>Blah</b>"
});
```
_[`this`]_ Set the pending `data` of various formats for clipboard injection. This particular
function signature (passing in an `Object`) will implicitly clear out any existing pending data.
#### `client.clearData(...)`
```js
client.clearData("text/plain");
```
_[`this`]_ Clear the pending data of type `format` for clipboard injection.
```js
client.clearData();
```
_[`this`]_ Clear the pending data of ALL formats for clipboard injection.
#### `client.getData(...)`
```js
var text = client.getData("text/plain");
```
_[`String`]_ Get the pending data of type `format` for clipboard injection.
```js
var dataObj = client.getData();
```
_[`Object`]_ Get a copy of the pending data of ALL formats for clipboard injection.
#### `client.clip(...)`
```js
client.clip(document.getElementById("d_clip_button"))
client.clip(document.querySelectorAll(".clip_button"));
client.clip(jQuery(".clip_button"));
```
_[`this`]_ Register clipboard actions for new element(s) to the client. This includes
automatically invoking `ZeroClipboard.focus` on the current element when it is hovered over,
unless the `autoActivate` configuration property is set to `false`.
#### `client.unclip(...)`
```js
client.unclip(document.getElementById("d_clip_button"))
client.unclip(document.querySelectorAll(".clip_button"));
client.unclip(jQuery(".clip_button"));
client.unclip();
```
_[`this`]_ Unregister the clipboard actions of previously registered element(s) on the page.
If no elements are provided, ALL clipped/registered elements will be unregistered.
#### `client.elements()`
```js
var els = client.elements();
```
_[`Array`]_ Get all of the elements to which this client is clipped/registered.
#### `client.on(...)`
```js
var listenerFn = function(e) { var client = this; /* ... */ };
client.on("ready", listenerFn);
var listenerObj = {
handleEvent: function(e) { var listenerObj = this; /* ... */ }
};
client.on("error", listenerObj);
```
_[`this`]_ Add a `listener` function/object for an `eventType` within this client instance.
```js
client.on("ready error", function(e) { /* ... */ });
```
_[`this`]_ Add a `listener` function/object for multiple `eventType`s within this client instance.
```js
client.on({
"ready": function(e) { /* ... */ },
"error": function(e) { /* ... */ }
});
```
_[`this`]_ Add a set of `eventType` to `listener` function/object mappings within this client instance.
#### `client.off(...)`
```js
client.off("ready", listenerFn);
client.off("error", listenerObj);
```
_[`this`]_ Remove a `listener` function/object for an `eventType` within this client instance.
```js
client.off("ready error", listenerFn);
```
_[`this`]_ Remove a `listener` function/object for multiple `eventType`s within this client instance.
```js
client.off({
"ready": readyListenerFn,
"error": errorListenerFn
});
```
_[`this`]_ Remove a set of `eventType` to `listener` function/object mappings within this client instance.
```js
client.off("ready");
```
_[`this`]_ Remove ALL listener functions/objects for an `eventType` within this client instance.
```js
client.off();
```
_[`this`]_ Remove ALL listener functions/objects for ALL registered event types within this client instance.
#### `client.emit(...)`
```js
client.emit("ready");
client.emit({
type: "error",
name: "flash-disabled"
});
```
_[`undefined`]_ Dispatch an event to all registered listeners within this client instance.
#### `client.handlers(...)`
```js
var listeners = client.handlers("ready");
```
_[`Array`]_ Retrieves a copy of the registered listener functions/objects for the given `eventType` within this client instance.
```js
var listeners = client.handlers();
```
_[`Object`]_ Retrieves a copy of the map of registered listener functions/objects for ALL event types within this client instance.
### Instance Events
See the [Static Events](#static-events) listing. The ZeroClipboard client instances regurgitate the `ZeroClipboard.Core` events, ONLY if the event is NOT impertinent to this particular client. The only difference is that the clients' event dispatching will update the `event` object to include a `client` property that references the relevant client instance, e.g.:
```js
var client = new ZeroClipboard();
client.on("ready", function(e) {
if (e.client === client && client === this) {
console.log("This client instance is ready!");
}
});
```
## Configuration Options
These are default values for the global configurations options. You should generally update these _before_ you create your clients.
```js
var _globalConfig = {
// SWF URL, relative to the page. Default value will be "ZeroClipboard.swf"
// under the same path as the ZeroClipboard JS file.
swfPath: _swfPath,
// SWF inbound scripting policy: page domains that the SWF should trust.
// (single string, or array of strings)
trustedDomains: window.location.host ? [window.location.host] : [],
// Include a "noCache" query parameter on requests for the SWF.
cacheBust: true,
// Enable use of the fancy "Desktop" clipboard, even on Linux where it is
// known to suck.
forceEnhancedClipboard: false,
// How many milliseconds to wait for the Flash SWF to load and respond before assuming that
// Flash is deactivated (e.g. click-to-play) in the user's browser. If you don't care about
// how long it takes to load the SWF, you can set this to `null`.
flashLoadTimeout: 30000,
// Setting this to `false` would allow users to handle calling `ZeroClipboard.focus(...);`
// themselves instead of relying on our per-element `mouseover` handler.
autoActivate: true,
// Bubble synthetic events in JavaScript after they are received by the Flash object.
bubbleEvents: true,
// Sets the ID of the `div` encapsulating the Flash object.
// Value is validated against the [HTML4 spec for `ID` tokens][valid_ids].
containerId: "global-zeroclipboard-html-bridge",
// Sets the class of the `div` encapsulating the Flash object.
containerClass: "global-zeroclipboard-container",
// Sets the ID and name of the Flash `object` element.
// Value is validated against the [HTML4 spec for `ID` and `Name` tokens][valid_ids].
swfObjectId: "global-zeroclipboard-flash-bridge",
// The class used to indicate that a clipped element is being hovered over.
hoverClass: "zeroclipboard-is-hover",
// The class used to indicate that a clipped element is active (is being clicked).
activeClass: "zeroclipboard-is-active",
// Forcibly set the hand cursor ("pointer") for all clipped elements.
// IMPORTANT: This configuration value CAN be modified while a SWF is actively embedded.
forceHandCursor: false,
// Sets the title of the `div` encapsulating the Flash object.
// IMPORTANT: This configuration value CAN be modified while a SWF is actively embedded.
title: null,
// The z-index used by the Flash object.
// Max value (32-bit): 2147483647.
// IMPORTANT: This configuration value CAN be modified while a SWF is actively embedded.
zIndex: 999999999
};
```
You can override the defaults by making calls like `ZeroClipboard.config({ swfPath: "new/path" });`
before you create any clients.
### SWF Inbound Scripting Access: The `trustedDomains` option
This allows other SWF files and HTML pages from the allowed domains to access/call publicly
exposed ActionScript code, e.g. functions shared via `ExternalInterface.addCallback`. In other
words, it controls the SWF inbound scripting access.
If your ZeroClipboard SWF is served from a different origin/domain than your page, you need to tell
the SWF that it's OK to trust your page. The default value of `[window.location.host]` is almost
_**always**_ what you will want unless you specifically want the SWF to communicate with pages from
other domains (e.g. in `iframe`s or child windows).
For more information about trusted domains, consult the [_official Flash documentation for `flash.system.Security.allowDomain(...)`_](http://help.adobe.com/en_US/FlashPlatform/reference/actionscript/3/flash/system/Security.html#allowDomain\(\)).
### SWF Outbound Scripting Access
The `allowScriptAccess` parameter (for Flash embedding markup) allows the SWF file to access/call
JavaScript/HTML functionality of HTML pages on allowed domains, e.g. invoking functions via
`ExternalInterface.call`. In other words, it controls the SWF outbound scripting access.
As of version `v2.0.0-alpha.2`, the `allowScriptAccess` configuration option no longer exists. The
appropriate value will be determined immediately before the Flash object is embedded on the page.
The value is based on a relationship between the current domain (`window.location.host`) and the
value of the `trustedDomains` configuration option.
For more information about `allowScriptAccess`, consult the [_official Flash documentation_](http://helpx.adobe.com/flash/kb/control-access-scripts-host-web.html).
### Cross-Protocol Limitations
ZeroClipboard was intentionally configured to _not_ allow the SWF to be served from a secure domain (HTTPS) but scripted by an insecure domain (HTTP).
If you find yourself in this situation (as in [Issue #170](https://github.com/zeroclipboard/zeroclipboard/issues/170)), please consider the following options:
1. Serve the SWF over HTTP instead of HTTPS. If the page's protocol can vary (e.g. authorized/unauthorized, staging/production, etc.), you should include add the SWF with a relative protocol (`//s3.amazonaws.com/blah/ZeroClipboard.swf`) instead of an absolute protocol (`https://s3.amazonaws.com/blah/ZeroClipboard.swf`).
2. Serve the page over HTTPS instead of HTTP. If the page's protocol can vary, see the note on the previous option (1).
3. Update ZeroClipboard's ActionScript codebase to call the [`allowInsecureDomain`](http://help.adobe.com/en_US/FlashPlatform/reference/actionscript/3/flash/system/Security.html#allowInsecureDomain\(\)) method, then recompile the SWF with your custom changes.
### `file://` Protocol Limitations
If you want to host a page locally on the `file://` protocol, you must specifically configure
ZeroClipboard to trust ALL domains for SWF interaction via a wildcard. This configuration must be
set _before_ creating ZeroClipboard client instances as a typical consumer, or before calling
`ZeroClipboard.create()` in a 3rd party wrapper:
```js
ZeroClipboard.config({ trustedDomains: ["*"] });
```
This wildcard configuration should _**NOT**_ be used in environments hosted over HTTP/HTTPS.

View File

@@ -0,0 +1,439 @@
### WARNING
**This `master` branch contains the `v2.x` codebase for ZeroClipboard! For the `v1.x` codebase, see the [`1.x-master`](https://github.com/zeroclipboard/zeroclipboard/tree/1.x-master) branch instead.**
# Overview
The ZeroClipboard library provides an easy way to copy text to the clipboard using an invisible [Adobe Flash](http://en.wikipedia.org/wiki/Adobe_Flash) movie and a [JavaScript](http://en.wikipedia.org/wiki/JavaScript) interface. The "Zero" signifies that the library is invisible and the user interface is left entirely up to you.
This is achieved by automatically floating the invisible movie on top of a [DOM](http://en.wikipedia.org/wiki/Document_Object_Model) element of your choice. Standard mouse events are even propagated out to your DOM element, so you can still have rollover and mousedown effects.
## Limitations
### User Interaction Required
Due to browser and Flash security restrictions, this clipboard injection can _**ONLY**_ occur when
the user clicks on the invisible Flash movie. A simulated `click` event from JavaScript will not
suffice as this would enable [clipboard poisoning](http://www.computerworld.com/s/article/9117268/Adobe_patches_Flash_clickjacking_and_clipboard_poisoning_bugs).
### Synchronicity Required During `copy`
If a handler of `copy` event intends to modify the pending data for clipboard
injection, it _MUST_ operate synchronously in order to maintain the temporarily elevated
permissions granted by the user's `click` event. The most common "gotcha" for this restriction is
if someone wants to make an asynchronous XMLHttpRequest in response to the `copy` event to get the
data to inject &mdash; this will not work. You must make it a _synchronous_ XMLHttpRequest instead, or do the
work in advance before the `copy` event is fired.
### OS-Specific Limitations
See [OS Considerations](#os-considerations) below.
## Installation
### [NPM](https://www.npmjs.org/) [![NPM version](https://badge.fury.io/js/zeroclipboard.png)](https://www.npmjs.org/package/zeroclipboard)
```shell
npm install zeroclipboard
```
### [Bower](http://bower.io/) [![Bower version](https://badge.fury.io/bo/zeroclipboard.png)](http://bower.io/search/?q=zeroclipboard)
```shell
bower install zeroclipboard
```
### [SPM](http://spmjs.io/) [![SPM version](http://spmjs.io/badge/zeroclipboard)](http://spmjs.io/package/zeroclipboard)
```shell
spm install zeroclipboard
```
### [PHP Composer](https://getcomposer.org/) [![PHP version](https://badge.fury.io/ph/zeroclipboard%2Fzeroclipboard.svg)](https://packagist.org/packages/zeroclipboard/zeroclipboard)
For any PHP Composer users, ZeroClipboard is also [available on Packagist](https://packagist.org/packages/zeroclipboard/zeroclipboard).
### [Ruby Gem](https://rubygems.org/)
For any Rails users, the [`zeroclipboard-rails` Ruby Gem](https://rubygems.org/gems/zeroclipboard-rails) is available to automatically add ZeroClipboard into your Rails asset pipeline.
## CDN Availability
If you'd like to use ZeroClipboard hosted via a CDN (content delivery network), you can try:
- **cdnjs**: http://cdnjs.com/libraries/zeroclipboard
- **jsDelivr**: http://www.jsdelivr.com/#!zeroclipboard
## Setup
To use the library, simply include the following JavaScript file in your page:
```html
<script type="text/javascript" src="ZeroClipboard.js"></script>
```
You also need to have the "`ZeroClipboard.swf`" file available to the browser. If this file is
located in the same directory as your web page, then it will work out of the box. However, if the
SWF file is hosted elsewhere, you need to set the URL like this (place this code _after_ the script
tag):
```js
ZeroClipboard.config( { swfPath: "http://YOURSERVER/path/ZeroClipboard.swf" } );
```
## Clients
Now you are ready to create one or more _clients_. A client is a single instance of the clipboard
library on the page, linked to one or more DOM elements. Here is how to create a client instance:
```js
var client = new ZeroClipboard();
```
You can also include an element or array of elements in the new client. _\*\*This example uses jQuery to find "copy buttons"._
```js
var client = new ZeroClipboard($(".copy-button"));
```
## API
For the full API documentation, see [api/ZeroClipboard.md](api/ZeroClipboard.md). The full set of
[Configuration Options](api/ZeroClipboard.md#configuration-options) are also documented there.
For developers who want to wrap ZeroClipboard into a 3rd party plugin
(e.g. [jquery.zeroclipboard](https://github.com/zeroclipboard/jquery.zeroclipboard)),
see the [api/ZeroClipboard.Core.md](api/ZeroClipboard.Core.md) documentation instead.
### Text To Copy
Setting the clipboard text can be done in 4 ways:
1. Add a `copy` event handler in which you call `event.clipboardData.setData` to set the appropriate data. This event is triggered every time ZeroClipboard tries to inject into the clipboard. Example:
```js
client.on( "copy", function (event) {
var clipboard = event.clipboardData;
clipboard.setData( "text/plain", "Copy me!" );
clipboard.setData( "text/html", "<b>Copy me!</b>" );
clipboard.setData( "application/rtf", "{\\rtf1\\ansi\n{\\b Copy me!}}" );
});
```
2. Set the "text/plain" [and _usually_ "text/html"] clipboard segments via `data-clipboard-target` attribute on the button. ZeroClipboard will look for the target element via ID and try to get the HTML value via `.value`, `.outerHTML`, or `.innerHTML`, and the text value via `.value`, `.textContent`, or `.innerText`. If the HTML and text values for the targeted element match, the value will only be placed into the "text/plain" segment of the clipboard (i.e. the "text/html" segment will cleared).
```html
<button id="my-button_text" data-clipboard-target="clipboard_text">Copy to Clipboard</button>
<button id="my-button_textarea" data-clipboard-target="clipboard_textarea">Copy to Clipboard</button>
<button id="my-button_pre" data-clipboard-target="clipboard_pre">Copy to Clipboard</button>
<input type="text" id="clipboard_text" value="Clipboard Text"/>
<textarea id="clipboard_textarea">Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod
tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam,
quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo
consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse
cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non
proident, sunt in culpa qui officia deserunt mollit anim id est laborum.</textarea>
<pre id="clipboard_pre">Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod
tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam,
quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo
consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse
cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non
proident, sunt in culpa qui officia deserunt mollit anim id est laborum.</pre>
```
3. Set the "text/plain" clipboard segment via `data-clipboard-text` attribute on the button. Doing this will let ZeroClipboard take care of the rest.
```html
<button id="my-button" data-clipboard-text="Copy me!">Copy to Clipboard</button>
```
4. Set the data via the `ZeroClipboard.setData` (any segment) method. You can call this function at any time: when the page first loads, or later like in a `copy` event handler. Example:
```js
ZeroClipboard.setData( "text/plain", "Copy me!" );
```
The important caveat of using `ZeroClipboard.setData` is that the data it sets is **transient** and _will only be used for a single copy operation_. As such, we do not particularly
recommend using `ZeroClipboard.setData` (and friends) other than inside of a `copy` event handler; however, the API will not prevent you from using it in other ways.
5. Set the data via the `client.setText` ("text/plain" segment), `client.setHtml` ("text/html" segment), `client.setRichText` ("application/rtf" segment), or `client.setData` (any segment) methods. You can call this function at any time: when the page first loads, or later like in a `copy` event handler. Example:
```js
client.setText( "Copy me!" );
```
The important caveat of using `client.setData` (and friends) is that the data it sets is **transient** and _will only be used for a single copy operation_. As such, we do not particularly
recommend using `client.setData` (and friends) other than inside of a `copy` event handler; however, the API will not prevent you from using it in other ways.
### Clipping
Clipping refers to the process of "linking" the Flash movie to a DOM element on the page. Since the Flash movie is completely transparent, the user sees nothing out of the ordinary.
The Flash movie receives the click event and copies the text to the clipboard. Also, mouse actions like hovering and `mousedown` generate events that you can capture (see [_Event Handlers_](#event-handlers) below).
To clip elements, you must pass an element, or array of elements to the `clip` function.
Here is how to clip your client library instance to a DOM element:
```js
client.clip( document.getElementById("d_clip_button") );
```
You can pass in a reference to the actual DOM element object itself or an array of DOM objects. The rest all happens automatically: the movie is created, all your options set, and it is floated above the element, awaiting clicks from the user.
### Example Implementation
```html
<button id="my-button" data-clipboard-text="Copy me!" title="Click to copy to clipboard.">Copy to Clipboard</button>
```
And the code:
```js
var client = new ZeroClipboard( $("button#my-button") );
```
## CSS Effects
Since the Flash movie is floating on top of your DOM element, it will receive all the mouse events before the browser has a chance to catch them. However, for convenience, these events are passed through to your clipboard client which you can capture (see _Event Handlers_ below), so long as the `bubbleEvents` configuration property remains set to `true`.
In addition to this, ZeroClipboard can also manage CSS classes on the clipped elements that mimic the CSS pseudo-classes ":hover" and ":active" on your DOM element. This essentially allows your elements to behave normally, even though the floating Flash movie is the first object receiving all the mouse events during the event bubbling phase. These "pseudo-pseudo-class" names are configurable via the `hoverClass` and `activeClass` configuration properties.
Example CSS, targeting a DOM element with a class of "clip_button":
```css
.clip_button {
width: 150px;
text-align: center;
border: 1px solid black;
background-color: #ccc;
margin: 10px;
padding: 10px;
}
.clip_button.zeroclipboard-is-hover { background-color: #eee; }
.clip_button.zeroclipboard-is-active { background-color: #aaa; }
```
## Examples
The following are complete, working examples of using the clipboard client library in HTML pages.
### Minimal Example
Here is a quick example using as few calls as possible:
```html
<html>
<body>
<div id="d_clip_button" class="clip_button" data-clipboard-text="Copy Me!" title="Click to copy." style="border:1px solid black; padding:20px;">Copy To Clipboard</div>
<script type="text/javascript" src="ZeroClipboard.js"></script>
<script type="text/javascript">
var client = new ZeroClipboard( document.getElementById('d_clip_button') );
</script>
</body>
</html>
```
When clicked, the text "Copy me!" will be copied to the clipboard.
### A More Complete Example
Here is a more complete example which exercises many of the configuration options and event handlers:
```html
<html>
<head>
<style type="text/css">
.clip_button {
text-align: center;
border: 1px solid black;
background-color: #ccc;
margin: 10px;
padding: 10px;
}
.clip_button.zeroclipboard-is-hover { background-color: #eee; }
.clip_button.zeroclipboard-is-active { background-color: #aaa; }
</style>
</head>
<body>
<script type="text/javascript" src="//ajax.googleapis.com/ajax/libs/jquery/1.10.2/jquery.min.js"></script>
<script type="text/javascript" src="ZeroClipboard.js"></script>
<div class="clip_button">Copy To Clipboard</div>
<div class="clip_button">Copy This Too!</div>
<script type="text/javascript">
var client = new ZeroClipboard( $('.clip_button') );
client.on( 'ready', function(event) {
// console.log( 'movie is loaded' );
client.on( 'copy', function(event) {
event.clipboardData.setData('text/plain', event.target.innerHTML);
} );
client.on( 'aftercopy', function(event) {
console.log('Copied text to clipboard: ' + event.data['text/plain']);
} );
} );
client.on( 'error', function(event) {
// console.log( 'ZeroClipboard error of type "' + event.name + '": ' + event.message );
ZeroClipboard.destroy();
} );
</script>
</body>
</html>
```
## Namespacing ZeroClipboard
ZeroClipboard creates DOM elements with pre-configured attributes, e.g. a `div` element with an ID of `"global-zeroclipboard-html-bridge"` to encapsulate the Flash object.
If you have a need to change the default values, they can be configured by passing in values for `containerId`, `containerClass`, and/or `swfObjectId` using the `ZeroClipboard.config` method. Configuration of these values is completely optional. These values cannot be reconfigured while the ZeroClipboard SWF is actively embedded, and so are completely ignored during that time.
Values for `containerId` and `swfObjectId` are validated against the [HTML4 spec for `ID` and `Name` tokens][valid_ids].
## AMD
If using [AMD](https://github.com/amdjs/amdjs-api/wiki/AMD) with a library such as [RequireJS](http://requirejs.org/), etc., you shouldn't need to do any special configuration for ZeroClipboard to work correctly as an AMD module.
## CommonJS
If using [CommonJS](http://wiki.commonjs.org/wiki/Modules/1.1) with a library such as [Browserify](http://browserify.org/), [Webmake](https://github.com/medikoo/modules-webmake), etc., you shouldn't need to do any special configuration for ZeroClipboard to work correctly as an CommonJS module.
## Known Conflicts With Other Libraries
### [IE freezes when clicking a ZeroClipboard clipped element within a Bootstrap Modal](https://github.com/zeroclipboard/zeroclipboard/issues/159).
- **Cause:** Bootstrap's Modal has an `enforceFocus` function that tries to keep the focus on the modal.
However, since the ZeroClipboard container is an immediate child of the `body`, this enforcement conflicts. Note that
this workaround actually _overrides_ a core Bootstrap Modal function, and as such must be kept in sync as this function
changes in future versions of Bootstrap.
- **Workaround:** _Targeted against [Bootstrap v3.x](https://github.com/twbs/bootstrap/blob/96a9e1bae06cb21f8cf72ec528b8e31b6ab27272/js/modal.js#L115-123)._
#### Workaround A
```js
if (/MSIE|Trident/.test(window.navigator.userAgent)) {
(function($) {
var zcContainerId = ZeroClipboard.config('containerId');
$('#' + zcContainerId).on('focusin', false);
})(window.jQuery);
}
```
#### Workaround B
```js
if (/MSIE|Trident/.test(window.navigator.userAgent)) {
(function($) {
var zcClass = '.' + ZeroClipboard.config('containerClass');
var proto = $.fn.modal.Constructor.prototype;
proto.enforceFocus = function() {
$(document)
.off('focusin.bs.modal') /* Guard against infinite focus loop */
.on('focusin.bs.modal', $.proxy(function(e) {
if (this.$element[0] !== e.target &&
!this.$element.has(e.target).length &&
/* Adding this final condition check is the only real change */
!$(e.target).closest(zcClass).length
) {
this.$element.focus();
}
}, this));
};
})(window.jQuery);
}
```
### [IE freezes when clicking a ZeroClipboard clipped element within a jQuery UI [Modal] Dialog](https://github.com/zeroclipboard/zeroclipboard/issues/159).
- **Cause:** jQuery UI's Dialog (with `{ modal: true }` set) has a `_keepFocus` function that tries to keep the focus on the modal.
However, since the ZeroClipboard container is an immediate child of the `body`, this enforcement conflicts. Luckily, jQuery UI offers
more natural extension points than Bootstrap, so the workaround is smaller and less likely to be broken in future versions.
- **Workaround:** _Targeted against [jQuery UI v1.10.x](https://github.com/jquery/jquery-ui/blob/457b275880b63b05b16b7c9ee6c22f29f682ebc8/ui/jquery.ui.dialog.js#L695-703)._
```js
if (/MSIE|Trident/.test(window.navigator.userAgent)) {
(function($) {
var zcClass = '.' + ZeroClipboard.config('containerClass');
$.widget( 'ui.dialog', $.ui.dialog, {
_allowInteraction: function( event ) {
return this._super(event) || $( event.target ).closest( zcClass ).length;
}
} );
})(window.jQuery);
}
```
## Support
This library is fully compatible with Flash Player 11.0.0 and above, which requires
that the clipboard copy operation be initiated by a user click event inside the
Flash movie. This is achieved by automatically floating the invisible movie on top
of a [DOM](http://en.wikipedia.org/wiki/Document_Object_Model) element of your
choice. Standard mouse events are even propagated out to your DOM element, so you
can still have rollover and mousedown effects with just a _little_ extra effort.
ZeroClipboard `v2.x` is expected to work in IE9+ and all of the evergreen browsers.
## OS Considerations
Because ZeroClipboard will be interacting with your users' system clipboards, there are some special considerations
specific to the users' operating systems that you should be aware of. With this information, you can make informed
decisions of how _your_ site should handle each of these situations.
- **Windows:**
- If you want to ensure that your Windows users will be able to paste their copied text into Windows
Notepad and have it honor line breaks, you'll need to ensure that the text uses the sequence `\r\n` instead of
just `\n` for line breaks. If the text to copy is based on user input (e.g. a `textarea`), then you can achieve
this transformation by utilizing the `copy` event handler, e.g.
```js
client.on('copy', function(event) {
var text = document.getElementById('yourTextArea').value;
var windowsText = text.replace(/\n/g, '\r\n');
event.clipboardData.setData('text/plain', windowsText);
});
```
- **Linux:**
- The Linux Clipboard system (a.k.a. "Selection Atoms" within the [X Consortium's Standard Inter-Client Communication Conventions Manual](http://www.x.org/docs/ICCCM/icccm.pdf)) is a complex but capable setup. However,
for normal end users, it stinks. Flash Player's Clipboard API can either:
1. Insert plain text into the "System Clipboard" and have it available everywhere; or
2. Insert plain, HTML, and RTF text into the "Desktop Clipboard" but it will only be available in applications whose UI are managed by the Desktop Manager System (e.g. GNOME, etc.). This, for example, means that a user on a typical Linux configuration would not be able to paste something copied with ZeroClipboard into a terminal shell but they may still be able to paste it into OpenOffice, the browser, etc.
As such, the default behavior for ZeroClipboard while running on Linux is to only inject plain text into the "System Clipboard" to cover the most bases. If you want to ignore that caution and use the "Desktop Clipboard" anyway, just set the `forceEnhancedClipboard` configuration option to `true`, i.e.:
```js
ZeroClipboard.config({
forceEnhancedClipboard: true
});
```
[valid_ids]: http://www.w3.org/TR/html4/types.html#type-id "HTML4 specification for `ID` and `Name` tokens"

View File

@@ -0,0 +1,9 @@
# Roadmap
These are things that we have expressed interest in but haven't implemented yet. There is no order, if you feel like you can complete one of the tasks. Feel free to fork the project and send a pull request with the new code.
## HTML5 Clipboard API
In a perfect world we wouldn't need ZeroClipboard, and the browsers would just take care of it. We would like to write ZeroClipboard to use the browser's clipboard API when available. See Issues [#171](https://github.com/zeroclipboard/zeroclipboard/issues/171) and [~~#105~~](https://github.com/zeroclipboard/zeroclipboard/issues/105).
## Flash Tests
We want to setup a unit test suite for the Flash SWF and/or its underlying ActionScript files. See Issue [#43](https://github.com/zeroclipboard/zeroclipboard/issues/43).

View File

@@ -0,0 +1,32 @@
# Security
We try our best to keep ZeroClipboard secure but there are some rules that you should consider following to keep your site safe.
## Existing Configuration
For the existing configuration options available for security, see [Configuration Options](api/ZeroClipboard.md#configuration-options).
## Rules
Basically, if an attacker gets access to the main window/global object via an XSS exploit, it's pretty much an instant "GAME OVER" unless **ALL** of the following are true:
1. The `ZeroClipboard` object itself is not globally accessible.
2. The `ZeroClipboard.prototype` object itself is not globally accessible.
3. No `ZeroClipboard` instances are globally accessible.
4. No callback functions for dispatched ZeroClipboard events are globally accessible.
5. If a variable is used to set the path to the SWF via `ZeroClipboard.config`, that variable must not be globally accessible.
6. The DOM is not accessible (due to built-in support for `data-clipboard-text` and `data-clipboard-target` attributes).
## Examples
1. Having `ZeroClipboard` instances globally accessible (versus encapsulated in a closure). This allows an attacker to manually call a client's `setText` method and inject their own text.
2. As with all globally accessible functions in JavaScript, any globally accessible callback functions (hooked to events) can be overridden by an attacker. This isn't terribly dangerous but could be annoying.
3. Overriding any of the `ZeroClipboard` or `ZeroClipboard.prototype` properties or methods, if globally accessible.
4. Adding `data-clipboard-text` or `data-clipboard-target` attributes to every element in the DOM.
### Responsible Disclosure
If you find any security holes that you believe can be patched, please submit a pull request or file an issue. We will be very appreciative!

View File

@@ -0,0 +1,29 @@
/*jshint node:true */
// Module exports
exports = module.exports = setup;
// Module dependencies
var http = require("http");
var send = require("send");
var root = __dirname;
var swf = "/ZeroClipboard.swf";
function setup() {
return http.createServer(onReq);
}
function onReq(req, res) {
send(req, swf)
.root(root)
.on("error", onError)
.pipe(res);
}
function onError(err) {
res.statusCode = err.status || 500;
res.end(err.message);
}

View File

@@ -0,0 +1,83 @@
{
"name": "zeroclipboard",
"title": "ZeroClipboard",
"version": "2.1.6",
"description": "The ZeroClipboard library provides an easy way to copy text to the clipboard using an invisible Adobe Flash movie and a JavaScript interface.",
"keywords": [
"flash",
"clipboard",
"copy",
"cut",
"paste",
"zclip",
"clip",
"clippy"
],
"homepage": "http://zeroclipboard.org/",
"licenses": [
{
"type": "MIT",
"url": "https://github.com/zeroclipboard/zeroclipboard/blob/master/LICENSE"
}
],
"contributors": [
{
"name": "Jon Rohan",
"url": "http://jonrohan.me/"
},
{
"name": "James M. Greene",
"email": "james.m.greene@gmail.com",
"url": "http://jamesgreene.net/"
}
],
"repository": {
"type": "git",
"url": "https://github.com/zeroclipboard/zeroclipboard.git"
},
"bugs": {
"url": "https://github.com/zeroclipboard/zeroclipboard/issues"
},
"dependencies": {
"send": "0"
},
"devDependencies": {
"flex-sdk": "~4.6.0-0",
"flexpmd": "^1.3.0-1",
"grunt": "^0.4.5",
"grunt-chmod": "^1.0.3",
"grunt-contrib-clean": "^0.5.0",
"grunt-contrib-concat": "^0.4.0",
"grunt-contrib-connect": "^0.8.0",
"grunt-contrib-jshint": "^0.10.0",
"grunt-contrib-uglify": "^0.5.0",
"grunt-contrib-watch": "^0.6.1",
"grunt-coveralls": "^0.3.0",
"grunt-flexpmd": "^0.1.2",
"grunt-mxmlc": "^0.5.1",
"grunt-qunit-istanbul": "^0.4.5",
"grunt-template": "^0.2.3",
"jquery": "^2.1.1",
"load-grunt-tasks": "^0.6.0",
"qunit-composite": "^1.0.1",
"qunitjs": "^1.14.0",
"spm": "^3.0.1"
},
"main": "./dist/ZeroClipboard.js",
"component": {
"scripts": {
"zeroclipboard": "./dist/ZeroClipboard.js"
}
},
"spm": {
"main": "dist/ZeroClipboard.js",
"output": [
"dist/ZeroClipboard.swf",
"dist/ZeroClipboard.Core.js"
]
},
"scripts": {
"test": "grunt travis --verbose",
"postpublish": "spm publish"
}
}

View File

@@ -0,0 +1,185 @@
package {
import flash.system.Capabilities;
import flash.system.System;
import flash.desktop.Clipboard;
import flash.desktop.ClipboardFormats;
import flash.utils.ByteArray;
/**
* An abstraction for injecting data into the user's clipboard.
*/
internal class ClipboardInjector {
/**
* Use the fancy "Desktop" clipboard for expanded text support (e.g. HTML, RTF, etc.) if not on Linux
*/
private var useEnhancedClipboard:Boolean = Capabilities.os.slice(0, 5).toLowerCase() !== "linux";
/**
* @constructor
*/
public function ClipboardInjector(forceEnhancedClipboard:Boolean = false) {
// The JIT Compiler does not compile constructors, so any
// cyclomatic complexity higher than 1 is discouraged.
this.ctor(forceEnhancedClipboard);
}
/**
* The real constructor.
*
* @return `undefined`
*/
private function ctor(forceEnhancedClipboard:Boolean = false): void {
// Should we use the fancy "Desktop" clipboard for expanded text support (e.g. HTML, RTF, etc.)?
this.useEnhancedClipboard = this.useEnhancedClipboard || forceEnhancedClipboard;
}
/**
* Inject data into the user's clipboard.
*
* @return A clipboard "results" object
*/
public function inject(
clipData:Object // NOPMD
): Object { // NOPMD
var results:Object = {}; // NOPMD
// Set all data formats' results to `false` (failed) initially
for (var dataFormat:String in clipData) {
if (dataFormat && clipData.hasOwnProperty(dataFormat)) {
results[dataFormat] = false;
}
}
// If there is any viable data to copy...
if (ClipboardInjector.hasData(clipData)) {
// ...and we only need to handle plain text...
if (!this.useEnhancedClipboard || ClipboardInjector.hasOnlyPlainText(clipData)) {
this.injectPlainTextOnly(clipData, results);
}
// ...else if there is viable data to copy and we can copy enhanced formats
else if (this.useEnhancedClipboard) {
this.injectEnhancedData(clipData, results);
}
}
return results;
}
/**
* Inject plain text into the System clipboard (i.e. Flash 9+ Clipboard).
*
* @return `undefined`
*/
private function injectPlainTextOnly(
clipData:Object, // NOPMD
results:Object // NOPMD
): void {
// Linux currently doesn't use the correct clipboard buffer with the new
// Flash 10 API, so we need to use this until we can figure out an alternative
try {
System.setClipboard(clipData.text);
results.text = true;
}
catch (e:Error) {
// Yes, this is already set but FlexPMD complains about empty `catch` blocks
results.text = false;
}
}
/**
* Inject plain text, HTML, and RTF into the Desktop clipboard (i.e. Flash 10+ Clipboard).
*
* @return `undefined`
*/
private function injectEnhancedData(
clipData:Object, // NOPMD
results:Object // NOPMD
): void {
// Clear out the clipboard before starting to copy data
Clipboard.generalClipboard.clear();
//
// Handle each data type in succession...
//
// Plain text
if (typeof clipData.text === "string" && clipData.text) {
try {
results.text = Clipboard.generalClipboard.setData(ClipboardFormats.TEXT_FORMAT, clipData.text);
}
catch (e:Error) {
results.text = false;
}
}
// HTML
if (typeof clipData.html === "string" && clipData.html) {
try {
results.html = Clipboard.generalClipboard.setData(ClipboardFormats.HTML_FORMAT, clipData.html);
}
catch (e:Error) {
results.html = false;
}
}
// Rich Text (RTF)
if (typeof clipData.rtf === "string" && clipData.rtf) {
try {
var bytes:ByteArray = new ByteArray();
bytes.writeUTFBytes(clipData.rtf);
if (bytes && bytes.length > 0) {
results.rtf = Clipboard.generalClipboard.setData(ClipboardFormats.RICH_TEXT_FORMAT, bytes);
}
}
catch (e:Error) {
results.rtf = false;
}
}
}
/**
* Check if data object contains any keys with associated values that are non-empty Strings.
*
* @return Boolean
*/
private static function hasData(
clipData:Object // NOPMD
): Boolean {
return typeof clipData === "object" && clipData &&
(
(typeof clipData.text === "string" && clipData.text) ||
(typeof clipData.html === "string" && clipData.html) ||
(typeof clipData.rtf === "string" && clipData.rtf )
);
}
/**
* Check if a data object's ONLY injectable data is plain text.
*
* @return Boolean
*/
private static function hasOnlyPlainText(
clipData:Object // NOPMD
): Boolean {
var hasPlainText:Boolean = false;
var hasOtherTypes:Boolean = false;
if (typeof clipData === "object" && clipData) {
hasPlainText = typeof clipData.text === "string" && clipData.text;
hasOtherTypes = (
(typeof clipData.html === "string" && clipData.html) ||
(typeof clipData.rtf === "string" && clipData.rtf )
);
}
return !hasOtherTypes && hasPlainText;
}
}
}

View File

@@ -0,0 +1,140 @@
package {
import flash.external.ExternalInterface;
import flash.net.navigateToURL;
import flash.net.URLRequest;
/**
* An abstraction for communicating with JavaScript from Flash.
*/
internal class JsProxy {
private var hosted:Boolean = false;
private var bidirectional:Boolean = false;
private var disabled:Boolean = false;
/**
* @constructor
*/
public function JsProxy(expectedObjectId:String = null) {
// The JIT Compiler does not compile constructors, so any
// cyclomatic complexity higher than 1 is discouraged.
this.ctor(expectedObjectId);
}
/**
* The real constructor.
*
* @return `undefined`
*/
private function ctor(expectedObjectId:String = null): void {
// Do we authoritatively know that this Flash object is hosted in a browser?
this.hosted = ExternalInterface.available === true &&
ExternalInterface.objectID &&
(expectedObjectId ? (expectedObjectId === ExternalInterface.objectID) : true);
// Can we retrieve values from JavaScript?
// Try this regardless of the return value of `ExternalInterface.call`.
try {
this.bidirectional = ExternalInterface.call("(function() { return true; })") === true;
}
catch (e:Error) {
// We do NOT authoritatively know if this Flash object is hosted in a browser,
// nor if JavaScript is disabled.
this.bidirectional = false;
}
// If hosted but cannot bidirectionally communicate with JavaScript,
// then JavaScript is disabled on the page!
this.disabled = this.hosted && !this.bidirectional;
}
/**
* Are we authoritatively certain that we can execute JavaScript bidirectionally?
*
* @return Boolean
*/
public function isComplete(): Boolean {
return this.hosted && this.bidirectional;
}
/**
* Register an ActionScript method as callable from the container's JavaScript
*
* This will execute the JavaScript ONLY if ExternalInterface is completely
* available (hosted in the browser AND supporting bidirectional communication).
*
* @return `undefined`
*/
public function addCallback(functionName:String, closure:Function): void {
if (this.isComplete()) {
ExternalInterface.addCallback(functionName, closure);
}
}
/**
* Execute a function expression or named function, with optional arguments,
* and receive its return value.
*
* This will execute the JavaScript ONLY if ExternalInterface is completely
* available (hosted in the browser AND supporting bidirectional communication).
*
* @example
* var jsProxy:JsProxy = new JsProxy("global-zeroclipboard-flash-bridge");
* var result:Object = jsProxy.call("ZeroClipboard.emit", [{ type: "copy" }]);
* jsProxy.call("(function(eventObj) { return ZeroClipboard.emit(eventObj); })", [{ type: "ready"}]);
*
* @return `undefined`, or anything
*/
public function call(
jsFuncExpr:String,
args:Array = null
): * { // NOPMD
var result:* = undefined; // NOPMD
if (jsFuncExpr && this.isComplete()) {
if (args == null) {
args = [];
}
result = ExternalInterface.call.apply(ExternalInterface, [jsFuncExpr].concat(args));
}
return result;
}
/**
* Execute a function expression or named function, with optional arguments.
* No return values will ever be received.
*
* This will attempt to execute the JavaScript, even if ExternalInterface is
* not available; in which case: the worst thing that can happen is that
* the JavaScript is not executed (i.e. if JavaScript is disabled, or if
* the SWF is not allowed to communicate with JavaScript on its host page).
*
* @return `undefined`
*/
public function send(jsFuncExpr:String, args:Array = null): void {
if (jsFuncExpr) {
if (this.isComplete()) {
this.call(jsFuncExpr, args);
}
else if (!this.disabled) {
if (args == null) {
args = [];
}
var argsStr:String = "";
for (var counter:int = 0; counter < args.length; counter++) {
argsStr += JSON.stringify(args[counter]);
if ((counter + 1) < args.length) {
argsStr += ", ";
}
}
navigateToURL(new URLRequest("javascript:" + jsFuncExpr + "(" + argsStr + ");"), "_self");
}
}
}
}
}

View File

@@ -0,0 +1,38 @@
package {
/**
* Utility methods for XSS attack prevention.
*/
internal class XssUtils {
/**
* Sanitize a string to avoid XSS vulnerabilities.
*
* @return an XSS safe String
* @static
*/
public static function sanitizeString(dirty:String): String {
return (typeof dirty === "string" && dirty) ? dirty.replace(/\\/g, "\\\\") : "";
}
/**
* Sanitize the Loader parameters by filtering out all URL query parameters,
* leaving ONLY parameters that were specified via FlashVars in the HTML
* embedding markup.
*
* @return a filtered parameters object, a.k.a. FlashVars
* @static
*/
public static function filterToFlashVars(
parameters:Object // NOPMD
): Object { // NOPMD
//
// TODO: Implement this for real
// See: https://github.com/zeroclipboard/zeroclipboard/pull/336
//
return parameters;
}
}
}

View File

@@ -0,0 +1,299 @@
package {
import flash.display.Stage;
import flash.display.StageAlign;
import flash.display.StageScaleMode;
import flash.display.StageQuality;
import flash.display.Sprite;
import flash.events.Event;
import flash.events.MouseEvent;
import flash.system.Security;
/**
* The ZeroClipboard class creates a simple Sprite button that will put
* text in the user's clipboard when clicked.
*/
[SWF(widthPercent="100%", heightPercent="100%", backgroundColor="#FFFFFF")]
public class ZeroClipboard extends Sprite {
/**
* Function through which JavaScript events are emitted. Accounts for scenarios
* in which ZeroClipboard is used via AMD/CommonJS module loaders, too.
*/
private var jsEmitter:String = null;
/**
* JavaScript proxy object.
*/
private var jsProxy:JsProxy = null;
/**
* Clipboard proxy object.
*/
private var clipboard:ClipboardInjector = null;
/**
* @constructor
*/
public function ZeroClipboard() {
// The JIT Compiler does not compile constructors, so ANY
// cyclomatic complexity higher than 1 is discouraged.
this.ctor();
}
/**
* The real constructor.
*
* @return `undefined`
*/
private function ctor(): void {
// If the `stage` is available, begin!
if (stage) {
this.init();
}
else {
// Otherwise, wait for the `stage`....
this.addEventListener(Event.ADDED_TO_STAGE, this.init);
}
}
/**
* Initialize the class when the Stage is ready.
*
* @return `undefined`
*/
private function init(): void {
// Remove the event listener, if any
this.removeEventListener(Event.ADDED_TO_STAGE, this.init);
// Get the flashvars
var flashvars:Object; // NOPMD
flashvars = XssUtils.filterToFlashVars(this.loaderInfo.parameters);
// Configure the SWF object's ID
var swfObjectId:String = "global-zeroclipboard-flash-bridge";
if (flashvars.swfObjectId && typeof flashvars.swfObjectId === "string") {
var swfId = XssUtils.sanitizeString(flashvars.swfObjectId);
// Validate the ID against the HTML4 spec for `ID` tokens.
if (/^[A-Za-z][A-Za-z0-9_:\-\.]*$/.test(swfId)) {
swfObjectId = swfId;
}
}
// Allow the SWF object to communicate with a page on a different origin than its own (e.g. SWF served from CDN)
if (flashvars.trustedOrigins && typeof flashvars.trustedOrigins === "string") {
var origins:Array = XssUtils.sanitizeString(flashvars.trustedOrigins).split(",");
Security.allowDomain.apply(Security, origins);
}
// Enable use of the fancy "Desktop" clipboard, even on Linux where it is known to suck
var forceEnhancedClipboard:Boolean = false;
if (flashvars.forceEnhancedClipboard === "true" || flashvars.forceEnhancedClipboard === true) {
forceEnhancedClipboard = true;
}
this.jsEmitter =
"(function(eventObj) {\n" +
" var objectId = '" + swfObjectId + "',\n" +
" ZC = null,\n" +
" swf = null;\n" +
" if (typeof ZeroClipboard === 'function' && typeof ZeroClipboard.emit === 'function') {\n" +
" \nZC = ZeroClipboard;\n" +
" }\n" +
" else {\n" +
" swf = document[objectId] || document.getElementById(objectId);\n" +
" if (swf && typeof swf.ZeroClipboard === 'function' && typeof swf.ZeroClipboard.emit === 'function') {\n" +
" ZC = swf.ZeroClipboard;\n" +
" }\n" +
" }\n" +
" if (!ZC) {\n" +
" throw new Error('ERROR: ZeroClipboard SWF could not locate ZeroClipboard JS object!\\n" +
"Expected element ID: ' + objectId);\n" +
" }\n" +
" return ZC.emit(eventObj);\n" +
"})";
// Create an invisible "button" and transparently fill the entire Stage
var button:Sprite = this.prepareUI();
// Configure the clipboard injector
this.clipboard = new ClipboardInjector(forceEnhancedClipboard);
// Establish a communication line with JavaScript
this.jsProxy = new JsProxy(swfObjectId);
// Only proceed if this SWF is hosted in the browser as expected
if (this.jsProxy.isComplete()) {
// Add the MouseEvent listeners
this.addMouseHandlers(button);
// Expose the external functions
this.jsProxy.addCallback(
"setHandCursor",
function(enabled:Boolean) {
button.useHandCursor = enabled === true;
}
);
// Signal to the browser that we are ready
this.emit("ready");
}
else {
// Signal to the browser that something is wrong
this.emit("error", {
name: "flash-unavailable"
});
}
}
/**
* Prepare the Stage and Button.
*
* @return Button
*/
private function prepareUI(): Sprite {
// Set the stage!
stage.align = StageAlign.TOP_LEFT;
stage.scaleMode = StageScaleMode.EXACT_FIT;
stage.quality = StageQuality.BEST;
// Create an invisible "button" and transparently fill the entire Stage
var button:Sprite = new Sprite();
button.graphics.beginFill(0xFFFFFF);
button.graphics.drawRect(0, 0, stage.stageWidth, stage.stageHeight);
button.alpha = 0.0;
// Act like a button. This includes:
// - Showing a hand cursor by default
// - Receiving click events
// - Receiving keypress events of space/"Enter" as click
// events IF AND ONLY IF the Sprite is focused.
button.buttonMode = true;
// Override the hand cursor default
button.useHandCursor = false;
// Add the invisible "button" to the stage!
this.addChild(button);
// Return the button for adding event listeners
return button;
}
/**
* Clears the clipboard and sets new clipboard text. It gets this from the "_clipData"
* variable on the JavaScript side. Once the text has been placed in the clipboard, it
* then signals to the JavaScript that it is done.
*
* @return `undefined`
*/
private function onClick(event:MouseEvent): void {
var clipData:Object; // NOPMD
var clipInjectSuccess:Object = {}; // NOPMD
// Allow for any "UI preparation" work before the "copy" event begins
this.emit("beforecopy");
// Request pending clipboard data from the page
clipData = this.emit("copy");
// Inject all pending data into the user's clipboard
clipInjectSuccess = this.clipboard.inject(clipData);
// Compose and serialize a results object, send it back to the page
this.emit(
"aftercopy",
{
success: clipInjectSuccess,
data: clipData
}
);
}
/**
* Emit events to JavaScript.
*
* @return `undefined`, or the new "_clipData" object
*/
private function emit(
eventType:String,
eventObj:Object = null // NOPMD
): Object { // NOPMD
if (eventObj == null) {
eventObj = {};
}
eventObj.type = eventType;
var result:Object = undefined; // NOPMD
if (this.jsProxy.isComplete()) {
result = this.jsProxy.call(this.jsEmitter, [eventObj]);
}
else {
this.jsProxy.send(this.jsEmitter, [eventObj]);
}
return result;
}
/**
* Signals to the page that a MouseEvent occurred.
*
* @return `undefined`
*/
private function onMouseEvent(event:MouseEvent): void {
var evtData:Object = {}; // NOPMD
// If an event is passed in, return what modifier keys are pressed, etc.
if (event) {
var props:Object; // NOPMD
props = {
"altKey": "altKey",
"commandKey": "metaKey",
"controlKey": "ctrlKey",
"shiftKey": "shiftKey",
"clickCount": "detail",
"movementX": "movementX",
"movementY": "movementY",
"stageX": "_stageX",
"stageY": "_stageY"
};
for (var prop in props) {
if (event.hasOwnProperty(prop) && event[prop] != null) {
evtData[props[prop]] = event[prop];
}
}
evtData.type = "_" + event.type.toLowerCase();
evtData._source = "swf";
}
this.emit(evtData.type, evtData);
}
/**
* Add mouse event handlers to the button.
*
* @return `undefined`
*/
private function addMouseHandlers(button:Sprite): Sprite {
button.addEventListener(MouseEvent.MOUSE_MOVE, this.onMouseEvent);
button.addEventListener(MouseEvent.MOUSE_OVER, this.onMouseEvent);
button.addEventListener(MouseEvent.MOUSE_OUT, this.onMouseEvent);
button.addEventListener(MouseEvent.MOUSE_DOWN, this.onMouseEvent);
button.addEventListener(MouseEvent.MOUSE_UP, this.onMouseEvent);
button.addEventListener(MouseEvent.CLICK, this.onClick);
button.addEventListener(MouseEvent.CLICK, this.onMouseEvent);
return button;
}
}
}

View File

@@ -0,0 +1,162 @@
/**
* Creates a new ZeroClipboard client instance.
* Optionally, auto-`clip` an element or collection of elements.
*
* @constructor
*/
ZeroClipboard._createClient = function(/* elements */) {
// Invoke the real constructor
_clientConstructor.apply(this, _args(arguments));
};
/**
* Register an event listener to the client.
*
* @returns `this`
*/
ZeroClipboard.prototype.on = function(/* eventType, listener */) {
return _clientOn.apply(this, _args(arguments));
};
/**
* Unregister an event handler from the client.
* If no `listener` function/object is provided, it will unregister all handlers for the provided `eventType`.
* If no `eventType` is provided, it will unregister all handlers for every event type.
*
* @returns `this`
*/
ZeroClipboard.prototype.off = function(/* eventType, listener */) {
return _clientOff.apply(this, _args(arguments));
};
/**
* Retrieve event listeners for an `eventType` from the client.
* If no `eventType` is provided, it will retrieve all listeners for every event type.
*
* @returns array of listeners for the `eventType`; if no `eventType`, then a map/hash object of listeners for all event types; or `null`
*/
ZeroClipboard.prototype.handlers = function(/* eventType */) {
return _clientListeners.apply(this, _args(arguments));
};
/**
* Event emission receiver from the Flash object for this client's registered JavaScript event listeners.
*
* @returns For the "copy" event, returns the Flash-friendly "clipData" object; otherwise `undefined`.
*/
ZeroClipboard.prototype.emit = function(/* event */) {
return _clientEmit.apply(this, _args(arguments));
};
/**
* Register clipboard actions for new element(s) to the client.
*
* @returns `this`
*/
ZeroClipboard.prototype.clip = function(/* elements */) {
return _clientClip.apply(this, _args(arguments));
};
/**
* Unregister the clipboard actions of previously registered element(s) on the page.
* If no elements are provided, ALL registered elements will be unregistered.
*
* @returns `this`
*/
ZeroClipboard.prototype.unclip = function(/* elements */) {
return _clientUnclip.apply(this, _args(arguments));
};
/**
* Get all of the elements to which this client is clipped.
*
* @returns array of clipped elements
*/
ZeroClipboard.prototype.elements = function() {
return _clientElements.apply(this, _args(arguments));
};
/**
* Self-destruct and clean up everything for a single client.
* This will NOT destroy the embedded Flash object.
*
* @returns `undefined`
*/
ZeroClipboard.prototype.destroy = function() {
return _clientDestroy.apply(this, _args(arguments));
};
/**
* Stores the pending plain text to inject into the clipboard.
*
* @returns `this`
*/
ZeroClipboard.prototype.setText = function(text) {
ZeroClipboard.setData("text/plain", text);
return this;
};
/**
* Stores the pending HTML text to inject into the clipboard.
*
* @returns `this`
*/
ZeroClipboard.prototype.setHtml = function(html) {
ZeroClipboard.setData("text/html", html);
return this;
};
/**
* Stores the pending rich text (RTF) to inject into the clipboard.
*
* @returns `this`
*/
ZeroClipboard.prototype.setRichText = function(richText) {
ZeroClipboard.setData("application/rtf", richText);
return this;
};
/**
* Stores the pending data to inject into the clipboard.
*
* @returns `this`
*/
ZeroClipboard.prototype.setData = function(/* format, data */) {
ZeroClipboard.setData.apply(this, _args(arguments));
return this;
};
/**
* Clears the pending data to inject into the clipboard.
* If no `format` is provided, all pending data formats will be cleared.
*
* @returns `this`
*/
ZeroClipboard.prototype.clearData = function(/* format */) {
ZeroClipboard.clearData.apply(this, _args(arguments));
return this;
};
/**
* Gets a copy of the pending data to inject into the clipboard.
* If no `format` is provided, a copy of ALL pending data formats will be returned.
*
* @returns `String` or `Object`
*/
ZeroClipboard.prototype.getData = function(/* format */) {
return ZeroClipboard.getData.apply(this, _args(arguments));
};

View File

@@ -0,0 +1,476 @@
/**
* The real constructor for `ZeroClipboard` client instances.
* @private
*/
var _clientConstructor = function(elements) {
// Save a closure reference for the following event handlers
var client = this;
// Assign an ID to the client instance
client.id = "" + (_clientIdCounter++);
// Create the meta information store for this client
_clientMeta[client.id] = {
instance: client,
elements: [],
handlers: {}
};
// If the elements argument exists, clip it
if (elements) {
client.clip(elements);
}
// ECHO! Our client's sounding board.
ZeroClipboard.on("*", function(event) {
return client.emit(event);
});
// Await imminent destruction...
ZeroClipboard.on("destroy", function() {
client.destroy();
});
// Move on: embed the SWF
ZeroClipboard.create();
};
/**
* The underlying implementation of `ZeroClipboard.Client.prototype.on`.
* @private
*/
var _clientOn = function(eventType, listener) {
// add user event handler for event
var i, len, events,
added = {},
handlers = _clientMeta[this.id] && _clientMeta[this.id].handlers;
if (typeof eventType === "string" && eventType) {
events = eventType.toLowerCase().split(/\s+/);
}
else if (typeof eventType === "object" && eventType && typeof listener === "undefined") {
for (i in eventType) {
if (_hasOwn.call(eventType, i) && typeof i === "string" && i && typeof eventType[i] === "function") {
this.on(i, eventType[i]);
}
}
}
if (events && events.length) {
for (i = 0, len = events.length; i < len; i++) {
eventType = events[i].replace(/^on/, "");
added[eventType] = true;
if (!handlers[eventType]) {
handlers[eventType] = [];
}
handlers[eventType].push(listener);
}
// The following events must be memorized and fired immediately if relevant as they only occur
// once per Flash object load.
// If the SWF was already loaded, we're à gogo!
if (added.ready && _flashState.ready) {
this.emit({
type: "ready",
client: this
});
}
if (added.error) {
var errorTypes = ["disabled", "outdated", "unavailable", "deactivated", "overdue"];
for (i = 0, len = errorTypes.length; i < len; i++) {
if (_flashState[errorTypes[i]]) {
this.emit({
type: "error",
name: "flash-" + errorTypes[i],
client: this
});
break;
}
}
}
}
return this;
};
/**
* The underlying implementation of `ZeroClipboard.Client.prototype.off`.
* @private
*/
var _clientOff = function(eventType, listener) {
var i, len, foundIndex, events, perEventHandlers,
handlers = _clientMeta[this.id] && _clientMeta[this.id].handlers;
if (arguments.length === 0) {
// Remove ALL of the handlers for ALL event types
events = _keys(handlers);
}
else if (typeof eventType === "string" && eventType) {
events = eventType.split(/\s+/);
}
else if (typeof eventType === "object" && eventType && typeof listener === "undefined") {
for (i in eventType) {
if (_hasOwn.call(eventType, i) && typeof i === "string" && i && typeof eventType[i] === "function") {
this.off(i, eventType[i]);
}
}
}
if (events && events.length) {
for (i = 0, len = events.length; i < len; i++) {
eventType = events[i].toLowerCase().replace(/^on/, "");
perEventHandlers = handlers[eventType];
if (perEventHandlers && perEventHandlers.length) {
if (listener) {
foundIndex = perEventHandlers.indexOf(listener);
while (foundIndex !== -1) {
perEventHandlers.splice(foundIndex, 1);
foundIndex = perEventHandlers.indexOf(listener, foundIndex);
}
}
else {
// If no `listener` was provided, remove ALL of the handlers for this event type
perEventHandlers.length = 0;
}
}
}
}
return this;
};
/**
* The underlying implementation of `ZeroClipboard.Client.prototype.handlers`.
* @private
*/
var _clientListeners = function(eventType) {
var copy = null,
handlers = _clientMeta[this.id] && _clientMeta[this.id].handlers;
if (handlers) {
if (typeof eventType === "string" && eventType) {
copy = handlers[eventType] ? handlers[eventType].slice(0) : [];
}
else {
// Make a deep copy of the handlers object
copy = _deepCopy(handlers);
}
}
return copy;
};
/**
* The underlying implementation of `ZeroClipboard.Client.prototype.emit`.
* @private
*/
var _clientEmit = function(event) {
if (_clientShouldEmit.call(this, event)) {
// Don't modify the original Event, if it is an object (as expected)
if (typeof event === "object" && event && typeof event.type === "string" && event.type) {
event = _extend({}, event);
}
var eventCopy = _extend({}, _createEvent(event), { client: this });
_clientDispatchCallbacks.call(this, eventCopy);
}
return this;
};
/**
* The underlying implementation of `ZeroClipboard.Client.prototype.clip`.
* @private
*/
var _clientClip = function(elements) {
elements = _prepClip(elements);
for (var i = 0; i < elements.length ; i++) {
if (_hasOwn.call(elements, i) && elements[i] && elements[i].nodeType === 1) {
// If the element hasn't been clipped to ANY client yet, add a metadata ID and event handler
if (!elements[i].zcClippingId) {
elements[i].zcClippingId = "zcClippingId_" + (_elementIdCounter++);
_elementMeta[elements[i].zcClippingId] = [this.id];
if (_globalConfig.autoActivate === true) {
_addMouseHandlers(elements[i]);
}
}
else if (_elementMeta[elements[i].zcClippingId].indexOf(this.id) === -1) {
_elementMeta[elements[i].zcClippingId].push(this.id);
}
// If the element hasn't been clipped to THIS client yet, add it
var clippedElements = _clientMeta[this.id] && _clientMeta[this.id].elements;
if (clippedElements.indexOf(elements[i]) === -1) {
clippedElements.push(elements[i]);
}
}
}
return this;
};
/**
* The underlying implementation of `ZeroClipboard.Client.prototype.unclip`.
* @private
*/
var _clientUnclip = function(elements) {
var meta = _clientMeta[this.id];
if (!meta) {
return this;
}
var clippedElements = meta.elements;
var arrayIndex;
// If no elements were provided, unclip ALL of this client's clipped elements
if (typeof elements === "undefined") {
elements = clippedElements.slice(0);
}
else {
elements = _prepClip(elements);
}
for (var i = elements.length; i--; ) {
if (_hasOwn.call(elements, i) && elements[i] && elements[i].nodeType === 1) {
// If the element was clipped to THIS client yet, remove it
arrayIndex = 0;
while ((arrayIndex = clippedElements.indexOf(elements[i], arrayIndex)) !== -1) {
clippedElements.splice(arrayIndex, 1);
}
// If the element isn't clipped to ANY other client, remove its metadata ID and event handler
var clientIds = _elementMeta[elements[i].zcClippingId];
if (clientIds) {
arrayIndex = 0;
while ((arrayIndex = clientIds.indexOf(this.id, arrayIndex)) !== -1) {
clientIds.splice(arrayIndex, 1);
}
if (clientIds.length === 0) {
if (_globalConfig.autoActivate === true) {
_removeMouseHandlers(elements[i]);
}
delete elements[i].zcClippingId;
}
}
}
}
return this;
};
/**
* The underlying implementation of `ZeroClipboard.Client.prototype.elements`.
* @private
*/
var _clientElements = function() {
var meta = _clientMeta[this.id];
return (meta && meta.elements) ? meta.elements.slice(0) : [];
};
/**
* The underlying implementation of `ZeroClipboard.Client.prototype.destroy`.
* @private
*/
var _clientDestroy = function() {
// Unclip all the elements
this.unclip();
// Remove all event handlers
this.off();
// Delete the client's metadata store
delete _clientMeta[this.id];
};
//
// Helper functions
//
/**
* Inspect an Event to see if the Client (`this`) should honor it for emission.
* @private
*/
var _clientShouldEmit = function(event) {
// If no event is received
if (!(event && event.type)) {
return false;
}
// If this event's `client` was specifically set to a client other than this client, bail out
if (event.client && event.client !== this) {
return false;
}
// If this event's targeted element(s) is/are not clipped by this client, bail out
// unless the event's `client` was specifically set to this client.
var clippedEls = _clientMeta[this.id] && _clientMeta[this.id].elements;
var hasClippedEls = !!clippedEls && clippedEls.length > 0;
var goodTarget = !event.target || (hasClippedEls && clippedEls.indexOf(event.target) !== -1);
var goodRelTarget = event.relatedTarget && hasClippedEls && clippedEls.indexOf(event.relatedTarget) !== -1;
var goodClient = event.client && event.client === this;
// At least one of these must be true....
if (!(goodTarget || goodRelTarget || goodClient)) {
return false;
}
// Otherwise... go for it!
return true;
};
/**
* Handle the actual dispatching of events to a client instance.
*
* @returns `this`
* @private
*/
var _clientDispatchCallbacks = function(event) {
if (!(typeof event === "object" && event && event.type)) {
return;
}
var async = _shouldPerformAsync(event);
// User defined handlers for events
var wildcardTypeHandlers = (_clientMeta[this.id] && _clientMeta[this.id].handlers["*"]) || [];
var specificTypeHandlers = (_clientMeta[this.id] && _clientMeta[this.id].handlers[event.type]) || [];
// Execute wildcard handlers before type-specific handlers
var handlers = wildcardTypeHandlers.concat(specificTypeHandlers);
if (handlers && handlers.length) {
var i, len, func, context, eventCopy,
originalContext = this;
for (i = 0, len = handlers.length; i < len; i++) {
func = handlers[i];
context = originalContext;
// If the user provided a string for their callback, grab that function
if (typeof func === "string" && typeof _window[func] === "function") {
func = _window[func];
}
if (typeof func === "object" && func && typeof func.handleEvent === "function") {
context = func;
func = func.handleEvent;
}
if (typeof func === "function") {
eventCopy = _extend({}, event);
_dispatchCallback(func, context, [eventCopy], async);
}
}
}
return this;
};
/**
* Prepares the elements for clipping/unclipping.
*
* @returns An Array of elements.
* @private
*/
var _prepClip = function(elements) {
// if elements is a string, ignore it
if (typeof elements === "string") {
elements = [];
}
// if the elements isn't an array, wrap it with one
return typeof elements.length !== "number" ? [elements] : elements;
};
/**
* Add a `mouseover` handler function for a clipped element.
*
* @returns `undefined`
* @private
*/
var _addMouseHandlers = function(element) {
if (!(element && element.nodeType === 1)) {
return;
}
// Create a `mouseout` handler function
var _suppressMouseEvents = function(event) {
if (!(event || (event = _window.event))) {
return;
}
// Don't allow this event to be handled by consumers unless it originated from ZeroClipboard
if (event._source !== "js") {
event.stopImmediatePropagation();
event.preventDefault();
}
delete event._source;
};
// Create a `mouseover` handler function
var _elementMouseOver = function(event) {
if (!(event || (event = _window.event))) {
return;
}
// Don't allow this event to be handled by consumers unless it originated from ZeroClipboard
_suppressMouseEvents(event);
// Set this as the new currently active element
ZeroClipboard.focus(element);
};
// Add the `mouseover` handler function
element.addEventListener("mouseover", _elementMouseOver, false);
// Add other mouse event handler functions
element.addEventListener("mouseout", _suppressMouseEvents, false);
element.addEventListener("mouseenter", _suppressMouseEvents, false);
element.addEventListener("mouseleave", _suppressMouseEvents, false);
element.addEventListener("mousemove", _suppressMouseEvents, false);
// Save these function references to a global variable
_mouseHandlers[element.zcClippingId] = {
mouseover: _elementMouseOver,
mouseout: _suppressMouseEvents,
mouseenter: _suppressMouseEvents,
mouseleave: _suppressMouseEvents,
mousemove: _suppressMouseEvents
};
};
/**
* Remove a `mouseover` handler function for a clipped element.
*
* @returns `undefined`
* @private
*/
var _removeMouseHandlers = function(element) {
if (!(element && element.nodeType === 1)) {
return;
}
// Retrieve these function references from a global variable
var mouseHandlers = _mouseHandlers[element.zcClippingId];
if (!(typeof mouseHandlers === "object" && mouseHandlers)) {
return;
}
// Remove the mouse event handlers
var key, val,
mouseEvents = ["move", "leave", "enter", "out", "over"];
for (var i = 0, len = mouseEvents.length; i < len; i++) {
key = "mouse" + mouseEvents[i];
val = mouseHandlers[key];
if (typeof val === "function") {
element.removeEventListener(key, val, false);
}
}
// Delete these function references from a global variable
delete _mouseHandlers[element.zcClippingId];
};

View File

@@ -0,0 +1,60 @@
/**
* Keep track of the ZeroClipboard client instance counter.
*/
var _clientIdCounter = 0;
/**
* Keep track of the state of the client instances.
*
* Entry structure:
* _clientMeta[client.id] = {
* instance: client,
* elements: [],
* handlers: {}
* };
*/
var _clientMeta = {};
/**
* Keep track of the ZeroClipboard clipped elements counter.
*/
var _elementIdCounter = 0;
/**
* Keep track of the state of the clipped element relationships to clients.
*
* Entry structure:
* _elementMeta[element.zcClippingId] = [client1.id, client2.id];
*/
var _elementMeta = {};
/**
* Keep track of the state of the mouse event handlers for clipped elements.
*
* Entry structure:
* _mouseHandlers[element.zcClippingId] = {
* mouseover: function(event) {},
* mouseout: function(event) {},
* mouseenter: function(event) {},
* mouseleave: function(event) {},
* mousemove: function(event) {}
* };
*/
var _mouseHandlers = {};
/**
* Extending the ZeroClipboard configuration defaults for the Client module.
*/
_extend(_globalConfig, {
// Setting this to `false` would allow users to handle calling
// `ZeroClipboard.focus(...);` themselves instead of relying on our
// per-element `mouseover` handler.
autoActivate: true
});

View File

@@ -0,0 +1,211 @@
/**
* A shell constructor for `ZeroClipboard` client instances.
*
* @constructor
*/
var ZeroClipboard = function() {
// Ensure the constructor is invoked with the `new` keyword.
if (!(this instanceof ZeroClipboard)) {
return new ZeroClipboard();
}
// EXTREMELY IMPORTANT!
// Ensure the `ZeroClipboard._createClient` function is invoked if available.
// This allows an extension point for 3rd parties to create their own
// interpretations of what a ZeroClipboard "Client" should be like.
if (typeof ZeroClipboard._createClient === "function") {
ZeroClipboard._createClient.apply(this, _args(arguments));
}
};
/**
* The ZeroClipboard library's version number.
*
* @static
* @readonly
* @property {string}
*/
_defineProperty(ZeroClipboard, "version", {
value: "<%= version %>",
writable: false,
configurable: true,
enumerable: true
});
/**
* Update or get a copy of the ZeroClipboard global configuration.
* Returns a copy of the current/updated configuration.
*
* @returns Object
* @static
*/
ZeroClipboard.config = function(/* options */) {
return _config.apply(this, _args(arguments));
};
/**
* Diagnostic method that describes the state of the browser, Flash Player, and ZeroClipboard.
*
* @returns Object
* @static
*/
ZeroClipboard.state = function() {
return _state.apply(this, _args(arguments));
};
/**
* Check if Flash is unusable for any reason: disabled, outdated, deactivated, etc.
*
* @returns Boolean
* @static
*/
ZeroClipboard.isFlashUnusable = function() {
return _isFlashUnusable.apply(this, _args(arguments));
};
/**
* Register an event listener.
*
* @returns `ZeroClipboard`
* @static
*/
ZeroClipboard.on = function(/* eventType, listener */) {
return _on.apply(this, _args(arguments));
};
/**
* Unregister an event listener.
* If no `listener` function/object is provided, it will unregister all listeners for the provided `eventType`.
* If no `eventType` is provided, it will unregister all listeners for every event type.
*
* @returns `ZeroClipboard`
* @static
*/
ZeroClipboard.off = function(/* eventType, listener */) {
return _off.apply(this, _args(arguments));
};
/**
* Retrieve event listeners for an `eventType`.
* If no `eventType` is provided, it will retrieve all listeners for every event type.
*
* @returns array of listeners for the `eventType`; if no `eventType`, then a map/hash object of listeners for all event types; or `null`
*/
ZeroClipboard.handlers = function(/* eventType */) {
return _listeners.apply(this, _args(arguments));
};
/**
* Event emission receiver from the Flash object, forwarding to any registered JavaScript event listeners.
*
* @returns For the "copy" event, returns the Flash-friendly "clipData" object; otherwise `undefined`.
* @static
*/
ZeroClipboard.emit = function(/* event */) {
return _emit.apply(this, _args(arguments));
};
/**
* Create and embed the Flash object.
*
* @returns The Flash object
* @static
*/
ZeroClipboard.create = function() {
return _create.apply(this, _args(arguments));
};
/**
* Self-destruct and clean up everything, including the embedded Flash object.
*
* @returns `undefined`
* @static
*/
ZeroClipboard.destroy = function() {
return _destroy.apply(this, _args(arguments));
};
/**
* Set the pending data for clipboard injection.
*
* @returns `undefined`
* @static
*/
ZeroClipboard.setData = function(/* format, data */) {
return _setData.apply(this, _args(arguments));
};
/**
* Clear the pending data for clipboard injection.
* If no `format` is provided, all pending data formats will be cleared.
*
* @returns `undefined`
* @static
*/
ZeroClipboard.clearData = function(/* format */) {
return _clearData.apply(this, _args(arguments));
};
/**
* Get a copy of the pending data for clipboard injection.
* If no `format` is provided, a copy of ALL pending data formats will be returned.
*
* @returns `String` or `Object`
* @static
*/
ZeroClipboard.getData = function(/* format */) {
return _getData.apply(this, _args(arguments));
};
/**
* Sets the current HTML object that the Flash object should overlay. This will put the global
* Flash object on top of the current element; depending on the setup, this may also set the
* pending clipboard text data as well as the Flash object's wrapping element's title attribute
* based on the underlying HTML element and ZeroClipboard configuration.
*
* @returns `undefined`
* @static
*/
ZeroClipboard.focus = ZeroClipboard.activate = function(/* element */) {
return _focus.apply(this, _args(arguments));
};
/**
* Un-overlays the Flash object. This will put the global Flash object off-screen; depending on
* the setup, this may also unset the Flash object's wrapping element's title attribute based on
* the underlying HTML element and ZeroClipboard configuration.
*
* @returns `undefined`
* @static
*/
ZeroClipboard.blur = ZeroClipboard.deactivate = function() {
return _blur.apply(this, _args(arguments));
};
/**
* Returns the currently focused/"activated" HTML element that the Flash object is wrapping.
*
* @returns `HTMLElement` or `null`
* @static
*/
ZeroClipboard.activeElement = function() {
return _activeElement.apply(this, _args(arguments));
};

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,147 @@
/**
* Keep track of the state of the Flash object.
* @private
*/
var _flashState = {
// Flash object reference
bridge: null,
// Flash metadata
version: "0.0.0",
pluginType: "unknown",
// Flash SWF state
disabled: null,
outdated: null,
unavailable: null,
deactivated: null,
overdue: null,
ready: null
};
/**
* The minimum Flash Player version required to use ZeroClipboard completely.
* @readonly
* @private
*/
var _minimumFlashVersion = "11.0.0";
/**
* Keep track of all event listener registrations.
* @private
*/
var _handlers = {};
/**
* Keep track of the currently activated element.
* @private
*/
var _currentElement;
/**
* Keep track of the element that was activated when a `copy` process started.
* @private
*/
var _copyTarget;
/**
* Keep track of data for the pending clipboard transaction.
* @private
*/
var _clipData = {};
/**
* Keep track of data formats for the pending clipboard transaction.
* @private
*/
var _clipDataFormatMap = null;
/**
* The `message` store for events
* @private
*/
var _eventMessages = {
"ready": "Flash communication is established",
"error": {
"flash-disabled": "Flash is disabled or not installed",
"flash-outdated": "Flash is too outdated to support ZeroClipboard",
"flash-unavailable": "Flash is unable to communicate bidirectionally with JavaScript",
"flash-deactivated": "Flash is too outdated for your browser and/or is configured as click-to-activate",
"flash-overdue": "Flash communication was established but NOT within the acceptable time limit"
}
};
/**
* ZeroClipboard configuration defaults for the Core module.
* @private
*/
var _globalConfig = {
// SWF URL, relative to the page. Default value will be "ZeroClipboard.swf"
// under the same path as the ZeroClipboard JS file.
swfPath: _getDefaultSwfPath(),
// SWF inbound scripting policy: page domains that the SWF should trust.
// (single string, or array of strings)
trustedDomains: window.location.host ? [window.location.host] : [],
// Include a "noCache" query parameter on requests for the SWF.
cacheBust: true,
// Enable use of the fancy "Desktop" clipboard, even on Linux where it is
// known to suck.
forceEnhancedClipboard: false,
// How many milliseconds to wait for the Flash SWF to load and respond before assuming that
// Flash is deactivated (e.g. click-to-play) in the user's browser. If you don't care about
// how long it takes to load the SWF, you can set this to `null`.
flashLoadTimeout: 30000,
// Setting this to `false` would allow users to handle calling `ZeroClipboard.focus(...);`
// themselves instead of relying on our per-element `mouseover` handler.
autoActivate: true,
// Bubble synthetic events in JavaScript after they are received by the Flash object.
bubbleEvents: true,
// Sets the ID of the `div` encapsulating the Flash object.
// Value is validated against the HTML4 spec for `ID` tokens.
containerId: "global-zeroclipboard-html-bridge",
// Sets the class of the `div` encapsulating the Flash object.
containerClass: "global-zeroclipboard-container",
// Sets the ID and name of the Flash `object` element.
// Value is validated against the HTML4 spec for `ID` and `Name` tokens.
swfObjectId: "global-zeroclipboard-flash-bridge",
// The class used to indicate that a clipped element is being hovered over.
hoverClass: "zeroclipboard-is-hover",
// The class used to indicate that a clipped element is active (is being clicked).
activeClass: "zeroclipboard-is-active",
// Forcibly set the hand cursor ("pointer") for all clipped elements.
// IMPORTANT: This configuration value CAN be modified while a SWF is actively embedded.
forceHandCursor: false,
// Sets the title of the `div` encapsulating the Flash object.
// IMPORTANT: This configuration value CAN be modified while a SWF is actively embedded.
title: null,
// The z-index used by the Flash object.
// Max value (32-bit): 2147483647.
// IMPORTANT: This configuration value CAN be modified while a SWF is actively embedded.
zIndex: 999999999
};

View File

@@ -0,0 +1,21 @@
// The AMDJS logic branch is evaluated first to avoid potential confusion over
// the CommonJS syntactical sugar offered by AMD.
if (typeof define === "function" && define.amd) {
define(function() {
return ZeroClipboard;
});
}
else if (typeof module === "object" && module && typeof module.exports === "object" && module.exports) {
// CommonJS module loaders....
module.exports = ZeroClipboard;
}
else {
window.ZeroClipboard = ZeroClipboard;
}
})((function() {
/*jshint strict: false */
return this || window;
})());

View File

@@ -0,0 +1,317 @@
/**
* Convert an `arguments` object into an Array.
*
* @returns The arguments as an Array
* @private
*/
var _args = function(argumentsObj) {
return _slice.call(argumentsObj, 0);
};
/**
* Shallow-copy the owned, enumerable properties of one object over to another, similar to jQuery's `$.extend`.
*
* @returns The target object, augmented
* @private
*/
var _extend = function() {
var i, len, arg, prop, src, copy,
args = _args(arguments),
target = args[0] || {};
for (i = 1, len = args.length; i < len; i++) {
// Only deal with non-null/undefined values
if ((arg = args[i]) != null) {
// Extend the base object
for (prop in arg) {
if (_hasOwn.call(arg, prop)) {
src = target[prop];
copy = arg[prop];
// Prevent never-ending loops and copying `undefined` valeus
if (target !== copy && copy !== undefined) {
target[prop] = copy;
}
}
}
}
}
return target;
};
/**
* Return a deep copy of the source object or array.
*
* @returns Object or Array
* @private
*/
var _deepCopy = function(source) {
var copy, i, len, prop;
// If not a non-null object, just return the original
if (typeof source !== "object" || source == null) {
copy = source;
}
// If an Array, iterate and recurse
else if (typeof source.length === "number") {
copy = [];
for (i = 0, len = source.length; i < len; i++) {
// Skip empty indices in sparse arrays
if (_hasOwn.call(source, i)) {
// Recurse
copy[i] = _deepCopy(source[i]);
}
}
}
// If an Object, enumerate and recurse
else {
copy = {};
for (prop in source) {
// Skip prototype properties
if (_hasOwn.call(source, prop)) {
copy[prop] = _deepCopy(source[prop]);
}
}
}
return copy;
};
/**
* Makes a shallow copy of `obj` (like `_extend`) but filters its properties based on a list of `keys` to keep.
* The inverse of `_omit`, mostly. The big difference is that these properties do NOT need to be enumerable to
* be kept.
*
* @returns A new filtered object.
* @private
*/
var _pick = function(obj, keys) {
var newObj = {};
for (var i = 0, len = keys.length; i < len; i++) {
if (keys[i] in obj) {
newObj[keys[i]] = obj[keys[i]];
}
}
return newObj;
};
/**
* Makes a shallow copy of `obj` (like `_extend`) but filters its properties based on a list of `keys` to omit.
* The inverse of `_pick`.
*
* @returns A new filtered object.
* @private
*/
var _omit = function(obj, keys) {
var newObj = {};
for (var prop in obj) {
if (keys.indexOf(prop) === -1) {
newObj[prop] = obj[prop];
}
}
return newObj;
};
/**
* Remove all owned, enumerable properties from an object.
*
* @returns The original object without its owned, enumerable properties.
* @private
*/
var _deleteOwnProperties = function(obj) {
if (obj) {
for (var prop in obj) {
if (_hasOwn.call(obj, prop)) {
delete obj[prop];
}
}
}
return obj;
};
/**
* Determine if an element is contained within another element.
*
* @returns Boolean
* @private
*/
var _containedBy = function(el, ancestorEl) {
if (
el && el.nodeType === 1 && el.ownerDocument &&
ancestorEl && (
(ancestorEl.nodeType === 1 && ancestorEl.ownerDocument && ancestorEl.ownerDocument === el.ownerDocument) ||
(ancestorEl.nodeType === 9 && !ancestorEl.ownerDocument && ancestorEl === el.ownerDocument)
)
) {
do {
if (el === ancestorEl) {
return true;
}
el = el.parentNode;
}
while (el);
}
return false;
};
/**
* Get the URL path's parent directory.
*
* @returns String or `undefined`
* @private
*/
var _getDirPathOfUrl = function(url) {
var dir;
if (typeof url === "string" && url) {
dir = url.split("#")[0].split("?")[0];
dir = url.slice(0, url.lastIndexOf("/") + 1);
}
return dir;
};
/**
* Get the current script's URL by throwing an `Error` and analyzing it.
*
* @returns String or `undefined`
* @private
*/
var _getCurrentScriptUrlFromErrorStack = function(stack) {
var url, matches;
if (typeof stack === "string" && stack) {
matches = stack.match(/^(?:|[^:@]*@|.+\)@(?=http[s]?|file)|.+?\s+(?: at |@)(?:[^:\(]+ )*[\(]?)((?:http[s]?|file):\/\/[\/]?.+?\/[^:\)]*?)(?::\d+)(?::\d+)?/);
if (matches && matches[1]) {
url = matches[1];
}
else {
matches = stack.match(/\)@((?:http[s]?|file):\/\/[\/]?.+?\/[^:\)]*?)(?::\d+)(?::\d+)?/);
if (matches && matches[1]) {
url = matches[1];
}
}
}
return url;
};
/**
* Get the current script's URL by throwing an `Error` and analyzing it.
*
* @returns String or `undefined`
* @private
*/
var _getCurrentScriptUrlFromError = function() {
/*jshint newcap:false */
var url, err;
try {
throw new _Error();
}
catch (e) {
err = e;
}
if (err) {
url = err.sourceURL || err.fileName || _getCurrentScriptUrlFromErrorStack(err.stack);
}
return url;
};
/**
* Get the current script's URL.
*
* @returns String or `undefined`
* @private
*/
var _getCurrentScriptUrl = function() {
var jsPath, scripts, i;
// Try to leverage the `currentScript` feature
if (_document.currentScript && (jsPath = _document.currentScript.src)) {
return jsPath;
}
// If it it not available, then seek the script out instead...
scripts = _document.getElementsByTagName("script");
// If there is only one script
if (scripts.length === 1) {
return scripts[0].src || undefined;
}
// If `script` elements have the `readyState` property in this browser
if ("readyState" in scripts[0]) {
for (i = scripts.length; i--; ) {
if (scripts[i].readyState === "interactive" && (jsPath = scripts[i].src)) {
return jsPath;
}
}
}
// If the document is still parsing, then the last script in the document is the one that is currently loading
if (_document.readyState === "loading" && (jsPath = scripts[scripts.length - 1].src)) {
return jsPath;
}
// Else take more drastic measures...
if ((jsPath = _getCurrentScriptUrlFromError())) {
return jsPath;
}
// Otherwise we cannot reliably know which exact script is executing....
return undefined;
};
/**
* Get the unanimous parent directory of ALL script tags.
* If any script tags are either (a) inline or (b) from differing parent
* directories, this method must return `undefined`.
*
* @returns String or `undefined`
* @private
*/
var _getUnanimousScriptParentDir = function() {
var i, jsDir, jsPath,
scripts = _document.getElementsByTagName("script");
// If every `script` has a `src` attribute AND they all come from the same directory
for (i = scripts.length; i--; ) {
if (!(jsPath = scripts[i].src)) {
jsDir = null;
break;
}
jsPath = _getDirPathOfUrl(jsPath);
if (jsDir == null) {
jsDir = jsPath;
}
else if (jsDir !== jsPath) {
jsDir = null;
break;
}
}
// Otherwise we cannot reliably know what script is executing....
return jsDir || undefined;
};
/**
* Get the presumed location of the "ZeroClipboard.swf" file, based on the location
* of the executing JavaScript file (e.g. "ZeroClipboard.js", etc.).
*
* @returns String
* @private
*/
var _getDefaultSwfPath = function() {
var jsDir = _getDirPathOfUrl(_getCurrentScriptUrl()) || _getUnanimousScriptParentDir() || "";
return jsDir + "ZeroClipboard.swf";
};

View File

@@ -0,0 +1,41 @@
/*jshint -W079 */
/**
* Store references to critically important global functions that may be
* overridden on certain web pages.
*/
var _window = window,
_document = _window.document,
_navigator = _window.navigator,
_setTimeout = _window.setTimeout,
_encodeURIComponent = _window.encodeURIComponent,
_ActiveXObject = _window.ActiveXObject,
_Error = _window.Error,
_parseInt = _window.Number.parseInt || _window.parseInt,
_parseFloat = _window.Number.parseFloat || _window.parseFloat,
_isNaN = _window.Number.isNaN || _window.isNaN,
_round = _window.Math.round,
_now = _window.Date.now,
_keys = _window.Object.keys,
_defineProperty = _window.Object.defineProperty,
_hasOwn = _window.Object.prototype.hasOwnProperty,
_slice = _window.Array.prototype.slice,
_unwrap = (function() {
var unwrapper = function(el) {
return el;
};
// For Polymer
if (typeof _window.wrap === "function" && typeof _window.unwrap === "function") {
try {
var div = _document.createElement("div");
var unwrappedDiv = _window.unwrap(div);
if (div.nodeType === 1 && unwrappedDiv && unwrappedDiv.nodeType === 1) {
unwrapper = _window.unwrap;
}
}
catch (e) {
// Some unreliable `window.unwrap` function is exposed
}
}
return unwrapper;
})();

View File

@@ -0,0 +1,2 @@
(function(window, undefined) {
"use strict";

View File

@@ -0,0 +1,8 @@
The MIT License (MIT)
Copyright (c) <%= grunt.template.today("yyyy") %> <%= _.pluck(contributors, "name").join(", ") %>
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

@@ -0,0 +1,17 @@
{
"name": "<%= name %>",
"description": "<%= description %>",
"version": "<%= version %>",
"main": ["./dist/ZeroClipboard.js", "./dist/ZeroClipboard.swf"],
"keywords": <%= JSON.stringify(keywords) %>,
"license": "<%= licenses[0].url %>",
"authors": <%= JSON.stringify(contributors) %>,
"homepage": "<%= homepage %>",
"repository": <%= JSON.stringify(repository) %>,
"location": "<%= repository.url.replace(/^https:\/\//, "git://") %>",
"ignore": [
"*",
"!/bower.json",
"!/dist/**"
]
}

View File

@@ -0,0 +1,33 @@
{
"name": "<%= name %>/<%= name %>",
"description": "<%= description %>",
"version": "<%= version %>",
"type": "library",
"keywords": <%= JSON.stringify(keywords) %>,
"license": "<%= licenses[0].type %>",
"authors": <%= JSON.stringify(_.filter(_.map(contributors, function(c) {
var a;
/* Does not currently support the NPM package.json string format (as opposed to object format) for "people" types */
if (c && typeof c === "object") {
/* The `name` property is required */
if (c.name) {
a = {};
a.name = c.name;
if (c.email) {
a.email = c.email;
}
if (c.url) {
a.homepage = c.url;
}
a.role = c.role || "Developer";
}
}
return a;
}), function(a) { return !!a; })) %>,
"homepage": "<%= homepage %>",
"support": {
"source": "<%= repository.url %>",
"issues": "<%= bugs.url %>"
}
}

View File

@@ -0,0 +1,8 @@
/*!
* <%= title || name %>
* <%= description %>
* Copyright (c) <%= grunt.template.today("yyyy") %> <%= _.pluck(contributors, "name").join(", ") %>
* Licensed <%= _.pluck(licenses, "type").join(", ") %>
* <%= homepage %>
* v<%= version %>
*/

View File

@@ -0,0 +1,68 @@
{
/* Enforcing options */
"bitwise": true,
"camelcase": true,
"curly": true,
"eqeqeq": true,
"es3": true,
"es5": false,
"forin": true,
"freeze": true,
"immed": true,
"indent": 2,
"latedef": true,
"newcap": true,
"noarg": true,
"noempty": true,
"nonbsp": true,
"nonew": true,
"plusplus": false,
"quotmark": "double",
"undef": true,
"unused": true,
"strict": true,
"trailing": true,
"maxparams": 2,
"maxdepth": 3,
"maxstatements": false,
"maxlen": false, /* IDEAL: 120? */
/* Relaxing options */
"asi": false,
"boss": false,
"debug": false,
"eqnull": true,
"esnext": false,
"evil": false,
"expr": false,
"funcscope": false,
"gcl": false,
"globalstrict": false,
"iterator": false,
"lastsemic": false,
"laxbreak": false,
"laxcomma": false,
"loopfunc": false,
"maxerr": 50,
"moz": false,
"multistr": false,
"notypeof": false,
"proto": false,
"scripturl": false,
"smarttabs": false,
"shadow": false,
"sub": false,
"supernew": false,
"validthis": false,
"noyield": false,
/* Environments */
"browser": true,
/* Global variables */
"globals": {
"$": false,
"QUnit": false
}
}

View File

@@ -0,0 +1,67 @@
/*global ZeroClipboard */
(function(module, test) {
"use strict";
// Helper functions
var TestUtils = {
getHtmlBridge: function() {
return document.getElementById(ZeroClipboard.config("containerId"));
}
};
var originalConfig, originalFlashDetect;
module("ZeroClipboard.Core.js (built) unit tests", {
setup: function() {
// Store
originalConfig = ZeroClipboard.config();
originalFlashDetect = ZeroClipboard.isFlashUnusable;
// Modify
ZeroClipboard.isFlashUnusable = function() {
return false;
};
},
teardown: function() {
// Restore
ZeroClipboard.destroy();
ZeroClipboard.config(originalConfig);
ZeroClipboard.isFlashUnusable = originalFlashDetect;
}
});
test("`swfPath` finds the expected default URL", function(assert) {
assert.expect(1);
// Assert, act, assert
var rootOrigin = window.location.protocol + "//" + window.location.host + "/";
var indexOfTest = window.location.pathname.toLowerCase().indexOf("/test/");
var rootDir = window.location.pathname.slice(1, indexOfTest + 1);
var rootPath = rootOrigin + rootDir;
//var zcJsUrl = rootPath + "dist/ZeroClipboard.Core.js";
var swfPathBasedOnZeroClipboardJsPath = rootPath + "dist/ZeroClipboard.swf";
// Test that the client has the expected default URL [even if it's not correct]
assert.strictEqual(ZeroClipboard.config("swfPath"), swfPathBasedOnZeroClipboardJsPath);
});
test("`destroy` destroys the bridge", function(assert) {
assert.expect(3);
// Arrange
ZeroClipboard.isFlashUnusable = function() {
return false;
};
// Assert, arrange, assert, act, assert
assert.equal(TestUtils.getHtmlBridge(), null, "The bridge does not exist before creating a client");
ZeroClipboard.create();
assert.notEqual(TestUtils.getHtmlBridge(), null, "The bridge does exist after creating a client");
ZeroClipboard.destroy();
assert.equal(TestUtils.getHtmlBridge(), null, "The bridge does not exist after calling `destroy`");
});
})(QUnit.module, QUnit.test);

View File

@@ -0,0 +1,43 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>ZeroClipboard.Core.js unit tests</title>
<link rel="stylesheet" href="../../node_modules/qunitjs/qunit/qunit.css">
<script src="../../node_modules/qunitjs/qunit/qunit.js"></script>
<script>
// Polyfill in future functionality: https://github.com/jquery/qunit/issues/490
if (!QUnit.assert.expect) {
QUnit.assert.expect = QUnit.expect;
}
// Require `expect` calls
QUnit.config.requireExpects = true;
// Prevent against Firefox/Firebug failing the global pollution check
var getInterface = null;
// Prevent against failing the global pollution check in all browsers other than IE
if (typeof window.ActiveXObject === "undefined") {
window.ActiveXObject = null;
}
// Prevent against Flash's ExternalInterface failing the global pollution check (seems to be only in IE < 11)
var __flash__arrayToXML = null,
__flash__argumentsToXML = null,
__flash__objectToXML = null,
__flash__escapeXML = null,
__flash__toXML = null,
__flash__addCallback = null,
__flash__removeCallback = null,
__flash__request = null;
</script>
<script src="../../dist/ZeroClipboard.Core.js"></script>
<script src="../../node_modules/jquery/dist/jquery.js"></script>
<script src="ZeroClipboard.Core.tests.js"></script>
</head>
<body>
<div id="qunit"></div>
<div id="qunit-fixture"></div>
</body>
</html>

View File

@@ -0,0 +1,228 @@
/*global ZeroClipboard */
(function(module, test) {
"use strict";
var originalConfig, originalFlashDetect;
// Helper functions
var TestUtils = {
getHtmlBridge: function() {
return document.getElementById(ZeroClipboard.config("containerId"));
}
};
module("ZeroClipboard.js (built) unit tests - Core", {
setup: function() {
// Store
originalConfig = ZeroClipboard.config();
originalFlashDetect = ZeroClipboard.isFlashUnusable;
// Modify
ZeroClipboard.isFlashUnusable = function() {
return false;
};
ZeroClipboard.config({ swfPath: originalConfig.swfPath.replace(/\/(?:src|test)\/.*$/i, "/dist/ZeroClipboard.swf") });
},
teardown: function() {
// Restore
ZeroClipboard.destroy();
ZeroClipboard.config(originalConfig);
ZeroClipboard.isFlashUnusable = originalFlashDetect;
}
});
test("`swfPath` finds the expected default URL", function(assert) {
assert.expect(1);
// Assert, act, assert
var rootOrigin = window.location.protocol + "//" + window.location.host + "/";
var indexOfTest = window.location.pathname.toLowerCase().indexOf("/test/");
var rootDir = window.location.pathname.slice(1, indexOfTest + 1);
var rootPath = rootOrigin + rootDir;
//var zcJsUrl = rootPath + "dist/ZeroClipboard.js";
var swfPathBasedOnZeroClipboardJsPath = rootPath + "dist/ZeroClipboard.swf";
// Test that the client has the expected default URL [even if it's not correct]
assert.strictEqual(ZeroClipboard.config("swfPath"), swfPathBasedOnZeroClipboardJsPath);
});
test("`destroy` destroys the bridge", function(assert) {
assert.expect(3);
// Arrange
ZeroClipboard.isFlashUnusable = function() {
return false;
};
// Assert, arrange, assert, act, assert
assert.equal(TestUtils.getHtmlBridge(), null, "The bridge does not exist before creating a client");
/*jshint nonew:false */
new ZeroClipboard();
assert.notEqual(TestUtils.getHtmlBridge(), null, "The bridge does exist after creating a client");
ZeroClipboard.destroy();
assert.equal(TestUtils.getHtmlBridge(), null, "The bridge does not exist after calling `destroy`");
});
module("ZeroClipboard.js (built) unit tests - Client", {
setup: function() {
// Store
originalConfig = ZeroClipboard.config();
originalFlashDetect = ZeroClipboard.isFlashUnusable;
// Modify
ZeroClipboard.isFlashUnusable = function() {
return false;
};
ZeroClipboard.config({ swfPath: originalConfig.swfPath.replace(/\/(?:src|test)\/.*$/i, "/dist/ZeroClipboard.swf") });
},
teardown: function() {
// Restore
ZeroClipboard.destroy();
ZeroClipboard.config(originalConfig);
ZeroClipboard.isFlashUnusable = originalFlashDetect;
}
});
test("`ZeroClipboard` exists", function(assert) {
assert.expect(1);
// Arrange -> N/A
// Act -> N/A
// Assert
assert.ok(ZeroClipboard);
});
test("Client is created properly", function(assert) {
assert.expect(2);
// Arrange & Act
var client = new ZeroClipboard();
// Assert
assert.ok(client);
assert.ok(client.id);
});
test("Client without selector doesn't have elements", function(assert) {
assert.expect(2);
// Arrange & Act
var client = new ZeroClipboard();
// Assert
assert.ok(client);
assert.deepEqual(client.elements(), []);
});
test("Object has a title", function(assert) {
assert.expect(1);
// Arrange
var client = new ZeroClipboard();
var currentEl = document.getElementById("d_clip_button");
// Act
client.clip(currentEl);
ZeroClipboard.activate(currentEl);
// Assert
assert.strictEqual(TestUtils.getHtmlBridge().getAttribute("title"), "Click me to copy to clipboard.");
// Revert
ZeroClipboard.deactivate();
});
test("Object has no title", function(assert) {
assert.expect(1);
// Arrange
var client = new ZeroClipboard();
var currentEl = document.getElementById("d_clip_button_no_title");
// Act
client.clip(currentEl);
ZeroClipboard.activate(currentEl);
// Assert
assert.ok(!TestUtils.getHtmlBridge().getAttribute("title"));
});
test("Object doesn't have data-clipboard-text", function(assert) {
assert.expect(1);
// Arrange
var client = new ZeroClipboard();
var currentEl = document.getElementById("d_clip_button_no_text");
// Act
client.clip(currentEl);
ZeroClipboard.activate(currentEl);
// Assert
assert.ok(!TestUtils.getHtmlBridge().getAttribute("data-clipboard-text"));
});
test("New client is not the same client (no singleton) but does share the same bridge", function(assert) {
assert.expect(6);
// Assert, arrange, assert, act, assert
var containerClass = "." + ZeroClipboard.config("containerClass");
assert.strictEqual($(containerClass).length, 0);
var client1 = new ZeroClipboard();
assert.ok(client1.id);
assert.strictEqual($(containerClass).length, 1);
var client2 = new ZeroClipboard();
assert.strictEqual($(containerClass).length, 1);
assert.notEqual(client2.id, client1.id);
assert.notEqual(client2, client1);
});
test("Calculations based on borderWidth never return NaN", function(assert) {
assert.expect(4);
// Arrange
var client = new ZeroClipboard();
var currentEl = document.getElementById("d_clip_button");
// Act
client.clip(currentEl);
ZeroClipboard.activate(currentEl);
// Assert
var htmlBridge = TestUtils.getHtmlBridge();
assert.strictEqual(/^-?[0-9\.]+px$/.test(htmlBridge.style.top), true);
assert.strictEqual(/^-?[0-9\.]+px$/.test(htmlBridge.style.left), true);
assert.strictEqual(/^-?[0-9\.]+px$/.test(htmlBridge.style.width), true);
assert.strictEqual(/^-?[0-9\.]+px$/.test(htmlBridge.style.height), true);
});
test("No more client singleton!", function(assert) {
assert.expect(7);
// Arrange
ZeroClipboard.isFlashUnusable = function() {
return false;
};
// Assert, arrange, assert, act, assert
assert.ok(!ZeroClipboard.prototype._singleton, "The client singleton does not exist on the prototype before creating a client");
var client1 = new ZeroClipboard();
assert.ok(!ZeroClipboard.prototype._singleton, "The client singleton does not exist on the prototype after creating a client");
assert.ok(!client1._singleton, "The client singleton does not exist on the client instance after creating a client");
var client2 = new ZeroClipboard();
assert.ok(!ZeroClipboard.prototype._singleton, "The client singleton does not exist on the prototype after creating a second client");
assert.ok(!client1._singleton, "The client singleton does not exist on the first client instance after creating a second client");
assert.ok(!client2._singleton, "The client singleton does not exist on the second client instance after creating a second client");
ZeroClipboard.destroy();
assert.ok(!ZeroClipboard.prototype._singleton, "The client singleton does not exist on the prototype after calling `destroy`");
});
})(QUnit.module, QUnit.test);

View File

@@ -0,0 +1,73 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>ZeroClipboard.js unit tests</title>
<link rel="stylesheet" href="../../node_modules/qunitjs/qunit/qunit.css">
<script src="../../node_modules/qunitjs/qunit/qunit.js"></script>
<script>
// Polyfill in future functionality: https://github.com/jquery/qunit/issues/490
if (!QUnit.assert.expect) {
QUnit.assert.expect = QUnit.expect;
}
// Require `expect` calls
QUnit.config.requireExpects = true;
// Prevent against Firefox/Firebug failing the global pollution check
var getInterface = null;
// Prevent against failing the global pollution check in all browsers other than IE
if (typeof window.ActiveXObject === "undefined") {
window.ActiveXObject = null;
}
// Prevent against Flash's ExternalInterface failing the global pollution check (seems to be only in IE < 11)
var __flash__arrayToXML = null,
__flash__argumentsToXML = null,
__flash__objectToXML = null,
__flash__escapeXML = null,
__flash__toXML = null,
__flash__addCallback = null,
__flash__removeCallback = null,
__flash__request = null;
</script>
<script src="../../dist/ZeroClipboard.js"></script>
<script src="../../node_modules/jquery/dist/jquery.js"></script>
<script src="ZeroClipboard.tests.js"></script>
</head>
<body>
<div id="qunit"></div>
<div id="qunit-fixture">
<p>
<button id="d_clip_button" class="my_clip_button" title="Click me to copy to clipboard." data-clipboard-text="Copy me!"><b>Copy To Clipboard...</b></button>
</p>
<p>
<button id="d_clip_button_no_title" class="my_clip_button" data-clipboard-text="Copy me!"><b>Copy To Clipboard...</b></button>
</p>
<p>
<button id="d_clip_button_no_text" class="my_clip_button"><b>Copy To Clipboard...</b></button>
</p>
<p>
<button id="d_clip_button_input_text" class="my_clip_button" data-clipboard-target="clipboard_text"><b>Copy To Clipboard...</b></button>
</p>
<p>
<button id="d_clip_button_textarea_text" class="my_clip_button" data-clipboard-target="clipboard_textarea"><b>Copy To Clipboard...</b></button>
</p>
<button id="d_clip_button_pre_text" class="my_clip_button" data-clipboard-target="clipboard_pre"><b>Copy To Clipboard...</b></button>
<input type="text" id="clipboard_text" value="Clipboard Text"/>
<textarea id="clipboard_textarea">Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod
tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam,
quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo
consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse
cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non
proident, sunt in culpa qui officia deserunt mollit anim id est laborum.</textarea>
<pre id="clipboard_pre">Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod
tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam,
quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo
consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse
cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non
proident, sunt in culpa qui officia deserunt mollit anim id est laborum.</pre>
</div>
</body>
</html>

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,83 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>ZeroClipboard unit tests: client/api.js</title>
<link rel="stylesheet" href="../../node_modules/qunitjs/qunit/qunit.css">
<script src="../../node_modules/qunitjs/qunit/qunit.js"></script>
<script>
// Polyfill in future functionality: https://github.com/jquery/qunit/issues/490
if (!QUnit.assert.expect) {
QUnit.assert.expect = QUnit.expect;
}
// Require `expect` calls
QUnit.config.requireExpects = true;
// Prevent against Firefox/Firebug failing the global pollution check
var getInterface = null;
// Prevent against failing the global pollution check in all browsers other than IE
if (typeof window.ActiveXObject === "undefined") {
window.ActiveXObject = null;
}
// Prevent against Flash's ExternalInterface failing the global pollution check (seems to be only in IE < 11)
var __flash__arrayToXML = null,
__flash__argumentsToXML = null,
__flash__objectToXML = null,
__flash__escapeXML = null,
__flash__toXML = null,
__flash__addCallback = null,
__flash__removeCallback = null,
__flash__request = null;
</script>
<script src="../../src/js/shared/state.js"></script>
<script src="../../src/js/shared/private.js"></script>
<script src="../../src/js/core/state.js"></script>
<script src="../../src/js/core/private.js"></script>
<script src="../../src/js/core/api.js"></script>
<script src="../../src/js/client/state.js"></script>
<script src="../../src/js/client/private.js"></script>
<script src="../../src/js/client/api.js"></script>
<script src="../../node_modules/jquery/dist/jquery.js"></script>
<script src="api.tests.js"></script>
</head>
<body>
<div id="qunit"></div>
<div id="qunit-fixture">
<p>
<button id="d_clip_button" class="my_clip_button" title="Click me to copy to clipboard." data-clipboard-text="Copy me!"><b>Copy To Clipboard...</b></button>
</p>
<p>
<button id="d_clip_button2" class="my_clip_button" title="Click me to copy to clipboard.2" data-clipboard-text="Copy me!"><b>Copy To Clipboard...</b></button>
</p>
<p>
<button id="d_clip_button3" class="my_clip_button" title="Click me to copy to clipboard.3" data-clipboard-text="Copy me!"><b>Copy To Clipboard...</b></button>
</p>
<p>
<button id="d_clip_button_no_text" class="my_clip_button"><b>Copy To Clipboard...</b></button>
</p>
<p>
<button id="d_clip_button_input_text" class="my_clip_button" data-clipboard-target="clipboard_text"><b>Copy To Clipboard...</b></button>
</p>
<p>
<button id="d_clip_button_textarea_text" class="my_clip_button" data-clipboard-target="clipboard_textarea"><b>Copy To Clipboard...</b></button>
</p>
<button id="d_clip_button_pre_text" class="my_clip_button" data-clipboard-target="clipboard_pre"><b>Copy To Clipboard...</b></button>
<input type="text" id="clipboard_text" value="Clipboard Text"/>
<textarea id="clipboard_textarea">Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod
tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam,
quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo
consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse
cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non
proident, sunt in culpa qui officia deserunt mollit anim id est laborum.</textarea>
<pre id="clipboard_pre">Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod
tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam,
quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo
consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse
cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non
proident, sunt in culpa qui officia deserunt mollit anim id est laborum.</pre>
</div>
</body>
</html>

View File

@@ -0,0 +1,40 @@
/*global ZeroClipboard, _clientConstructor, _clientClip, _clientShouldEmit */
(function(module, test) {
"use strict";
module("client/private.js unit tests");
test("`_clientShouldEmit` works", function(assert) {
assert.expect(9);
// Arrange
var currentEl = document.getElementById("d_clip_button");
var client = new ZeroClipboard();
_clientConstructor.call(client);
// Act
var actual1 = _clientShouldEmit.call(client, null);
var actual2 = _clientShouldEmit.call(client, {});
var actual3 = _clientShouldEmit.call(client, { type: "beforecopy", client: {} });
var actual4 = _clientShouldEmit.call(client, { type: "beforecopy", target: {} });
_clientClip.call(client, currentEl);
var actual5 = _clientShouldEmit.call(client, { type: "beforecopy", target: {}, relatedTarget: {} });
var actual6 = _clientShouldEmit.call(client, { type: "beforecopy", client: client });
var actual7 = _clientShouldEmit.call(client, { type: "beforecopy", target: null });
var actual8 = _clientShouldEmit.call(client, { type: "beforecopy", target: currentEl });
var actual9 = _clientShouldEmit.call(client, { type: "beforecopy", relatedTarget: currentEl });
// Assert
assert.strictEqual(actual1, false, "Non-event returns `false`");
assert.strictEqual(actual2, false, "Event without `type` returns `false`");
assert.strictEqual(actual3, false, "Event with non-matching `client` returns `false`");
assert.strictEqual(actual4, false, "Event with non-clipped `target` returns `false`");
assert.strictEqual(actual5, false, "Event with non-clipped `relatedTarget` returns `false`");
assert.strictEqual(actual6, true, "Event with matching `client` returns `true`");
assert.strictEqual(actual7, true, "Event with `target` of `null` returns `true`");
assert.strictEqual(actual8, true, "Event with clipped `target` returns `true`");
assert.strictEqual(actual9, true, "Event with clipped `relatedTarget` returns `true`");
});
})(QUnit.module, QUnit.test);

View File

@@ -0,0 +1,53 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>ZeroClipboard unit tests: client/private.js</title>
<link rel="stylesheet" href="../../node_modules/qunitjs/qunit/qunit.css">
<script src="../../node_modules/qunitjs/qunit/qunit.js"></script>
<script>
// Polyfill in future functionality: https://github.com/jquery/qunit/issues/490
if (!QUnit.assert.expect) {
QUnit.assert.expect = QUnit.expect;
}
// Require `expect` calls
QUnit.config.requireExpects = true;
// Prevent against Firefox/Firebug failing the global pollution check
var getInterface = null;
// Prevent against failing the global pollution check in all browsers other than IE
if (typeof window.ActiveXObject === "undefined") {
window.ActiveXObject = null;
}
// Prevent against Flash's ExternalInterface failing the global pollution check (seems to be only in IE < 11)
var __flash__arrayToXML = null,
__flash__argumentsToXML = null,
__flash__objectToXML = null,
__flash__escapeXML = null,
__flash__toXML = null,
__flash__addCallback = null,
__flash__removeCallback = null,
__flash__request = null;
</script>
<script src="../../src/js/shared/state.js"></script>
<script src="../../src/js/shared/private.js"></script>
<script src="../../src/js/core/state.js"></script>
<script src="../../src/js/core/private.js"></script>
<script src="../../src/js/core/api.js"></script>
<script src="../../src/js/client/state.js"></script>
<script src="../../src/js/client/private.js"></script>
<script src="../../node_modules/jquery/dist/jquery.js"></script>
<script src="private.tests.js"></script>
</head>
<body>
<div id="qunit"></div>
<div id="qunit-fixture">
<p>
<button id="d_clip_button" class="my_clip_button" title="Click me to copy to clipboard." data-clipboard-text="Copy me!"><b>Copy To Clipboard...</b></button>
</p>
</div>
</body>
</html>

View File

@@ -0,0 +1,441 @@
/*global ZeroClipboard, _globalConfig:true, _flashState, _clipData, _clipDataFormatMap, _deleteOwnProperties */
(function(module, test) {
"use strict";
// Helper functions
var TestUtils = {
getHtmlBridge: function() {
return document.getElementById("global-zeroclipboard-html-bridge");
}
};
var originalConfig, originalFlashDetect;
module("core/api.js unit tests - state");
test("`state` produces expected result", function(assert) {
assert.expect(8);
// Act
var result = ZeroClipboard.state();
// Assert
assert.deepEqual(Object.keys(result), ["browser", "flash", "zeroclipboard"], "Has all expected keys");
assert.strictEqual(typeof result.browser, "object", ".browser is an object");
assert.notStrictEqual(result.browser, null, ".browser is a non-null object");
assert.strictEqual(typeof result.flash, "object", ".flash is an object");
assert.notStrictEqual(result.flash, null, ".flash is a non-null object");
assert.strictEqual(typeof result.zeroclipboard, "object", ".zeroclipboard is an object");
assert.notStrictEqual(result.zeroclipboard, null, ".zeroclipboard is a non-null object");
assert.deepEqual(Object.keys(result.zeroclipboard), ["version", "config"], ".zeroclipboard has all expected keys");
});
module("core/api.js unit tests - config", {
setup: function() {
originalConfig = ZeroClipboard.config();
},
teardown: function() {
_globalConfig = originalConfig;
}
});
test("`swfPath` finds the expected default URL", function(assert) {
assert.expect(1);
// Assert, act, assert
var rootOrigin = window.location.protocol + "//" + window.location.host + "/";
var indexOfTest = window.location.pathname.toLowerCase().indexOf("/test/");
var rootDir = window.location.pathname.slice(1, indexOfTest + 1);
var rootPath = rootOrigin + rootDir;
//var stateJsUrl = rootPath + "src/js/core/state.js";
// This is, for the record, a totally incorrect path due to being the development
// file structure but it IS the correct URL based on calculated assumption of using
// the built distributable versions of the library
var swfPathBasedOnStateJsPath = rootPath + "src/js/core/ZeroClipboard.swf";
// Test that the client has the expected default URL [even if it's not correct]
assert.strictEqual(ZeroClipboard.config("swfPath"), swfPathBasedOnStateJsPath);
});
test("Changing `trustedDomains` works", function(assert) {
assert.expect(5);
// Arrange
var currentHost = window.location.host;
var originalValue = currentHost ? [currentHost] : [];
var updatedValue = currentHost ? [currentHost, "otherDomain.com"] : ["otherDomain.com"];
// Assert, act, assert
// Test that the client has the default value
assert.deepEqual(ZeroClipboard.config("trustedDomains"), originalValue);
assert.deepEqual(ZeroClipboard.config().trustedDomains, originalValue);
// Change the value
var updatedConfig = ZeroClipboard.config({ trustedDomains: updatedValue });
// Test that the client has the changed value
assert.deepEqual(updatedConfig.trustedDomains, updatedValue);
assert.deepEqual(ZeroClipboard.config("trustedDomains"), updatedValue);
assert.deepEqual(ZeroClipboard.config().trustedDomains, updatedValue);
});
test("Some config values are ignored if SWF is actively embedded", function(assert) {
assert.expect(2);
// Arrange
var _swfPath = ZeroClipboard.config("swfPath");
var expectedBefore = {
swfPath: _swfPath,
trustedDomains: window.location.host ? [window.location.host] : [],
cacheBust: true,
forceEnhancedClipboard: false,
flashLoadTimeout: 30000,
autoActivate: true,
containerId: "global-zeroclipboard-html-bridge",
containerClass: "global-zeroclipboard-container",
swfObjectId: "global-zeroclipboard-flash-bridge",
hoverClass: "zeroclipboard-is-hover",
activeClass: "zeroclipboard-is-active",
// These configuration values CAN be modified while a SWF is actively embedded.
bubbleEvents: true,
forceHandCursor: false,
title: null,
zIndex: 999999999
};
var expectedAfter = {
swfPath: _swfPath,
trustedDomains: window.location.host ? [window.location.host] : [],
cacheBust: true,
forceEnhancedClipboard: false,
flashLoadTimeout: 30000,
autoActivate: true,
containerId: "global-zeroclipboard-html-bridge",
containerClass: "global-zeroclipboard-container",
swfObjectId: "global-zeroclipboard-flash-bridge",
hoverClass: "zeroclipboard-is-hover",
activeClass: "zeroclipboard-is-active",
// These configuration values CAN be modified while a SWF is actively embedded.
bubbleEvents: false,
forceHandCursor: true,
title: "test",
zIndex: 1000
};
// Act
var actualBefore = ZeroClipboard.config();
_flashState.bridge = {};
var actualAfter = ZeroClipboard.config({
swfPath: "/path/to/test.swf",
trustedDomains: ["test.domain.com"],
cacheBust: false,
forceEnhancedClipboard: true,
flashLoadTimeout: 15000,
autoActivate: false,
containerId: "test-id",
containerClass: "test-class",
swfObjectId: "test-swf",
hoverClass: "test-hover",
activeClass: "test-active",
// These configuration values CAN be modified while a SWF is actively embedded.
bubbleEvents: false,
forceHandCursor: true,
title: "test",
zIndex: 1000
});
// Assert
assert.deepEqual(actualBefore, expectedBefore, "Original config is as expected");
assert.deepEqual(actualAfter, expectedAfter, "Updated config is as expected");
});
module("core/api.js unit tests - clipboard", {
teardown: function() {
_deleteOwnProperties(_clipData);
}
});
test("`setData` works", function(assert) {
assert.expect(4);
// Assert, Act, repeat ad nauseam
assert.deepEqual(_clipData, {}, "`_clipData` is empty");
ZeroClipboard.setData("text/plain", "zc4evar");
assert.deepEqual(_clipData, { "text/plain": "zc4evar" }, "`_clipData` contains expected text");
ZeroClipboard.setData("text/x-markdown", "**ZeroClipboard**");
assert.deepEqual(_clipData, { "text/plain": "zc4evar", "text/x-markdown": "**ZeroClipboard**" }, "`_clipData` contains expected text and custom format");
ZeroClipboard.setData({ "text/html": "<b>Win</b>" });
assert.deepEqual(_clipData, { "text/html": "<b>Win</b>" }, "`_clipData` contains expected HTML and cleared out old data because an object was passed in");
});
test("`clearData` works", function(assert) {
assert.expect(4);
// Assert
assert.deepEqual(_clipData, {}, "`_clipData` is empty");
// Arrange & Assert
_clipData["text/plain"] = "zc4evar";
_clipData["text/html"] = "<b>Win</b>";
_clipData["text/x-markdown"] = "**ZeroClipboard**";
assert.deepEqual(_clipData, {
"text/plain": "zc4evar",
"text/html": "<b>Win</b>",
"text/x-markdown": "**ZeroClipboard**"
}, "`_clipData` contains all expected data");
// Act & Assert
ZeroClipboard.clearData("text/html");
assert.deepEqual(_clipData, {
"text/plain": "zc4evar",
"text/x-markdown": "**ZeroClipboard**"
}, "`_clipData` had 'text/html' successfully removed");
// Act & Assert
ZeroClipboard.clearData();
assert.deepEqual(_clipData, {}, "`_clipData` had all data successfully removed");
});
module("core/api.js unit tests - flash", {
setup: function() {
// Store
originalFlashDetect = ZeroClipboard.isFlashUnusable;
// Modify
ZeroClipboard.isFlashUnusable = function() {
return false;
};
},
teardown: function() {
// Restore
ZeroClipboard.isFlashUnusable = originalFlashDetect;
ZeroClipboard.destroy();
}
});
test("Flash object is ready after emitting `ready`", function(assert) {
assert.expect(2);
// Arrange
ZeroClipboard.isFlashUnusable = function() {
return false;
};
ZeroClipboard.create();
// Assert, act, assert
assert.strictEqual(_flashState.ready, false);
// `emit`-ing event handlers are async (generally) but the internal `ready` state is set synchronously
ZeroClipboard.emit("ready");
assert.strictEqual(_flashState.ready, true);
});
test("Object has a title", function(assert) {
assert.expect(1);
// Arrange
var currentEl = document.getElementById("d_clip_button");
ZeroClipboard.create();
// Act
ZeroClipboard.activate(currentEl);
// Assert
assert.strictEqual(TestUtils.getHtmlBridge().getAttribute("title"), "Click me to copy to clipboard.");
// Revert
ZeroClipboard.deactivate();
});
test("Object has no title", function(assert) {
assert.expect(1);
// Arrange
var currentEl = document.getElementById("d_clip_button_no_title");
ZeroClipboard.create();
// Act
ZeroClipboard.activate(currentEl);
// Assert
assert.ok(!TestUtils.getHtmlBridge().getAttribute("title"));
// Revert
ZeroClipboard.deactivate();
});
test("Object has data-clipboard-text", function(assert) {
assert.expect(3);
// Arrange
var currentEl = document.getElementById("d_clip_button");
ZeroClipboard.create();
// Act
ZeroClipboard.activate(currentEl);
var pendingText = ZeroClipboard.emit("copy");
// Assert
assert.deepEqual(_clipData, { "text/plain": "Copy me!" });
assert.deepEqual(pendingText, { "text": "Copy me!" });
assert.deepEqual(_clipDataFormatMap, { "text": "text/plain" });
// Revert
ZeroClipboard.deactivate();
});
test("Object has data-clipboard-target textarea", function(assert) {
assert.expect(3);
// Arrange
var currentEl = document.getElementById("d_clip_button_textarea_text");
var expectedText =
"Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod\n"+
"tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam,\n"+
"quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo\n"+
"consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse\n"+
"cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non\n"+
"proident, sunt in culpa qui officia deserunt mollit anim id est laborum.";
ZeroClipboard.create();
// Act
ZeroClipboard.activate(currentEl);
var pendingText = ZeroClipboard.emit("copy");
// Assert
assert.strictEqual(_clipData["text/plain"].replace(/\r\n/g, "\n"), expectedText);
assert.strictEqual(pendingText.text.replace(/\r\n/g, "\n"), expectedText);
assert.deepEqual(_clipDataFormatMap, { "text": "text/plain" });
// Revert
ZeroClipboard.deactivate();
});
test("Object has data-clipboard-target pre", function(assert) {
assert.expect(5);
// Arrange
var currentEl = document.getElementById("d_clip_button_pre_text");
var expectedText =
"Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod\n"+
"tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam,\n"+
"quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo\n"+
"consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse\n"+
"cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non\n"+
"proident, sunt in culpa qui officia deserunt mollit anim id est laborum.";
var expectedHtml =
"<pre id=\"clipboard_pre\">"+
"Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod\n"+
"tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam,\n"+
"quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo\n"+
"consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse\n"+
"cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non\n"+
"proident, sunt in culpa qui officia deserunt mollit anim id est laborum."+
"</pre>";
ZeroClipboard.create();
// Act
ZeroClipboard.activate(currentEl);
var pendingText = ZeroClipboard.emit("copy");
// Assert
assert.strictEqual(_clipData["text/plain"].replace(/\r\n/g, "\n"), expectedText);
assert.strictEqual(
_clipData["text/html"]
.replace(/\r\n/g, "\n")
.replace(/<\/?pre(?:\s+[^>]*)?>/gi, function($0) { return $0.toLowerCase(); }),
expectedHtml
);
assert.strictEqual(pendingText.text.replace(/\r\n/g, "\n"), expectedText);
assert.strictEqual(
pendingText.html
.replace(/\r\n/g, "\n")
.replace(/<\/?pre(?:\s+[^>]*)?>/gi, function($0) { return $0.toLowerCase(); }),
expectedHtml
);
assert.deepEqual(_clipDataFormatMap, { "text": "text/plain", "html": "text/html" });
// Revert
ZeroClipboard.deactivate();
});
test("Object has data-clipboard-target input", function(assert) {
assert.expect(3);
// Arrange
var currentEl = document.getElementById("d_clip_button_input_text");
ZeroClipboard.create();
// Act
ZeroClipboard.activate(currentEl);
var pendingText = ZeroClipboard.emit("copy");
// Assert
assert.deepEqual(_clipData, { "text/plain": "Clipboard Text" });
assert.deepEqual(pendingText, { "text": "Clipboard Text" });
assert.deepEqual(_clipDataFormatMap, { "text": "text/plain" });
// Revert
ZeroClipboard.deactivate();
});
test("Object doesn't have data-clipboard-text", function(assert) {
assert.expect(1);
// Arrange
var currentEl = document.getElementById("d_clip_button_no_text");
ZeroClipboard.create();
// Act
ZeroClipboard.activate(currentEl);
// Assert
assert.ok(!TestUtils.getHtmlBridge().getAttribute("data-clipboard-text"));
// Revert
ZeroClipboard.deactivate();
});
test("Calculations based on borderWidth never return NaN", function(assert) {
assert.expect(4);
// Arrange
var currentEl = document.getElementById("d_clip_button");
ZeroClipboard.create();
// Act
ZeroClipboard.activate(currentEl);
// Assert
assert.strictEqual(/^-?[0-9\.]+px$/.test(TestUtils.getHtmlBridge().style.top), true);
assert.strictEqual(/^-?[0-9\.]+px$/.test(TestUtils.getHtmlBridge().style.left), true);
assert.strictEqual(/^-?[0-9\.]+px$/.test(TestUtils.getHtmlBridge().style.width), true);
assert.strictEqual(/^-?[0-9\.]+px$/.test(TestUtils.getHtmlBridge().style.height), true);
});
})(QUnit.module, QUnit.test);

View File

@@ -0,0 +1,77 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>ZeroClipboard unit tests: core/api.js</title>
<link rel="stylesheet" href="../../node_modules/qunitjs/qunit/qunit.css">
<script src="../../node_modules/qunitjs/qunit/qunit.js"></script>
<script>
// Polyfill in future functionality: https://github.com/jquery/qunit/issues/490
if (!QUnit.assert.expect) {
QUnit.assert.expect = QUnit.expect;
}
// Require `expect` calls
QUnit.config.requireExpects = true;
// Prevent against Firefox/Firebug failing the global pollution check
var getInterface = null;
// Prevent against failing the global pollution check in all browsers other than IE
if (typeof window.ActiveXObject === "undefined") {
window.ActiveXObject = null;
}
// Prevent against Flash's ExternalInterface failing the global pollution check (seems to be only in IE < 11)
var __flash__arrayToXML = null,
__flash__argumentsToXML = null,
__flash__objectToXML = null,
__flash__escapeXML = null,
__flash__toXML = null,
__flash__addCallback = null,
__flash__removeCallback = null,
__flash__request = null;
</script>
<script src="../../src/js/shared/state.js"></script>
<script src="../../src/js/shared/private.js"></script>
<script src="../../src/js/core/state.js"></script>
<script src="../../src/js/core/private.js"></script>
<script src="../../src/js/core/api.js"></script>
<script src="../../node_modules/jquery/dist/jquery.js"></script>
<script src="api.tests.js"></script>
</head>
<body>
<div id="qunit"></div>
<div id="qunit-fixture">
<p>
<button id="d_clip_button" class="my_clip_button" title="Click me to copy to clipboard." data-clipboard-text="Copy me!"><b>Copy To Clipboard...</b></button>
</p>
<p>
<button id="d_clip_button_no_title" class="my_clip_button" data-clipboard-text="Copy me!"><b>Copy To Clipboard...</b></button>
</p>
<p>
<button id="d_clip_button_no_text" class="my_clip_button"><b>Copy To Clipboard...</b></button>
</p>
<p>
<button id="d_clip_button_input_text" class="my_clip_button" data-clipboard-target="clipboard_text"><b>Copy To Clipboard...</b></button>
</p>
<p>
<button id="d_clip_button_textarea_text" class="my_clip_button" data-clipboard-target="clipboard_textarea"><b>Copy To Clipboard...</b></button>
</p>
<button id="d_clip_button_pre_text" class="my_clip_button" data-clipboard-target="clipboard_pre"><b>Copy To Clipboard...</b></button>
<input type="text" id="clipboard_text" value="Clipboard Text"/>
<textarea id="clipboard_textarea">Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod
tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam,
quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo
consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse
cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non
proident, sunt in culpa qui officia deserunt mollit anim id est laborum.</textarea>
<pre id="clipboard_pre">Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod
tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam,
quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo
consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse
cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non
proident, sunt in culpa qui officia deserunt mollit anim id est laborum.</pre>
</div>
</body>
</html>

View File

@@ -0,0 +1,494 @@
/*global _flashState:true, _currentElement:true, _copyTarget:true, _extend, _getStyle, _removeClass, _addClass, _vars, _cacheBust, _extractDomain, _determineScriptAccess, _mapClipDataToFlash, _mapClipResultsFromFlash, _createEvent, _preprocessEvent, _getRelatedTarget, _shouldPerformAsync, _dispatchCallback, _detectFlashSupport */
(function(module, test) {
"use strict";
var mimeType, ax, flashState;
module("core/private.js unit tests - utils");
test("`_getStyle` returns computed styles", function(assert) {
assert.expect(5);
// Arrange
var pointerEl = $("a.no_cursor_style")[0];
var nonPointerEl = $("a.no_pointer_anchor")[0];
var zIndexAutoEl = $(".zindex-auto")[0];
var clipButtonEl = $("#d_clip_button")[0];
var bigBorderEl = $(".big-border")[0];
// Act
var pointerElComputedCursor = _getStyle(pointerEl, "cursor");
var nonPointerElComputedCursor = _getStyle(nonPointerEl, "cursor");
var zIndexAutoElComputedZIndex = _getStyle(zIndexAutoEl, "z-index");
var clipButtonElComputedBorderLeftWidth = _getStyle(clipButtonEl, "border-left-width");
var bigBorderElComputedBorderLeftWith = _getStyle(bigBorderEl, "border-left-width");
// Assert
assert.strictEqual(pointerElComputedCursor, "pointer");
assert.notStrictEqual(nonPointerElComputedCursor, "pointer");
// Returns 0 in IE7, "auto" everywhere else
assert.strictEqual(/^(?:auto|0)$/.test(zIndexAutoElComputedZIndex), true);
// This varies between "0px" and "3px" depending on the browser (WAT?)
assert.strictEqual(/^[0-3]px$/.test(clipButtonElComputedBorderLeftWidth), true);
assert.strictEqual(bigBorderElComputedBorderLeftWith, "10px");
});
test("`_removeClass` removes classes from element", function(assert) {
assert.expect(5);
// Arrange
var div = $("<div></div>").addClass("class1 class-2 class_3")[0];
// Act & Assert
_removeClass(div, "class1");
assert.strictEqual(div.className, "class-2 class_3");
_removeClass(div, "classd");
assert.strictEqual(div.className, "class-2 class_3");
_removeClass(div, "class-2");
assert.strictEqual(div.className, "class_3");
_removeClass(div, "class_3");
assert.strictEqual(div.className, "");
_removeClass(div, "class-3");
assert.strictEqual(div.className, "");
div = null;
});
test("`_removeClass` doesn't remove partial class names", function(assert) {
assert.expect(3);
// Arrange
var div = $("<div></div>").addClass("class1 class-2 class_3")[0];
// Act & Assert
_removeClass(div, "ass");
assert.strictEqual(div.className, "class1 class-2 class_3");
_removeClass(div, "-2");
assert.strictEqual(div.className, "class1 class-2 class_3");
_removeClass(div, "_3");
assert.strictEqual(div.className, "class1 class-2 class_3");
div = null;
});
test("`_addClass` adds a class name", function(assert) {
assert.expect(4);
// Arrange
var div = $("<div></div>")[0];
// Act & Assert
_addClass(div, "class1");
assert.strictEqual(div.className, "class1");
_addClass(div, "class-2");
assert.strictEqual(div.className, "class1 class-2");
_addClass(div, "class_3");
assert.strictEqual(div.className, "class1 class-2 class_3");
_addClass(div, "class_3");
assert.strictEqual(div.className, "class1 class-2 class_3");
div = null;
});
test("`_vars` builds FlashVars", function(assert) {
assert.expect(6);
// Arrange
var clipOptionsEmpty = {};
var clipOptionsTrustedDomains = {
trustedDomains: ["*"]
};
var clipOptionsEnhancedClipboardFalse = {
forceEnhancedClipboard: false
};
var clipOptionsEnhancedClipboardTrue = {
forceEnhancedClipboard: true
};
var clipOptionsTrustedDomainsPlusEnhancedClipboardFalse = {
trustedDomains: ["*"],
forceEnhancedClipboard: false
};
var clipOptionsTrustedDomainsPlusEnhancedClipboardTrue = {
trustedDomains: ["*"],
forceEnhancedClipboard: true
};
// Act & Assert
assert.strictEqual(_vars(clipOptionsEmpty), "");
assert.strictEqual(_vars(clipOptionsTrustedDomains), "trustedOrigins=*");
assert.strictEqual(_vars(clipOptionsEnhancedClipboardFalse), "");
assert.strictEqual(_vars(clipOptionsEnhancedClipboardTrue), "forceEnhancedClipboard=true");
assert.strictEqual(_vars(clipOptionsTrustedDomainsPlusEnhancedClipboardFalse), "trustedOrigins=*");
assert.strictEqual(_vars(clipOptionsTrustedDomainsPlusEnhancedClipboardTrue), "trustedOrigins=*&forceEnhancedClipboard=true");
});
test("`_cacheBust` adds cache-buster appropriately", function(assert) {
assert.expect(2);
// Arrange
var pathWithoutQuery = "path.com/z.swf";
var pathWithQuery = "path.com/z.swf?q=jon";
// Act & Assert
assert.strictEqual(_cacheBust(pathWithoutQuery).indexOf("?noCache="), 0);
assert.strictEqual(_cacheBust(pathWithQuery).indexOf("&noCache="), 0);
});
test("`_cacheBust` can be disabled", function(assert) {
assert.expect(2);
// Arrange
var pathWithoutQuery = "path.com/z.swf";
var pathWithQuery = "path.com/z.swf?q=jon";
var options = {
cacheBust: false
};
// Act & Assert
assert.strictEqual(_cacheBust(pathWithoutQuery, options), "");
assert.strictEqual(_cacheBust(pathWithQuery, options), "");
});
test("`_extractDomain` extracts domains from origins and URLs", function(assert) {
assert.expect(20);
// Arrange
var inputToExpectedMap = {
"": null,
" ": null,
"ZeroClipboard.swf": null,
"js/ZeroClipboard.swf": null,
"/js/ZeroClipboard.swf": null,
"/zeroclipboard/zeroclipboard/": null,
"zeroclipboard/zeroclipboard/": null,
"*": "*",
"github.com": "github.com",
"http://github.com": "github.com",
"https://github.com": "github.com",
"github.com:80": "github.com:80",
"http://github.com:80": "github.com:80",
"https://github.com:443": "github.com:443",
"http://github.com/zeroclipboard/zeroclipboard/": "github.com",
"https://github.com/zeroclipboard/zeroclipboard/": "github.com",
"http://github.com:80/zeroclipboard/zeroclipboard/": "github.com:80",
"https://github.com:443/zeroclipboard/zeroclipboard/": "github.com:443"
};
// Act & Assert
assert.strictEqual(_extractDomain(undefined), null, "Processing: `undefined`");
assert.strictEqual(_extractDomain(null), null, "Processing: `null`");
for (var originOrUrl in inputToExpectedMap) {
if (inputToExpectedMap.hasOwnProperty(originOrUrl)) {
assert.strictEqual(_extractDomain(originOrUrl), inputToExpectedMap[originOrUrl], "Processing: \"" + originOrUrl + "\"");
}
}
});
test("`_determineScriptAccess` determines the appropriate script access level", function(assert) {
// Arrange
var i, len, tmp;
var currentDomain = window.location.host || "localhost";
var _globalConfig = {
swfPath: "ZeroClipboard.swf",
trustedDomains: [currentDomain]
};
var inputToExpectedMap = [
// Same-domain SWF
{ args: [currentDomain, _globalConfig], result: "sameDomain" },
{ args: [currentDomain, _extend({}, _globalConfig, { trustedDomains: [] })], result: "never" },
{ args: [currentDomain, _extend({}, _globalConfig, { trustedDomains: ["*"] })], result: "always" },
{ args: [currentDomain, _extend({}, _globalConfig, { trustedDomains: [currentDomain, "otherDomain.com"] })], result: "always" },
{ args: [currentDomain, _extend({}, _globalConfig, { trustedDomains: ["otherDomain.com"] })], result: "never" },
// Cross-domain SWF
{ args: [currentDomain, _extend({}, _globalConfig, { swfPath: "//otherDomain.com/ZeroClipboard.swf" })], result: "always" },
{ args: [currentDomain, _extend({}, _globalConfig, { swfPath: "//otherDomain.com/ZeroClipboard.swf", trustedDomains: [] })], result: "never" },
{ args: [currentDomain, _extend({}, _globalConfig, { swfPath: "//otherDomain.com/ZeroClipboard.swf", trustedDomains: ["*"] })], result: "always" },
{ args: [currentDomain, _extend({}, _globalConfig, { swfPath: "//otherDomain.com/ZeroClipboard.swf", trustedDomains: [currentDomain, "otherDomain.com"] })], result: "always" }
];
// Act & Assert
assert.expect(9);
for (i = 0, len = inputToExpectedMap.length; i < len; i++) {
tmp = inputToExpectedMap[i];
assert.strictEqual(_determineScriptAccess.apply(this, tmp.args), tmp.result, "Processing: " + JSON.stringify(tmp));
}
});
test("`_mapClipDataToFlash` works", function(assert) {
assert.expect(1);
// Arrange
var clipData = {
"text/plain": "Zero",
"text/html": "<b>Zero</b>"
};
var expectedOutput = {
data: {
"text": "Zero",
"html": "<b>Zero</b>"
},
formatMap: {
"text": "text/plain",
"html": "text/html"
}
};
// Act
var actual = _mapClipDataToFlash(clipData);
// Assert
assert.deepEqual(actual, expectedOutput, "Converted keys to Flash-friendly names and provided a format map");
});
test("`_mapClipResultsFromFlash` works", function(assert) {
assert.expect(2);
// Arrange
var clipResults = {
type: "aftercopy",
success: {
"text": true,
"html": false
},
data: {
"text": "Zero",
"html": "<b>Zero</b>"
}
};
var formatMap = {
"text": "text/plain",
"html": "text/html"
};
var expectedOutput = {
type: "aftercopy",
success: {
"text/plain": true,
"text/html": false
},
data: {
"text/plain": "Zero",
"text/html": "<b>Zero</b>"
}
};
// Act & Assert
var thisWontChange = _mapClipResultsFromFlash(clipResults, null);
assert.deepEqual(thisWontChange, clipResults, "Should return the original object if it cannot map it");
// Act & Assert
var revisedClipResults = _mapClipResultsFromFlash(clipResults, formatMap);
assert.deepEqual(revisedClipResults, expectedOutput, "Should reverse the key mapping process");
});
test("`_createEvent` works", function(assert) {
assert.expect(2);
var actual = _createEvent("ready");
assert.strictEqual(typeof actual === "object" && actual != null, true, "Returns non-null object");
assert.strictEqual(actual.type, "ready", "Object has a `type` property of 'ready'");
// etc.
});
// Tests fix for: https://github.com/zeroclipboard/zeroclipboard/issues/467
test("`_copyTarget` element is handled appropriately", function(assert) {
assert.expect(18);
// Arrange
var el1 = $("#d_clip_button")[0];
var el2 = $("#goodTargetId")[0];
_currentElement = el1;
_copyTarget = null;
// Act
var evt = _createEvent("beforecopy");
_preprocessEvent(evt);
// Assert
assert.strictEqual(_currentElement, el1, "`_currentElement` is 'el1'");
assert.strictEqual(_copyTarget, el1, "`_copyTarget` is 'el1'");
assert.strictEqual(evt.target, el1, "`beforecopy` target is 'el1'");
// Act some more
_currentElement = el2;
evt = _createEvent("copy");
_preprocessEvent(evt);
// Assert some more
assert.strictEqual(_currentElement, el2, "`_currentElement` is 'el2'");
assert.strictEqual(_copyTarget, el1, "`_copyTarget` is 'el1'");
assert.strictEqual(evt.target, el1, "`copy` target is 'el1'");
// Act some more: interruption due to mouse movement (only happens in Firefox,
// though similar issues occur in Chrome for Windows if the user clicks on
// another clipped element)
evt = _createEvent("_mouseover");
_preprocessEvent(evt);
// Assert some more
assert.strictEqual(_currentElement, el2, "`_currentElement` is 'el2'");
assert.strictEqual(_copyTarget, el1, "`_copyTarget` is 'el1'");
assert.strictEqual(evt.target, el2, "`_mouseover` target is 'el2'");
// Act some more
evt = _createEvent("aftercopy");
_preprocessEvent(evt);
// Assert some more
assert.strictEqual(_currentElement, el2, "`_currentElement` is 'el2'");
assert.strictEqual(_copyTarget, el1, "`_copyTarget` is 'el1'");
assert.strictEqual(evt.target, el1, "`aftercopy` target is 'el1'");
// Act some more
evt = _createEvent("_click");
// Assert some more
assert.strictEqual(_currentElement, el2, "`_currentElement` is 'el2'");
assert.strictEqual(_copyTarget, el1, "`_copyTarget` is 'el1'");
assert.strictEqual(evt.target, el1, "`_click` target is 'el1'");
// Act some more
_preprocessEvent(evt);
// Assert some more
assert.strictEqual(_currentElement, el2, "`_currentElement` is 'el2'");
assert.strictEqual(_copyTarget, null, "`_copyTarget` is `null`");
assert.strictEqual(evt.target, el1, "`_click` target is 'el1'");
// Reset
_currentElement = _copyTarget = el1 = el2 = null;
});
test("`_getRelatedTarget` works", function(assert) {
assert.expect(4);
var relTarget = $("#relTargetId")[0];
var goodTarget = $("#goodTargetId")[0];
var badTarget1 = $("#badTargetId1")[0];
var badTarget2 = $("#badTargetId2")[0];
assert.notEqual(relTarget, null, "The related target is `null`");
assert.strictEqual(_getRelatedTarget(goodTarget), relTarget, "Element with `data-clipboard-target` returns `null`");
assert.strictEqual(_getRelatedTarget(badTarget1), null, "Element with `data-clipboard-target` that doesn't much any elements returns `null`");
assert.strictEqual(_getRelatedTarget(badTarget2), null, "Element without `data-clipboard-target` returns `null`");
});
test("`_shouldPerformAsync` works", function(assert) {
assert.expect(4);
// Act & Assert
assert.strictEqual(_shouldPerformAsync({ type: "beforecopy" }), false, "`beforecopy` should be performed synchronously");
assert.strictEqual(_shouldPerformAsync({ type: "copy" }), false, "`copy` should be performed synchronously");
assert.strictEqual(_shouldPerformAsync({ type: "destroy" }), false, "`destroy` should be performed synchronously");
assert.strictEqual(_shouldPerformAsync({ type: "ready" }), true, "All other event types should be performed asynchronously");
});
test("`_dispatchCallback` can fire asynchronously", function(assert) {
assert.expect(6);
// Arrange
var syncExec = false;
var syncProof = false;
var syncProveIt = function() {
syncProof = true;
};
var asyncExec = true;
var asyncProof = false;
var asyncProveIt = function() {
// Resume test evaluation
QUnit.start();
assert.strictEqual(asyncProof, false);
asyncProof = true;
assert.strictEqual(asyncProof, true);
};
// Act & Assert
// Synchronous
assert.strictEqual(syncProof, false);
_dispatchCallback(syncProveIt, null, null, syncExec);
assert.strictEqual(syncProof, true);
// Asynchronous
assert.strictEqual(asyncProof, false);
_dispatchCallback(asyncProveIt, null, null, asyncExec);
assert.strictEqual(asyncProof, false);
// Stop test evaluation
QUnit.stop();
});
module("core/private.js unit tests - flash", {
setup: function() {
flashState = _flashState;
mimeType = window.navigator.mimeTypes["application/x-shockwave-flash"];
ax = window.ActiveXObject;
},
teardown: function() {
window.navigator.mimeTypes["application/x-shockwave-flash"] = mimeType;
window.ActiveXObject = ax;
_flashState = flashState;
}
});
test("Detecting no Flash", function(assert) {
assert.expect(1);
// Arrange
window.navigator.mimeTypes["application/x-shockwave-flash"] = undefined;
window.ActiveXObject = undefined;
// Act
_detectFlashSupport(window.ActiveXObject);
// Assert
assert.strictEqual(_flashState.disabled, true);
});
test("Detecting has Flash mimetype", function(assert) {
assert.expect(1);
// Arrange
window.navigator.mimeTypes["application/x-shockwave-flash"] = {};
window.ActiveXObject = function() { };
// Act
_detectFlashSupport(window.ActiveXObject);
// Assert
assert.strictEqual(_flashState.disabled, false);
});
})(QUnit.module, QUnit.test);

View File

@@ -0,0 +1,68 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>ZeroClipboard unit tests: core/private.js</title>
<link rel="stylesheet" href="../../node_modules/qunitjs/qunit/qunit.css">
<script src="../../node_modules/qunitjs/qunit/qunit.js"></script>
<script>
// Polyfill in future functionality: https://github.com/jquery/qunit/issues/490
if (!QUnit.assert.expect) {
QUnit.assert.expect = QUnit.expect;
}
// Require `expect` calls
QUnit.config.requireExpects = true;
// Prevent against Firefox/Firebug failing the global pollution check
var getInterface = null;
// Prevent against failing the global pollution check in all browsers other than IE
if (typeof window.ActiveXObject === "undefined") {
window.ActiveXObject = null;
}
// Prevent against Flash's ExternalInterface failing the global pollution check (seems to be only in IE < 11)
var __flash__arrayToXML = null,
__flash__argumentsToXML = null,
__flash__objectToXML = null,
__flash__escapeXML = null,
__flash__toXML = null,
__flash__addCallback = null,
__flash__removeCallback = null,
__flash__request = null;
// Provide a shell of the `ZeroClipboard` object to pass through a few tests
var ZeroClipboard = {
setData: function() {},
clearData: function() {},
focus: function(el) {
_currentElement = el;
}
};
</script>
<script src="../../src/js/shared/state.js"></script>
<script src="../../src/js/shared/private.js"></script>
<script src="../../src/js/core/state.js"></script>
<script src="../../src/js/core/private.js"></script>
<script src="../../node_modules/jquery/dist/jquery.js"></script>
<script src="private.tests.js"></script>
</head>
<body>
<div id="qunit"></div>
<div id="qunit-fixture">
<div id="goodTargetId" data-clipboard-target="relTargetId"></div>
<div id="relTargetId"></div>
<div id="badTargetId1" data-clipboard-target="elementIdWithoutMatch"></div>
<div id="badTargetId2"></div>
<button id="d_clip_button" class="my_clip_button" title="Click me to copy to clipboard." data-clipboard-text="Copy me!">
<b>Copy To Clipboard...</b>
</button>
<a href="" class="no_pointer_anchor" style="cursor:default;"></a>
<a href="" class="no_cursor_style" style="cursor:auto;"></a>
<span class="zindex-auto" style="z-index:auto;"></span>
<span class="big-border" style="border: 10px solid red;"></span>
</div>
</body>
</html>

View File

@@ -0,0 +1,28 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>ZeroClipboard unit tests</title>
<link rel="stylesheet" href="../node_modules/qunitjs/qunit/qunit.css">
<link rel="stylesheet" href="../node_modules/qunit-composite/qunit-composite.css">
<script src="../node_modules/qunitjs/qunit/qunit.js"></script>
<script src="../node_modules/qunit-composite/qunit-composite.js"></script>
<script>
(function() {
var queryParams = "?noglobals=true";
QUnit.testSuites([
"shared/private.tests.js.html" + queryParams,
"core/private.tests.js.html" + queryParams,
"core/api.tests.js.html" + queryParams,
"client/private.tests.js.html" + queryParams,
"client/api.tests.js.html" + queryParams,
"built/ZeroClipboard.Core.tests.js.html" + queryParams,
"built/ZeroClipboard.tests.js.html" + queryParams
]);
})();
</script>
</head>
<body>
<div id="qunit"></div>
</body>
</html>

View File

@@ -0,0 +1,911 @@
/*global _document:true, _Error:true, _args, _extend, _deepCopy, _pick, _omit, _deleteOwnProperties, _containedBy, _getDirPathOfUrl, _getCurrentScriptUrlFromErrorStack, _getCurrentScriptUrlFromError:true, _getCurrentScriptUrl, _getUnanimousScriptParentDir, _getDefaultSwfPath */
(function(module, test) {
"use strict";
var doc, errorDef, getUrlFromError;
module("shared/private.js unit tests", {
setup: function() {
doc = _document;
errorDef = _Error;
getUrlFromError = _getCurrentScriptUrlFromError;
},
teardown: function() {
_document = doc;
_Error = errorDef;
_getCurrentScriptUrlFromError = getUrlFromError;
}
});
test("`_args` works", function(assert) {
assert.expect(4);
// Arrange
var _arguments = function() {
return arguments;
};
var fn = function() {};
var expectedOutput1 = [1, 2, 3];
var expectedOutput2 = [fn];
var expectedOutput3 = [{ foo: "bar" }];
var expectedOutput4 = [[1, 2, 3]];
var inputArgs1 = _arguments(1, 2, 3);
var inputArgs2 = _arguments(fn);
var inputArgs3 = _arguments({ foo: "bar" });
var inputArgs4 = _arguments([1, 2, 3]);
// Act
var actualOutput1 = _args(inputArgs1);
var actualOutput2 = _args(inputArgs2);
var actualOutput3 = _args(inputArgs3);
var actualOutput4 = _args(inputArgs4);
// Arrange
assert.deepEqual(actualOutput1, expectedOutput1);
assert.deepEqual(actualOutput2, expectedOutput2);
assert.deepEqual(actualOutput3, expectedOutput3);
assert.deepEqual(actualOutput4, expectedOutput4);
});
test("`_extend` works on plain objects", function(assert) {
assert.expect(5);
// Plain objects
var a = {
"a": "apple",
"c": "cantalope"
},
b = {
"b": "banana",
"c": "cherry" // cuz cantalope sucks ;)
},
c = {
"a": "apple",
"b": "banana",
"c": "cherry"
};
assert.deepEqual(_extend({}, a), a, "actual equals expected, `target` is updated, `source` is unaffected");
assert.deepEqual(_extend({}, b), b, "actual equals expected, `target` is updated, `source` is unaffected");
assert.deepEqual(_extend({}, c), c, "actual equals expected, `target` is updated, `source` is unaffected");
assert.deepEqual(_extend(a, b), c, "actual equals expected");
assert.deepEqual(a, c, "`a` equals `c` because `_extend` updates the `target` argument");
});
test("`_extend` only copies owned properties", function(assert) {
assert.expect(1);
// Now prototypes...
var SomeClass = function() {
this.b = "banana";
};
SomeClass.prototype.c = "cantalope"; // cuz cantalope sucks ;)
var a = {
"a": "apple",
"c": "cherry"
},
b = new SomeClass(),
c = {
"a": "apple",
"b": "banana",
"c": "cherry"
};
assert.deepEqual(_extend(a, b), c, "actual equals expected because `_extend` does not copy over prototype properties");
});
test("`_extend` only copies owned properties from Array source", function(assert) {
assert.expect(3);
var a = {
"a": "apple",
"b": "banana"
},
b = ["zero", "one", "two"],
c = {
"a": "apple",
"b": "banana",
"0": "zero",
"1": "one",
"2": "two"
};
assert.deepEqual(_extend(a, b), c, "actual equals expected because `_extend` does not copy over prototype properties");
assert.strictEqual("length" in a, false, "`a` should not have gained a `length` property");
assert.strictEqual("length" in b, true, "`b` should still have a `length` property");
});
test("`_extend` will merge multiple objects", function(assert) {
assert.expect(2);
var a = {
"a": "apple",
"c": "cantalope",
"d": "dragon fruit"
},
b = {
"b": "banana",
"c": "cherry" // cuz cantalope sucks ;)
},
c = {
"a": "apricot",
"b": "blueberry"
},
d = {
"a": "apricot",
"b": "blueberry",
"c": "cherry",
"d": "dragon fruit"
};
assert.deepEqual(_extend({}, a, b, c), d, "actual equals expected, `target` is updated, `source` is unaffected");
assert.deepEqual(_extend(a, b, c), d, "actual equals expected");
});
test("`_deepCopy` works", function(assert) {
assert.expect(13);
// Arrange
var input1 = {
"a": "b",
"b": {
"c": "d"
}
};
var input2 = [[1, 2], 2];
var expected1 = {
"a": "b",
"b": {
"c": "d"
}
};
var expected2 = [[1, 2], 2];
// Act
var actual1 = _deepCopy(input1);
var actual2 = _deepCopy(input2);
// Assert
assert.deepEqual(actual1, expected1, "Objects are deeply equal");
assert.notStrictEqual(actual1, expected1, "Objects are not strictly equal");
assert.strictEqual(actual1.a, expected1.a, "Objects' non-object properties are strictly equal");
assert.deepEqual(actual1.b, expected1.b, "Objects' object properties are deeply equal");
assert.notStrictEqual(actual1.b, expected1.b, "Objects' object properties are not strictly equal");
assert.strictEqual(actual1.b.c, expected1.b.c, "Objects' object properties' non-object properties are strictly equal");
assert.deepEqual(actual2, expected2, "Arrays are deeply equal");
assert.notStrictEqual(actual2, expected2, "Arrays are not strictly equal");
assert.deepEqual(actual2[0], expected2[0], "Sub-arrays are deeply equal");
assert.notStrictEqual(actual2[0], expected2[0], "Sub-arrays are not strictly equal");
assert.strictEqual(actual2[0][0], expected2[0][0], "Sub-arrays' first items are strictly equal");
assert.strictEqual(actual2[0][1], expected2[0][1], "Sub-arrays' second items are strictly equal");
assert.strictEqual(actual2[1], expected2[1], "Sub-items are strictly equal");
});
test("`_pick` works", function(assert) {
assert.expect(6);
// Arrange
var obj1 = {};
var obj2 = {
"name": "Zero",
"version": "v2.x",
"other": "test"
};
var filter1 = [];
var filter2 = ["name", "version"];
var filter3 = ["name", "version", "other"];
var expected1x = {};
var expected21 = {};
var expected22 = {
"name": "Zero",
"version": "v2.x"
};
var expected23 = {
"name": "Zero",
"version": "v2.x",
"other": "test"
};
// Act
var result11 = _pick(obj1, filter1);
var result12 = _pick(obj1, filter2);
var result13 = _pick(obj1, filter3);
var result21 = _pick(obj2, filter1);
var result22 = _pick(obj2, filter2);
var result23 = _pick(obj2, filter3);
// Assert
assert.deepEqual(result11, expected1x, "An empty object cannot have any properties picked");
assert.deepEqual(result12, expected1x, "An empty object cannot have any properties picked");
assert.deepEqual(result13, expected1x, "An empty object cannot have any properties picked");
assert.deepEqual(result21, expected21, "An object with an empty pick list will have nothing picked");
assert.deepEqual(result22, expected22, "An object with a subset pick list will have only those properties picked");
assert.deepEqual(result23, expected23, "An object with a complete pick list will have all of its properties picked");
});
test("`_omit` works", function(assert) {
assert.expect(6);
// Arrange
var obj1 = {};
var obj2 = {
"name": "Zero",
"version": "v2.x",
"other": "test"
};
var filter1 = [];
var filter2 = ["name", "version"];
var filter3 = ["name", "version", "other"];
var expected1x = {};
var expected21 = {
"name": "Zero",
"version": "v2.x",
"other": "test"
};
var expected22 = {
"other": "test"
};
var expected23 = {};
// Act
var result11 = _omit(obj1, filter1);
var result12 = _omit(obj1, filter2);
var result13 = _omit(obj1, filter3);
var result21 = _omit(obj2, filter1);
var result22 = _omit(obj2, filter2);
var result23 = _omit(obj2, filter3);
// Assert
assert.deepEqual(result11, expected1x, "An empty object cannot have any properties picked");
assert.deepEqual(result12, expected1x, "An empty object cannot have any properties picked");
assert.deepEqual(result13, expected1x, "An empty object cannot have any properties picked");
assert.deepEqual(result21, expected21, "An object with an empty omit list will have everything picked");
assert.deepEqual(result22, expected22, "An object with a subset omit list will have everything but those properties picked");
assert.deepEqual(result23, expected23, "An object with a complete omit list will have nothing picked");
});
test("`_deleteOwnProperties` will delete all owned enumerable properties", function(assert) {
assert.expect(24);
var getNonObjectKeys = function(obj) {
var prop,
keys = [];
if (obj) {
for (prop in obj) {
if (obj.hasOwnProperty(prop)) {
keys.push(prop);
}
}
}
return keys;
};
var getProtoKeys = function(obj) {
var prop,
keys = [];
if (obj) {
for (prop in obj) {
if (!obj.hasOwnProperty(prop)) {
keys.push(prop);
}
}
}
return keys;
};
var a = {
"a": "apple",
"c": "cantalope",
"d": "dragon fruit"
},
b = {},
c = ["banana", "cherry"],
d = (function() {
function SomePrototype() {
this.protoProp = "foo";
}
function SomeClass() {
this.ownedProp = "bar";
}
SomeClass.prototype = new SomePrototype();
SomeClass.prototype.constructor = SomeClass;
return new SomeClass();
})(),
e = null,
f; // = undefined;
assert.deepEqual(Object.keys(a), ["a", "c", "d"]);
assert.deepEqual(getProtoKeys(a), []);
_deleteOwnProperties(a);
assert.deepEqual(Object.keys(a), []);
assert.deepEqual(getProtoKeys(a), []);
assert.deepEqual(Object.keys(b), []);
assert.deepEqual(getProtoKeys(b), []);
_deleteOwnProperties(b);
assert.deepEqual(Object.keys(b), []);
assert.deepEqual(getProtoKeys(b), []);
assert.deepEqual(getNonObjectKeys(c), ["0", "1"]);
assert.deepEqual(getProtoKeys(c), []);
_deleteOwnProperties(c);
assert.deepEqual(getNonObjectKeys(c), []);
assert.deepEqual(getProtoKeys(c), []);
assert.deepEqual(Object.keys(d), ["ownedProp"]);
assert.deepEqual(getProtoKeys(d), ["protoProp", "constructor"]);
_deleteOwnProperties(d);
assert.deepEqual(Object.keys(d), []);
assert.deepEqual(getProtoKeys(d), ["protoProp", "constructor"]);
assert.deepEqual(getNonObjectKeys(e), []);
assert.deepEqual(getProtoKeys(e), []);
_deleteOwnProperties(e);
assert.deepEqual(getNonObjectKeys(e), []);
assert.deepEqual(getProtoKeys(e), []);
assert.deepEqual(getNonObjectKeys(f), []);
assert.deepEqual(getProtoKeys(f), []);
_deleteOwnProperties(f);
assert.deepEqual(getNonObjectKeys(f), []);
assert.deepEqual(getProtoKeys(f), []);
});
test("`_containedBy` works", function(assert) {
/*jshint camelcase:false */
assert.expect(29);
// Arrange
var fixture = document.getElementById("qunit-fixture");
fixture.innerHTML =
"<div id='container'>" +
"<div id='contained1'>" +
"<div id='contained1_1'></div>" +
"<div id='contained1_2'>" +
"<div id='contained1_2_1'></div>" +
"</div>" +
"</div>" +
"<div id='contained2'></div>" +
"</div>" +
"<div id='not_container'>" +
"<div id='not_contained'></div>" +
"</div>";
var container = document.getElementById("container");
var contained1 = document.getElementById("contained1");
var contained1_1 = document.getElementById("contained1_1");
var contained1_2 = document.getElementById("contained1_2");
var contained1_2_1 = document.getElementById("contained1_2_1");
var contained2 = document.getElementById("contained2");
var not_container = document.getElementById("not_container");
var not_contained = document.getElementById("not_contained");
// Act & Assert
assert.strictEqual(_containedBy(contained1_2_1, contained1_2_1), true);
assert.strictEqual(_containedBy(contained1_2_1, contained1_2), true);
assert.strictEqual(_containedBy(contained1_2_1, contained1), true);
assert.strictEqual(_containedBy(contained1_2_1, container), true);
assert.strictEqual(_containedBy(contained1_2_1, fixture), true);
assert.strictEqual(_containedBy(contained1_2_1, not_container), false);
assert.strictEqual(_containedBy(contained1_1, contained1_1), true);
assert.strictEqual(_containedBy(contained1_1, contained1), true);
assert.strictEqual(_containedBy(contained1_1, container), true);
assert.strictEqual(_containedBy(contained1_1, fixture), true);
assert.strictEqual(_containedBy(contained1_1, not_container), false);
assert.strictEqual(_containedBy(contained1, contained1), true);
assert.strictEqual(_containedBy(contained1, container), true);
assert.strictEqual(_containedBy(contained1, fixture), true);
assert.strictEqual(_containedBy(contained1, not_container), false);
assert.strictEqual(_containedBy(contained2, contained2), true);
assert.strictEqual(_containedBy(contained2, container), true);
assert.strictEqual(_containedBy(contained2, fixture), true);
assert.strictEqual(_containedBy(contained2, not_container), false);
assert.strictEqual(_containedBy(container, container), true);
assert.strictEqual(_containedBy(container, fixture), true);
assert.strictEqual(_containedBy(container, not_container), false);
assert.strictEqual(_containedBy(not_contained, not_contained), true);
assert.strictEqual(_containedBy(not_contained, not_container), true);
assert.strictEqual(_containedBy(not_contained, fixture), true);
assert.strictEqual(_containedBy(not_contained, container), false);
assert.strictEqual(_containedBy(not_container, not_container), true);
assert.strictEqual(_containedBy(not_container, fixture), true);
assert.strictEqual(_containedBy(not_container, container), false);
});
test("`_getDirPathOfUrl` works", function(assert) {
assert.expect(8);
// Arrange
var input1 = "http://example.com/blah/foo/index.html";
var input2 = "http://example.com/blah/foo/index.html?q=p";
var input3 = "http://example.com/blah/foo/index.html?q=p&x=z";
var input4 = "http://example.com/blah/foo/index.html?#xyz";
var input5 = "http://example.com/blah/foo/index.html?q=p#xyz";
var input6 = "http://example.com/blah/foo/index.html?q=p&x=z#xyz";
var input7 = "http://example.com/blah/foo/";
var input8 = "";
var expected1 = "http://example.com/blah/foo/";
var expected2 = "http://example.com/blah/foo/";
var expected3 = "http://example.com/blah/foo/";
var expected4 = "http://example.com/blah/foo/";
var expected5 = "http://example.com/blah/foo/";
var expected6 = "http://example.com/blah/foo/";
var expected7 = "http://example.com/blah/foo/";
var expected8;
// Act
var actual1 = _getDirPathOfUrl(input1);
var actual2 = _getDirPathOfUrl(input2);
var actual3 = _getDirPathOfUrl(input3);
var actual4 = _getDirPathOfUrl(input4);
var actual5 = _getDirPathOfUrl(input5);
var actual6 = _getDirPathOfUrl(input6);
var actual7 = _getDirPathOfUrl(input7);
var actual8 = _getDirPathOfUrl(input8);
// Assert
assert.strictEqual(actual1, expected1);
assert.strictEqual(actual2, expected2);
assert.strictEqual(actual3, expected3);
assert.strictEqual(actual4, expected4);
assert.strictEqual(actual5, expected5);
assert.strictEqual(actual6, expected6);
assert.strictEqual(actual7, expected7);
assert.strictEqual(actual8, expected8);
});
test("`_getCurrentScriptUrlFromErrorStack` works", function(assert) {
assert.expect(25);
// Arrange
var localStacks = [
"Error: my uncaught error\n at http://example.com/index.html:123:4\n at jQuery.event.dispatch (http://code.jquery.com/blah.js:567:8)\n at foo",
"http://example.com/index.html:123:4\ndispatch@http://code.jquery.com/blah.js:567:8\nfoo",
"@http://example.com/index.html:123\ndispatch@http://code.jquery.com/blah.js:567\nfoo",
"<anonymous function>([arguments not available])@http://example.com/index.html:123\n<anonymous function: dispatch>([arguments not available])@http://code.jquery.com/blah.js:567\nfoo",
"Error: my error\n at http://example.com/index.html:123\n at http://code.jquery.com/blah.js:567\nfoo",
"Error(\"my error\")@:0\u000a([object Object])@http://example.com/index.html:123\u000a([object Object])@http://code.jquery.com/blah.js:567\u000afoo",
"Error: my error\n at Anonymous function (http://example.com/index.html:123:4)\n at dispatch (http://code.jquery.com/blah.js:567:8)\n at foo",
"Error: my sneaky error message has a URL in it at http://google.com/mean.js:987\n at Anonymous function (http://example.com/index.html:123:4)\n at dispatch (http://code.jquery.com/blah.js:567:8)\n at foo"
];
var remoteStacks = [
"Error: my error\n at window.onload (http://example.com/blah/foo.js:95:11)\n at jQuery.event.dispatch (http://code.jquery.com/blah.js:567:8)\n at foo",
"onload@http://example.com/blah/foo.js:95:11\ndispatch@http://code.jquery.com/blah.js:567:8\nfoo",
"onload@http://example.com/blah/foo.js:95\ndispatch@http://code.jquery.com/blah.js:567\nfoo",
"<anonymous function: window.onload>([arguments not available])@http://example.com/blah/foo.js:95\n<anonymous function: dispatch>([arguments not available])@http://code.jquery.com/blah.js:567\nfoo",
"Error: my error\n at http://example.com/blah/foo.js:95\n at http://code.jquery.com/blah.js:567\nfoo",
"Error(\"my error\")@:0\u000a@http://example.com/blah/foo.js:95\u000a([object Object])@http://code.jquery.com/blah.js:567\u000afoo",
"Error: my error\n at onload (http://example.com/blah/foo.js:95:11)\n at dispatch (http://code.jquery.com/blah.js:567:8)\n at foo",
"Error: my sneaky error message has a URL in it at http://google.com/mean.js:987\n at onload (http://example.com/blah/foo.js:95:11)\n at dispatch (http://code.jquery.com/blah.js:567:8)\n at foo"
];
var badStacks = [
"blah",
"",
[],
{},
null,
undefined
];
var localExpected = "http://example.com/index.html",
remoteExpected = "http://example.com/blah/foo.js",
badExpected;
// Act & Assert
assert.strictEqual(localStacks.length, 8, "Local stacks x 8");
assert.strictEqual(_getCurrentScriptUrlFromErrorStack(localStacks[0]), localExpected, "Inline script: Chrome");
assert.strictEqual(_getCurrentScriptUrlFromErrorStack(localStacks[1]), localExpected, "Inline script: Safari 6.1+");
assert.strictEqual(_getCurrentScriptUrlFromErrorStack(localStacks[2]), localExpected, "Inline script: Safari 6.0");
assert.strictEqual(_getCurrentScriptUrlFromErrorStack(localStacks[3]), localExpected, "Inline script: Opera");
assert.strictEqual(_getCurrentScriptUrlFromErrorStack(localStacks[4]), localExpected, "Inline script: PhantomJS");
assert.strictEqual(_getCurrentScriptUrlFromErrorStack(localStacks[5]), localExpected, "Inline script: Firefox 3.6");
assert.strictEqual(_getCurrentScriptUrlFromErrorStack(localStacks[6]), localExpected, "Inline script: IE 10.0");
assert.strictEqual(_getCurrentScriptUrlFromErrorStack(localStacks[7]), localExpected, "Inline script: SneakyError");
assert.strictEqual(remoteStacks.length, 8, "Remote stacks x 8");
assert.strictEqual(_getCurrentScriptUrlFromErrorStack(remoteStacks[0]), remoteExpected, "External script: Chrome");
assert.strictEqual(_getCurrentScriptUrlFromErrorStack(remoteStacks[1]), remoteExpected, "External script: Safari 6.1+");
assert.strictEqual(_getCurrentScriptUrlFromErrorStack(remoteStacks[2]), remoteExpected, "External script: Safari 6.0");
assert.strictEqual(_getCurrentScriptUrlFromErrorStack(remoteStacks[3]), remoteExpected, "External script: Opera");
assert.strictEqual(_getCurrentScriptUrlFromErrorStack(remoteStacks[4]), remoteExpected, "External script: PhantomJS");
assert.strictEqual(_getCurrentScriptUrlFromErrorStack(remoteStacks[5]), remoteExpected, "External script: Firefox 3.6");
assert.strictEqual(_getCurrentScriptUrlFromErrorStack(remoteStacks[6]), remoteExpected, "External script: IE 10.0");
assert.strictEqual(_getCurrentScriptUrlFromErrorStack(remoteStacks[7]), remoteExpected, "External script: SneakyError");
assert.strictEqual(badStacks.length, 6, "Bad stacks x 6");
assert.strictEqual(_getCurrentScriptUrlFromErrorStack(badStacks[0]), badExpected, "Useless stack");
assert.strictEqual(_getCurrentScriptUrlFromErrorStack(badStacks[1]), badExpected, "Empty string");
assert.strictEqual(_getCurrentScriptUrlFromErrorStack(badStacks[2]), badExpected, "Array");
assert.strictEqual(_getCurrentScriptUrlFromErrorStack(badStacks[3]), badExpected, "Object");
assert.strictEqual(_getCurrentScriptUrlFromErrorStack(badStacks[4]), badExpected, "`null`");
assert.strictEqual(_getCurrentScriptUrlFromErrorStack(badStacks[5]), badExpected, "`undefined`");
});
test("`_getCurrentScriptUrlFromError` works", function(assert) {
assert.expect(4);
// Arrange
var actual1, actual2, actual3, actual4;
var expected = "http://example.com/blah/foo.js";
var _sourceUrl, _fileName, _stack;
// Do NOT inherit from the real `Error` definition
_Error = function() {
this.sourceURL = _sourceUrl;
this.fileName = _fileName;
this.stack = _stack;
};
// Act
_sourceUrl = expected;
_fileName = undefined;
_stack = undefined;
actual1 = _getCurrentScriptUrlFromError();
_sourceUrl = undefined;
_fileName = expected;
_stack = undefined;
actual2 = _getCurrentScriptUrlFromError();
_sourceUrl = undefined;
_fileName = undefined;
_stack = "Error: my uncaught error\n at " + expected + ":123:4\n at jQuery.event.dispatch (http://code.jquery.com/blah.js:123:0)\n at foo";
actual3 = _getCurrentScriptUrlFromError();
_sourceUrl = undefined;
_fileName = undefined;
_stack = undefined;
actual4 = _getCurrentScriptUrlFromError();
// Assert
assert.strictEqual(actual1, expected, "Current script derived from `err.sourceURL`");
assert.strictEqual(actual2, expected, "Current script derived from `err.fileName`");
assert.strictEqual(actual3, expected, "Current script derived from `err.stack`");
assert.strictEqual(actual4, undefined, "Current script cannot be derived from the Error");
});
test("`_getCurrentScriptUrl` works", function(assert) {
assert.expect(9);
// Arrange
var actual1, actual2, actual3, actual4, actual5, actual6, actual7, actual8, actual9;
var expected1, expected2, expected3, expected4, expected5, expected6, expected7, expected8, expected9;
expected1 = expected2 = expected3 = expected4 = expected5 = "http://example.com/blah/foo/bar.js";
// Arrange & Act
_document = {
currentScript: {
src: "http://example.com/blah/foo/bar.js"
}
};
_getCurrentScriptUrlFromError = function() {};
actual1 = _getCurrentScriptUrl();
_document = {
getElementsByTagName: function(/* tagName */) {
return [
{ src: "http://example.com/blah/foo/bar.js" }
];
}
};
actual2 = _getCurrentScriptUrl();
_document = {
getElementsByTagName: function(/* tagName */) {
return [
{ src: "http://example.com/blah/foo.js", readyState: "complete" },
{ src: "http://example.com/blah/foo/bar.js", readyState: "interactive" }
];
}
};
actual3 = _getCurrentScriptUrl();
_document = {
readyState: "loading",
getElementsByTagName: function(/* tagName */) {
return [
{ src: "http://example.com/blah/foo.js" },
{ src: "http://example.com/blah/wat.js" },
{ src: "http://example.com/blah/foo/bar.js" }
];
}
};
actual4 = _getCurrentScriptUrl();
_document = {
getElementsByTagName: function(/* tagName */) {
return [
{ src: "http://example.com/blah/foo.js" },
{ src: "http://example.com/blah/wat.js" },
{ src: "http://example.com/blah/foo/bar.js" }
];
}
};
_getCurrentScriptUrlFromError = function() {
return "http://example.com/blah/foo/bar.js";
};
actual5 = _getCurrentScriptUrl();
_document = {
getElementsByTagName: function(/* tagName */) {
return [
{ src: "http://example.com/blah/foo.js" },
{ src: "http://example.com/blah/wat.js" }
];
}
};
_getCurrentScriptUrlFromError = function() {};
actual6 = _getCurrentScriptUrl();
_document = {
getElementsByTagName: function(/* tagName */) {
return [
{ src: "http://example.com/blah/foo.js" },
{ /* inline script */ },
{ src: "http://example.com/blah/wat.js" }
];
}
};
actual7 = _getCurrentScriptUrl();
_document = {
getElementsByTagName: function(/* tagName */) {
return [
{ src: "http://example.com/blah/foo.js" },
{ src: "http://example.com/blah/wat.js" },
{ /* inline script */ }
];
}
};
actual8 = _getCurrentScriptUrl();
_document = {
getElementsByTagName: function(/* tagName */) {
return [
{ /* inline script */ }
];
}
};
actual9 = _getCurrentScriptUrl();
assert.strictEqual(actual1, expected1, "Value from `document.currentScript`");
assert.strictEqual(actual2, expected2, "Value from the only script");
assert.strictEqual(actual3, expected3, "Value from `scripts[i].readyState === \"interactive\"`");
assert.strictEqual(actual4, expected4, "Value from last script while `document.readyState === \"loading\"");
assert.strictEqual(actual5, expected5, "Value from `_getCurrentScriptUrlFromError`");
assert.strictEqual(actual6, expected6, "No value can be confirmed");
assert.strictEqual(actual7, expected7, "No value can be confirmed as there is at least one inline script (middle)");
assert.strictEqual(actual8, expected8, "No value can be confirmed as there is at least one inline script (last)");
assert.strictEqual(actual9, expected9, "No value can be confirmed as the only script tag is an inline script");
});
test("`_getUnanimousScriptParentDir` works", function(assert) {
assert.expect(5);
// Arrange
var actual1, actual2, actual3, actual4, actual5;
var expected1, expected2, expected3, expected4, expected5;
var _scripts = [];
_document = {
getElementsByTagName: function(/* tagName */) {
return _scripts;
}
};
expected1 = "http://example.com/blah/";
// Arrange & Act
_scripts.length = 0;
_scripts.push.apply(_scripts, [
{ src: "http://example.com/blah/foo.js" },
{ src: "http://example.com/blah/wat.js" },
{ src: "http://example.com/blah/bar.js" }
]);
actual1 = _getUnanimousScriptParentDir();
_scripts.length = 0;
_scripts.push.apply(_scripts, [
{ src: "http://example.org/blah/foo.js" },
{ src: "http://example.net/blah/wat.js" },
{ src: "http://example.com/blah/bar.js" }
]);
actual2 = _getUnanimousScriptParentDir();
_scripts.length = 0;
_scripts.push.apply(_scripts, [
{ src: "http://example.com/blah/foo.js" },
{ src: "http://example.com/blah/wat.js" },
{ src: "http://example.com/blah/foo/bar.js" }
]);
actual3 = _getUnanimousScriptParentDir();
_scripts.length = 0;
_scripts.push.apply(_scripts, [
{ src: "http://example.com/blah/foo.js" },
{ /* inline script */ },
{ src: "http://example.com/blah/foo/bar.js" }
]);
actual4 = _getUnanimousScriptParentDir();
_scripts.length = 0;
_scripts.push.apply(_scripts, [
{ src: "http://example.com/blah/foo.js" },
{ src: "http://example.com/blah/wat.js" },
{ /* inline script */ }
]);
actual5 = _getUnanimousScriptParentDir();
// Assert
assert.strictEqual(actual1, expected1, "All script tags have the same parent directory");
assert.strictEqual(actual2, expected2, "Not all script tags have the same domains");
assert.strictEqual(actual3, expected3, "Not all script tags have the same parent directory");
assert.strictEqual(actual4, expected4, "Not all script tags have `src` URLs (middle)");
assert.strictEqual(actual5, expected5, "Not all script tags have `src` URLs (last)");
});
test("`_getDefaultSwfPath` works", function(assert) {
assert.expect(11);
// Arrange
var actual1, actual2, actual3, actual4, actual5, actual6, actual7, actual8, actual9, actual10, actual11;
var expected1, expected2, expected3, expected4, expected5, expected6, expected7, expected8, expected9, expected10, expected11;
expected1 = expected2 = expected3 = expected4 = expected5 = "http://example.com/blah/foo/ZeroClipboard.swf";
expected6 = "http://example.com/blah/ZeroClipboard.swf";
expected7 = expected8 = expected9 = expected10 = expected11 = "ZeroClipboard.swf";
// Arrange & Act
_document = {
currentScript: {
src: "http://example.com/blah/foo/bar.js"
}
};
actual1 = _getDefaultSwfPath();
_document = {
getElementsByTagName: function(/* tagName */) {
return [
{ src: "http://example.com/blah/foo/bar.js" }
];
}
};
actual2 = _getDefaultSwfPath();
_document = {
getElementsByTagName: function(/* tagName */) {
return [
{ src: "http://example.com/blah/foo.js", readyState: "complete" },
{ src: "http://example.com/blah/foo/bar.js", readyState: "interactive" }
];
}
};
actual3 = _getDefaultSwfPath();
_document = {
readyState: "loading",
getElementsByTagName: function(/* tagName */) {
return [
{ src: "http://example.com/blah/foo.js" },
{ src: "http://example.com/blah/wat.js" },
{ src: "http://example.com/blah/foo/bar.js" }
];
}
};
actual4 = _getDefaultSwfPath();
_document = {
getElementsByTagName: function(/* tagName */) {
return [
{ src: "http://example.com/blah/foo.js" },
{ src: "http://example.com/blah/wat.js" },
{ src: "http://example.com/blah/foo/bar.js" }
];
}
};
_getCurrentScriptUrlFromError = function() {
return "http://example.com/blah/foo/bar.js";
};
actual5 = _getDefaultSwfPath();
_document = {
getElementsByTagName: function(/* tagName */) {
return [
{ src: "http://example.com/blah/foo.js" },
{ src: "http://example.com/blah/wat.js" },
{ src: "http://example.com/blah/bar.js" }
];
}
};
_getCurrentScriptUrlFromError = function() {};
actual6 = _getDefaultSwfPath();
_document = {
getElementsByTagName: function(/* tagName */) {
return [
{ src: "http://example.org/blah/foo.js" },
{ src: "http://example.net/blah/wat.js" },
{ src: "http://example.com/blah/bar.js" }
];
}
};
actual7 = _getDefaultSwfPath();
_document = {
getElementsByTagName: function(/* tagName */) {
return [
{ src: "http://example.com/blah/foo.js" },
{ src: "http://example.com/blah/wat.js" },
{ src: "http://example.com/blah/foo/bar.js" }
];
}
};
actual8 = _getDefaultSwfPath();
_document = {
getElementsByTagName: function(/* tagName */) {
return [
{ src: "http://example.com/blah/foo.js" },
{ /* inline script */ },
{ src: "http://example.com/blah/foo/bar.js" }
];
}
};
actual9 = _getDefaultSwfPath();
_document = {
getElementsByTagName: function(/* tagName */) {
return [
{ src: "http://example.com/blah/foo.js" },
{ src: "http://example.com/blah/wat.js" },
{ /* inline script */ }
];
}
};
actual10 = _getDefaultSwfPath();
_document = {
getElementsByTagName: function(/* tagName */) {
return [
{ /* inline script */ }
];
}
};
actual11 = _getDefaultSwfPath();
assert.strictEqual(actual1, expected1, "Value derived from `document.currentScript`");
assert.strictEqual(actual2, expected2, "Value derived from the only script");
assert.strictEqual(actual3, expected3, "Value derived from `scripts[i].readyState === \"interactive\"`");
assert.strictEqual(actual4, expected4, "Value derived from last script while `document.readyState === \"loading\"");
assert.strictEqual(actual5, expected5, "Value derived from Error stack");
assert.strictEqual(actual6, expected6, "Value derived from confirming all scripts have the same parent directory");
assert.strictEqual(actual7, expected7, "No value can be confirmed due to differing script domains");
assert.strictEqual(actual8, expected8, "No value can be confirmed due to differing script parent directories");
assert.strictEqual(actual9, expected9, "No value can be confirmed due to the existence of inline scripts");
assert.strictEqual(actual10, expected10, "No value can be confirmed as the last script is an inline script");
assert.strictEqual(actual11, expected11, "No value can be confirmed as the only script is an inline script");
});
})(QUnit.module, QUnit.test);

View File

@@ -0,0 +1,28 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>ZeroClipboard unit tests: shared/private.js</title>
<link rel="stylesheet" href="../../node_modules/qunitjs/qunit/qunit.css">
<script src="../../node_modules/qunitjs/qunit/qunit.js"></script>
<script>
// Polyfill in future functionality: https://github.com/jquery/qunit/issues/490
if (!QUnit.assert.expect) {
QUnit.assert.expect = QUnit.expect;
}
// Require `expect` calls
QUnit.config.requireExpects = true;
// Prevent against Firefox/Firebug failing the global pollution check
var getInterface = null;
</script>
<script src="../../src/js/shared/state.js"></script>
<script src="../../src/js/shared/private.js"></script>
<script src="private.tests.js"></script>
</head>
<body>
<div id="qunit"></div>
<div id="qunit-fixture"></div>
</body>
</html>