michael@0: /* This Source Code Form is subject to the terms of the Mozilla Public michael@0: * License, v. 2.0. If a copy of the MPL was not distributed with this michael@0: * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ michael@0: michael@0: let Ci = Components.interfaces; michael@0: let Cc = Components.classes; michael@0: michael@0: this.kXLinkNamespace = "http://www.w3.org/1999/xlink"; michael@0: michael@0: dump("### ContextMenuHandler.js loaded\n"); michael@0: michael@0: var ContextMenuHandler = { michael@0: _types: [], michael@0: _previousState: null, michael@0: michael@0: init: function ch_init() { michael@0: // Events we catch from content during the bubbling phase michael@0: addEventListener("contextmenu", this, false); michael@0: addEventListener("pagehide", this, false); michael@0: michael@0: // Messages we receive from browser michael@0: // Command sent over from browser that only we can handle. michael@0: addMessageListener("Browser:ContextCommand", this, false); michael@0: michael@0: this.popupNode = null; michael@0: }, michael@0: michael@0: handleEvent: function ch_handleEvent(aEvent) { michael@0: switch (aEvent.type) { michael@0: case "contextmenu": michael@0: this._onContentContextMenu(aEvent); michael@0: break; michael@0: case "pagehide": michael@0: this.reset(); michael@0: break; michael@0: } michael@0: }, michael@0: michael@0: receiveMessage: function ch_receiveMessage(aMessage) { michael@0: switch (aMessage.name) { michael@0: case "Browser:ContextCommand": michael@0: this._onContextCommand(aMessage); michael@0: break; michael@0: } michael@0: }, michael@0: michael@0: /* michael@0: * Handler for commands send over from browser's ContextCommands.js michael@0: * in response to certain context menu actions only we can handle. michael@0: */ michael@0: _onContextCommand: function _onContextCommand(aMessage) { michael@0: let node = this.popupNode; michael@0: let command = aMessage.json.command; michael@0: michael@0: switch (command) { michael@0: case "cut": michael@0: this._onCut(); michael@0: break; michael@0: michael@0: case "copy": michael@0: this._onCopy(); michael@0: break; michael@0: michael@0: case "paste": michael@0: this._onPaste(); michael@0: break; michael@0: michael@0: case "select-all": michael@0: this._onSelectAll(); michael@0: break; michael@0: michael@0: case "copy-image-contents": michael@0: this._onCopyImage(); michael@0: break; michael@0: } michael@0: }, michael@0: michael@0: /****************************************************** michael@0: * Event handlers michael@0: */ michael@0: michael@0: reset: function ch_reset() { michael@0: this.popupNode = null; michael@0: this._target = null; michael@0: }, michael@0: michael@0: // content contextmenu handler michael@0: _onContentContextMenu: function _onContentContextMenu(aEvent) { michael@0: if (aEvent.defaultPrevented) michael@0: return; michael@0: michael@0: // Don't let these bubble up to input.js michael@0: aEvent.stopPropagation(); michael@0: aEvent.preventDefault(); michael@0: michael@0: this._processPopupNode(aEvent.originalTarget, aEvent.clientX, michael@0: aEvent.clientY, aEvent.mozInputSource); michael@0: }, michael@0: michael@0: /****************************************************** michael@0: * ContextCommand handlers michael@0: */ michael@0: michael@0: _onSelectAll: function _onSelectAll() { michael@0: if (Util.isTextInput(this._target)) { michael@0: // select all text in the input control michael@0: this._target.select(); michael@0: } else if (Util.isEditableContent(this._target)) { michael@0: this._target.ownerDocument.execCommand("selectAll", false); michael@0: } else { michael@0: // select the entire document michael@0: content.getSelection().selectAllChildren(content.document); michael@0: } michael@0: this.reset(); michael@0: }, michael@0: michael@0: _onPaste: function _onPaste() { michael@0: // paste text if this is an input control michael@0: if (Util.isTextInput(this._target)) { michael@0: let edit = this._target.QueryInterface(Ci.nsIDOMNSEditableElement); michael@0: if (edit) { michael@0: edit.editor.paste(Ci.nsIClipboard.kGlobalClipboard); michael@0: } else { michael@0: Util.dumpLn("error: target element does not support nsIDOMNSEditableElement"); michael@0: } michael@0: } else if (Util.isEditableContent(this._target)) { michael@0: try { michael@0: this._target.ownerDocument.execCommand("paste", michael@0: false, michael@0: Ci.nsIClipboard.kGlobalClipboard); michael@0: } catch (ex) { michael@0: dump("ContextMenuHandler: exception pasting into contentEditable: " + ex.message + "\n"); michael@0: } michael@0: } michael@0: this.reset(); michael@0: }, michael@0: michael@0: _onCopyImage: function _onCopyImage() { michael@0: Util.copyImageToClipboard(this._target); michael@0: }, michael@0: michael@0: _onCut: function _onCut() { michael@0: if (Util.isTextInput(this._target)) { michael@0: let edit = this._target.QueryInterface(Ci.nsIDOMNSEditableElement); michael@0: if (edit) { michael@0: edit.editor.cut(); michael@0: } else { michael@0: Util.dumpLn("error: target element does not support nsIDOMNSEditableElement"); michael@0: } michael@0: } else if (Util.isEditableContent(this._target)) { michael@0: try { michael@0: this._target.ownerDocument.execCommand("cut", false); michael@0: } catch (ex) { michael@0: dump("ContextMenuHandler: exception cutting from contentEditable: " + ex.message + "\n"); michael@0: } michael@0: } michael@0: this.reset(); michael@0: }, michael@0: michael@0: _onCopy: function _onCopy() { michael@0: if (Util.isTextInput(this._target)) { michael@0: let edit = this._target.QueryInterface(Ci.nsIDOMNSEditableElement); michael@0: if (edit) { michael@0: edit.editor.copy(); michael@0: } else { michael@0: Util.dumpLn("error: target element does not support nsIDOMNSEditableElement"); michael@0: } michael@0: } else if (Util.isEditableContent(this._target)) { michael@0: try { michael@0: this._target.ownerDocument.execCommand("copy", false); michael@0: } catch (ex) { michael@0: dump("ContextMenuHandler: exception copying from contentEditable: " + michael@0: ex.message + "\n"); michael@0: } michael@0: } else { michael@0: let selectionText = this._previousState.string; michael@0: michael@0: Cc["@mozilla.org/widget/clipboardhelper;1"] michael@0: .getService(Ci.nsIClipboardHelper).copyString(selectionText); michael@0: } michael@0: this.reset(); michael@0: }, michael@0: michael@0: /****************************************************** michael@0: * Utility routines michael@0: */ michael@0: michael@0: /* michael@0: * _processPopupNode - Generate and send a Content:ContextMenu message michael@0: * to browser detailing the underlying content types at this.popupNode. michael@0: * Note the event we receive targets the sub frame (if there is one) of michael@0: * the page. michael@0: */ michael@0: _processPopupNode: function _processPopupNode(aPopupNode, aX, aY, aInputSrc) { michael@0: if (!aPopupNode) michael@0: return; michael@0: michael@0: let { targetWindow: targetWindow, michael@0: offsetX: offsetX, michael@0: offsetY: offsetY } = michael@0: Util.translateToTopLevelWindow(aPopupNode); michael@0: michael@0: let popupNode = this.popupNode = aPopupNode; michael@0: let imageUrl = ""; michael@0: michael@0: let state = { michael@0: types: [], michael@0: label: "", michael@0: linkURL: "", michael@0: linkTitle: "", michael@0: linkProtocol: null, michael@0: mediaURL: "", michael@0: contentType: "", michael@0: contentDisposition: "", michael@0: string: "", michael@0: }; michael@0: let uniqueStateTypes = new Set(); michael@0: michael@0: // Do checks for nodes that never have children. michael@0: if (popupNode.nodeType == Ci.nsIDOMNode.ELEMENT_NODE) { michael@0: // See if the user clicked on an image. michael@0: if (popupNode instanceof Ci.nsIImageLoadingContent && popupNode.currentURI) { michael@0: uniqueStateTypes.add("image"); michael@0: state.label = state.mediaURL = popupNode.currentURI.spec; michael@0: imageUrl = state.mediaURL; michael@0: this._target = popupNode; michael@0: michael@0: // Retrieve the type of image from the cache since the url can fail to michael@0: // provide valuable informations michael@0: try { michael@0: let imageCache = Cc["@mozilla.org/image/cache;1"].getService(Ci.imgICache); michael@0: let props = imageCache.findEntryProperties(popupNode.currentURI, michael@0: content.document.characterSet); michael@0: if (props) { michael@0: state.contentType = String(props.get("type", Ci.nsISupportsCString)); michael@0: state.contentDisposition = String(props.get("content-disposition", michael@0: Ci.nsISupportsCString)); michael@0: } michael@0: } catch (ex) { michael@0: Util.dumpLn(ex.message); michael@0: // Failure to get type and content-disposition off the image is non-fatal michael@0: } michael@0: } michael@0: } michael@0: michael@0: let elem = popupNode; michael@0: let isText = false; michael@0: let isEditableText = false; michael@0: michael@0: while (elem) { michael@0: if (elem.nodeType == Ci.nsIDOMNode.ELEMENT_NODE) { michael@0: // is the target a link or a descendant of a link? michael@0: if (Util.isLink(elem)) { michael@0: // If this is an image that links to itself, don't include both link and michael@0: // image otpions. michael@0: if (imageUrl == this._getLinkURL(elem)) { michael@0: elem = elem.parentNode; michael@0: continue; michael@0: } michael@0: michael@0: uniqueStateTypes.add("link"); michael@0: state.label = state.linkURL = this._getLinkURL(elem); michael@0: linkUrl = state.linkURL; michael@0: state.linkTitle = popupNode.textContent || popupNode.title; michael@0: state.linkProtocol = this._getProtocol(this._getURI(state.linkURL)); michael@0: // mark as text so we can pickup on selection below michael@0: isText = true; michael@0: break; michael@0: } michael@0: // is the target contentEditable (not just inheriting contentEditable) michael@0: // or the entire document in designer mode. michael@0: else if (elem.contentEditable == "true" || michael@0: Util.isOwnerDocumentInDesignMode(elem)) { michael@0: this._target = elem; michael@0: isEditableText = true; michael@0: isText = true; michael@0: uniqueStateTypes.add("input-text"); michael@0: michael@0: if (elem.textContent.length) { michael@0: uniqueStateTypes.add("selectable"); michael@0: } else { michael@0: uniqueStateTypes.add("input-empty"); michael@0: } michael@0: break; michael@0: } michael@0: // is the target a text input michael@0: else if (Util.isTextInput(elem)) { michael@0: this._target = elem; michael@0: isEditableText = true; michael@0: uniqueStateTypes.add("input-text"); michael@0: michael@0: let selectionStart = elem.selectionStart; michael@0: let selectionEnd = elem.selectionEnd; michael@0: michael@0: // Don't include "copy" for password fields. michael@0: if (!(elem instanceof Ci.nsIDOMHTMLInputElement) || elem.mozIsTextField(true)) { michael@0: // If there is a selection add cut and copy michael@0: if (selectionStart != selectionEnd) { michael@0: uniqueStateTypes.add("cut"); michael@0: uniqueStateTypes.add("copy"); michael@0: state.string = elem.value.slice(selectionStart, selectionEnd); michael@0: } else if (elem.value && elem.textLength) { michael@0: // There is text and it is not selected so add selectable items michael@0: uniqueStateTypes.add("selectable"); michael@0: state.string = elem.value; michael@0: } michael@0: } michael@0: michael@0: if (!elem.textLength) { michael@0: uniqueStateTypes.add("input-empty"); michael@0: } michael@0: break; michael@0: } michael@0: // is the target an element containing text content michael@0: else if (Util.isText(elem)) { michael@0: isText = true; michael@0: } michael@0: // is the target a media element michael@0: else if (elem instanceof Ci.nsIDOMHTMLMediaElement || michael@0: elem instanceof Ci.nsIDOMHTMLVideoElement) { michael@0: state.label = state.mediaURL = (elem.currentSrc || elem.src); michael@0: uniqueStateTypes.add((elem.paused || elem.ended) ? michael@0: "media-paused" : "media-playing"); michael@0: if (elem instanceof Ci.nsIDOMHTMLVideoElement) { michael@0: uniqueStateTypes.add("video"); michael@0: } michael@0: } michael@0: } michael@0: michael@0: elem = elem.parentNode; michael@0: } michael@0: michael@0: // Over arching text tests michael@0: if (isText) { michael@0: // If this is text and has a selection, we want to bring michael@0: // up the copy option on the context menu. michael@0: let selection = targetWindow.getSelection(); michael@0: if (selection && this._tapInSelection(selection, aX, aY)) { michael@0: state.string = targetWindow.getSelection().toString(); michael@0: uniqueStateTypes.add("copy"); michael@0: uniqueStateTypes.add("selected-text"); michael@0: if (isEditableText) { michael@0: uniqueStateTypes.add("cut"); michael@0: } michael@0: } else { michael@0: // Add general content text if this isn't anything specific michael@0: if (!( michael@0: uniqueStateTypes.has("image") || michael@0: uniqueStateTypes.has("media") || michael@0: uniqueStateTypes.has("video") || michael@0: uniqueStateTypes.has("link") || michael@0: uniqueStateTypes.has("input-text") michael@0: )) { michael@0: uniqueStateTypes.add("content-text"); michael@0: } michael@0: } michael@0: } michael@0: michael@0: // Is paste applicable here? michael@0: if (isEditableText) { michael@0: let flavors = ["text/unicode"]; michael@0: let cb = Cc["@mozilla.org/widget/clipboard;1"].getService(Ci.nsIClipboard); michael@0: let hasData = cb.hasDataMatchingFlavors(flavors, michael@0: flavors.length, michael@0: Ci.nsIClipboard.kGlobalClipboard); michael@0: // add paste if there's data michael@0: if (hasData && !elem.readOnly) { michael@0: uniqueStateTypes.add("paste"); michael@0: } michael@0: } michael@0: // populate position and event source michael@0: state.xPos = offsetX + aX; michael@0: state.yPos = offsetY + aY; michael@0: state.source = aInputSrc; michael@0: michael@0: for (let i = 0; i < this._types.length; i++) michael@0: if (this._types[i].handler(state, popupNode)) michael@0: uniqueStateTypes.add(this._types[i].name); michael@0: michael@0: state.types = [type for (type of uniqueStateTypes)]; michael@0: this._previousState = state; michael@0: michael@0: sendAsyncMessage("Content:ContextMenu", state); michael@0: }, michael@0: michael@0: _tapInSelection: function (aSelection, aX, aY) { michael@0: if (!aSelection || !aSelection.rangeCount) { michael@0: return false; michael@0: } michael@0: for (let idx = 0; idx < aSelection.rangeCount; idx++) { michael@0: let range = aSelection.getRangeAt(idx); michael@0: let rect = range.getBoundingClientRect(); michael@0: if (Util.pointWithinDOMRect(aX, aY, rect)) { michael@0: return true; michael@0: } michael@0: } michael@0: return false; michael@0: }, michael@0: michael@0: _getLinkURL: function ch_getLinkURL(aLink) { michael@0: let href = aLink.href; michael@0: if (href) michael@0: return href; michael@0: michael@0: href = aLink.getAttributeNS(kXLinkNamespace, "href"); michael@0: if (!href || !href.match(/\S/)) { michael@0: // Without this we try to save as the current doc, michael@0: // for example, HTML case also throws if empty michael@0: throw "Empty href"; michael@0: } michael@0: michael@0: return Util.makeURLAbsolute(aLink.baseURI, href); michael@0: }, michael@0: michael@0: _getURI: function ch_getURI(aURL) { michael@0: try { michael@0: return Util.makeURI(aURL); michael@0: } catch (ex) { } michael@0: michael@0: return null; michael@0: }, michael@0: michael@0: _getProtocol: function ch_getProtocol(aURI) { michael@0: if (aURI) michael@0: return aURI.scheme; michael@0: return null; michael@0: }, michael@0: michael@0: /** michael@0: * For add-ons to add new types and data to the ContextMenu message. michael@0: * michael@0: * @param aName A string to identify the new type. michael@0: * @param aHandler A function that takes a state object and a target element. michael@0: * If aHandler returns true, then aName will be added to the list of types. michael@0: * The function may also modify the state object. michael@0: */ michael@0: registerType: function registerType(aName, aHandler) { michael@0: this._types.push({name: aName, handler: aHandler}); michael@0: }, michael@0: michael@0: /** Remove all handlers registered for a given type. */ michael@0: unregisterType: function unregisterType(aName) { michael@0: this._types = this._types.filter(function(type) type.name != aName); michael@0: } michael@0: }; michael@0: this.ContextMenuHandler = ContextMenuHandler; michael@0: michael@0: ContextMenuHandler.init();