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

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

mercurial