browser/metro/base/content/helperui/MenuUI.js

Thu, 22 Jan 2015 13:21:57 +0100

author
Michael Schloh von Bennewitz <michael@schloh.com>
date
Thu, 22 Jan 2015 13:21:57 +0100
branch
TOR_BUG_9701
changeset 15
b8a032363ba2
permissions
-rw-r--r--

Incorporate requested changes from Mozilla in review:
https://bugzilla.mozilla.org/show_bug.cgi?id=1123480#c6

     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 // Positioning buffer enforced between the edge of a context menu
     6 // and the edge of the screen.
     7 const kPositionPadding = 10;
     9 var AutofillMenuUI = {
    10   _popupState: null,
    11   __menuPopup: null,
    13   get _panel() { return document.getElementById("autofill-container"); },
    14   get _popup() { return document.getElementById("autofill-popup"); },
    15   get commands() { return this._popup.childNodes[0]; },
    17   get _menuPopup() {
    18     if (!this.__menuPopup) {
    19       this.__menuPopup = new MenuPopup(this._panel, this._popup);
    20       this.__menuPopup._wantTypeBehind = true;
    21       this.__menuPopup.controller = this;
    22     }
    23     return this.__menuPopup;
    24   },
    26   _firePopupEvent: function _firePopupEvent(aEventName) {
    27     let menupopup = this._currentControl.menupopup;
    28     if (menupopup.hasAttribute(aEventName)) {
    29       let func = new Function("event", menupopup.getAttribute(aEventName));
    30       func.call(this);
    31     }
    32   },
    34   _emptyCommands: function _emptyCommands() {
    35     while (this.commands.firstChild)
    36       this.commands.removeChild(this.commands.firstChild);
    37   },
    39   _positionOptions: function _positionOptions() {
    40     return {
    41       bottomAligned: false,
    42       leftAligned: true,
    43       xPos: this._anchorRect.x,
    44       yPos: this._anchorRect.y + this._anchorRect.height,
    45       maxWidth: this._anchorRect.width,
    46       maxHeight: 350,
    47       source: Ci.nsIDOMMouseEvent.MOZ_SOURCE_TOUCH
    48     };
    49   },
    51   show: function show(aAnchorRect, aSuggestionsList) {
    52     this.commands.addEventListener("select", this, true);
    54     this._anchorRect = aAnchorRect;
    55     this._emptyCommands();
    56     for (let idx = 0; idx < aSuggestionsList.length; idx++) {
    57       let item = document.createElement("richlistitem");
    58       let label = document.createElement("label");
    59       label.setAttribute("value", aSuggestionsList[idx].label);
    60       item.setAttribute("value", aSuggestionsList[idx].value);
    61       item.setAttribute("data", aSuggestionsList[idx].value);
    62       item.appendChild(label);
    63       this.commands.appendChild(item);
    64     }
    65     this._menuPopup.show(this._positionOptions());
    66   },
    68   selectByIndex: function mn_selectByIndex(aIndex) {
    69     this._menuPopup.hide();
    70     FormHelperUI.doAutoComplete(this.commands.childNodes[aIndex].getAttribute("data"));
    71   },
    73   hide: function hide () {
    74     this.commands.removeEventListener("select", this, true);
    76     this._menuPopup.hide();
    77   },
    79   handleEvent: function (aEvent) {
    80     switch (aEvent.type) {
    81       case "select":
    82         FormHelperUI.doAutoComplete(this.commands.value);
    83         break;
    84     }
    85   }
    86 };
    88 var ContextMenuUI = {
    89   _popupState: null,
    90   __menuPopup: null,
    91   _defaultPositionOptions: {
    92     bottomAligned: true,
    93     rightAligned: false,
    94     centerHorizontally: true,
    95     moveBelowToFit: true
    96   },
    98   get _panel() { return document.getElementById("context-container"); },
    99   get _popup() { return document.getElementById("context-popup"); },
   100   get commands() { return this._popup.childNodes[0]; },
   102   get _menuPopup() {
   103     if (!this.__menuPopup) {
   104       this.__menuPopup = new MenuPopup(this._panel, this._popup);
   105       this.__menuPopup.controller = this;
   106     }
   107     return this.__menuPopup;
   108   },
   110   /*******************************************
   111    * External api
   112    */
   114   /*
   115    * popupState - return the json object for this context. Called
   116    * by context command to invoke actions on the target.
   117    */
   118   get popupState() {
   119     return this._popupState;
   120   },
   122   /*
   123    * showContextMenu - display a context sensitive menu based
   124    * on the data provided in a json data structure.
   125    *
   126    * @param aMessage data structure containing information about
   127    * the context.
   128    *  aMessage.json - json data structure described below.
   129    *  aMessage.target - target element on which to evoke
   130    *
   131    * @returns true if the context menu was displayed,
   132    * false otherwise.
   133    *
   134    * json: TBD
   135    */
   136   showContextMenu: function ch_showContextMenu(aMessage) {
   137     this._popupState = aMessage.json;
   138     this._popupState.target = aMessage.target;
   139     let contentTypes = this._popupState.types;
   141     /*
   142      * Types in ContextMenuHandler:
   143      * image
   144      * link
   145      * input-text     - generic form input control
   146      * copy           - form input that has some selected text
   147      * selectable     - form input with text that can be selected
   148      * input-empty    - form input (empty)
   149      * paste          - form input and there's text on the clipboard
   150      * selected-text  - generic content text that is selected
   151      * content-text   - generic content text
   152      * video
   153      * media-paused, media-playing
   154      * paste-url      - url bar w/text on the clipboard
   155      */
   157     Util.dumpLn("contentTypes:", contentTypes);
   159     // Defines whether or not low priority items in images, text, and
   160     // links are displayed.
   161     let multipleMediaTypes = false;
   162     if (contentTypes.indexOf("link") != -1 &&
   163         (contentTypes.indexOf("image") != -1  ||
   164          contentTypes.indexOf("video") != -1 ||
   165          contentTypes.indexOf("selected-text") != -1))
   166       multipleMediaTypes = true;
   168     for (let command of Array.slice(this.commands.childNodes)) {
   169       command.hidden = true;
   170       command.selected = false;
   171     }
   173     let optionsAvailable = false;
   174     for (let command of Array.slice(this.commands.childNodes)) {
   175       let types = command.getAttribute("type").split(",");
   176       let lowPriority = (command.hasAttribute("priority") &&
   177         command.getAttribute("priority") == "low");
   178       let searchTextItem = (command.id == "context-search");
   180       // filter low priority items if we have more than one media type.
   181       if (multipleMediaTypes && lowPriority)
   182         continue;
   184       for (let i = 0; i < types.length; i++) {
   185         // If one of the item's types has '!' before it, treat it as an exclusion rule.
   186         if (types[i].charAt(0) == '!' && contentTypes.indexOf(types[i].substring(1)) != -1) {
   187           break;
   188         }
   189         if (contentTypes.indexOf(types[i]) != -1) {
   190           // If this is the special search text item, we need to set its label dynamically.
   191           if (searchTextItem && !ContextCommands.searchTextSetup(command, this._popupState.string)) {
   192             break;
   193           }
   194           optionsAvailable = true;
   195           command.hidden = false;
   196           break;
   197         }
   198       }
   199     }
   201     if (!optionsAvailable) {
   202       this._popupState = null;
   203       return false;
   204     }
   206     let coords = { x: aMessage.json.xPos, y: aMessage.json.yPos };
   208     // chrome calls don't need to be translated and as such
   209     // don't provide target.
   210     if (aMessage.target && aMessage.target.localName === "browser") {
   211       coords = aMessage.target.msgBrowserToClient(aMessage, true);
   212     }
   213     this._menuPopup.show(Util.extend({}, this._defaultPositionOptions, {
   214       xPos: coords.x,
   215       yPos: coords.y,
   216       source: aMessage.json.source
   217     }));
   218     return true;
   219   },
   221   hide: function hide () {
   222     for (let command of this.commands.querySelectorAll("richlistitem[selected]")) {
   223       command.removeAttribute("selected");
   224     }
   225     this._menuPopup.hide();
   226     this._popupState = null;
   227   },
   229   reset: function reset() {
   230     this._popupState = null;
   231   }
   232 };
   234 var MenuControlUI = {
   235   _currentControl: null,
   236   __menuPopup: null,
   238   get _panel() { return document.getElementById("menucontrol-container"); },
   239   get _popup() { return document.getElementById("menucontrol-popup"); },
   240   get commands() { return this._popup.childNodes[0]; },
   242   get _menuPopup() {
   243     if (!this.__menuPopup) {
   244       this.__menuPopup = new MenuPopup(this._panel, this._popup);
   245       this.__menuPopup.controller = this;
   246     }
   247     return this.__menuPopup;
   248   },
   250   _firePopupEvent: function _firePopupEvent(aEventName) {
   251     let menupopup = this._currentControl.menupopup;
   252     if (menupopup.hasAttribute(aEventName)) {
   253       let func = new Function("event", menupopup.getAttribute(aEventName));
   254       func.call(this);
   255     }
   256   },
   258   _emptyCommands: function _emptyCommands() {
   259     while (this.commands.firstChild)
   260       this.commands.removeChild(this.commands.firstChild);
   261   },
   263   _positionOptions: function _positionOptions() {
   264     let position = this._currentControl.menupopup.position || "after_start";
   265     let rect = this._currentControl.getBoundingClientRect();
   267     let options = {};
   269     // TODO: Detect text direction and flip for RTL.
   271     switch (position) {
   272       case "before_start":
   273         options.xPos = rect.left;
   274         options.yPos = rect.top;
   275         options.bottomAligned = true;
   276         options.leftAligned = true;
   277         break;
   278       case "before_end":
   279         options.xPos = rect.right;
   280         options.yPos = rect.top;
   281         options.bottomAligned = true;
   282         options.rightAligned = true;
   283         break;
   284       case "after_start":
   285         options.xPos = rect.left;
   286         options.yPos = rect.bottom;
   287         options.topAligned = true;
   288         options.leftAligned = true;
   289         break;
   290       case "after_end":
   291         options.xPos = rect.right;
   292         options.yPos = rect.bottom;
   293         options.topAligned = true;
   294         options.rightAligned = true;
   295         break;
   297       // TODO: Support other popup positions.
   298     }
   300     return options;
   301   },
   303   show: function show(aMenuControl) {
   304     this._currentControl = aMenuControl;
   305     this._panel.setAttribute("for", aMenuControl.id);
   306     this._firePopupEvent("onpopupshowing");
   308     this._emptyCommands();
   309     let children = this._currentControl.menupopup.children;
   310     for (let i = 0; i < children.length; i++) {
   311       let child = children[i];
   312       let item = document.createElement("richlistitem");
   314       if (child.disabled)
   315         item.setAttribute("disabled", "true");
   317       if (child.hidden)
   318         item.setAttribute("hidden", "true");
   320       // Add selected as a class name instead of an attribute to not being overidden
   321       // by the richlistbox behavior (it sets the "current" and "selected" attribute
   322       if (child.selected)
   323         item.setAttribute("class", "selected");
   325       let image = document.createElement("image");
   326       image.setAttribute("src", child.image || "");
   327       item.appendChild(image);
   329       let label = document.createElement("label");
   330       label.setAttribute("value", child.label);
   331       item.appendChild(label);
   333       this.commands.appendChild(item);
   334     }
   336     this._menuPopup.show(this._positionOptions());
   337   },
   339   selectByIndex: function mn_selectByIndex(aIndex) {
   340     this._currentControl.selectedIndex = aIndex;
   342     // Dispatch a xul command event to the attached menulist
   343     if (this._currentControl.dispatchEvent) {
   344       let evt = document.createEvent("XULCommandEvent");
   345       evt.initCommandEvent("command", true, true, window, 0, false, false, false, false, null);
   346       this._currentControl.dispatchEvent(evt);
   347     }
   349     this._menuPopup.hide();
   350   }
   351 };
   353 function MenuPopup(aPanel, aPopup) {
   354   this._panel = aPanel;
   355   this._popup = aPopup;
   356   this._wantTypeBehind = false;
   358   window.addEventListener('MozAppbarShowing', this, false);
   359 }
   360 MenuPopup.prototype = {
   361   get visible() { return !this._panel.hidden; },
   362   get commands() { return this._popup.childNodes[0]; },
   364   show: function (aPositionOptions) {
   365     if (!this.visible) {
   366       this._animateShow(aPositionOptions);
   367     }
   368   },
   370   hide: function () {
   371     if (this.visible) {
   372       this._animateHide();
   373     }
   374   },
   376   _position: function _position(aPositionOptions) {
   377     let aX = aPositionOptions.xPos;
   378     let aY = aPositionOptions.yPos;
   379     let aSource = aPositionOptions.source;
   381     // Set these first so they are set when we do misc. calculations below.
   382     if (aPositionOptions.maxWidth) {
   383       this._popup.style.maxWidth = aPositionOptions.maxWidth + "px";
   384     }
   385     if (aPositionOptions.maxHeight) {
   386       this._popup.style.maxHeight = aPositionOptions.maxHeight + "px";
   387     }
   389     let width = this._popup.boxObject.width;
   390     let height = this._popup.boxObject.height;
   391     let halfWidth = width / 2;
   392     let screenWidth = ContentAreaObserver.width;
   393     let screenHeight = ContentAreaObserver.height;
   395     if (aPositionOptions.rightAligned)
   396       aX -= width;
   398     if (aPositionOptions.bottomAligned)
   399       aY -= height;
   401     if (aPositionOptions.centerHorizontally)
   402       aX -= halfWidth;
   404     // Always leave some padding.
   405     if (aX < kPositionPadding) {
   406       aX = kPositionPadding;
   407     } else if (aX + width + kPositionPadding > screenWidth){
   408       // Don't let the popup overflow to the right.
   409       aX = Math.max(screenWidth - width - kPositionPadding, kPositionPadding);
   410     }
   412     if (aY < kPositionPadding  && aPositionOptions.moveBelowToFit) {
   413       // show context menu below when it doesn't fit.
   414       aY = aPositionOptions.yPos;
   415     }
   417     if (aY < kPositionPadding) {
   418       aY = kPositionPadding;
   419     } else if (aY + height + kPositionPadding > screenHeight){
   420       aY = Math.max(screenHeight - height - kPositionPadding, kPositionPadding);
   421     }
   423     this._panel.left = aX;
   424     this._panel.top = aY;
   426     if (!aPositionOptions.maxHeight) {
   427       // Make sure it fits in the window.
   428       let popupHeight = Math.min(aY + height + kPositionPadding, screenHeight - aY - kPositionPadding);
   429       this._popup.style.maxHeight = popupHeight + "px";
   430     }
   432     if (!aPositionOptions.maxWidth) {
   433       let popupWidth = Math.min(aX + width + kPositionPadding, screenWidth - aX - kPositionPadding);
   434       this._popup.style.maxWidth = popupWidth + "px";
   435     }
   436   },
   438   _animateShow: function (aPositionOptions) {
   439     let deferred = Promise.defer();
   441     window.addEventListener("keypress", this, true);
   442     window.addEventListener("mousedown", this, true);
   443     window.addEventListener("touchstart", this, true);
   444     window.addEventListener("scroll", this, true);
   445     window.addEventListener("blur", this, true);
   446     Elements.stack.addEventListener("PopupChanged", this, false);
   448     this._panel.hidden = false;
   449     let popupFrom = !aPositionOptions.bottomAligned ? "above" : "below";
   450     this._panel.setAttribute("showingfrom", popupFrom);
   452     // This triggers a reflow, which sets transitionability.
   453     // All animation/transition setup must happen before here.
   454     this._position(aPositionOptions || {});
   456     let self = this;
   457     this._panel.addEventListener("transitionend", function popupshown () {
   458       self._panel.removeEventListener("transitionend", popupshown);
   459       self._panel.removeAttribute("showingfrom");
   461       self._dispatch("popupshown");
   462       deferred.resolve();
   463     });
   465     this._panel.setAttribute("showing", "true");
   466     return deferred.promise;
   467   },
   469   _animateHide: function () {
   470     let deferred = Promise.defer();
   472     window.removeEventListener("keypress", this, true);
   473     window.removeEventListener("mousedown", this, true);
   474     window.removeEventListener("touchstart", this, true);
   475     window.removeEventListener("scroll", this, true);
   476     window.removeEventListener("blur", this, true);
   477     Elements.stack.removeEventListener("PopupChanged", this, false);
   479     let self = this;
   480     this._panel.addEventListener("transitionend", function popuphidden() {
   481       self._panel.removeEventListener("transitionend", popuphidden);
   482       self._panel.removeAttribute("hiding");
   483       self._panel.hidden = true;
   484       self._popup.style.maxWidth = "none";
   485       self._popup.style.maxHeight = "none";
   487       self._dispatch("popuphidden");
   488       deferred.resolve();
   489     });
   491     this._panel.setAttribute("hiding", "true");
   492     this._panel.removeAttribute("showing");
   493     return deferred.promise;
   494   },
   496   _dispatch: function _dispatch(aName) {
   497     let event = document.createEvent("Events");
   498     event.initEvent(aName, true, false);
   499     this._panel.dispatchEvent(event);
   500   },
   502   handleEvent: function handleEvent(aEvent) {
   503     switch (aEvent.type) {
   504       case "keypress":
   505         // this.commands is not holding focus and not processing key events.
   506         // Proxying events so that they're handled properly.
   508         // Avoid recursion
   509         if (aEvent.mine)
   510           break;
   512         let ev = document.createEvent("KeyboardEvent");
   513         ev.initKeyEvent(
   514           "keypress",        //  in DOMString typeArg,
   515           false,             //  in boolean canBubbleArg,
   516           true,              //  in boolean cancelableArg,
   517           null,              //  in nsIDOMAbstractView viewArg,  Specifies UIEvent.view. This value may be null.
   518           aEvent.ctrlKey,    //  in boolean ctrlKeyArg,
   519           aEvent.altKey,     //  in boolean altKeyArg,
   520           aEvent.shiftKey,   //  in boolean shiftKeyArg,
   521           aEvent.metaKey,    //  in boolean metaKeyArg,
   522           aEvent.keyCode,    //  in unsigned long keyCodeArg,
   523           aEvent.charCode);  //  in unsigned long charCodeArg);
   525         ev.mine = true;
   527         switch (aEvent.keyCode) {
   528           case aEvent.DOM_VK_ESCAPE:
   529             this.hide();
   530             break;
   532           case aEvent.DOM_VK_RETURN:
   533             this.commands.currentItem.click();
   534             break;
   535         }
   537         if (Util.isNavigationKey(aEvent.keyCode)) {
   538           aEvent.stopPropagation();
   539           aEvent.preventDefault();
   540           this.commands.dispatchEvent(ev);
   541         } else if (!this._wantTypeBehind) {
   542           // Hide the context menu so you can't type behind it.
   543           aEvent.stopPropagation();
   544           aEvent.preventDefault();
   545           this.hide();
   546         }
   547         break;
   548       case "blur":
   549       case "mousedown":
   550       case "touchstart":
   551       case "scroll":
   552         if (!this._popup.contains(aEvent.target)) {
   553           aEvent.stopPropagation();
   554           this.hide();
   555         }
   556         break;
   557       case "PopupChanged":
   558         if (aEvent.detail) {
   559           this.hide();
   560         }
   561         break;
   562       case "MozAppbarShowing":
   563         if (this.controller && this.controller.hide) {
   564           this.controller.hide()
   565         } else {
   566           this.hide();
   567         }
   568         break;
   569     }
   570   }
   571 };

mercurial