app/assets/javascripts/zeroclipboard/ZeroClipboard.js in zeroclipboard-rails-0.1.0 vs app/assets/javascripts/zeroclipboard/ZeroClipboard.js in zeroclipboard-rails-0.1.1

- old
+ new

@@ -1,20 +1,34 @@ /*! * ZeroClipboard * The ZeroClipboard library provides an easy way to copy text to the clipboard using an invisible Adobe Flash movie and a JavaScript interface. - * Copyright (c) 2014 Jon Rohan, James M. Greene + * Copyright (c) 2009-2014 Jon Rohan, James M. Greene * Licensed MIT * http://zeroclipboard.org/ - * v2.1.2 + * v2.2.0 */ (function(window, undefined) { "use strict"; /** * 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, _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; + var _window = window, _document = _window.document, _navigator = _window.navigator, _setTimeout = _window.setTimeout, _clearTimeout = _window.clearTimeout, _setInterval = _window.setInterval, _clearInterval = _window.clearInterval, _getComputedStyle = _window.getComputedStyle, _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, _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; + }; + 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) {} + } + return unwrapper; + }(); /** * Convert an `arguments` object into an Array. * * @returns The arguments as an Array * @private @@ -51,11 +65,11 @@ * @returns Object or Array * @private */ var _deepCopy = function(source) { var copy, i, len, prop; - if (typeof source !== "object" || source == null) { + if (typeof source !== "object" || source == null || typeof source.nodeType === "number") { copy = source; } else if (typeof source.length === "number") { copy = []; for (i = 0, len = source.length; i < len; i++) { if (_hasOwn.call(source, i)) { @@ -137,20 +151,148 @@ } 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() { + 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; + if (_document.currentScript && (jsPath = _document.currentScript.src)) { + return jsPath; + } + scripts = _document.getElementsByTagName("script"); + if (scripts.length === 1) { + return scripts[0].src || undefined; + } + if ("readyState" in scripts[0]) { + for (i = scripts.length; i--; ) { + if (scripts[i].readyState === "interactive" && (jsPath = scripts[i].src)) { + return jsPath; + } + } + } + if (_document.readyState === "loading" && (jsPath = scripts[scripts.length - 1].src)) { + return jsPath; + } + if (jsPath = _getCurrentScriptUrlFromError()) { + return jsPath; + } + 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"); + 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; + } + } + 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"; + }; + /** + * Keep track of if the page is framed (in an `iframe`). This can never change. + * @private + */ + var _pageIsFramed = function() { + return window.opener == null && (!!window.top && window != window.top || !!window.parent && window != window.parent); + }(); + /** * Keep track of the state of the Flash object. * @private */ var _flashState = { bridge: null, version: "0.0.0", pluginType: "unknown", disabled: null, outdated: null, + sandboxed: null, unavailable: null, + degraded: null, deactivated: null, overdue: null, ready: null }; /** @@ -158,93 +300,101 @@ * @readonly * @private */ var _minimumFlashVersion = "11.0.0"; /** + * The ZeroClipboard library version number, as reported by Flash, at the time the SWF was compiled. + */ + var _zcSwfVersion; + /** * 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; /** + * Keep track of the Flash availability check timeout. + * @private + */ + var _flashCheckTimeout = 0; + /** + * Keep track of SWF network errors interval polling. + * @private + */ + var _swfFallbackCheckInterval = 0; + /** * The `message` store for events * @private */ var _eventMessages = { ready: "Flash communication is established", error: { - "flash-disabled": "Flash is disabled or not installed", + "flash-disabled": "Flash is disabled or not installed. May also be attempting to run Flash in a sandboxed iframe, which is impossible.", "flash-outdated": "Flash is too outdated to support ZeroClipboard", + "flash-sandboxed": "Attempting to run Flash in a sandboxed iframe, which is impossible", "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" + "flash-degraded": "Flash is unable to preserve data fidelity when communicating with JavaScript", + "flash-deactivated": "Flash is too outdated for your browser and/or is configured as click-to-activate.\nThis may also mean that the ZeroClipboard SWF object could not be loaded, so please check your `swfPath` configuration and/or network connectivity.\nMay also be attempting to run Flash in a sandboxed iframe, which is impossible.", + "flash-overdue": "Flash communication was established but NOT within the acceptable time limit", + "version-mismatch": "ZeroClipboard JS version number does not match ZeroClipboard SWF version number", + "clipboard-error": "At least one error was thrown while ZeroClipboard was attempting to inject your data into the clipboard", + "config-mismatch": "ZeroClipboard configuration does not match Flash's reality", + "swf-not-found": "The ZeroClipboard SWF object could not be loaded, so please check your `swfPath` configuration and/or network connectivity" } }; /** - * The presumed location of the "ZeroClipboard.swf" file, based on the location - * of the executing JavaScript file (e.g. "ZeroClipboard.js", etc.). + * The `name`s of `error` events that can only occur is Flash has at least + * been able to load the SWF successfully. * @private */ - var _swfPath = function() { - var i, jsDir, tmpJsPath, jsPath, swfPath = "ZeroClipboard.swf"; - if (!(_document.currentScript && (jsPath = _document.currentScript.src))) { - var scripts = _document.getElementsByTagName("script"); - if ("readyState" in scripts[0]) { - for (i = scripts.length; i--; ) { - if (scripts[i].readyState === "interactive" && (jsPath = scripts[i].src)) { - break; - } - } - } else if (_document.readyState === "loading") { - jsPath = scripts[scripts.length - 1].src; - } else { - for (i = scripts.length; i--; ) { - tmpJsPath = scripts[i].src; - if (!tmpJsPath) { - jsDir = null; - break; - } - tmpJsPath = tmpJsPath.split("#")[0].split("?")[0]; - tmpJsPath = tmpJsPath.slice(0, tmpJsPath.lastIndexOf("/") + 1); - if (jsDir == null) { - jsDir = tmpJsPath; - } else if (jsDir !== tmpJsPath) { - jsDir = null; - break; - } - } - if (jsDir !== null) { - jsPath = jsDir; - } - } - } - if (jsPath) { - jsPath = jsPath.split("#")[0].split("?")[0]; - swfPath = jsPath.slice(0, jsPath.lastIndexOf("/") + 1) + swfPath; - } - return swfPath; - }(); + var _errorsThatOnlyOccurAfterFlashLoads = [ "flash-unavailable", "flash-degraded", "flash-overdue", "version-mismatch", "config-mismatch", "clipboard-error" ]; /** + * The `name`s of `error` events that should likely result in the `_flashState` + * variable's property values being updated. + * @private + */ + var _flashStateErrorNames = [ "flash-disabled", "flash-outdated", "flash-sandboxed", "flash-unavailable", "flash-degraded", "flash-deactivated", "flash-overdue" ]; + /** + * A RegExp to match the `name` property of `error` events related to Flash. + * @private + */ + var _flashStateErrorNameMatchingRegex = new RegExp("^flash-(" + _flashStateErrorNames.map(function(errorName) { + return errorName.replace(/^flash-/, ""); + }).join("|") + ")$"); + /** + * A RegExp to match the `name` property of `error` events related to Flash, + * which is enabled. + * @private + */ + var _flashStateEnabledErrorNameMatchingRegex = new RegExp("^flash-(" + _flashStateErrorNames.slice(1).map(function(errorName) { + return errorName.replace(/^flash-/, ""); + }).join("|") + ")$"); + /** * ZeroClipboard configuration defaults for the Core module. * @private */ var _globalConfig = { - swfPath: _swfPath, + swfPath: _getDefaultSwfPath(), trustedDomains: window.location.host ? [ window.location.host ] : [], cacheBust: true, forceEnhancedClipboard: false, flashLoadTimeout: 3e4, autoActivate: true, @@ -293,10 +443,11 @@ /** * The underlying implementation of `ZeroClipboard.state`. * @private */ var _state = function() { + _detectSandbox(); return { browser: _pick(_navigator, [ "userAgent", "platform", "appName" ]), flash: _omit(_flashState, [ "bridge" ]), zeroclipboard: { version: ZeroClipboard.version, @@ -307,11 +458,11 @@ /** * The underlying implementation of `ZeroClipboard.isFlashUnusable`. * @private */ var _isFlashUnusable = function() { - return !!(_flashState.disabled || _flashState.outdated || _flashState.unavailable || _flashState.deactivated); + return !!(_flashState.disabled || _flashState.outdated || _flashState.sandboxed || _flashState.unavailable || _flashState.degraded || _flashState.deactivated); }; /** * The underlying implementation of `ZeroClipboard.on`. * @private */ @@ -339,20 +490,27 @@ ZeroClipboard.emit({ type: "ready" }); } if (added.error) { - var errorTypes = [ "disabled", "outdated", "unavailable", "deactivated", "overdue" ]; - for (i = 0, len = errorTypes.length; i < len; i++) { - if (_flashState[errorTypes[i]] === true) { + for (i = 0, len = _flashStateErrorNames.length; i < len; i++) { + if (_flashState[_flashStateErrorNames[i].replace(/^flash-/, "")] === true) { ZeroClipboard.emit({ type: "error", - name: "flash-" + errorTypes[i] + name: _flashStateErrorNames[i] }); break; } } + if (_zcSwfVersion !== undefined && ZeroClipboard.version !== _zcSwfVersion) { + ZeroClipboard.emit({ + type: "error", + name: "version-mismatch", + jsVersion: ZeroClipboard.version, + swfVersion: _zcSwfVersion + }); + } } } return ZeroClipboard; }; /** @@ -435,17 +593,25 @@ /** * The underlying implementation of `ZeroClipboard.create`. * @private */ var _create = function() { + var previousState = _flashState.sandboxed; + _detectSandbox(); if (typeof _flashState.ready !== "boolean") { _flashState.ready = false; } - if (!ZeroClipboard.isFlashUnusable() && _flashState.bridge === null) { + if (_flashState.sandboxed !== previousState && _flashState.sandboxed === true) { + _flashState.ready = false; + ZeroClipboard.emit({ + type: "error", + name: "flash-sandboxed" + }); + } else if (!ZeroClipboard.isFlashUnusable() && _flashState.bridge === null) { var maxWait = _globalConfig.flashLoadTimeout; if (typeof maxWait === "number" && maxWait >= 0) { - _setTimeout(function() { + _flashCheckTimeout = _setTimeout(function() { if (typeof _flashState.deactivated !== "boolean") { _flashState.deactivated = true; } if (_flashState.deactivated === true) { ZeroClipboard.emit({ @@ -550,11 +716,11 @@ if (htmlBridge) { htmlBridge.removeAttribute("title"); htmlBridge.style.left = "0px"; htmlBridge.style.top = "-9999px"; htmlBridge.style.width = "1px"; - htmlBridge.style.top = "1px"; + htmlBridge.style.height = "1px"; } if (_currentElement) { _removeClass(_currentElement, _globalConfig.hoverClass); _removeClass(_currentElement, _globalConfig.activeClass); _currentElement = null; @@ -587,12 +753,16 @@ eventType = event.type; } if (!eventType) { return; } + eventType = eventType.toLowerCase(); + if (!event.target && (/^(copy|aftercopy|_click)$/.test(eventType) || eventType === "error" && event.name === "clipboard-error")) { + event.target = _copyTarget; + } _extend(event, { - type: eventType.toLowerCase(), + type: eventType, target: event.target || _currentElement || null, relatedTarget: event.relatedTarget || null, currentTarget: _flashState && _flashState.bridge || null, timeStamp: event.timeStamp || _now() || null }); @@ -608,17 +778,17 @@ target: null, version: _flashState.version }); } if (event.type === "error") { - if (/^flash-(disabled|outdated|unavailable|deactivated|overdue)$/.test(event.name)) { + if (_flashStateErrorNameMatchingRegex.test(event.name)) { _extend(event, { target: null, minimumVersion: _minimumFlashVersion }); } - if (/^flash-(outdated|unavailable|deactivated|overdue)$/.test(event.name)) { + if (_flashStateEnabledErrorNameMatchingRegex.test(event.name)) { _extend(event, { version: _flashState.version }); } } @@ -632,12 +802,11 @@ event = _mapClipResultsFromFlash(event, _clipDataFormatMap); } if (event.target && !event.relatedTarget) { event.relatedTarget = _getRelatedTarget(event.target); } - event = _addMouseData(event); - return event; + return _addMouseData(event); }; /** * Get a relatedTarget from the target's `data-clipboard-target` attribute * @private */ @@ -652,11 +821,11 @@ var _addMouseData = function(event) { if (event && /^_(?:click|mouse(?:over|out|down|up|move))$/.test(event.type)) { var srcElement = event.target; var fromElement = event.type === "_mouseover" && event.relatedTarget ? event.relatedTarget : undefined; var toElement = event.type === "_mouseout" && event.relatedTarget ? event.relatedTarget : undefined; - var pos = _getDOMObjectPosition(srcElement); + var pos = _getElementPosition(srcElement); var screenLeft = _window.screenLeft || _window.screenX || 0; var screenTop = _window.screenTop || _window.screenY || 0; var scrollLeft = _document.body.scrollLeft + _document.documentElement.scrollLeft; var scrollTop = _document.body.scrollTop + _document.documentElement.scrollTop; var pageX = pos.left + (typeof event._stageX === "number" ? event._stageX : 0); @@ -749,45 +918,81 @@ } } return this; }; /** + * Check an `error` event's `name` property to see if Flash has + * already loaded, which rules out possible `iframe` sandboxing. + * @private + */ + var _getSandboxStatusFromErrorEvent = function(event) { + var isSandboxed = null; + if (_pageIsFramed === false || event && event.type === "error" && event.name && _errorsThatOnlyOccurAfterFlashLoads.indexOf(event.name) !== -1) { + isSandboxed = false; + } + return isSandboxed; + }; + /** * Preprocess any special behaviors, reactions, or state changes after receiving this event. * Executes only once per event emitted, NOT once per client. * @private */ var _preprocessEvent = function(event) { var element = event.target || _currentElement || null; var sourceIsSwf = event._source === "swf"; delete event._source; - var flashErrorNames = [ "flash-disabled", "flash-outdated", "flash-unavailable", "flash-deactivated", "flash-overdue" ]; switch (event.type) { case "error": - if (flashErrorNames.indexOf(event.name) !== -1) { + var isSandboxed = event.name === "flash-sandboxed" || _getSandboxStatusFromErrorEvent(event); + if (typeof isSandboxed === "boolean") { + _flashState.sandboxed = isSandboxed; + } + if (_flashStateErrorNames.indexOf(event.name) !== -1) { _extend(_flashState, { disabled: event.name === "flash-disabled", outdated: event.name === "flash-outdated", unavailable: event.name === "flash-unavailable", + degraded: event.name === "flash-degraded", deactivated: event.name === "flash-deactivated", overdue: event.name === "flash-overdue", ready: false }); + } else if (event.name === "version-mismatch") { + _zcSwfVersion = event.swfVersion; + _extend(_flashState, { + disabled: false, + outdated: false, + unavailable: false, + degraded: false, + deactivated: false, + overdue: false, + ready: false + }); } + _clearTimeoutsAndPolling(); break; case "ready": + _zcSwfVersion = event.swfVersion; var wasDeactivated = _flashState.deactivated === true; _extend(_flashState, { disabled: false, outdated: false, + sandboxed: false, unavailable: false, + degraded: false, deactivated: false, overdue: wasDeactivated, ready: !wasDeactivated }); + _clearTimeoutsAndPolling(); break; + case "beforecopy": + _copyTarget = element; + break; + case "copy": var textContent, htmlContent, targetEl = event.relatedTarget; if (!(_clipData["text/html"] || _clipData["text/plain"]) && targetEl && (htmlContent = targetEl.value || targetEl.outerHTML || targetEl.innerHTML) && (textContent = targetEl.value || targetEl.textContent || targetEl.innerText)) { event.clipboardData.clearData(); event.clipboardData.setData("text/plain", textContent); @@ -799,10 +1004,11 @@ event.clipboardData.setData("text/plain", textContent); } break; case "aftercopy": + _queueEmitClipboardErrors(event); ZeroClipboard.clearData(); if (element && element !== _safeActiveElement() && element.focus) { element.focus(); } break; @@ -856,10 +1062,18 @@ })); } break; case "_click": + _copyTarget = null; + if (_globalConfig.bubbleEvents === true && sourceIsSwf) { + _fireMouseEvent(_extend({}, event, { + type: event.type.slice(1) + })); + } + break; + case "_mousemove": if (_globalConfig.bubbleEvents === true && sourceIsSwf) { _fireMouseEvent(_extend({}, event, { type: event.type.slice(1) })); @@ -869,10 +1083,27 @@ if (/^_(?:click|mouse(?:over|out|down|up|move))$/.test(event.type)) { return true; } }; /** + * Check an "aftercopy" event for clipboard errors and emit a corresponding "error" event. + * @private + */ + var _queueEmitClipboardErrors = function(aftercopyEvent) { + if (aftercopyEvent.errors && aftercopyEvent.errors.length > 0) { + var errorEvent = _deepCopy(aftercopyEvent); + _extend(errorEvent, { + type: "error", + name: "clipboard-error" + }); + delete errorEvent.success; + _setTimeout(function() { + ZeroClipboard.emit(errorEvent); + }, 0); + } + }; + /** * Dispatch a synthetic MouseEvent. * * @returns `undefined` * @private */ @@ -899,10 +1130,44 @@ target.dispatchEvent(e); } } }; /** + * Continuously poll the DOM until either: + * (a) the fallback content becomes visible, or + * (b) we receive an event from SWF (handled elsewhere) + * + * IMPORTANT: + * This is NOT a necessary check but it can result in significantly faster + * detection of bad `swfPath` configuration and/or network/server issues [in + * supported browsers] than waiting for the entire `flashLoadTimeout` duration + * to elapse before detecting that the SWF cannot be loaded. The detection + * duration can be anywhere from 10-30 times faster [in supported browsers] by + * using this approach. + * + * @returns `undefined` + * @private + */ + var _watchForSwfFallbackContent = function() { + var maxWait = _globalConfig.flashLoadTimeout; + if (typeof maxWait === "number" && maxWait >= 0) { + var pollWait = Math.min(1e3, maxWait / 10); + var fallbackContentId = _globalConfig.swfObjectId + "_fallbackContent"; + _swfFallbackCheckInterval = _setInterval(function() { + var el = _document.getElementById(fallbackContentId); + if (_isElementVisible(el)) { + _clearTimeoutsAndPolling(); + _flashState.deactivated = null; + ZeroClipboard.emit({ + type: "error", + name: "swf-not-found" + }); + } + }, pollWait); + } + }; + /** * Create the HTML bridge element to embed the Flash object into. * @private */ var _createHtmlBridge = function() { var container = _document.createElement("div"); @@ -936,23 +1201,26 @@ var _embedSwf = function() { var len, flashBridge = _flashState.bridge, container = _getHtmlBridge(flashBridge); if (!flashBridge) { var allowScriptAccess = _determineScriptAccess(_window.location.host, _globalConfig); var allowNetworking = allowScriptAccess === "never" ? "none" : "all"; - var flashvars = _vars(_globalConfig); + var flashvars = _vars(_extend({ + jsVersion: ZeroClipboard.version + }, _globalConfig)); var swfUrl = _globalConfig.swfPath + _cacheBust(_globalConfig.swfPath, _globalConfig); container = _createHtmlBridge(); var divToBeReplaced = _document.createElement("div"); container.appendChild(divToBeReplaced); _document.body.appendChild(container); var tmpDiv = _document.createElement("div"); - var oldIE = _flashState.pluginType === "activex"; - tmpDiv.innerHTML = '<object id="' + _globalConfig.swfObjectId + '" name="' + _globalConfig.swfObjectId + '" ' + 'width="100%" height="100%" ' + (oldIE ? 'classid="clsid:d27cdb6e-ae6d-11cf-96b8-444553540000"' : 'type="application/x-shockwave-flash" data="' + swfUrl + '"') + ">" + (oldIE ? '<param name="movie" value="' + swfUrl + '"/>' : "") + '<param name="allowScriptAccess" value="' + allowScriptAccess + '"/>' + '<param name="allowNetworking" value="' + allowNetworking + '"/>' + '<param name="menu" value="false"/>' + '<param name="wmode" value="transparent"/>' + '<param name="flashvars" value="' + flashvars + '"/>' + "</object>"; + var usingActiveX = _flashState.pluginType === "activex"; + tmpDiv.innerHTML = '<object id="' + _globalConfig.swfObjectId + '" name="' + _globalConfig.swfObjectId + '" ' + 'width="100%" height="100%" ' + (usingActiveX ? 'classid="clsid:d27cdb6e-ae6d-11cf-96b8-444553540000"' : 'type="application/x-shockwave-flash" data="' + swfUrl + '"') + ">" + (usingActiveX ? '<param name="movie" value="' + swfUrl + '"/>' : "") + '<param name="allowScriptAccess" value="' + allowScriptAccess + '"/>' + '<param name="allowNetworking" value="' + allowNetworking + '"/>' + '<param name="menu" value="false"/>' + '<param name="wmode" value="transparent"/>' + '<param name="flashvars" value="' + flashvars + '"/>' + '<div id="' + _globalConfig.swfObjectId + '_fallbackContent">&nbsp;</div>' + "</object>"; flashBridge = tmpDiv.firstChild; tmpDiv = null; - flashBridge.ZeroClipboard = ZeroClipboard; + _unwrap(flashBridge).ZeroClipboard = ZeroClipboard; container.replaceChild(flashBridge, divToBeReplaced); + _watchForSwfFallbackContent(); } if (!flashBridge) { flashBridge = _document[_globalConfig.swfObjectId]; if (flashBridge && (len = flashBridge.length)) { flashBridge = flashBridge[len - 1]; @@ -999,13 +1267,15 @@ if (htmlBridge.parentNode) { htmlBridge.parentNode.removeChild(htmlBridge); } } } + _clearTimeoutsAndPolling(); _flashState.ready = null; _flashState.bridge = null; _flashState.deactivated = null; + _zcSwfVersion = undefined; } }; /** * Map the data format names of the "clipData" to Flash-friendly names. * @@ -1067,19 +1337,24 @@ return clipResults; } var newResults = {}; for (var prop in clipResults) { if (_hasOwn.call(clipResults, prop)) { - if (prop !== "success" && prop !== "data") { + if (prop === "errors") { + newResults[prop] = clipResults[prop] ? clipResults[prop].slice() : []; + for (var i = 0, len = newResults[prop].length; i < len; i++) { + newResults[prop][i].format = formatMap[newResults[prop][i].format]; + } + } else if (prop !== "success" && prop !== "data") { newResults[prop] = clipResults[prop]; - continue; - } - newResults[prop] = {}; - var tmpHash = clipResults[prop]; - for (var dataFormat in tmpHash) { - if (dataFormat && _hasOwn.call(tmpHash, dataFormat) && _hasOwn.call(formatMap, dataFormat)) { - newResults[prop][formatMap[dataFormat]] = tmpHash[dataFormat]; + } else { + newResults[prop] = {}; + var tmpHash = clipResults[prop]; + for (var dataFormat in tmpHash) { + if (dataFormat && _hasOwn.call(tmpHash, dataFormat) && _hasOwn.call(formatMap, dataFormat)) { + newResults[prop][formatMap[dataFormat]] = tmpHash[dataFormat]; + } } } } } return newResults; @@ -1139,10 +1414,13 @@ str += (str ? "&" : "") + "forceEnhancedClipboard=true"; } if (typeof options.swfObjectId === "string" && options.swfObjectId) { str += (str ? "&" : "") + "swfObjectId=" + _encodeURIComponent(options.swfObjectId); } + if (typeof options.jsVersion === "string" && options.jsVersion) { + str += (str ? "&" : "") + "jsVersion=" + _encodeURIComponent(options.jsVersion); + } return str; }; /** * Extract the domain (e.g. "github.com") from an origin (e.g. "https://github.com") or * URL (e.g. "https://github.com/zeroclipboard/zeroclipboard/"). @@ -1235,33 +1513,27 @@ * * @returns The element, with its new class added. * @private */ var _addClass = function(element, value) { - if (!element || element.nodeType !== 1) { - return element; + var c, cl, className, classNames = []; + if (typeof value === "string" && value) { + classNames = value.split(/\s+/); } - if (element.classList) { - if (!element.classList.contains(value)) { - element.classList.add(value); - } - return element; - } - if (value && typeof value === "string") { - var classNames = (value || "").split(/\s+/); - if (element.nodeType === 1) { - if (!element.className) { - element.className = value; - } else { - var className = " " + element.className + " ", setClass = element.className; - for (var c = 0, cl = classNames.length; c < cl; c++) { - if (className.indexOf(" " + classNames[c] + " ") < 0) { - setClass += " " + classNames[c]; - } + if (element && element.nodeType === 1 && classNames.length > 0) { + if (element.classList) { + for (c = 0, cl = classNames.length; c < cl; c++) { + element.classList.add(classNames[c]); + } + } else if (element.hasOwnProperty("className")) { + className = " " + element.className + " "; + for (c = 0, cl = classNames.length; c < cl; c++) { + if (className.indexOf(" " + classNames[c] + " ") === -1) { + className += classNames[c] + " "; } - element.className = setClass.replace(/^\s+|\s+$/g, ""); } + element.className = className.replace(/^\s+|\s+$/g, ""); } } return element; }; /** @@ -1269,24 +1541,22 @@ * * @returns The element, with its class removed. * @private */ var _removeClass = function(element, value) { - if (!element || element.nodeType !== 1) { - return element; - } - if (element.classList) { - if (element.classList.contains(value)) { - element.classList.remove(value); - } - return element; - } + var c, cl, className, classNames = []; if (typeof value === "string" && value) { - var classNames = value.split(/\s+/); - if (element.nodeType === 1 && element.className) { - var className = (" " + element.className + " ").replace(/[\n\t]/g, " "); - for (var c = 0, cl = classNames.length; c < cl; c++) { + classNames = value.split(/\s+/); + } + if (element && element.nodeType === 1 && classNames.length > 0) { + if (element.classList && element.classList.length > 0) { + for (c = 0, cl = classNames.length; c < cl; c++) { + element.classList.remove(classNames[c]); + } + } else if (element.className) { + className = (" " + element.className + " ").replace(/[\r\n\t]/g, " "); + for (c = 0, cl = classNames.length; c < cl; c++) { className = className.replace(" " + classNames[c] + " ", " "); } element.className = className.replace(/^\s+|\s+$/g, ""); } } @@ -1299,80 +1569,96 @@ * * @returns The computed style property. * @private */ var _getStyle = function(el, prop) { - var value = _window.getComputedStyle(el, null).getPropertyValue(prop); + var value = _getComputedStyle(el, null).getPropertyValue(prop); if (prop === "cursor") { if (!value || value === "auto") { if (el.nodeName === "A") { return "pointer"; } } } return value; }; /** - * Get the zoom factor of the browser. Always returns `1.0`, except at - * non-default zoom levels in IE<8 and some older versions of WebKit. + * Get the absolutely positioned coordinates of a DOM element. * - * @returns Floating unit percentage of the zoom factor (e.g. 150% = `1.5`). - * @private - */ - var _getZoomFactor = function() { - var rect, physicalWidth, logicalWidth, zoomFactor = 1; - if (typeof _document.body.getBoundingClientRect === "function") { - rect = _document.body.getBoundingClientRect(); - physicalWidth = rect.right - rect.left; - logicalWidth = _document.body.offsetWidth; - zoomFactor = _round(physicalWidth / logicalWidth * 100) / 100; - } - return zoomFactor; - }; - /** - * Get the DOM positioning info of an element. - * * @returns Object containing the element's position, width, and height. * @private */ - var _getDOMObjectPosition = function(obj) { - var info = { + var _getElementPosition = function(el) { + var pos = { left: 0, top: 0, width: 0, height: 0 }; - if (obj.getBoundingClientRect) { - var rect = obj.getBoundingClientRect(); - var pageXOffset, pageYOffset, zoomFactor; - if ("pageXOffset" in _window && "pageYOffset" in _window) { - pageXOffset = _window.pageXOffset; - pageYOffset = _window.pageYOffset; - } else { - zoomFactor = _getZoomFactor(); - pageXOffset = _round(_document.documentElement.scrollLeft / zoomFactor); - pageYOffset = _round(_document.documentElement.scrollTop / zoomFactor); - } + if (el.getBoundingClientRect) { + var elRect = el.getBoundingClientRect(); + var pageXOffset = _window.pageXOffset; + var pageYOffset = _window.pageYOffset; var leftBorderWidth = _document.documentElement.clientLeft || 0; var topBorderWidth = _document.documentElement.clientTop || 0; - info.left = rect.left + pageXOffset - leftBorderWidth; - info.top = rect.top + pageYOffset - topBorderWidth; - info.width = "width" in rect ? rect.width : rect.right - rect.left; - info.height = "height" in rect ? rect.height : rect.bottom - rect.top; + var leftBodyOffset = 0; + var topBodyOffset = 0; + if (_getStyle(_document.body, "position") === "relative") { + var bodyRect = _document.body.getBoundingClientRect(); + var htmlRect = _document.documentElement.getBoundingClientRect(); + leftBodyOffset = bodyRect.left - htmlRect.left || 0; + topBodyOffset = bodyRect.top - htmlRect.top || 0; + } + pos.left = elRect.left + pageXOffset - leftBorderWidth - leftBodyOffset; + pos.top = elRect.top + pageYOffset - topBorderWidth - topBodyOffset; + pos.width = "width" in elRect ? elRect.width : elRect.right - elRect.left; + pos.height = "height" in elRect ? elRect.height : elRect.bottom - elRect.top; } - return info; + return pos; }; /** + * Determine is an element is visible somewhere within the document (page). + * + * @returns Boolean + * @private + */ + var _isElementVisible = function(el) { + if (!el) { + return false; + } + var styles = _getComputedStyle(el, null); + var hasCssHeight = _parseFloat(styles.height) > 0; + var hasCssWidth = _parseFloat(styles.width) > 0; + var hasCssTop = _parseFloat(styles.top) >= 0; + var hasCssLeft = _parseFloat(styles.left) >= 0; + var cssKnows = hasCssHeight && hasCssWidth && hasCssTop && hasCssLeft; + var rect = cssKnows ? null : _getElementPosition(el); + var isVisible = styles.display !== "none" && styles.visibility !== "collapse" && (cssKnows || !!rect && (hasCssHeight || rect.height > 0) && (hasCssWidth || rect.width > 0) && (hasCssTop || rect.top >= 0) && (hasCssLeft || rect.left >= 0)); + return isVisible; + }; + /** + * Clear all existing timeouts and interval polling delegates. + * + * @returns `undefined` + * @private + */ + var _clearTimeoutsAndPolling = function() { + _clearTimeout(_flashCheckTimeout); + _flashCheckTimeout = 0; + _clearInterval(_swfFallbackCheckInterval); + _swfFallbackCheckInterval = 0; + }; + /** * Reposition the Flash object to cover the currently activated element. * * @returns `undefined` * @private */ var _reposition = function() { var htmlBridge; if (_currentElement && (htmlBridge = _getHtmlBridge(_flashState.bridge))) { - var pos = _getDOMObjectPosition(_currentElement); + var pos = _getElementPosition(_currentElement); _extend(htmlBridge.style, { width: pos.width + "px", height: pos.height + "px", top: pos.top + "px", left: pos.left + "px", @@ -1412,10 +1698,58 @@ zIndex = _getSafeZIndex(_parseInt(val, 10)); } return typeof zIndex === "number" ? zIndex : "auto"; }; /** + * Attempt to detect if ZeroClipboard is executing inside of a sandboxed iframe. + * If it is, Flash Player cannot be used, so ZeroClipboard is dead in the water. + * + * @see {@link http://lists.w3.org/Archives/Public/public-whatwg-archive/2014Dec/0002.html} + * @see {@link https://github.com/zeroclipboard/zeroclipboard/issues/511} + * @see {@link http://zeroclipboard.org/test-iframes.html} + * + * @returns `true` (is sandboxed), `false` (is not sandboxed), or `null` (uncertain) + * @private + */ + var _detectSandbox = function(doNotReassessFlashSupport) { + var effectiveScriptOrigin, frame, frameError, previousState = _flashState.sandboxed, isSandboxed = null; + doNotReassessFlashSupport = doNotReassessFlashSupport === true; + if (_pageIsFramed === false) { + isSandboxed = false; + } else { + try { + frame = window.frameElement || null; + } catch (e) { + frameError = { + name: e.name, + message: e.message + }; + } + if (frame && frame.nodeType === 1 && frame.nodeName === "IFRAME") { + try { + isSandboxed = frame.hasAttribute("sandbox"); + } catch (e) { + isSandboxed = null; + } + } else { + try { + effectiveScriptOrigin = document.domain || null; + } catch (e) { + effectiveScriptOrigin = null; + } + if (effectiveScriptOrigin === null || frameError && frameError.name === "SecurityError" && /(^|[\s\(\[@])sandbox(es|ed|ing|[\s\.,!\)\]@]|$)/.test(frameError.message.toLowerCase())) { + isSandboxed = true; + } + } + } + _flashState.sandboxed = isSandboxed; + if (previousState !== isSandboxed && !doNotReassessFlashSupport) { + _detectFlashSupport(_ActiveXObject); + } + return isSandboxed; + }; + /** * Detect the Flash Player status, version, and plugin type. * * @see {@link https://code.google.com/p/doctype-mirror/wiki/ArticleDetectFlash#The_code} * @see {@link http://stackoverflow.com/questions/12866060/detecting-pepper-ppapi-flash-with-javascript} * @@ -1493,10 +1827,14 @@ /** * Invoke the Flash detection algorithms immediately upon inclusion so we're not waiting later. */ _detectFlashSupport(_ActiveXObject); /** + * Always assess the `sandboxed` state of the page at important Flash-related moments. + */ + _detectSandbox(true); + /** * A shell constructor for `ZeroClipboard` client instances. * * @constructor */ var ZeroClipboard = function() { @@ -1513,11 +1851,11 @@ * @static * @readonly * @property {string} */ _defineProperty(ZeroClipboard, "version", { - value: "2.1.2", + value: "2.2.0", writable: false, configurable: true, enumerable: true }); /** @@ -1736,11 +2074,14 @@ /** * The underlying implementation of `ZeroClipboard.Client.prototype.on`. * @private */ var _clientOn = function(eventType, listener) { - var i, len, events, added = {}, handlers = _clientMeta[this.id] && _clientMeta[this.id].handlers; + var i, len, events, added = {}, meta = _clientMeta[this.id], handlers = meta && meta.handlers; + if (!meta) { + throw new Error("Attempted to add new listener(s) to a destroyed ZeroClipboard client instance"); + } 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") { @@ -1762,31 +2103,41 @@ 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]]) { + for (i = 0, len = _flashStateErrorNames.length; i < len; i++) { + if (_flashState[_flashStateErrorNames[i].replace(/^flash-/, "")]) { this.emit({ type: "error", - name: "flash-" + errorTypes[i], + name: _flashStateErrorNames[i], client: this }); break; } } + if (_zcSwfVersion !== undefined && ZeroClipboard.version !== _zcSwfVersion) { + this.emit({ + type: "error", + name: "version-mismatch", + jsVersion: ZeroClipboard.version, + swfVersion: _zcSwfVersion + }); + } } } 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; + var i, len, foundIndex, events, perEventHandlers, meta = _clientMeta[this.id], handlers = meta && meta.handlers; + if (!handlers) { + return this; + } if (arguments.length === 0) { events = _keys(handlers); } else if (typeof eventType === "string" && eventType) { events = eventType.split(/\s+/); } else if (typeof eventType === "object" && eventType && typeof listener === "undefined") { @@ -1849,10 +2200,13 @@ /** * The underlying implementation of `ZeroClipboard.Client.prototype.clip`. * @private */ var _clientClip = function(elements) { + if (!_clientMeta[this.id]) { + throw new Error("Attempted to clip element(s) to a destroyed ZeroClipboard client instance"); + } elements = _prepClip(elements); for (var i = 0; i < elements.length; i++) { if (_hasOwn.call(elements, i) && elements[i] && elements[i].nodeType === 1) { if (!elements[i].zcClippingId) { elements[i].zcClippingId = "zcClippingId_" + _elementIdCounter++; @@ -1921,10 +2275,13 @@ /** * The underlying implementation of `ZeroClipboard.Client.prototype.destroy`. * @private */ var _clientDestroy = function() { + if (!_clientMeta[this.id]) { + return; + } this.unclip(); this.off(); delete _clientMeta[this.id]; }; /** @@ -1936,33 +2293,35 @@ return false; } if (event.client && event.client !== this) { return false; } - var clippedEls = _clientMeta[this.id] && _clientMeta[this.id].elements; + var meta = _clientMeta[this.id]; + var clippedEls = meta && meta.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; - if (!(goodTarget || goodRelTarget || goodClient)) { + if (!meta || !(goodTarget || goodRelTarget || goodClient)) { return false; } return true; }; /** * Handle the actual dispatching of events to a client instance. * - * @returns `this` + * @returns `undefined` * @private */ var _clientDispatchCallbacks = function(event) { - if (!(typeof event === "object" && event && event.type)) { + var meta = _clientMeta[this.id]; + if (!(typeof event === "object" && event && event.type && meta)) { return; } var async = _shouldPerformAsync(event); - var wildcardTypeHandlers = _clientMeta[this.id] && _clientMeta[this.id].handlers["*"] || []; - var specificTypeHandlers = _clientMeta[this.id] && _clientMeta[this.id].handlers[event.type] || []; + var wildcardTypeHandlers = meta && meta.handlers["*"] || []; + var specificTypeHandlers = meta && meta.handlers[event.type] || []; 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]; @@ -1978,11 +2337,10 @@ eventCopy = _extend({}, event); _dispatchCallback(func, context, [ eventCopy ], async); } } } - return this; }; /** * Prepares the elements for clipping/unclipping. * * @returns An Array of elements. @@ -2140,56 +2498,74 @@ * Stores the pending plain text to inject into the clipboard. * * @returns `this` */ ZeroClipboard.prototype.setText = function(text) { + if (!_clientMeta[this.id]) { + throw new Error("Attempted to set pending clipboard data from a destroyed ZeroClipboard client instance"); + } ZeroClipboard.setData("text/plain", text); return this; }; /** * Stores the pending HTML text to inject into the clipboard. * * @returns `this` */ ZeroClipboard.prototype.setHtml = function(html) { + if (!_clientMeta[this.id]) { + throw new Error("Attempted to set pending clipboard data from a destroyed ZeroClipboard client instance"); + } 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) { + if (!_clientMeta[this.id]) { + throw new Error("Attempted to set pending clipboard data from a destroyed ZeroClipboard client instance"); + } ZeroClipboard.setData("application/rtf", richText); return this; }; /** * Stores the pending data to inject into the clipboard. * * @returns `this` */ ZeroClipboard.prototype.setData = function() { + if (!_clientMeta[this.id]) { + throw new Error("Attempted to set pending clipboard data from a destroyed ZeroClipboard client instance"); + } 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() { + if (!_clientMeta[this.id]) { + throw new Error("Attempted to clear pending clipboard data from a destroyed ZeroClipboard client instance"); + } 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() { + if (!_clientMeta[this.id]) { + throw new Error("Attempted to get pending clipboard data from a destroyed ZeroClipboard client instance"); + } return ZeroClipboard.getData.apply(this, _args(arguments)); }; if (typeof define === "function" && define.amd) { define(function() { return ZeroClipboard;