1.1 --- /dev/null Thu Jan 01 00:00:00 1970 +0000 1.2 +++ b/browser/metro/base/content/contenthandlers/ContextMenuHandler.js Wed Dec 31 06:09:35 2014 +0100 1.3 @@ -0,0 +1,448 @@ 1.4 +/* This Source Code Form is subject to the terms of the Mozilla Public 1.5 + * License, v. 2.0. If a copy of the MPL was not distributed with this 1.6 + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 1.7 + 1.8 +let Ci = Components.interfaces; 1.9 +let Cc = Components.classes; 1.10 + 1.11 +this.kXLinkNamespace = "http://www.w3.org/1999/xlink"; 1.12 + 1.13 +dump("### ContextMenuHandler.js loaded\n"); 1.14 + 1.15 +var ContextMenuHandler = { 1.16 + _types: [], 1.17 + _previousState: null, 1.18 + 1.19 + init: function ch_init() { 1.20 + // Events we catch from content during the bubbling phase 1.21 + addEventListener("contextmenu", this, false); 1.22 + addEventListener("pagehide", this, false); 1.23 + 1.24 + // Messages we receive from browser 1.25 + // Command sent over from browser that only we can handle. 1.26 + addMessageListener("Browser:ContextCommand", this, false); 1.27 + 1.28 + this.popupNode = null; 1.29 + }, 1.30 + 1.31 + handleEvent: function ch_handleEvent(aEvent) { 1.32 + switch (aEvent.type) { 1.33 + case "contextmenu": 1.34 + this._onContentContextMenu(aEvent); 1.35 + break; 1.36 + case "pagehide": 1.37 + this.reset(); 1.38 + break; 1.39 + } 1.40 + }, 1.41 + 1.42 + receiveMessage: function ch_receiveMessage(aMessage) { 1.43 + switch (aMessage.name) { 1.44 + case "Browser:ContextCommand": 1.45 + this._onContextCommand(aMessage); 1.46 + break; 1.47 + } 1.48 + }, 1.49 + 1.50 + /* 1.51 + * Handler for commands send over from browser's ContextCommands.js 1.52 + * in response to certain context menu actions only we can handle. 1.53 + */ 1.54 + _onContextCommand: function _onContextCommand(aMessage) { 1.55 + let node = this.popupNode; 1.56 + let command = aMessage.json.command; 1.57 + 1.58 + switch (command) { 1.59 + case "cut": 1.60 + this._onCut(); 1.61 + break; 1.62 + 1.63 + case "copy": 1.64 + this._onCopy(); 1.65 + break; 1.66 + 1.67 + case "paste": 1.68 + this._onPaste(); 1.69 + break; 1.70 + 1.71 + case "select-all": 1.72 + this._onSelectAll(); 1.73 + break; 1.74 + 1.75 + case "copy-image-contents": 1.76 + this._onCopyImage(); 1.77 + break; 1.78 + } 1.79 + }, 1.80 + 1.81 + /****************************************************** 1.82 + * Event handlers 1.83 + */ 1.84 + 1.85 + reset: function ch_reset() { 1.86 + this.popupNode = null; 1.87 + this._target = null; 1.88 + }, 1.89 + 1.90 + // content contextmenu handler 1.91 + _onContentContextMenu: function _onContentContextMenu(aEvent) { 1.92 + if (aEvent.defaultPrevented) 1.93 + return; 1.94 + 1.95 + // Don't let these bubble up to input.js 1.96 + aEvent.stopPropagation(); 1.97 + aEvent.preventDefault(); 1.98 + 1.99 + this._processPopupNode(aEvent.originalTarget, aEvent.clientX, 1.100 + aEvent.clientY, aEvent.mozInputSource); 1.101 + }, 1.102 + 1.103 + /****************************************************** 1.104 + * ContextCommand handlers 1.105 + */ 1.106 + 1.107 + _onSelectAll: function _onSelectAll() { 1.108 + if (Util.isTextInput(this._target)) { 1.109 + // select all text in the input control 1.110 + this._target.select(); 1.111 + } else if (Util.isEditableContent(this._target)) { 1.112 + this._target.ownerDocument.execCommand("selectAll", false); 1.113 + } else { 1.114 + // select the entire document 1.115 + content.getSelection().selectAllChildren(content.document); 1.116 + } 1.117 + this.reset(); 1.118 + }, 1.119 + 1.120 + _onPaste: function _onPaste() { 1.121 + // paste text if this is an input control 1.122 + if (Util.isTextInput(this._target)) { 1.123 + let edit = this._target.QueryInterface(Ci.nsIDOMNSEditableElement); 1.124 + if (edit) { 1.125 + edit.editor.paste(Ci.nsIClipboard.kGlobalClipboard); 1.126 + } else { 1.127 + Util.dumpLn("error: target element does not support nsIDOMNSEditableElement"); 1.128 + } 1.129 + } else if (Util.isEditableContent(this._target)) { 1.130 + try { 1.131 + this._target.ownerDocument.execCommand("paste", 1.132 + false, 1.133 + Ci.nsIClipboard.kGlobalClipboard); 1.134 + } catch (ex) { 1.135 + dump("ContextMenuHandler: exception pasting into contentEditable: " + ex.message + "\n"); 1.136 + } 1.137 + } 1.138 + this.reset(); 1.139 + }, 1.140 + 1.141 + _onCopyImage: function _onCopyImage() { 1.142 + Util.copyImageToClipboard(this._target); 1.143 + }, 1.144 + 1.145 + _onCut: function _onCut() { 1.146 + if (Util.isTextInput(this._target)) { 1.147 + let edit = this._target.QueryInterface(Ci.nsIDOMNSEditableElement); 1.148 + if (edit) { 1.149 + edit.editor.cut(); 1.150 + } else { 1.151 + Util.dumpLn("error: target element does not support nsIDOMNSEditableElement"); 1.152 + } 1.153 + } else if (Util.isEditableContent(this._target)) { 1.154 + try { 1.155 + this._target.ownerDocument.execCommand("cut", false); 1.156 + } catch (ex) { 1.157 + dump("ContextMenuHandler: exception cutting from contentEditable: " + ex.message + "\n"); 1.158 + } 1.159 + } 1.160 + this.reset(); 1.161 + }, 1.162 + 1.163 + _onCopy: function _onCopy() { 1.164 + if (Util.isTextInput(this._target)) { 1.165 + let edit = this._target.QueryInterface(Ci.nsIDOMNSEditableElement); 1.166 + if (edit) { 1.167 + edit.editor.copy(); 1.168 + } else { 1.169 + Util.dumpLn("error: target element does not support nsIDOMNSEditableElement"); 1.170 + } 1.171 + } else if (Util.isEditableContent(this._target)) { 1.172 + try { 1.173 + this._target.ownerDocument.execCommand("copy", false); 1.174 + } catch (ex) { 1.175 + dump("ContextMenuHandler: exception copying from contentEditable: " + 1.176 + ex.message + "\n"); 1.177 + } 1.178 + } else { 1.179 + let selectionText = this._previousState.string; 1.180 + 1.181 + Cc["@mozilla.org/widget/clipboardhelper;1"] 1.182 + .getService(Ci.nsIClipboardHelper).copyString(selectionText); 1.183 + } 1.184 + this.reset(); 1.185 + }, 1.186 + 1.187 + /****************************************************** 1.188 + * Utility routines 1.189 + */ 1.190 + 1.191 + /* 1.192 + * _processPopupNode - Generate and send a Content:ContextMenu message 1.193 + * to browser detailing the underlying content types at this.popupNode. 1.194 + * Note the event we receive targets the sub frame (if there is one) of 1.195 + * the page. 1.196 + */ 1.197 + _processPopupNode: function _processPopupNode(aPopupNode, aX, aY, aInputSrc) { 1.198 + if (!aPopupNode) 1.199 + return; 1.200 + 1.201 + let { targetWindow: targetWindow, 1.202 + offsetX: offsetX, 1.203 + offsetY: offsetY } = 1.204 + Util.translateToTopLevelWindow(aPopupNode); 1.205 + 1.206 + let popupNode = this.popupNode = aPopupNode; 1.207 + let imageUrl = ""; 1.208 + 1.209 + let state = { 1.210 + types: [], 1.211 + label: "", 1.212 + linkURL: "", 1.213 + linkTitle: "", 1.214 + linkProtocol: null, 1.215 + mediaURL: "", 1.216 + contentType: "", 1.217 + contentDisposition: "", 1.218 + string: "", 1.219 + }; 1.220 + let uniqueStateTypes = new Set(); 1.221 + 1.222 + // Do checks for nodes that never have children. 1.223 + if (popupNode.nodeType == Ci.nsIDOMNode.ELEMENT_NODE) { 1.224 + // See if the user clicked on an image. 1.225 + if (popupNode instanceof Ci.nsIImageLoadingContent && popupNode.currentURI) { 1.226 + uniqueStateTypes.add("image"); 1.227 + state.label = state.mediaURL = popupNode.currentURI.spec; 1.228 + imageUrl = state.mediaURL; 1.229 + this._target = popupNode; 1.230 + 1.231 + // Retrieve the type of image from the cache since the url can fail to 1.232 + // provide valuable informations 1.233 + try { 1.234 + let imageCache = Cc["@mozilla.org/image/cache;1"].getService(Ci.imgICache); 1.235 + let props = imageCache.findEntryProperties(popupNode.currentURI, 1.236 + content.document.characterSet); 1.237 + if (props) { 1.238 + state.contentType = String(props.get("type", Ci.nsISupportsCString)); 1.239 + state.contentDisposition = String(props.get("content-disposition", 1.240 + Ci.nsISupportsCString)); 1.241 + } 1.242 + } catch (ex) { 1.243 + Util.dumpLn(ex.message); 1.244 + // Failure to get type and content-disposition off the image is non-fatal 1.245 + } 1.246 + } 1.247 + } 1.248 + 1.249 + let elem = popupNode; 1.250 + let isText = false; 1.251 + let isEditableText = false; 1.252 + 1.253 + while (elem) { 1.254 + if (elem.nodeType == Ci.nsIDOMNode.ELEMENT_NODE) { 1.255 + // is the target a link or a descendant of a link? 1.256 + if (Util.isLink(elem)) { 1.257 + // If this is an image that links to itself, don't include both link and 1.258 + // image otpions. 1.259 + if (imageUrl == this._getLinkURL(elem)) { 1.260 + elem = elem.parentNode; 1.261 + continue; 1.262 + } 1.263 + 1.264 + uniqueStateTypes.add("link"); 1.265 + state.label = state.linkURL = this._getLinkURL(elem); 1.266 + linkUrl = state.linkURL; 1.267 + state.linkTitle = popupNode.textContent || popupNode.title; 1.268 + state.linkProtocol = this._getProtocol(this._getURI(state.linkURL)); 1.269 + // mark as text so we can pickup on selection below 1.270 + isText = true; 1.271 + break; 1.272 + } 1.273 + // is the target contentEditable (not just inheriting contentEditable) 1.274 + // or the entire document in designer mode. 1.275 + else if (elem.contentEditable == "true" || 1.276 + Util.isOwnerDocumentInDesignMode(elem)) { 1.277 + this._target = elem; 1.278 + isEditableText = true; 1.279 + isText = true; 1.280 + uniqueStateTypes.add("input-text"); 1.281 + 1.282 + if (elem.textContent.length) { 1.283 + uniqueStateTypes.add("selectable"); 1.284 + } else { 1.285 + uniqueStateTypes.add("input-empty"); 1.286 + } 1.287 + break; 1.288 + } 1.289 + // is the target a text input 1.290 + else if (Util.isTextInput(elem)) { 1.291 + this._target = elem; 1.292 + isEditableText = true; 1.293 + uniqueStateTypes.add("input-text"); 1.294 + 1.295 + let selectionStart = elem.selectionStart; 1.296 + let selectionEnd = elem.selectionEnd; 1.297 + 1.298 + // Don't include "copy" for password fields. 1.299 + if (!(elem instanceof Ci.nsIDOMHTMLInputElement) || elem.mozIsTextField(true)) { 1.300 + // If there is a selection add cut and copy 1.301 + if (selectionStart != selectionEnd) { 1.302 + uniqueStateTypes.add("cut"); 1.303 + uniqueStateTypes.add("copy"); 1.304 + state.string = elem.value.slice(selectionStart, selectionEnd); 1.305 + } else if (elem.value && elem.textLength) { 1.306 + // There is text and it is not selected so add selectable items 1.307 + uniqueStateTypes.add("selectable"); 1.308 + state.string = elem.value; 1.309 + } 1.310 + } 1.311 + 1.312 + if (!elem.textLength) { 1.313 + uniqueStateTypes.add("input-empty"); 1.314 + } 1.315 + break; 1.316 + } 1.317 + // is the target an element containing text content 1.318 + else if (Util.isText(elem)) { 1.319 + isText = true; 1.320 + } 1.321 + // is the target a media element 1.322 + else if (elem instanceof Ci.nsIDOMHTMLMediaElement || 1.323 + elem instanceof Ci.nsIDOMHTMLVideoElement) { 1.324 + state.label = state.mediaURL = (elem.currentSrc || elem.src); 1.325 + uniqueStateTypes.add((elem.paused || elem.ended) ? 1.326 + "media-paused" : "media-playing"); 1.327 + if (elem instanceof Ci.nsIDOMHTMLVideoElement) { 1.328 + uniqueStateTypes.add("video"); 1.329 + } 1.330 + } 1.331 + } 1.332 + 1.333 + elem = elem.parentNode; 1.334 + } 1.335 + 1.336 + // Over arching text tests 1.337 + if (isText) { 1.338 + // If this is text and has a selection, we want to bring 1.339 + // up the copy option on the context menu. 1.340 + let selection = targetWindow.getSelection(); 1.341 + if (selection && this._tapInSelection(selection, aX, aY)) { 1.342 + state.string = targetWindow.getSelection().toString(); 1.343 + uniqueStateTypes.add("copy"); 1.344 + uniqueStateTypes.add("selected-text"); 1.345 + if (isEditableText) { 1.346 + uniqueStateTypes.add("cut"); 1.347 + } 1.348 + } else { 1.349 + // Add general content text if this isn't anything specific 1.350 + if (!( 1.351 + uniqueStateTypes.has("image") || 1.352 + uniqueStateTypes.has("media") || 1.353 + uniqueStateTypes.has("video") || 1.354 + uniqueStateTypes.has("link") || 1.355 + uniqueStateTypes.has("input-text") 1.356 + )) { 1.357 + uniqueStateTypes.add("content-text"); 1.358 + } 1.359 + } 1.360 + } 1.361 + 1.362 + // Is paste applicable here? 1.363 + if (isEditableText) { 1.364 + let flavors = ["text/unicode"]; 1.365 + let cb = Cc["@mozilla.org/widget/clipboard;1"].getService(Ci.nsIClipboard); 1.366 + let hasData = cb.hasDataMatchingFlavors(flavors, 1.367 + flavors.length, 1.368 + Ci.nsIClipboard.kGlobalClipboard); 1.369 + // add paste if there's data 1.370 + if (hasData && !elem.readOnly) { 1.371 + uniqueStateTypes.add("paste"); 1.372 + } 1.373 + } 1.374 + // populate position and event source 1.375 + state.xPos = offsetX + aX; 1.376 + state.yPos = offsetY + aY; 1.377 + state.source = aInputSrc; 1.378 + 1.379 + for (let i = 0; i < this._types.length; i++) 1.380 + if (this._types[i].handler(state, popupNode)) 1.381 + uniqueStateTypes.add(this._types[i].name); 1.382 + 1.383 + state.types = [type for (type of uniqueStateTypes)]; 1.384 + this._previousState = state; 1.385 + 1.386 + sendAsyncMessage("Content:ContextMenu", state); 1.387 + }, 1.388 + 1.389 + _tapInSelection: function (aSelection, aX, aY) { 1.390 + if (!aSelection || !aSelection.rangeCount) { 1.391 + return false; 1.392 + } 1.393 + for (let idx = 0; idx < aSelection.rangeCount; idx++) { 1.394 + let range = aSelection.getRangeAt(idx); 1.395 + let rect = range.getBoundingClientRect(); 1.396 + if (Util.pointWithinDOMRect(aX, aY, rect)) { 1.397 + return true; 1.398 + } 1.399 + } 1.400 + return false; 1.401 + }, 1.402 + 1.403 + _getLinkURL: function ch_getLinkURL(aLink) { 1.404 + let href = aLink.href; 1.405 + if (href) 1.406 + return href; 1.407 + 1.408 + href = aLink.getAttributeNS(kXLinkNamespace, "href"); 1.409 + if (!href || !href.match(/\S/)) { 1.410 + // Without this we try to save as the current doc, 1.411 + // for example, HTML case also throws if empty 1.412 + throw "Empty href"; 1.413 + } 1.414 + 1.415 + return Util.makeURLAbsolute(aLink.baseURI, href); 1.416 + }, 1.417 + 1.418 + _getURI: function ch_getURI(aURL) { 1.419 + try { 1.420 + return Util.makeURI(aURL); 1.421 + } catch (ex) { } 1.422 + 1.423 + return null; 1.424 + }, 1.425 + 1.426 + _getProtocol: function ch_getProtocol(aURI) { 1.427 + if (aURI) 1.428 + return aURI.scheme; 1.429 + return null; 1.430 + }, 1.431 + 1.432 + /** 1.433 + * For add-ons to add new types and data to the ContextMenu message. 1.434 + * 1.435 + * @param aName A string to identify the new type. 1.436 + * @param aHandler A function that takes a state object and a target element. 1.437 + * If aHandler returns true, then aName will be added to the list of types. 1.438 + * The function may also modify the state object. 1.439 + */ 1.440 + registerType: function registerType(aName, aHandler) { 1.441 + this._types.push({name: aName, handler: aHandler}); 1.442 + }, 1.443 + 1.444 + /** Remove all handlers registered for a given type. */ 1.445 + unregisterType: function unregisterType(aName) { 1.446 + this._types = this._types.filter(function(type) type.name != aName); 1.447 + } 1.448 +}; 1.449 +this.ContextMenuHandler = ContextMenuHandler; 1.450 + 1.451 +ContextMenuHandler.init();