Created
October 20, 2015 22:29
-
-
Save hallvors/07f0f1a79a042098641b to your computer and use it in GitHub Desktop.
An experimental HTML5 version of ZeroClipboard, API not quite complete yet
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
(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