Skip to content

Instantly share code, notes, and snippets.

@hallvors
Created October 20, 2015 22:29
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save hallvors/07f0f1a79a042098641b to your computer and use it in GitHub Desktop.
Save hallvors/07f0f1a79a042098641b to your computer and use it in GitHub Desktop.
An experimental HTML5 version of ZeroClipboard, API not quite complete yet
(function(window, undefined) {
"use strict";
/* -------->8 above stuff is start.js */
/* -------->8 below stuff is shared/private-html5.js */
var _globalConfig = {};
var _document = window.document,
_Error = Error,
_hasOwn = window.Object.prototype.hasOwnProperty;
/**
* 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 a relatedTarget from the target's `data-clipboard-target` attribute
* @private
*/
var _getRelatedTarget = function(targetEl) {
var relatedTargetId = targetEl && targetEl.getAttribute && targetEl.getAttribute("data-clipboard-target");
return relatedTargetId ? _document.getElementById(relatedTargetId) : null;
};
/**
* Detect HTML5 clipboard API support.
* @returns `undefined`
* @private
*
*/
var _detectHTML5API = function() {
try{
/* In some browsers, in particular Firefox < 40, queryCommandSupported() will
* return true because the command is "supported" in scripts with extra privileges
* - but trying to use the API will throw. We use both functions below, but the order
* matters: if queryCommandEnabled() throws, we will not use queryCommandSupported().
* queryCommandEnabled() is expected to return false when not called from a
* user-triggered thread, so it's only called here to see if it throws..
*/
/*
* This will fail with the early Chrome implementation because it runs
* before the user interacts with document -
* https://code.google.com/p/chromium/issues/detail?id=476508
* It will fall back to use Flash.
*/
return document.queryCommandEnabled("copy") || document.queryCommandSupported("copy");
}catch(e){}
return false;
};
/* -------->8 above stuff is shared/private-html5.js */
/* -------->8 below stuff is core/state-html5.js */
/**
* Tracked elements are elements we're instructed to handle copying for
*/
var _trackedElements = [];
/**
* A method to add tracked element(s)
* Note: if jQuery is used, we want the *actual* elements, not
* jQuery objects
*/
var _trackElements = function(elements){
if (elements.nodeType === 1) { // Argument is a single element
_trackedElements.push(elements);
} else if (elements.length) {
for (var i = 0, elem; elem = elements[i]; i++) {
_trackedElements.push(elem);
}
}
};
/**
* A method to forget tracked element(s)
* Note: if jQuery is used, we want the *actual* elements, not
* jQuery objects
*/
var _untrackElements = function(elements){
if (elements.nodeType === 1) { // Argument is a single element
while(_trackedElements.indexOf(elements) > -1) {
_trackedElements.splice(_trackedElements.indexOf(elements), 1);
}
} else if (elements.length) {
for (var i = 0, elem; elem = elements[i]; i++) {
_untrackElements(elem);
}
}
};
/**
* The _clipData object will remember all the data passed to
* ZeroClipboard while we're processing a copy operation
*/
var _clipData = {};
/**
* The _eventListeners object will keep track of event listeners
* for the ZC events
*/
var _eventListeners = {
'*': [],
'copy': []
};
/* -------->8 above stuff is core/state-html5.js */
/* -------->8 below stuff is core/private-html5.js */
/**
* The 'copy' event listener that does the real work
*/
var _copy = function(e){
if (_hasOwn.call(_clipData,"text/plain")) {
e.clipboardData.setData("text/plain", _clipData["text/plain"]);
e.preventDefault();
}
if (_hasOwn.call(_clipData, "text/html")) {
e.clipboardData.setData("text/html", _clipData["text/html"]);
e.preventDefault();
}
// We might have event listeners registered by scripts
ZeroClipboard.emit(e);
};
/**
* One global click listener is all we need
*/
var _onclick = function(event) {
var textContent,
htmlContent,
_copyTarget = event.target,
targetEl = _getRelatedTarget(event.target);
if(_trackedElements.indexOf(_copyTarget) === -1) {
return;
}
if (
!(_clipData["text/html"] || _clipData["text/plain"]) &&
targetEl &&
(htmlContent = targetEl.value || targetEl.outerHTML || targetEl.innerHTML) &&
(textContent = targetEl.value || targetEl.textContent || targetEl.innerText)
) {
ZeroClipboard.clearData();
ZeroClipboard.setData("text/plain", textContent);
if (htmlContent !== textContent) {
ZeroClipboard.setData("text/html", htmlContent);
}
}
else if (!_clipData["text/plain"] && event.target && (textContent = event.target.getAttribute("data-clipboard-text"))) {
ZeroClipboard.clearData();
ZeroClipboard.setData("text/plain", textContent);
}
if (_copyTarget && _copyTarget.hasAttribute("data-clipboard-text")) {
_clipData["text/plain"] = _copyTarget.getAttribute("data-clipboard-text");
}
if (targetEl || _clipData["text/plain"] !== null) {
// This element was meant for some clipboard action
// Potential sources of data:
// * A selection? (should work to just trigger document.execCommand()? - but on what document??)
// * setData() calls in a synthetic copy event fired by us
// * data-clipboard-text
// * data-clipboard-target
// So: first fire copy event. If setData() is used, obey. Otherwise,
// look for data-clipboard-target. If it is an element, get text content and
// innerHTML (or outer?). If it is an IFRAME and the subdocument contains a
// selection, fire execCommand() on that document. Otherwise, look for data-clipboard-text
// and use the value as input. Otherwise, just call execCommand("copy")..?
document.addEventListener("copy", _copy, false);
/* This command will trigger our copy listener */
document.execCommand("copy", null, false);
document.removeEventListener("copy", _copy, false)
ZeroClipboard.clearData();
}
};
document.addEventListener('click', _onclick, false);
/* -------->8 above stuff is core/state-html5.js */
/* -------->8 below stuff is core/api-html5.js */
var ZeroClipboard = function(){
return ZeroClipboard; // There will only ever be one instance
}
ZeroClipboard.clip = function(elements) {
_trackElements(elements);
}
ZeroClipboard.unclip = function(elements) {
_untrackElements(elements);
}
ZeroClipboard.clearData = function() {
_clipData = {};
}
ZeroClipboard.setData = function(type, data) {
_clipData[type] = data;
}
ZeroClipboard.on = function(event, func) {
if(_eventListeners[event] === undefined){
throw _Error('No such event: ' + event);
}
_eventListeners[event].push(func);
}
ZeroClipboard.off = function(event, func) {
if(_eventListeners[event] === undefined){
throw _Error('No such event: ' + event);
}
if(_eventListeners[event].indexOf(func)) {
_eventListeners[event].splice(_eventListeners[event].indexOf(func), 1);
}
}
ZeroClipboard.emit = function(event) {
if(_eventListeners['*']) {
for(var i = 0; i < _eventListeners['*'].length; i++) {
_eventListeners['*'][i].call(this, event);
event.preventDefault();
}
}
if(_eventListeners[event.type]) {
for(var i = 0; i < _eventListeners[event.type].length; i++) {
_eventListeners[event.type][i].call(this, event);
event.preventDefault(); // Can't say it too often, right? Mea culpa for requiring it..
}
}
}
/* -------->8 above stuff is core/api-html5.js */
/* -------->8 below stuff is end.js */
/**
* Invoke the HTML5 detection algorithms immediately.
* If HTML5 clipboard is *not* supported, add the Flash-based
* version of ZeroClipboard to the document instead
*/
if (!_detectHTML5API() || _globalConfig.useFlash) {
var zcSourceURL = _getCurrentScriptUrl();
var isMin = /\.min\./.test(zcSourceURL);
var newURL = _getDirPathOfUrl(zcSourceURL) + (isMin ? "ZeroClipboardFlash.min.js" : "ZeroClipboardFlash.js");
document.documentElement.appendChild(document.createElement("script")).src = newURL;
return; // we don't have more work to do
}
// 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;
})());
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment