browser/devtools/shared/autocomplete-popup.js

Wed, 31 Dec 2014 06:09:35 +0100

author
Michael Schloh von Bennewitz <michael@schloh.com>
date
Wed, 31 Dec 2014 06:09:35 +0100
changeset 0
6474c204b198
permissions
-rw-r--r--

Cloned upstream origin tor-browser at tor-browser-31.3.0esr-4.5-1-build1
revision ID fc1c9ff7c1b2defdbc039f12214767608f46423f for hacking purpose.

     1 /* vim: set ft=javascript ts=2 et sw=2 tw=80: */
     2 /* This Source Code Form is subject to the terms of the Mozilla Public
     3  * License, v. 2.0. If a copy of the MPL was not distributed with this
     4  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
     6 "use strict";
     8 const {Cc, Ci, Cu} = require("chrome");
     9 const XUL_NS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
    11 loader.lazyImporter(this, "Services", "resource://gre/modules/Services.jsm");
    12 loader.lazyImporter(this, "gDevTools", "resource:///modules/devtools/gDevTools.jsm");
    14 /**
    15  * Autocomplete popup UI implementation.
    16  *
    17  * @constructor
    18  * @param nsIDOMDocument aDocument
    19  *        The document you want the popup attached to.
    20  * @param Object aOptions
    21  *        An object consiting any of the following options:
    22  *        - panelId {String} The id for the popup panel.
    23  *        - listBoxId {String} The id for the richlistbox inside the panel.
    24  *        - position {String} The position for the popup panel.
    25  *        - theme {String} String related to the theme of the popup.
    26  *        - autoSelect {Boolean} Boolean to allow the first entry of the popup
    27  *                     panel to be automatically selected when the popup shows.
    28  *        - direction {String} The direction of the text in the panel. rtl or ltr
    29  *        - onSelect {String} The select event handler for the richlistbox
    30  *        - onClick {String} The click event handler for the richlistbox.
    31  *        - onKeypress {String} The keypress event handler for the richlistitems.
    32  */
    33 function AutocompletePopup(aDocument, aOptions = {})
    34 {
    35   this._document = aDocument;
    37   this.autoSelect = aOptions.autoSelect || false;
    38   this.position = aOptions.position || "after_start";
    39   this.direction = aOptions.direction || "ltr";
    41   this.onSelect = aOptions.onSelect;
    42   this.onClick = aOptions.onClick;
    43   this.onKeypress = aOptions.onKeypress;
    45   let id = aOptions.panelId || "devtools_autoCompletePopup";
    46   let theme = aOptions.theme || "dark";
    47   // If theme is auto, use the devtools.theme pref
    48   if (theme == "auto") {
    49     theme = Services.prefs.getCharPref("devtools.theme");
    50     this.autoThemeEnabled = true;
    51     // Setup theme change listener.
    52     this._handleThemeChange = this._handleThemeChange.bind(this);
    53     gDevTools.on("pref-changed", this._handleThemeChange);
    54   }
    55   // Reuse the existing popup elements.
    56   this._panel = this._document.getElementById(id);
    57   if (!this._panel) {
    58     this._panel = this._document.createElementNS(XUL_NS, "panel");
    59     this._panel.setAttribute("id", id);
    60     this._panel.className = "devtools-autocomplete-popup devtools-monospace "
    61                             + theme + "-theme";
    63     this._panel.setAttribute("noautofocus", "true");
    64     this._panel.setAttribute("level", "top");
    65     if (!aOptions.onKeypress) {
    66       this._panel.setAttribute("ignorekeys", "true");
    67     }
    69     let mainPopupSet = this._document.getElementById("mainPopupSet");
    70     if (mainPopupSet) {
    71       mainPopupSet.appendChild(this._panel);
    72     }
    73     else {
    74       this._document.documentElement.appendChild(this._panel);
    75     }
    76   }
    77   else {
    78     this._list = this._panel.firstChild;
    79   }
    81   if (!this._list) {
    82     this._list = this._document.createElementNS(XUL_NS, "richlistbox");
    83     this._panel.appendChild(this._list);
    85     // Open and hide the panel, so we initialize the API of the richlistbox.
    86     this._panel.openPopup(null, this.position, 0, 0);
    87     this._panel.hidePopup();
    88   }
    90   this._list.setAttribute("flex", "1");
    91   this._list.setAttribute("seltype", "single");
    93   if (aOptions.listBoxId) {
    94     this._list.setAttribute("id", aOptions.listBoxId);
    95   }
    96   this._list.className = "devtools-autocomplete-listbox " + theme + "-theme";
    98   if (this.onSelect) {
    99     this._list.addEventListener("select", this.onSelect, false);
   100   }
   102   if (this.onClick) {
   103     this._list.addEventListener("click", this.onClick, false);
   104   }
   106   if (this.onKeypress) {
   107     this._list.addEventListener("keypress", this.onKeypress, false);
   108   }
   109 }
   110 exports.AutocompletePopup = AutocompletePopup;
   112 AutocompletePopup.prototype = {
   113   _document: null,
   114   _panel: null,
   115   _list: null,
   116   __scrollbarWidth: null,
   118   // Event handlers.
   119   onSelect: null,
   120   onClick: null,
   121   onKeypress: null,
   123   /**
   124    * Open the autocomplete popup panel.
   125    *
   126    * @param nsIDOMNode aAnchor
   127    *        Optional node to anchor the panel to.
   128    * @param Number aXOffset
   129    *        Horizontal offset in pixels from the left of the node to the left
   130    *        of the popup.
   131    * @param Number aYOffset
   132    *        Vertical offset in pixels from the top of the node to the starting
   133    *        of the popup.
   134    */
   135   openPopup: function AP_openPopup(aAnchor, aXOffset = 0, aYOffset = 0)
   136   {
   137     this.__maxLabelLength = -1;
   138     this._updateSize();
   139     this._panel.openPopup(aAnchor, this.position, aXOffset, aYOffset);
   141     if (this.autoSelect) {
   142       this.selectFirstItem();
   143     }
   144   },
   146   /**
   147    * Hide the autocomplete popup panel.
   148    */
   149   hidePopup: function AP_hidePopup()
   150   {
   151     this._panel.hidePopup();
   152   },
   154   /**
   155    * Check if the autocomplete popup is open.
   156    */
   157   get isOpen() {
   158     return this._panel.state == "open" || this._panel.state == "showing";
   159   },
   161   /**
   162    * Destroy the object instance. Please note that the panel DOM elements remain
   163    * in the DOM, because they might still be in use by other instances of the
   164    * same code. It is the responsability of the client code to perform DOM
   165    * cleanup.
   166    */
   167   destroy: function AP_destroy()
   168   {
   169     if (this.isOpen) {
   170       this.hidePopup();
   171     }
   172     this.clearItems();
   174     if (this.onSelect) {
   175       this._list.removeEventListener("select", this.onSelect, false);
   176     }
   178     if (this.onClick) {
   179       this._list.removeEventListener("click", this.onClick, false);
   180     }
   182     if (this.onKeypress) {
   183       this._list.removeEventListener("keypress", this.onKeypress, false);
   184     }
   186     if (this.autoThemeEnabled) {
   187       gDevTools.off("pref-changed", this._handleThemeChange);
   188     }
   190     this._document = null;
   191     this._list = null;
   192     this._panel = null;
   193   },
   195   /**
   196    * Get the autocomplete items array.
   197    *
   198    * @param Number aIndex The index of the item what is wanted.
   199    *
   200    * @return The autocomplete item at index aIndex.
   201    */
   202   getItemAtIndex: function AP_getItemAtIndex(aIndex)
   203   {
   204     return this._list.getItemAtIndex(aIndex)._autocompleteItem;
   205   },
   207   /**
   208    * Get the autocomplete items array.
   209    *
   210    * @return array
   211    *         The array of autocomplete items.
   212    */
   213   getItems: function AP_getItems()
   214   {
   215     let items = [];
   217     Array.forEach(this._list.childNodes, function(aItem) {
   218       items.push(aItem._autocompleteItem);
   219     });
   221     return items;
   222   },
   224   /**
   225    * Set the autocomplete items list, in one go.
   226    *
   227    * @param array aItems
   228    *        The list of items you want displayed in the popup list.
   229    */
   230   setItems: function AP_setItems(aItems)
   231   {
   232     this.clearItems();
   233     aItems.forEach(this.appendItem, this);
   235     // Make sure that the new content is properly fitted by the XUL richlistbox.
   236     if (this.isOpen) {
   237       if (this.autoSelect) {
   238         this.selectFirstItem();
   239       }
   240       this._updateSize();
   241     }
   242   },
   244   /**
   245    * Selects the first item of the richlistbox. Note that first item here is the
   246    * item closes to the input element, which means that 0th index if position is
   247    * below, and last index if position is above.
   248    */
   249   selectFirstItem: function AP_selectFirstItem()
   250   {
   251     if (this.position.contains("before")) {
   252       this.selectedIndex = this.itemCount - 1;
   253     }
   254     else {
   255       this.selectedIndex = 0;
   256     }
   257     this._list.ensureIndexIsVisible(this._list.selectedIndex);
   258   },
   260   __maxLabelLength: -1,
   262   get _maxLabelLength() {
   263     if (this.__maxLabelLength != -1) {
   264       return this.__maxLabelLength;
   265     }
   267     let max = 0;
   268     for (let i = 0; i < this._list.childNodes.length; i++) {
   269       let item = this._list.childNodes[i]._autocompleteItem;
   270       let str = item.label;
   271       if (item.count) {
   272         str += (item.count + "");
   273       }
   274       max = Math.max(str.length, max);
   275     }
   277     this.__maxLabelLength = max;
   278     return this.__maxLabelLength;
   279   },
   281   /**
   282    * Update the panel size to fit the content.
   283    *
   284    * @private
   285    */
   286   _updateSize: function AP__updateSize()
   287   {
   288     if (!this._panel) {
   289       return;
   290     }
   292     this._list.style.width = (this._maxLabelLength + 3) +"ch";
   293     this._list.ensureIndexIsVisible(this._list.selectedIndex);
   294   },
   296   /**
   297    * Clear all the items from the autocomplete list.
   298    */
   299   clearItems: function AP_clearItems()
   300   {
   301     // Reset the selectedIndex to -1 before clearing the list
   302     this.selectedIndex = -1;
   304     while (this._list.hasChildNodes()) {
   305       this._list.removeChild(this._list.firstChild);
   306     }
   308     this.__maxLabelLength = -1;
   310     // Reset the panel and list dimensions. New dimensions are calculated when
   311     // a new set of items is added to the autocomplete popup.
   312     this._list.width = "";
   313     this._list.style.width = "";
   314     this._list.height = "";
   315     this._panel.width = "";
   316     this._panel.height = "";
   317     this._panel.top = "";
   318     this._panel.left = "";
   319   },
   321   /**
   322    * Getter for the index of the selected item.
   323    *
   324    * @type number
   325    */
   326   get selectedIndex() {
   327     return this._list.selectedIndex;
   328   },
   330   /**
   331    * Setter for the selected index.
   332    *
   333    * @param number aIndex
   334    *        The number (index) of the item you want to select in the list.
   335    */
   336   set selectedIndex(aIndex) {
   337     this._list.selectedIndex = aIndex;
   338     if (this.isOpen && this._list.ensureIndexIsVisible) {
   339       this._list.ensureIndexIsVisible(this._list.selectedIndex);
   340     }
   341   },
   343   /**
   344    * Getter for the selected item.
   345    * @type object
   346    */
   347   get selectedItem() {
   348     return this._list.selectedItem ?
   349            this._list.selectedItem._autocompleteItem : null;
   350   },
   352   /**
   353    * Setter for the selected item.
   354    *
   355    * @param object aItem
   356    *        The object you want selected in the list.
   357    */
   358   set selectedItem(aItem) {
   359     this._list.selectedItem = this._findListItem(aItem);
   360     if (this.isOpen) {
   361       this._list.ensureIndexIsVisible(this._list.selectedIndex);
   362     }
   363   },
   365   /**
   366    * Append an item into the autocomplete list.
   367    *
   368    * @param object aItem
   369    *        The item you want appended to the list.
   370    *        The item object can have the following properties:
   371    *        - label {String} Property which is used as the displayed value.
   372    *        - preLabel {String} [Optional] The String that will be displayed
   373    *                   before the label indicating that this is the already
   374    *                   present text in the input box, and label is the text
   375    *                   that will be auto completed. When this property is
   376    *                   present, |preLabel.length| starting characters will be
   377    *                   removed from label.
   378    *        - count {Number} [Optional] The number to represent the count of
   379    *                autocompleted label.
   380    */
   381   appendItem: function AP_appendItem(aItem)
   382   {
   383     let listItem = this._document.createElementNS(XUL_NS, "richlistitem");
   384     if (this.direction) {
   385       listItem.setAttribute("dir", this.direction);
   386     }
   387     let label = this._document.createElementNS(XUL_NS, "label");
   388     label.setAttribute("value", aItem.label);
   389     label.setAttribute("class", "autocomplete-value");
   390     if (aItem.preLabel) {
   391       let preDesc = this._document.createElementNS(XUL_NS, "label");
   392       preDesc.setAttribute("value", aItem.preLabel);
   393       preDesc.setAttribute("class", "initial-value");
   394       listItem.appendChild(preDesc);
   395       label.setAttribute("value", aItem.label.slice(aItem.preLabel.length));
   396     }
   397     listItem.appendChild(label);
   398     if (aItem.count && aItem.count > 1) {
   399       let countDesc = this._document.createElementNS(XUL_NS, "label");
   400       countDesc.setAttribute("value", aItem.count);
   401       countDesc.setAttribute("flex", "1");
   402       countDesc.setAttribute("class", "autocomplete-count");
   403       listItem.appendChild(countDesc);
   404     }
   405     listItem._autocompleteItem = aItem;
   407     this._list.appendChild(listItem);
   408   },
   410   /**
   411    * Find the richlistitem element that belongs to an item.
   412    *
   413    * @private
   414    *
   415    * @param object aItem
   416    *        The object you want found in the list.
   417    *
   418    * @return nsIDOMNode|null
   419    *         The nsIDOMNode that belongs to the given item object. This node is
   420    *         the richlistitem element.
   421    */
   422   _findListItem: function AP__findListItem(aItem)
   423   {
   424     for (let i = 0; i < this._list.childNodes.length; i++) {
   425       let child = this._list.childNodes[i];
   426       if (child._autocompleteItem == aItem) {
   427         return child;
   428       }
   429     }
   430     return null;
   431   },
   433   /**
   434    * Remove an item from the popup list.
   435    *
   436    * @param object aItem
   437    *        The item you want removed.
   438    */
   439   removeItem: function AP_removeItem(aItem)
   440   {
   441     let item = this._findListItem(aItem);
   442     if (!item) {
   443       throw new Error("Item not found!");
   444     }
   445     this._list.removeChild(item);
   446   },
   448   /**
   449    * Getter for the number of items in the popup.
   450    * @type number
   451    */
   452   get itemCount() {
   453     return this._list.childNodes.length;
   454   },
   456   /**
   457    * Getter for the height of each item in the list.
   458    *
   459    * @private
   460    *
   461    * @type number
   462    */
   463   get _itemHeight() {
   464     return this._list.selectedItem.clientHeight;
   465   },
   467   /**
   468    * Select the next item in the list.
   469    *
   470    * @return object
   471    *         The newly selected item object.
   472    */
   473   selectNextItem: function AP_selectNextItem()
   474   {
   475     if (this.selectedIndex < (this.itemCount - 1)) {
   476       this.selectedIndex++;
   477     }
   478     else {
   479       this.selectedIndex = 0;
   480     }
   482     return this.selectedItem;
   483   },
   485   /**
   486    * Select the previous item in the list.
   487    *
   488    * @return object
   489    *         The newly-selected item object.
   490    */
   491   selectPreviousItem: function AP_selectPreviousItem()
   492   {
   493     if (this.selectedIndex > 0) {
   494       this.selectedIndex--;
   495     }
   496     else {
   497       this.selectedIndex = this.itemCount - 1;
   498     }
   500     return this.selectedItem;
   501   },
   503   /**
   504    * Select the top-most item in the next page of items or
   505    * the last item in the list.
   506    *
   507    * @return object
   508    *         The newly-selected item object.
   509    */
   510   selectNextPageItem: function AP_selectNextPageItem()
   511   {
   512     let itemsPerPane = Math.floor(this._list.scrollHeight / this._itemHeight);
   513     let nextPageIndex = this.selectedIndex + itemsPerPane + 1;
   514     this.selectedIndex = nextPageIndex > this.itemCount - 1 ?
   515       this.itemCount - 1 : nextPageIndex;
   517     return this.selectedItem;
   518   },
   520   /**
   521    * Select the bottom-most item in the previous page of items,
   522    * or the first item in the list.
   523    *
   524    * @return object
   525    *         The newly-selected item object.
   526    */
   527   selectPreviousPageItem: function AP_selectPreviousPageItem()
   528   {
   529     let itemsPerPane = Math.floor(this._list.scrollHeight / this._itemHeight);
   530     let prevPageIndex = this.selectedIndex - itemsPerPane - 1;
   531     this.selectedIndex = prevPageIndex < 0 ? 0 : prevPageIndex;
   533     return this.selectedItem;
   534   },
   536   /**
   537    * Focuses the richlistbox.
   538    */
   539   focus: function AP_focus()
   540   {
   541     this._list.focus();
   542   },
   544   /**
   545    * Manages theme switching for the popup based on the devtools.theme pref.
   546    *
   547    * @private
   548    *
   549    * @param String aEvent
   550    *        The name of the event. In this case, "pref-changed".
   551    * @param Object aData
   552    *        An object passed by the emitter of the event. In this case, the
   553    *        object consists of three properties:
   554    *        - pref {String} The name of the preference that was modified.
   555    *        - newValue {Object} The new value of the preference.
   556    *        - oldValue {Object} The old value of the preference.
   557    */
   558   _handleThemeChange: function AP__handleThemeChange(aEvent, aData)
   559   {
   560     if (aData.pref == "devtools.theme") {
   561       this._panel.classList.toggle(aData.oldValue + "-theme", false);
   562       this._panel.classList.toggle(aData.newValue + "-theme", true);
   563       this._list.classList.toggle(aData.oldValue + "-theme", false);
   564       this._list.classList.toggle(aData.newValue + "-theme", true);
   565     }
   566   },
   567 };

mercurial