browser/metro/base/content/contenthandlers/ContextMenuHandler.js

Wed, 31 Dec 2014 06:55:50 +0100

author
Michael Schloh von Bennewitz <michael@schloh.com>
date
Wed, 31 Dec 2014 06:55:50 +0100
changeset 2
7e26c7da4463
permissions
-rw-r--r--

Added tag UPSTREAM_283F7C6 for changeset ca08bd8f51b2

michael@0 1 /* This Source Code Form is subject to the terms of the Mozilla Public
michael@0 2 * License, v. 2.0. If a copy of the MPL was not distributed with this
michael@0 3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
michael@0 4
michael@0 5 let Ci = Components.interfaces;
michael@0 6 let Cc = Components.classes;
michael@0 7
michael@0 8 this.kXLinkNamespace = "http://www.w3.org/1999/xlink";
michael@0 9
michael@0 10 dump("### ContextMenuHandler.js loaded\n");
michael@0 11
michael@0 12 var ContextMenuHandler = {
michael@0 13 _types: [],
michael@0 14 _previousState: null,
michael@0 15
michael@0 16 init: function ch_init() {
michael@0 17 // Events we catch from content during the bubbling phase
michael@0 18 addEventListener("contextmenu", this, false);
michael@0 19 addEventListener("pagehide", this, false);
michael@0 20
michael@0 21 // Messages we receive from browser
michael@0 22 // Command sent over from browser that only we can handle.
michael@0 23 addMessageListener("Browser:ContextCommand", this, false);
michael@0 24
michael@0 25 this.popupNode = null;
michael@0 26 },
michael@0 27
michael@0 28 handleEvent: function ch_handleEvent(aEvent) {
michael@0 29 switch (aEvent.type) {
michael@0 30 case "contextmenu":
michael@0 31 this._onContentContextMenu(aEvent);
michael@0 32 break;
michael@0 33 case "pagehide":
michael@0 34 this.reset();
michael@0 35 break;
michael@0 36 }
michael@0 37 },
michael@0 38
michael@0 39 receiveMessage: function ch_receiveMessage(aMessage) {
michael@0 40 switch (aMessage.name) {
michael@0 41 case "Browser:ContextCommand":
michael@0 42 this._onContextCommand(aMessage);
michael@0 43 break;
michael@0 44 }
michael@0 45 },
michael@0 46
michael@0 47 /*
michael@0 48 * Handler for commands send over from browser's ContextCommands.js
michael@0 49 * in response to certain context menu actions only we can handle.
michael@0 50 */
michael@0 51 _onContextCommand: function _onContextCommand(aMessage) {
michael@0 52 let node = this.popupNode;
michael@0 53 let command = aMessage.json.command;
michael@0 54
michael@0 55 switch (command) {
michael@0 56 case "cut":
michael@0 57 this._onCut();
michael@0 58 break;
michael@0 59
michael@0 60 case "copy":
michael@0 61 this._onCopy();
michael@0 62 break;
michael@0 63
michael@0 64 case "paste":
michael@0 65 this._onPaste();
michael@0 66 break;
michael@0 67
michael@0 68 case "select-all":
michael@0 69 this._onSelectAll();
michael@0 70 break;
michael@0 71
michael@0 72 case "copy-image-contents":
michael@0 73 this._onCopyImage();
michael@0 74 break;
michael@0 75 }
michael@0 76 },
michael@0 77
michael@0 78 /******************************************************
michael@0 79 * Event handlers
michael@0 80 */
michael@0 81
michael@0 82 reset: function ch_reset() {
michael@0 83 this.popupNode = null;
michael@0 84 this._target = null;
michael@0 85 },
michael@0 86
michael@0 87 // content contextmenu handler
michael@0 88 _onContentContextMenu: function _onContentContextMenu(aEvent) {
michael@0 89 if (aEvent.defaultPrevented)
michael@0 90 return;
michael@0 91
michael@0 92 // Don't let these bubble up to input.js
michael@0 93 aEvent.stopPropagation();
michael@0 94 aEvent.preventDefault();
michael@0 95
michael@0 96 this._processPopupNode(aEvent.originalTarget, aEvent.clientX,
michael@0 97 aEvent.clientY, aEvent.mozInputSource);
michael@0 98 },
michael@0 99
michael@0 100 /******************************************************
michael@0 101 * ContextCommand handlers
michael@0 102 */
michael@0 103
michael@0 104 _onSelectAll: function _onSelectAll() {
michael@0 105 if (Util.isTextInput(this._target)) {
michael@0 106 // select all text in the input control
michael@0 107 this._target.select();
michael@0 108 } else if (Util.isEditableContent(this._target)) {
michael@0 109 this._target.ownerDocument.execCommand("selectAll", false);
michael@0 110 } else {
michael@0 111 // select the entire document
michael@0 112 content.getSelection().selectAllChildren(content.document);
michael@0 113 }
michael@0 114 this.reset();
michael@0 115 },
michael@0 116
michael@0 117 _onPaste: function _onPaste() {
michael@0 118 // paste text if this is an input control
michael@0 119 if (Util.isTextInput(this._target)) {
michael@0 120 let edit = this._target.QueryInterface(Ci.nsIDOMNSEditableElement);
michael@0 121 if (edit) {
michael@0 122 edit.editor.paste(Ci.nsIClipboard.kGlobalClipboard);
michael@0 123 } else {
michael@0 124 Util.dumpLn("error: target element does not support nsIDOMNSEditableElement");
michael@0 125 }
michael@0 126 } else if (Util.isEditableContent(this._target)) {
michael@0 127 try {
michael@0 128 this._target.ownerDocument.execCommand("paste",
michael@0 129 false,
michael@0 130 Ci.nsIClipboard.kGlobalClipboard);
michael@0 131 } catch (ex) {
michael@0 132 dump("ContextMenuHandler: exception pasting into contentEditable: " + ex.message + "\n");
michael@0 133 }
michael@0 134 }
michael@0 135 this.reset();
michael@0 136 },
michael@0 137
michael@0 138 _onCopyImage: function _onCopyImage() {
michael@0 139 Util.copyImageToClipboard(this._target);
michael@0 140 },
michael@0 141
michael@0 142 _onCut: function _onCut() {
michael@0 143 if (Util.isTextInput(this._target)) {
michael@0 144 let edit = this._target.QueryInterface(Ci.nsIDOMNSEditableElement);
michael@0 145 if (edit) {
michael@0 146 edit.editor.cut();
michael@0 147 } else {
michael@0 148 Util.dumpLn("error: target element does not support nsIDOMNSEditableElement");
michael@0 149 }
michael@0 150 } else if (Util.isEditableContent(this._target)) {
michael@0 151 try {
michael@0 152 this._target.ownerDocument.execCommand("cut", false);
michael@0 153 } catch (ex) {
michael@0 154 dump("ContextMenuHandler: exception cutting from contentEditable: " + ex.message + "\n");
michael@0 155 }
michael@0 156 }
michael@0 157 this.reset();
michael@0 158 },
michael@0 159
michael@0 160 _onCopy: function _onCopy() {
michael@0 161 if (Util.isTextInput(this._target)) {
michael@0 162 let edit = this._target.QueryInterface(Ci.nsIDOMNSEditableElement);
michael@0 163 if (edit) {
michael@0 164 edit.editor.copy();
michael@0 165 } else {
michael@0 166 Util.dumpLn("error: target element does not support nsIDOMNSEditableElement");
michael@0 167 }
michael@0 168 } else if (Util.isEditableContent(this._target)) {
michael@0 169 try {
michael@0 170 this._target.ownerDocument.execCommand("copy", false);
michael@0 171 } catch (ex) {
michael@0 172 dump("ContextMenuHandler: exception copying from contentEditable: " +
michael@0 173 ex.message + "\n");
michael@0 174 }
michael@0 175 } else {
michael@0 176 let selectionText = this._previousState.string;
michael@0 177
michael@0 178 Cc["@mozilla.org/widget/clipboardhelper;1"]
michael@0 179 .getService(Ci.nsIClipboardHelper).copyString(selectionText);
michael@0 180 }
michael@0 181 this.reset();
michael@0 182 },
michael@0 183
michael@0 184 /******************************************************
michael@0 185 * Utility routines
michael@0 186 */
michael@0 187
michael@0 188 /*
michael@0 189 * _processPopupNode - Generate and send a Content:ContextMenu message
michael@0 190 * to browser detailing the underlying content types at this.popupNode.
michael@0 191 * Note the event we receive targets the sub frame (if there is one) of
michael@0 192 * the page.
michael@0 193 */
michael@0 194 _processPopupNode: function _processPopupNode(aPopupNode, aX, aY, aInputSrc) {
michael@0 195 if (!aPopupNode)
michael@0 196 return;
michael@0 197
michael@0 198 let { targetWindow: targetWindow,
michael@0 199 offsetX: offsetX,
michael@0 200 offsetY: offsetY } =
michael@0 201 Util.translateToTopLevelWindow(aPopupNode);
michael@0 202
michael@0 203 let popupNode = this.popupNode = aPopupNode;
michael@0 204 let imageUrl = "";
michael@0 205
michael@0 206 let state = {
michael@0 207 types: [],
michael@0 208 label: "",
michael@0 209 linkURL: "",
michael@0 210 linkTitle: "",
michael@0 211 linkProtocol: null,
michael@0 212 mediaURL: "",
michael@0 213 contentType: "",
michael@0 214 contentDisposition: "",
michael@0 215 string: "",
michael@0 216 };
michael@0 217 let uniqueStateTypes = new Set();
michael@0 218
michael@0 219 // Do checks for nodes that never have children.
michael@0 220 if (popupNode.nodeType == Ci.nsIDOMNode.ELEMENT_NODE) {
michael@0 221 // See if the user clicked on an image.
michael@0 222 if (popupNode instanceof Ci.nsIImageLoadingContent && popupNode.currentURI) {
michael@0 223 uniqueStateTypes.add("image");
michael@0 224 state.label = state.mediaURL = popupNode.currentURI.spec;
michael@0 225 imageUrl = state.mediaURL;
michael@0 226 this._target = popupNode;
michael@0 227
michael@0 228 // Retrieve the type of image from the cache since the url can fail to
michael@0 229 // provide valuable informations
michael@0 230 try {
michael@0 231 let imageCache = Cc["@mozilla.org/image/cache;1"].getService(Ci.imgICache);
michael@0 232 let props = imageCache.findEntryProperties(popupNode.currentURI,
michael@0 233 content.document.characterSet);
michael@0 234 if (props) {
michael@0 235 state.contentType = String(props.get("type", Ci.nsISupportsCString));
michael@0 236 state.contentDisposition = String(props.get("content-disposition",
michael@0 237 Ci.nsISupportsCString));
michael@0 238 }
michael@0 239 } catch (ex) {
michael@0 240 Util.dumpLn(ex.message);
michael@0 241 // Failure to get type and content-disposition off the image is non-fatal
michael@0 242 }
michael@0 243 }
michael@0 244 }
michael@0 245
michael@0 246 let elem = popupNode;
michael@0 247 let isText = false;
michael@0 248 let isEditableText = false;
michael@0 249
michael@0 250 while (elem) {
michael@0 251 if (elem.nodeType == Ci.nsIDOMNode.ELEMENT_NODE) {
michael@0 252 // is the target a link or a descendant of a link?
michael@0 253 if (Util.isLink(elem)) {
michael@0 254 // If this is an image that links to itself, don't include both link and
michael@0 255 // image otpions.
michael@0 256 if (imageUrl == this._getLinkURL(elem)) {
michael@0 257 elem = elem.parentNode;
michael@0 258 continue;
michael@0 259 }
michael@0 260
michael@0 261 uniqueStateTypes.add("link");
michael@0 262 state.label = state.linkURL = this._getLinkURL(elem);
michael@0 263 linkUrl = state.linkURL;
michael@0 264 state.linkTitle = popupNode.textContent || popupNode.title;
michael@0 265 state.linkProtocol = this._getProtocol(this._getURI(state.linkURL));
michael@0 266 // mark as text so we can pickup on selection below
michael@0 267 isText = true;
michael@0 268 break;
michael@0 269 }
michael@0 270 // is the target contentEditable (not just inheriting contentEditable)
michael@0 271 // or the entire document in designer mode.
michael@0 272 else if (elem.contentEditable == "true" ||
michael@0 273 Util.isOwnerDocumentInDesignMode(elem)) {
michael@0 274 this._target = elem;
michael@0 275 isEditableText = true;
michael@0 276 isText = true;
michael@0 277 uniqueStateTypes.add("input-text");
michael@0 278
michael@0 279 if (elem.textContent.length) {
michael@0 280 uniqueStateTypes.add("selectable");
michael@0 281 } else {
michael@0 282 uniqueStateTypes.add("input-empty");
michael@0 283 }
michael@0 284 break;
michael@0 285 }
michael@0 286 // is the target a text input
michael@0 287 else if (Util.isTextInput(elem)) {
michael@0 288 this._target = elem;
michael@0 289 isEditableText = true;
michael@0 290 uniqueStateTypes.add("input-text");
michael@0 291
michael@0 292 let selectionStart = elem.selectionStart;
michael@0 293 let selectionEnd = elem.selectionEnd;
michael@0 294
michael@0 295 // Don't include "copy" for password fields.
michael@0 296 if (!(elem instanceof Ci.nsIDOMHTMLInputElement) || elem.mozIsTextField(true)) {
michael@0 297 // If there is a selection add cut and copy
michael@0 298 if (selectionStart != selectionEnd) {
michael@0 299 uniqueStateTypes.add("cut");
michael@0 300 uniqueStateTypes.add("copy");
michael@0 301 state.string = elem.value.slice(selectionStart, selectionEnd);
michael@0 302 } else if (elem.value && elem.textLength) {
michael@0 303 // There is text and it is not selected so add selectable items
michael@0 304 uniqueStateTypes.add("selectable");
michael@0 305 state.string = elem.value;
michael@0 306 }
michael@0 307 }
michael@0 308
michael@0 309 if (!elem.textLength) {
michael@0 310 uniqueStateTypes.add("input-empty");
michael@0 311 }
michael@0 312 break;
michael@0 313 }
michael@0 314 // is the target an element containing text content
michael@0 315 else if (Util.isText(elem)) {
michael@0 316 isText = true;
michael@0 317 }
michael@0 318 // is the target a media element
michael@0 319 else if (elem instanceof Ci.nsIDOMHTMLMediaElement ||
michael@0 320 elem instanceof Ci.nsIDOMHTMLVideoElement) {
michael@0 321 state.label = state.mediaURL = (elem.currentSrc || elem.src);
michael@0 322 uniqueStateTypes.add((elem.paused || elem.ended) ?
michael@0 323 "media-paused" : "media-playing");
michael@0 324 if (elem instanceof Ci.nsIDOMHTMLVideoElement) {
michael@0 325 uniqueStateTypes.add("video");
michael@0 326 }
michael@0 327 }
michael@0 328 }
michael@0 329
michael@0 330 elem = elem.parentNode;
michael@0 331 }
michael@0 332
michael@0 333 // Over arching text tests
michael@0 334 if (isText) {
michael@0 335 // If this is text and has a selection, we want to bring
michael@0 336 // up the copy option on the context menu.
michael@0 337 let selection = targetWindow.getSelection();
michael@0 338 if (selection && this._tapInSelection(selection, aX, aY)) {
michael@0 339 state.string = targetWindow.getSelection().toString();
michael@0 340 uniqueStateTypes.add("copy");
michael@0 341 uniqueStateTypes.add("selected-text");
michael@0 342 if (isEditableText) {
michael@0 343 uniqueStateTypes.add("cut");
michael@0 344 }
michael@0 345 } else {
michael@0 346 // Add general content text if this isn't anything specific
michael@0 347 if (!(
michael@0 348 uniqueStateTypes.has("image") ||
michael@0 349 uniqueStateTypes.has("media") ||
michael@0 350 uniqueStateTypes.has("video") ||
michael@0 351 uniqueStateTypes.has("link") ||
michael@0 352 uniqueStateTypes.has("input-text")
michael@0 353 )) {
michael@0 354 uniqueStateTypes.add("content-text");
michael@0 355 }
michael@0 356 }
michael@0 357 }
michael@0 358
michael@0 359 // Is paste applicable here?
michael@0 360 if (isEditableText) {
michael@0 361 let flavors = ["text/unicode"];
michael@0 362 let cb = Cc["@mozilla.org/widget/clipboard;1"].getService(Ci.nsIClipboard);
michael@0 363 let hasData = cb.hasDataMatchingFlavors(flavors,
michael@0 364 flavors.length,
michael@0 365 Ci.nsIClipboard.kGlobalClipboard);
michael@0 366 // add paste if there's data
michael@0 367 if (hasData && !elem.readOnly) {
michael@0 368 uniqueStateTypes.add("paste");
michael@0 369 }
michael@0 370 }
michael@0 371 // populate position and event source
michael@0 372 state.xPos = offsetX + aX;
michael@0 373 state.yPos = offsetY + aY;
michael@0 374 state.source = aInputSrc;
michael@0 375
michael@0 376 for (let i = 0; i < this._types.length; i++)
michael@0 377 if (this._types[i].handler(state, popupNode))
michael@0 378 uniqueStateTypes.add(this._types[i].name);
michael@0 379
michael@0 380 state.types = [type for (type of uniqueStateTypes)];
michael@0 381 this._previousState = state;
michael@0 382
michael@0 383 sendAsyncMessage("Content:ContextMenu", state);
michael@0 384 },
michael@0 385
michael@0 386 _tapInSelection: function (aSelection, aX, aY) {
michael@0 387 if (!aSelection || !aSelection.rangeCount) {
michael@0 388 return false;
michael@0 389 }
michael@0 390 for (let idx = 0; idx < aSelection.rangeCount; idx++) {
michael@0 391 let range = aSelection.getRangeAt(idx);
michael@0 392 let rect = range.getBoundingClientRect();
michael@0 393 if (Util.pointWithinDOMRect(aX, aY, rect)) {
michael@0 394 return true;
michael@0 395 }
michael@0 396 }
michael@0 397 return false;
michael@0 398 },
michael@0 399
michael@0 400 _getLinkURL: function ch_getLinkURL(aLink) {
michael@0 401 let href = aLink.href;
michael@0 402 if (href)
michael@0 403 return href;
michael@0 404
michael@0 405 href = aLink.getAttributeNS(kXLinkNamespace, "href");
michael@0 406 if (!href || !href.match(/\S/)) {
michael@0 407 // Without this we try to save as the current doc,
michael@0 408 // for example, HTML case also throws if empty
michael@0 409 throw "Empty href";
michael@0 410 }
michael@0 411
michael@0 412 return Util.makeURLAbsolute(aLink.baseURI, href);
michael@0 413 },
michael@0 414
michael@0 415 _getURI: function ch_getURI(aURL) {
michael@0 416 try {
michael@0 417 return Util.makeURI(aURL);
michael@0 418 } catch (ex) { }
michael@0 419
michael@0 420 return null;
michael@0 421 },
michael@0 422
michael@0 423 _getProtocol: function ch_getProtocol(aURI) {
michael@0 424 if (aURI)
michael@0 425 return aURI.scheme;
michael@0 426 return null;
michael@0 427 },
michael@0 428
michael@0 429 /**
michael@0 430 * For add-ons to add new types and data to the ContextMenu message.
michael@0 431 *
michael@0 432 * @param aName A string to identify the new type.
michael@0 433 * @param aHandler A function that takes a state object and a target element.
michael@0 434 * If aHandler returns true, then aName will be added to the list of types.
michael@0 435 * The function may also modify the state object.
michael@0 436 */
michael@0 437 registerType: function registerType(aName, aHandler) {
michael@0 438 this._types.push({name: aName, handler: aHandler});
michael@0 439 },
michael@0 440
michael@0 441 /** Remove all handlers registered for a given type. */
michael@0 442 unregisterType: function unregisterType(aName) {
michael@0 443 this._types = this._types.filter(function(type) type.name != aName);
michael@0 444 }
michael@0 445 };
michael@0 446 this.ContextMenuHandler = ContextMenuHandler;
michael@0 447
michael@0 448 ContextMenuHandler.init();

mercurial