/* * file: Clipboard.js * * @BEGINLICENSE * Copyright 2010 Brook Novak (email : brooknovak@seaweed-editor.com) * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA * @ENDLICENSE */ /* * System-Copy pipelines for copy/cut key strokes: * * IE: window.clipboardData -> Fall-through-events * Moz: XUL -> Fall-through-events * WK: Fall-through-events * Op: Fall-through-events * * ZeroClipboard can be used for copying via buttons (outside of package). * Internal System-Copy pipelines for copy/cut buttons: * * WK: ExecCommand * IE: window.clipboardData * Moz: XUL * * System-Paste pipelines for paste buttons: * IE: window.clipboardData * Moz: XUL * */ bootstrap.provides("Clipboard"); (function() { $enqueueInit("Clipboard", function() { // Setup platform independant event handlers for Accel+C, Accel+V and Accel+X key strokes switch(_engine) { case _Platform.TRIDENT: _addHandler(document, "keydown", onIEKeyDown); break; case _Platform.GECKO: _addHandler(document, "keypress", onGeckoKeyPress); break; case _Platform.PRESTO: _addHandler(document, "keydown", onPrestoKeyDown); break; case _Platform.WEBKIT: _addHandler(document, "copy", onWKCopy); _addHandler(document, "paste", onWKPaste); _addHandler(document, "keydown", onWKKeyDown); // Cutting in all webkit, copy/paste in safari mac break; } // Create the multi-lined text box for capturing clipboard ketstrokes clipInputEle = $createElement("textarea"); _setFullStyle(clipInputEle, "width:1px;height:1px;border-style:none"); clipContainer = $createElement("div"); _setClassName(clipContainer, _PROTECTED_CLASS); _setFullStyle(clipContainer, "position:absolute;width:1px;height:1px;display:none;z-index:-500"); clipContainer.appendChild(clipInputEle); docBody.appendChild(clipContainer); }, "events.Events"); /* The internal clipboard text - stored whenever a user copies. */ var intClipText, /* The internal clipboard DOM - stored whenever a user copies.*/ intClipDOM, /* True if managed to copy the current internal clip text to the system clipboard */ isSysClip, /* Used for copy/cut/paste keystroke hijacking. */ clipInputEle, /* Used for copy/cut/paste keystroke hijacking. */ clipContainer, /* Used for copy/cut/paste keystroke hijacking. */ clipboardTOID = null, /* The cursor descriptor to restore to after a native copy. NULL if there is no cursor to restore. */ restoreCursor; /** * Converts a DOM tree to a textual version. * @param {Node} node A dom tree to convert to text * @return {String} The text equivalent of the given root node of the dom tree. */ function domToText(node) { var text = "", child; if (node.nodeType == Node.TEXT_NODE && _doesTextSupportNonWS(node)) { text = node.nodeValue.replace(/[\t\n\r]/g, " "); // Make all HTML-whitespace symbols actual whitespace } else if (node.nodeType == Node.ELEMENT_NODE) { switch (_nodeName(node)) { case "br": text += "\n"; break; case "li": text += "\n * "; break; default: if (_isBlockLevel(node)) text += "\n"; } // Recurse child = node.firstChild; while (child) { text += domToText(child); child = child.nextSibling; } // Block-elements have line breaks before and after if (_isBlockLevel(node)) text += "\n"; } return text; } /** * Copies the documents selection to the internal clipboard. * Sets the locals intClipText, intClipDOM and isSysClip appropriatly if there is something to copy. * * @return {String} The text that is copied to the internal clipboard. * Null if there was nothing to copy (the clipboard state will be unchanged in this case). */ function internalCopy() { // Get the current document selections dom var selection = de.selection.getHighlightedDOM(); if (!selection) return null; // Store duplicated dom intClipDOM = selection; // Special case: if the selection root is a list element then we need to // get the list element type (ol/ul) // TODO // Convert DOM into text intClipText = domToText(intClipDOM); // Chop off leading and trailing new line if the dom tree's root is block level if (_isBlockLevel(intClipDOM)) intClipText = intClipText.replace(/^\n/, "").replace(/\n$/, ""); // Reset system clip flag isSysClip = false; return intClipText; } /** * Copies and removes the documents selection to the internal clipboard. * * @return {String} The text that is copied to the internal clipboard. * Null if there was nothing to cut (the clipboard and document state will be unchanged in this case). * * @see internalCopy */ function internalCut() { var res = internalCopy(); // If something was copied, remove any selection from the document if (res) de.selection.remove(); return res; } /** * Pastes text, or DOM, into the document, if the cursor is in an editable section. * @param {String} sysClipText The textual contents of the system clipboard if available. */ function internalPaste(sysClipText) { // Don't try paste if the cursor does not exist if (!de.cursor.exists()) return; // Check permissions of cursor position.... // Remove the current selection if any de.selection.remove(); var cursorDesc = de.cursor.getCurrentCursorDesc(); debug.assert(cursorDesc != null); var textToPaste, domToPaste; // Has there ever been anything copied internally in this session before? if (intClipText) { // If the internal clipboard content was unable to be copied to the system clipboard, // then unfortunatly we will have to use this. if (!isSysClip) domToPaste = intClipDOM; // If the internal clipboard text matches the system clipboard text, then use the // DOM content since it is the riches content. else if (intClipText.replace(/\s/g,"") == sysClipText.replace(/\s/g,"")) domToPaste = intClipDOM; // If the system clipboard text is available, use that else if (sysClipText) textToPaste = sysClipText; // If all else fails, use the internal clip text else domToPaste = intClipDOM; } else textToPaste = sysClipText; // If available, use the text in the system clipboard // TMP - for debugging text pasting etc.. // if (domToPaste) { // domToPaste = null; // textToPaste = intClipText; // } // Is there anything to paste? if (domToPaste || textToPaste) { // Calculate the cursor index var index = cursorDesc.relIndex; if (cursorDesc.isRightOf) index++; if (_nodeName(cursorDesc.domNode) == "br") index = 1; var es = de.doc.getEditSectionContainer(cursorDesc.domNode); if (es) { var esProps = de.doc.getEditProperties(es); if (!esProps.singleLine && domToPaste) { // Can we paste DOM content? // TODO, HTML validation, DEdit filters... should this be in the insert HTML command? // LOW PRIORITY // This will take a lot of thought... // TEMP HACK: Just past inline HTML var inlineContentHolder = $createElement("div"); _visitAllNodes(domToPaste, domToPaste, true, function(node) { // Add text nodes to inline content holder if (node.nodeType == Node.TEXT_NODE) inlineContentHolder.appendChild(node.cloneNode(false)); else if (_isInlineLevel(node) && _isValidRelationship(node, cursorDesc.domNode.parentNode)) { // If this node is inline and can be validly inserted into char position, // see if all of its children are inline var isAllInline = 1; _visitAllNodes(node, node, true, function(innerNode) { // End search once exits subtree if (!_isAncestor(node,innerNode)) return false; // Is a node found to be block level? if (_isBlockLevel(innerNode)) { isAllInline = 0; return false; } }); // If all inline/text then copy this sub tree if (isAllInline && !(esProps.singleLine && _nodeName(node) == "br")) { inlineContentHolder.appendChild(node.cloneNode(true)); return 1; } } }); if (inlineContentHolder.firstChild) de.UndoMan.execute("InsertHTML", inlineContentHolder.innerHTML, cursorDesc.domNode.parentNode, cursorDesc.domNode, index); } else if (textToPaste) { // Can we paste text content? // Decide on insertion action and perform it if (!esProps.singleLine && /\n/.test(textToPaste)) // If has newlines then replace with line breaks de.UndoMan.execute("InsertHTML", _escapeTextToHTML(textToPaste, true), cursorDesc.domNode.parentNode, cursorDesc.domNode, index); else de.UndoMan.execute("InsertText", cursorDesc.domNode, textToPaste, index); } } } } /** * IE Only. * Attempts to copy the text to the clipboard. * @param {String} text the text to copy * @return {Boolean} True iff the text was successfully copied to the system clipboard. */ function ieClipboardCopy(text) { var didSucceed = window.clipboardData.setData('Text', text); return didSucceed === $undefined || didSucceed; } /** * IE Only. * @return {String} The system clipboard's text. Null if unavailable */ function ieClipboardRetrieve() { var clipText = window.clipboardData.getData('Text'); if (clipText === "") { // Could be empty, or failed // Verify failure if (!window.clipboardData.setData('Text', clipText)) clipText = null; } return clipText; } /** * IE's On key down event * @param {Event} e The dom event */ function onIEKeyDown(e) { e = e || window.event; if (!de.events.Keyboard.isAcceleratorDown(e)) return; switch(e.keyCode) { case 67: // COPY (C) case 88: // CUT (X) // Perform internal copy var textToCopy = e.keyCode == 67 ? internalCopy() : internalCut(); if (textToCopy) { // Try to copy the text to the system clipboard the IE way if (ieClipboardCopy(textToCopy)) isSysClip = true; else fallThroughCopyEvent(textToCopy); } break; case 86: // PASTE (V) var sysClipContents = ieClipboardRetrieve(); if (sysClipContents) internalPaste(sysClipContents); else fallThoughPasteEvent(); break; } } /** * For mozilla platforms only. * @return {Boolean} True iff this session has privileges to access XPConnect resources */ function hasXPCPriv() { try { if (netscape.security.PrivilegeManager.enablePrivilege) netscape.security.PrivilegeManager.enablePrivilege("UniversalXPConnect"); else return false; } catch (ex) { return false; } return true; } /** * Mozilla Only. * Attempts to copy the text to the clipboard. * @param {String} text the text to copy * @return {Boolean} True iff the text was successfully copied to the system clipboard. */ function mozClipboardCopy(text) { try { if (!hasXPCPriv()) return false; var str = Components.classes["@mozilla.org/supports-string;1"].createInstance(Components.interfaces.nsISupportsString); str.data = text; var trans = Components.classes["@mozilla.org/widget/transferable;1"].createInstance(Components.interfaces.nsITransferable); if (!trans) return false; trans.addDataFlavor("text/unicode"); trans.setTransferData("text/unicode", str, copytext.length * 2); var clipid = Components.interfaces.nsIClipboard; var clip = Components.classes["@mozilla.org/widget/clipboard;1"].getService(clipid); if (!clip) return false; clip.setData(trans, null, clipid.kGlobalClipboard); }catch(e) { // FF Sometimes throws random errors on blanks lines return false; } } /** * Mozilla Only. * @return {String} The system clipboard's text. Null if unavailable */ function mozClipboardRetrieve() { try { if (!hasXPCPriv()) return null; var clip = Components.classes["@mozilla.org/widget/clipboard;1"].getService(Components.interfaces.nsIClipboard); if (!clip) return null; var trans = Components.classes["@mozilla.org/widget/transferable;1"].createInstance(Components.interfaces.nsITransferable); if (!trans) return null; trans.addDataFlavor("text/unicode"); clip.getData(trans, clip.kGlobalClipboard); var str = {}, strLength = {}, pastetext = ""; trans.getTransferData("text/unicode", str, strLength); if (str) str = str.value.QueryInterface(Components.interfaces.nsISupportsString); if (str) pastetext = str.data.substring(0, strLength.value / 2); return pastetext; } catch (e) { // FF Sometimes throws random errors on blanks lines return null; } } /** * Mozilla's On key press event * @param {Event} e The dom event */ function onGeckoKeyPress(e) { if (!de.events.Keyboard.isAcceleratorDown(e)) return; switch(e.which) { case 99: // COPY (C) case 67: case 120: // CUT (X) case 88: // Perform internal copy var textToCopy = (e.which == 67 || e.which == 99) ? internalCopy() : internalCut(); if (textToCopy) { // Try to copy the text to the system clipboard the XUL way if (mozClipboardCopy(textToCopy)) isSysClip = true; else { fallThroughCopyEvent(textToCopy); } } break; case 118: // PASTE (V) case 86: var sysClipContents = mozClipboardRetrieve(); if (sysClipContents) internalPaste(sysClipContents); else fallThoughPasteEvent(); break; } } /** * Opera's On key down event * @param {Event} e The dom event */ function onPrestoKeyDown(e) { if (!de.events.Keyboard.isAcceleratorDown(e)) return; switch(e.keyCode) { case 67: // COPY (C) var textToCopy = internalCopy(); if (textToCopy) fallThroughCopyEvent(textToCopy); break; case 88: // CUT (X) var textToCopy = internalCut(); if (textToCopy) fallThroughCopyEvent(textToCopy); break; case 86: // PASTE (V) fallThoughPasteEvent(); break; } } /** * Webkit's on copy event * @param {Event} e The dom event */ function onWKCopy(e) { // Webkit has a bug where the clipboard data cannot be set in the clipboard // events, even though the specificatoin states that it can be set. Therefore // must resort to fall-through event capturing for a workaround if (clipboardTOID === null) { // If not currently using fall-through method... var textToCopy = internalCopy(); if (textToCopy) fallThroughCopyEvent(textToCopy); } } /** * Webkit's on paste event * @param {Event} e The dom event */ function onWKPaste(e) { // clipboardData is available for access only in this event if (de.cursor.exists() && clipboardTOID === null) { // If not currently using fall-through method... and something is selected internalPaste(e.clipboardData.getData("Text")); e.preventDefault(); // NOTE: Only prevent default if pasting in editable section, other allow pasting in native controls } } /** * Webkit's on key down event (Cutting only) * @param {Event} e The dom event */ function onWKKeyDown(e) { if (de.events.Keyboard.isAcceleratorDown(e)) { switch(e.keyCode) { case 88: // X: Cut events in webkit dont work var textToCopy = internalCut(); if (textToCopy) fallThroughCopyEvent(textToCopy); break; case 67: // C: Copy events via keyboard in safari mac dont work if (_browser == _Platform.SAFARI && _os == _Platform.MAC && de.cursor.exists()) { // Observation: Safari 4 on mac does not allow copy events to occur if // not coping in native text controls. // Perform internal copy var textToCopy = internalCopy(); if (textToCopy) fallThroughCopyEvent(textToCopy); } break; case 86: // V: Paste events via keyboard in safari mac dont work if (_browser == _Platform.SAFARI && _os == _Platform.MAC && de.cursor.exists()) // Observation: Safari 4 on mac does not allow paste events to occur if // not pasting in native text controls. fallThoughPasteEvent(); break; } } } /** * Invoked just before the browser is about to execute default/native code * which copies the documents current native selection. * * @param {String} textToCopy The text to copy to the clipboard. Null/undefined if pasting */ function fallThroughCopyEvent(textToCopy) { fallThoughClipEventBase(textToCopy); } function fallThoughPasteEvent() { fallThoughClipEventBase(); } function fallThoughClipEventBase(textToCopy) { restoreCursor = de.cursor.getCurrentCursorDesc(); // Avoid race conditions with pending timeout if (clipboardTOID) clearTimeout(clipboardTOID); // Void removing text input event // Set/reset the inputbox contents clipInputEle.value = textToCopy ? textToCopy : ""; // Get the scrollbar state and set the clipboard capturer position in the viewport // to avoid scrolling the document var scrollPos = _getDocumentScrollPos(); // Position the float (container) at the top left of the viewport, // but if the scroll bars are at zero, then place the float // outside of the document... this will completely conceal the float clipContainer.style.left = (scrollPos.left == 0 ? -50 : scrollPos.left + 10) + "px"; clipContainer.style.top = (scrollPos.top == 0 ? -50 : scrollPos.top + 10) + "px"; // Reveal the container clipContainer.style.display = ""; // Select the "revealed" input box try { clipInputEle.focus(); clipInputEle.select(); } catch (e){} // Mozilla sometimes throws XPConnect security exceptions var timeOutFunc = textToCopy ? afterNativeCopyClipInput : afterNativePasteClipInput; // Queue input-box removal function directly after native copy/paste executes clipboardTOID = setTimeout(timeOutFunc, 0); } /** * Safely hides the "temporary" clipboard input control */ function hideClipInput() { clipContainer.style.display = "none"; } /** * Invoked after the browser natively copies the "temporary" clipboard input control's content */ function afterNativeCopyClipInput() { clipboardTOID = null; hideClipInput(); isSysClip = true; window.focus(); } /** * Invoked after the browser natively pastes the system clipboard text to the "temporary" clipboard input control. */ function afterNativePasteClipInput() { clipboardTOID = null; hideClipInput(); // Ensure the cursor did not change/clear if (restoreCursor) { var curCursor = de.cursor.getCurrentCursorDesc(); if (!curCursor || curCursor.domNode != restoreCursor.domNode || curCursor.relIndex != restoreCursor.relIndex) de.cursor.setCursor(restoreCursor); } internalPaste(clipInputEle.value); window.focus(); } })();