toolkit/content/widgets/autocomplete.xml

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 <?xml version="1.0"?>
     3 # -*- Mode: HTML -*-
     4 # This Source Code Form is subject to the terms of the Mozilla Public
     5 # License, v. 2.0. If a copy of the MPL was not distributed with this
     6 # file, You can obtain one at http://mozilla.org/MPL/2.0/.
     8 <!DOCTYPE bindings [
     9 <!ENTITY % actionsDTD SYSTEM "chrome://global/locale/actions.dtd">
    10 %actionsDTD;
    11 ]>
    13 <bindings id="autocompleteBindings"
    14           xmlns="http://www.mozilla.org/xbl"
    15           xmlns:html="http://www.w3.org/1999/xhtml"
    16           xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
    17           xmlns:xbl="http://www.mozilla.org/xbl">
    19   <binding id="autocomplete" role="xul:combobox"
    20            extends="chrome://global/content/bindings/textbox.xml#textbox">
    21     <resources>
    22       <stylesheet src="chrome://global/skin/autocomplete.css"/>
    23     </resources>
    25     <content sizetopopup="pref">
    26       <xul:hbox class="autocomplete-textbox-container" flex="1" xbl:inherits="focused">
    27         <children includes="image|deck|stack|box">
    28           <xul:image class="autocomplete-icon" allowevents="true"/>
    29         </children>
    31         <xul:hbox anonid="textbox-input-box" class="textbox-input-box" flex="1" xbl:inherits="tooltiptext=inputtooltiptext">
    32           <children/>
    33           <html:input anonid="input" class="autocomplete-textbox textbox-input"
    34                       allowevents="true"
    35                       xbl:inherits="tooltiptext=inputtooltiptext,value,type,maxlength,disabled,size,readonly,placeholder,tabindex,accesskey,mozactionhint"/>
    36         </xul:hbox>
    37         <children includes="hbox"/>
    38       </xul:hbox>
    40       <xul:dropmarker anonid="historydropmarker" class="autocomplete-history-dropmarker"
    41                       allowevents="true"
    42                       xbl:inherits="open,enablehistory,parentfocused=focused"/>
    44       <xul:popupset anonid="popupset" class="autocomplete-result-popupset"/>
    46       <children includes="toolbarbutton"/>
    47     </content>
    49     <implementation implements="nsIAutoCompleteInput, nsIDOMXULMenuListElement">
    50       <field name="mController">null</field>
    51       <field name="mSearchNames">null</field>
    52       <field name="mIgnoreInput">false</field>
    53       <field name="mEnterEvent">null</field>
    55       <field name="_searchBeginHandler">null</field>
    56       <field name="_searchCompleteHandler">null</field>
    57       <field name="_textEnteredHandler">null</field>
    58       <field name="_textRevertedHandler">null</field>
    60       <constructor><![CDATA[
    61         this.mController = Components.classes["@mozilla.org/autocomplete/controller;1"].
    62           getService(Components.interfaces.nsIAutoCompleteController);
    64         this._searchBeginHandler = this.initEventHandler("searchbegin");
    65         this._searchCompleteHandler = this.initEventHandler("searchcomplete");
    66         this._textEnteredHandler = this.initEventHandler("textentered");
    67         this._textRevertedHandler = this.initEventHandler("textreverted");
    69         // For security reasons delay searches on pasted values.
    70         this.inputField.controllers.insertControllerAt(0, this._pasteController);
    71       ]]></constructor>
    73       <destructor><![CDATA[
    74         this.inputField.controllers.removeController(this._pasteController);
    75       ]]></destructor>
    77       <!-- =================== nsIAutoCompleteInput =================== -->
    79       <field name="popup"><![CDATA[
    80         // Wrap in a block so that the let statements don't
    81         // create properties on 'this' (bug 635252).
    82         {
    83           let popup = null;
    84           let popupId = this.getAttribute("autocompletepopup");
    85           if (popupId)
    86             popup = document.getElementById(popupId);
    87           if (!popup) {
    88             popup = document.createElement("panel");
    89             popup.setAttribute("type", "autocomplete");
    90             popup.setAttribute("noautofocus", "true");
    92             let popupset = document.getAnonymousElementByAttribute(this, "anonid", "popupset");
    93             popupset.appendChild(popup);
    94           }
    95           popup.mInput = this;
    96           popup;
    97         }
    98       ]]></field>
   100       <property name="controller" onget="return this.mController;" readonly="true"/>
   102       <property name="popupOpen"
   103                 onget="return this.popup.popupOpen;"
   104                 onset="if (val) this.openPopup(); else this.closePopup();"/>
   106       <property name="disableAutoComplete"
   107                 onset="this.setAttribute('disableautocomplete', val); return val;"
   108                 onget="return this.getAttribute('disableautocomplete') == 'true';"/>
   110       <property name="completeDefaultIndex"
   111                 onset="this.setAttribute('completedefaultindex', val); return val;"
   112                 onget="return this.getAttribute('completedefaultindex') == 'true';"/>
   114       <property name="completeSelectedIndex"
   115                 onset="this.setAttribute('completeselectedindex', val); return val;"
   116                 onget="return this.getAttribute('completeselectedindex') == 'true';"/>
   118       <property name="forceComplete"
   119                 onset="this.setAttribute('forcecomplete', val); return val;"
   120                 onget="return this.getAttribute('forcecomplete') == 'true';"/>
   122       <property name="minResultsForPopup"
   123                 onset="this.setAttribute('minresultsforpopup', val); return val;"
   124                 onget="var m = parseInt(this.getAttribute('minresultsforpopup')); return isNaN(m) ? 1 : m;"/>
   126       <property name="showCommentColumn"
   127                 onset="this.setAttribute('showcommentcolumn', val); return val;"
   128                 onget="return this.getAttribute('showcommentcolumn') == 'true';"/>
   130       <property name="showImageColumn"
   131                 onset="this.setAttribute('showimagecolumn', val); return val;"
   132                 onget="return this.getAttribute('showimagecolumn') == 'true';"/>
   134       <property name="timeout"
   135                 onset="this.setAttribute('timeout', val); return val;">
   136         <getter><![CDATA[
   137           // For security reasons delay searches on pasted values. 
   138           if (this._valueIsPasted) {
   139             let t = parseInt(this.getAttribute('pastetimeout'));
   140             return isNaN(t) ? 1000 : t;
   141           }
   143           let t = parseInt(this.getAttribute('timeout'));
   144           return isNaN(t) ? 50 : t;
   145         ]]></getter>
   146       </property>
   148       <property name="searchParam"
   149                 onget="return this.getAttribute('autocompletesearchparam') || '';"
   150                 onset="this.setAttribute('autocompletesearchparam', val); return val;"/>
   152       <property name="searchCount" readonly="true"
   153                 onget="this.initSearchNames(); return this.mSearchNames.length;"/>
   155       <field name="PrivateBrowsingUtils" readonly="true">
   156         let utils = {};
   157         Components.utils.import("resource://gre/modules/PrivateBrowsingUtils.jsm", utils);
   158         utils.PrivateBrowsingUtils
   159       </field>
   161       <property name="inPrivateContext" readonly="true"
   162                 onget="return this.PrivateBrowsingUtils.isWindowPrivate(window);"/>
   164       <!-- This is the maximum number of drop-down rows we get when we
   165             hit the drop marker beside fields that have it (like the URLbar).-->
   166       <field name="maxDropMarkerRows" readonly="true">14</field>
   168       <method name="getSearchAt">
   169         <parameter name="aIndex"/>
   170         <body><![CDATA[
   171           this.initSearchNames();
   172           return this.mSearchNames[aIndex];
   173         ]]></body>
   174       </method>
   176       <property name="textValue"
   177                 onget="return this.value;">
   178         <setter><![CDATA[
   179           // Completing a result should simulate the user typing the result,
   180           // so fire an input event.
   181           // Trim popup selected values, but never trim results coming from
   182           // autofill.
   183           if (this.popup.selectedIndex == -1)
   184             this._disableTrim = true;
   185           this.value = val;
   186           this._disableTrim = false;
   188           var evt = document.createEvent("UIEvents");
   189           evt.initUIEvent("input", true, false, window, 0);
   190           this.mIgnoreInput = true;
   191           this.dispatchEvent(evt);
   192           this.mIgnoreInput = false;
   193           return this.value;
   194         ]]></setter>
   195       </property>
   197       <method name="selectTextRange">
   198         <parameter name="aStartIndex"/>
   199         <parameter name="aEndIndex"/>
   200         <body><![CDATA[
   201           this.inputField.setSelectionRange(aStartIndex, aEndIndex);
   202         ]]></body>
   203       </method>
   205       <method name="onSearchBegin">
   206         <body><![CDATA[
   207           if (this._searchBeginHandler)
   208             this._searchBeginHandler();
   209         ]]></body>
   210       </method>
   212       <method name="onSearchComplete">
   213         <body><![CDATA[
   214           if (this.mController.matchCount == 0)
   215             this.setAttribute("nomatch", "true");
   216           else
   217             this.removeAttribute("nomatch");
   219           if (this._searchCompleteHandler)
   220             this._searchCompleteHandler();
   221         ]]></body>
   222       </method>
   224       <method name="onTextEntered">
   225         <body><![CDATA[
   226           let rv = false;
   227           if (this._textEnteredHandler)
   228             rv = this._textEnteredHandler(this.mEnterEvent);
   229           this.mEnterEvent = null;
   230           return rv;
   231         ]]></body>
   232       </method>
   234       <method name="onTextReverted">
   235         <body><![CDATA[
   236           if (this._textRevertedHandler)
   237             return this._textRevertedHandler();
   238           return false;
   239         ]]></body>
   240       </method>
   242       <!-- =================== nsIDOMXULMenuListElement =================== -->
   244       <property name="editable" readonly="true"
   245                 onget="return true;" />
   247       <property name="crop"
   248                 onset="this.setAttribute('crop',val); return val;"
   249                 onget="return this.getAttribute('crop');"/>
   251       <property name="open"
   252                 onget="return this.getAttribute('open') == 'true';">
   253         <setter><![CDATA[
   254           if (val)
   255             this.showHistoryPopup();
   256           else
   257             this.closePopup();
   258         ]]></setter>
   259       </property>
   261       <!-- =================== PUBLIC MEMBERS =================== -->
   263       <field name="valueIsTyped">false</field>
   264       <field name="_disableTrim">false</field>
   265       <property name="value">
   266         <getter><![CDATA[
   267           if (typeof this.onBeforeValueGet == "function") {
   268             var result = this.onBeforeValueGet();
   269             if (result)
   270               return result.value;
   271           }
   272           return this.inputField.value;
   273         ]]></getter>
   274         <setter><![CDATA[
   275           this.mIgnoreInput = true;
   277           if (typeof this.onBeforeValueSet == "function")
   278             val = this.onBeforeValueSet(val);
   280           if (typeof this.trimValue == "function" && !this._disableTrim)
   281             val = this.trimValue(val);
   283           this.valueIsTyped = false;
   284           this.inputField.value = val;
   286           if (typeof this.formatValue == "function")
   287             this.formatValue();
   289           this.mIgnoreInput = false;
   290           var event = document.createEvent('Events');
   291           event.initEvent('ValueChange', true, true);
   292           this.inputField.dispatchEvent(event);
   293           return val;
   294         ]]></setter>
   295       </property>
   297       <property name="focused" readonly="true"
   298                 onget="return this.getAttribute('focused') == 'true';"/>
   300       <!-- maximum number of rows to display at a time -->
   301       <property name="maxRows"
   302                 onset="this.setAttribute('maxrows', val); return val;"
   303                 onget="return parseInt(this.getAttribute('maxrows')) || 0;"/>
   305       <!-- option to allow scrolling through the list via the tab key, rather than
   306            tab moving focus out of the textbox -->
   307       <property name="tabScrolling"
   308                 onset="return this.setAttribute('tabscrolling', val); return val;"
   309                 onget="return this.getAttribute('tabscrolling') == 'true';"/>
   311       <!-- disable key navigation handling in the popup results -->
   312       <property name="disableKeyNavigation"
   313                 onset="this.setAttribute('disablekeynavigation', val); return val;"
   314                 onget="return this.getAttribute('disablekeynavigation') == 'true';"/>
   316       <!-- option to completely ignore any blur events while
   317            searches are still going on.  This is useful so that nothing
   318            gets autopicked if the window is required to lose focus for
   319            some reason (eg in LDAP autocomplete, another window may be
   320            brought up so that the user can enter a password to authenticate
   321            to an LDAP server).  -->
   322       <property name="ignoreBlurWhileSearching"
   323                 onset="this.setAttribute('ignoreblurwhilesearching', val); return val;"
   324                 onget="return this.getAttribute('ignoreblurwhilesearching') == 'true';"/>
   326       <!-- option to highlight entries that don't have any matches -->
   327       <property name="highlightNonMatches"
   328                 onset="this.setAttribute('highlightnonmatches', val); return val;"
   329                 onget="return this.getAttribute('highlightnonmatches') == 'true';"/>
   331       <!-- =================== PRIVATE MEMBERS =================== -->
   333       <!-- ::::::::::::: autocomplete controller ::::::::::::: -->
   335       <method name="attachController">
   336         <body><![CDATA[
   337           this.mController.input = this;
   338         ]]></body>
   339       </method>
   341       <method name="detachController">
   342         <body><![CDATA[
   343           try {
   344             if  (this.mController.input == this)
   345             this.mController.input = null;
   346           } catch (ex) {
   347             // nothing really to do.
   348           }
   349         ]]></body>
   350       </method>
   352       <!-- ::::::::::::: popup opening ::::::::::::: -->
   354       <method name="openPopup">
   355         <body><![CDATA[
   356           this.popup.openAutocompletePopup(this, this);
   357         ]]></body>
   358       </method>
   360       <method name="closePopup">
   361         <body><![CDATA[
   362           this.popup.setAttribute("consumeoutsideclicks", "false");
   363           this.popup.closePopup();
   364         ]]></body>
   365       </method>
   367       <method name="showHistoryPopup">
   368         <body><![CDATA[
   369           // history dropmarker pushed state
   370           function cleanup(popup) {
   371             popup.removeEventListener("popupshowing", onShow, false);
   372           }
   373           function onShow(event) {
   374             var popup = event.target, input = popup.input;
   375             cleanup(popup);
   376             input.setAttribute("open", "true");
   377             function onHide() {
   378               input.removeAttribute("open");
   379               popup.setAttribute("consumeoutsideclicks", "false");
   380               popup.removeEventListener("popuphiding", onHide, false);
   381             }
   382             popup.addEventListener("popuphiding", onHide, false);
   383           }
   384           this.popup.addEventListener("popupshowing", onShow, false);
   385           setTimeout(cleanup, 1000, this.popup);
   387           // Store our "normal" maxRows on the popup, so that it can reset the
   388           // value when the popup is hidden.
   389           this.popup._normalMaxRows = this.maxRows;
   391           // Increase our maxRows temporarily, since we want the dropdown to
   392           // be bigger in this case. The popup's popupshowing/popuphiding
   393           // handlers will take care of resetting this.
   394           this.maxRows = this.maxDropMarkerRows;
   396           // Ensure that we have focus.
   397           if (!this.focused)
   398             this.focus();
   399           this.popup.setAttribute("consumeoutsideclicks", "true");
   400           this.attachController();
   401           this.mController.startSearch("");
   402         ]]></body>
   403       </method>
   405       <method name="toggleHistoryPopup">
   406         <body><![CDATA[
   407           if (!this.popup.popupOpen)
   408             this.showHistoryPopup();
   409           else
   410             this.closePopup();
   411         ]]></body>
   412       </method>
   414       <!-- ::::::::::::: event dispatching ::::::::::::: -->
   416       <method name="initEventHandler">
   417         <parameter name="aEventType"/>
   418         <body><![CDATA[
   419           let handlerString = this.getAttribute("on" + aEventType);
   420           if (handlerString) {
   421             return (new Function("eventType", "param", handlerString)).bind(this, aEventType);
   422           }
   423           return null;
   424         ]]></body>
   425       </method>
   427       <!-- ::::::::::::: key handling ::::::::::::: -->
   429       <method name="onKeyPress">
   430         <parameter name="aEvent"/>
   431         <body><![CDATA[
   432           if (aEvent.target.localName != "textbox")
   433             return true; // Let child buttons of autocomplete take input
   435           //XXXpch this is so bogus...
   436           if (aEvent.defaultPrevented)
   437             return false;
   439           var cancel = false;
   441           // Catch any keys that could potentially move the caret. Ctrl can be
   442           // used in combination with these keys on Windows and Linux; and Alt
   443           // can be used on OS X, so make sure the unused one isn't used.
   444           if (!this.disableKeyNavigation &&
   445 #ifdef XP_MACOSX
   446               !aEvent.ctrlKey) {
   447 #else
   448               !aEvent.altKey) {
   449 #endif
   450             switch (aEvent.keyCode) {
   451               case KeyEvent.DOM_VK_LEFT:
   452               case KeyEvent.DOM_VK_RIGHT:
   453               case KeyEvent.DOM_VK_HOME:
   454                 cancel = this.mController.handleKeyNavigation(aEvent.keyCode);
   455                 break;
   456             }
   457           }
   459           // Handle keys that are not part of a keyboard shortcut (no Ctrl or Alt)
   460           if (!this.disableKeyNavigation && !aEvent.ctrlKey && !aEvent.altKey) {
   461             switch (aEvent.keyCode) {
   462               case KeyEvent.DOM_VK_TAB:
   463                 if (this.tabScrolling && this.popup.popupOpen)
   464                   cancel = this.mController.handleKeyNavigation(aEvent.shiftKey ?
   465                                                                 KeyEvent.DOM_VK_UP :
   466                                                                 KeyEvent.DOM_VK_DOWN);
   467                 else if (this.forceComplete && this.mController.matchCount >= 1)
   468                   this.mController.handleTab();
   469                 break;
   470               case KeyEvent.DOM_VK_UP:
   471               case KeyEvent.DOM_VK_DOWN:
   472               case KeyEvent.DOM_VK_PAGE_UP:
   473               case KeyEvent.DOM_VK_PAGE_DOWN:
   474                 cancel = this.mController.handleKeyNavigation(aEvent.keyCode);
   475                 break;
   476             }
   477           }
   479           // Handle keys we know aren't part of a shortcut, even with Alt or
   480           // Ctrl.
   481           switch (aEvent.keyCode) {
   482             case KeyEvent.DOM_VK_ESCAPE:
   483               cancel = this.mController.handleEscape();
   484               break;
   485             case KeyEvent.DOM_VK_RETURN:
   486 #ifdef XP_MACOSX
   487               // Prevent the default action, since it will beep on Mac
   488               if (aEvent.metaKey)
   489                 aEvent.preventDefault();
   490 #endif
   491               this.mEnterEvent = aEvent;
   492               cancel = this.mController.handleEnter(false);
   493               break;
   494             case KeyEvent.DOM_VK_DELETE:
   495 #ifdef XP_MACOSX
   496             case KeyEvent.DOM_VK_BACK_SPACE:
   497               if (aEvent.shiftKey)
   498 #endif
   499               cancel = this.mController.handleDelete();
   500               break;
   501             case KeyEvent.DOM_VK_DOWN:
   502             case KeyEvent.DOM_VK_UP:
   503               if (aEvent.altKey)
   504                 this.toggleHistoryPopup();
   505               break;
   506 #ifndef XP_MACOSX
   507             case KeyEvent.DOM_VK_F4:
   508               this.toggleHistoryPopup();
   509               break;
   510 #endif
   511           }
   513           if (cancel) {
   514             aEvent.stopPropagation();
   515             aEvent.preventDefault();
   516           }
   518           return true;
   519         ]]></body>
   520       </method>
   522       <!-- ::::::::::::: miscellaneous ::::::::::::: -->
   524       <method name="initSearchNames">
   525         <body><![CDATA[
   526           if (!this.mSearchNames) {
   527             var names = this.getAttribute("autocompletesearch");
   528             if (!names)
   529               this.mSearchNames = [];
   530             else
   531               this.mSearchNames = names.split(" ");
   532           }
   533         ]]></body>
   534       </method>
   536       <method name="_focus">
   537         <!-- doesn't reset this.mController -->
   538         <body><![CDATA[
   539           this._dontBlur = true;
   540           this.focus();
   541           this._dontBlur = false;
   542         ]]></body>
   543       </method>
   545       <method name="resetActionType">
   546         <body><![CDATA[
   547           if (this.mIgnoreInput)
   548             return;
   549           this.removeAttribute("actiontype");
   550         ]]></body>
   551       </method>
   553       <field name="_valueIsPasted">false</field>
   554       <field name="_pasteController"><![CDATA[
   555         ({
   556           _autocomplete: this,
   557           _kGlobalClipboard: Components.interfaces.nsIClipboard.kGlobalClipboard,
   558           supportsCommand: function(aCommand) aCommand == "cmd_paste",
   559           doCommand: function(aCommand) {
   560             this._autocomplete._valueIsPasted = true;
   561             this._autocomplete.editor.paste(this._kGlobalClipboard);
   562             this._autocomplete._valueIsPasted = false;
   563           },
   564           isCommandEnabled: function(aCommand) {
   565             return this._autocomplete.editor.isSelectionEditable &&
   566                    this._autocomplete.editor.canPaste(this._kGlobalClipboard);
   567           },
   568           onEvent: function() {}
   569         })
   570       ]]></field>
   571     </implementation>
   573     <handlers>
   574       <handler event="input"><![CDATA[
   575         if (!this.mIgnoreInput && this.mController.input == this) {
   576           this.valueIsTyped = true;
   577           this.mController.handleText();
   578         }
   579         this.resetActionType();
   580       ]]></handler>
   582       <handler event="keypress" phase="capturing"
   583                action="return this.onKeyPress(event);"/>
   585       <handler event="compositionstart" phase="capturing"
   586                action="if (this.mController.input == this) this.mController.handleStartComposition();"/>
   588       <handler event="compositionend" phase="capturing"
   589                action="if (this.mController.input == this) this.mController.handleEndComposition();"/>
   591       <handler event="focus" phase="capturing"
   592                action="this.attachController();"/>
   594       <handler event="blur" phase="capturing"
   595                action="if (!this._dontBlur) this.detachController();"/>
   596     </handlers>
   597   </binding>
   599   <binding id="autocomplete-result-popup" extends="chrome://global/content/bindings/autocomplete.xml#autocomplete-base-popup">
   600     <resources>
   601       <stylesheet src="chrome://global/skin/tree.css"/>
   602       <stylesheet src="chrome://global/skin/autocomplete.css"/>
   603     </resources>
   605     <content ignorekeys="true" level="top" consumeoutsideclicks="false">
   606       <xul:tree anonid="tree" class="autocomplete-tree plain" hidecolumnpicker="true" flex="1" seltype="single">
   607         <xul:treecols anonid="treecols">
   608           <xul:treecol id="treecolAutoCompleteValue" class="autocomplete-treecol" flex="1" overflow="true"/>
   609         </xul:treecols>
   610         <xul:treechildren class="autocomplete-treebody"/>
   611       </xul:tree>
   612     </content>
   614     <implementation>
   615       <field name="mShowCommentColumn">false</field>
   616       <field name="mShowImageColumn">false</field>
   618       <property name="showCommentColumn"
   619                    onget="return this.mShowCommentColumn;">
   620         <setter>
   621           <![CDATA[
   622           if (!val && this.mShowCommentColumn) {
   623             // reset the flex on the value column and remove the comment column
   624             document.getElementById("treecolAutoCompleteValue").setAttribute("flex", 1);
   625             this.removeColumn("treecolAutoCompleteComment");
   626           } else if (val && !this.mShowCommentColumn) {
   627             // reset the flex on the value column and add the comment column
   628             document.getElementById("treecolAutoCompleteValue").setAttribute("flex", 2);
   629             this.addColumn({id: "treecolAutoCompleteComment", flex: 1});
   630           }
   631           this.mShowCommentColumn = val;
   632           return val;
   633         ]]>
   634         </setter>
   635       </property>
   637       <property name="showImageColumn"
   638                 onget="return this.mShowImageColumn;">
   639         <setter>
   640           <![CDATA[
   641           if (!val && this.mShowImageColumn) {
   642             // remove the image column
   643             this.removeColumn("treecolAutoCompleteImage");
   644           } else if (val && !this.mShowImageColumn) {
   645             // add the image column
   646             this.addColumn({id: "treecolAutoCompleteImage", flex: 1});
   647           }
   648           this.mShowImageColumn = val;
   649           return val;
   650         ]]>
   651         </setter>
   652       </property>
   655       <method name="addColumn">
   656         <parameter name="aAttrs"/>
   657         <body>
   658           <![CDATA[
   659           var col = document.createElement("treecol");
   660           col.setAttribute("class", "autocomplete-treecol");
   661           for (var name in aAttrs)
   662             col.setAttribute(name, aAttrs[name]);
   663           this.treecols.appendChild(col);
   664           return col;
   665         ]]>
   666         </body>
   667       </method>
   669       <method name="removeColumn">
   670         <parameter name="aColId"/>
   671         <body>
   672           <![CDATA[
   673           return this.treecols.removeChild(document.getElementById(aColId));
   674         ]]>
   675         </body>
   676       </method>
   678       <property name="selectedIndex"
   679                 onget="return this.tree.currentIndex;">
   680         <setter>
   681           <![CDATA[
   682           this.tree.view.selection.select(val);
   683           if (this.tree.treeBoxObject.height > 0)
   684             this.tree.treeBoxObject.ensureRowIsVisible(val < 0 ? 0 : val);
   685           // Fire select event on xul:tree so that accessibility API
   686           // support layer can fire appropriate accessibility events.
   687           var event = document.createEvent('Events');
   688           event.initEvent("select", true, true);
   689           this.tree.dispatchEvent(event);
   690           return val;
   691         ]]></setter>
   692       </property>
   694       <method name="adjustHeight">
   695         <body>
   696           <![CDATA[
   697           // detect the desired height of the tree
   698           var bx = this.tree.treeBoxObject;
   699           var view = this.tree.view;
   700           if (!view)
   701             return;
   702           var rows = this.maxRows;
   703           if (!view.rowCount || (rows && view.rowCount < rows))
   704             rows = view.rowCount;
   706           var height = rows * bx.rowHeight;
   708           if (height == 0)
   709             this.tree.setAttribute("collapsed", "true");
   710           else {
   711             if (this.tree.hasAttribute("collapsed"))
   712               this.tree.removeAttribute("collapsed");
   714             this.tree.setAttribute("height", height);
   715           }
   716           this.tree.setAttribute("hidescrollbar", view.rowCount <= rows);
   717         ]]>
   718         </body>
   719       </method>
   721       <method name="openAutocompletePopup">
   722         <parameter name="aInput"/>
   723         <parameter name="aElement"/>
   724         <body><![CDATA[
   725           // until we have "baseBinding", (see bug #373652) this allows
   726           // us to override openAutocompletePopup(), but still call
   727           // the method on the base class
   728           this._openAutocompletePopup(aInput, aElement);
   729         ]]></body>
   730       </method>
   732       <method name="_openAutocompletePopup">
   733         <parameter name="aInput"/>
   734         <parameter name="aElement"/>
   735         <body><![CDATA[
   736           if (!this.mPopupOpen) {
   737             this.mInput = aInput;
   738             this.view = aInput.controller.QueryInterface(Components.interfaces.nsITreeView);
   739             this.invalidate();
   741             this.showCommentColumn = this.mInput.showCommentColumn;
   742             this.showImageColumn = this.mInput.showImageColumn;
   744             var rect = aElement.getBoundingClientRect();
   745             var nav = aElement.ownerDocument.defaultView.QueryInterface(Components.interfaces.nsIInterfaceRequestor)
   746                               .getInterface(Components.interfaces.nsIWebNavigation);
   747             var docShell = nav.QueryInterface(Components.interfaces.nsIDocShell);
   748             var docViewer = docShell.contentViewer.QueryInterface(Components.interfaces.nsIMarkupDocumentViewer);
   749             var width = (rect.right - rect.left) * docViewer.fullZoom;
   750             this.setAttribute("width", width > 100 ? width : 100);
   752             // Adjust the direction of the autocomplete popup list based on the textbox direction, bug 649840
   753             var popupDirection = aElement.ownerDocument.defaultView.getComputedStyle(aElement).direction;
   754             this.style.direction = popupDirection;
   756             this.openPopup(aElement, "after_start", 0, 0, false, false);
   757           }
   758         ]]></body>
   759       </method>
   761       <method name="invalidate">
   762         <body><![CDATA[
   763           this.adjustHeight();
   764           this.tree.treeBoxObject.invalidate();
   765         ]]></body>
   766       </method>
   768       <method name="selectBy">
   769         <parameter name="aReverse"/>
   770         <parameter name="aPage"/>
   771         <body><![CDATA[
   772           try {
   773             var amount = aPage ? 5 : 1;
   774             this.selectedIndex = this.getNextIndex(aReverse, amount, this.selectedIndex, this.tree.view.rowCount-1);
   775             if (this.selectedIndex == -1) {
   776               this.input._focus();
   777             }
   778           } catch (ex) {
   779             // do nothing - occasionally timer-related js errors happen here
   780             // e.g. "this.selectedIndex has no properties", when you type fast and hit a
   781             // navigation key before this popup has opened
   782           }
   783         ]]></body>
   784       </method>
   786       <!-- =================== PUBLIC MEMBERS =================== -->
   788       <field name="tree">
   789         document.getAnonymousElementByAttribute(this, "anonid", "tree");
   790       </field>
   792       <field name="treecols">
   793         document.getAnonymousElementByAttribute(this, "anonid", "treecols");
   794       </field>
   796       <property name="view"
   797                 onget="return this.mView;">
   798         <setter><![CDATA[
   799           // We must do this by hand because the tree binding may not be ready yet
   800           this.mView = val;
   801           var bx = this.tree.boxObject;
   802           bx = bx.QueryInterface(Components.interfaces.nsITreeBoxObject);
   803           bx.view = val;
   804         ]]></setter>
   805       </property>
   807     </implementation>
   808   </binding>
   810   <binding id="autocomplete-base-popup" role="none"
   811 extends="chrome://global/content/bindings/popup.xml#popup">
   812     <implementation implements="nsIAutoCompletePopup">
   813       <field name="mInput">null</field>
   814       <field name="mPopupOpen">false</field>
   816       <!-- =================== nsIAutoCompletePopup =================== -->
   818       <property name="input" readonly="true"
   819                 onget="return this.mInput"/>
   821       <property name="overrideValue" readonly="true"
   822                 onget="return null;"/>
   824       <property name="popupOpen" readonly="true"
   825                 onget="return this.mPopupOpen;"/>
   827       <method name="closePopup">
   828         <body>
   829           <![CDATA[
   830           if (this.mPopupOpen) {
   831             this.hidePopup();
   832             this.removeAttribute("width");
   833           }
   834         ]]>
   835         </body>
   836       </method>
   838       <!-- This is the default number of rows that we give the autocomplete
   839            popup when the textbox doesn't have a "maxrows" attribute
   840            for us to use. -->
   841       <field name="defaultMaxRows" readonly="true">6</field>
   843       <!-- In some cases (e.g. when the input's dropmarker button is clicked),
   844            the input wants to display a popup with more rows. In that case, it
   845            should increase its maxRows property and store the "normal" maxRows
   846            in this field. When the popup is hidden, we restore the input's
   847            maxRows to the value stored in this field.
   849            This field is set to -1 between uses so that we can tell when it's
   850            been set by the input and when we need to set it in the popupshowing
   851            handler. -->
   852       <field name="_normalMaxRows">-1</field>
   854       <property name="maxRows" readonly="true">
   855         <getter>
   856           <![CDATA[
   857           return (this.mInput && this.mInput.maxRows) || this.defaultMaxRows;
   858         ]]>
   859         </getter>
   860       </property>
   862       <method name="getNextIndex">
   863         <parameter name="aReverse"/>
   864         <parameter name="aAmount"/>
   865         <parameter name="aIndex"/>
   866         <parameter name="aMaxRow"/>
   867         <body><![CDATA[
   868           if (aMaxRow < 0)
   869             return -1;
   871           var newIdx = aIndex + (aReverse?-1:1)*aAmount;
   872           if (aReverse && aIndex == -1 || newIdx > aMaxRow && aIndex != aMaxRow)
   873             newIdx = aMaxRow;
   874           else if (!aReverse && aIndex == -1 || newIdx < 0 && aIndex != 0)
   875             newIdx = 0;
   877           if (newIdx < 0 && aIndex == 0 || newIdx > aMaxRow && aIndex == aMaxRow)
   878             aIndex = -1;
   879           else
   880             aIndex = newIdx;
   882           return aIndex;
   883         ]]></body>
   884       </method>
   886       <method name="onPopupClick">
   887         <parameter name="aEvent"/>
   888         <body><![CDATA[
   889           var controller = this.view.QueryInterface(Components.interfaces.nsIAutoCompleteController);
   890           controller.handleEnter(true);
   891         ]]></body>
   892       </method>
   893     </implementation>
   895     <handlers>
   896       <handler event="popupshowing"><![CDATA[
   897         // If normalMaxRows wasn't already set by the input, then set it here
   898         // so that we restore the correct number when the popup is hidden.
   899         if (this._normalMaxRows < 0)
   900           this._normalMaxRows = this.mInput.maxRows;
   902         this.mPopupOpen = true;
   903       ]]></handler>
   905       <handler event="popuphiding"><![CDATA[
   906         var isListActive = true;
   907         if (this.selectedIndex == -1)
   908           isListActive = false;
   909         var controller = this.view.QueryInterface(Components.interfaces.nsIAutoCompleteController);
   910         controller.stopSearch();
   912         this.mPopupOpen = false;
   914         // Reset the maxRows property to the cached "normal" value, and reset
   915         // _normalMaxRows so that we can detect whether it was set by the input
   916         // when the popupshowing handler runs.
   917         this.mInput.maxRows = this._normalMaxRows;
   918         this._normalMaxRows = -1;
   919         // If the list was being navigated and then closed, make sure
   920         // we fire accessible focus event back to textbox
   921         if (isListActive) {
   922           this.mInput.mIgnoreFocus = true;
   923           this.mInput._focus();
   924           this.mInput.mIgnoreFocus = false;
   925         }
   926       ]]></handler>
   927     </handlers>
   928   </binding>
   930   <binding id="autocomplete-rich-result-popup" extends="chrome://global/content/bindings/autocomplete.xml#autocomplete-base-popup">
   931     <resources>
   932       <stylesheet src="chrome://global/skin/autocomplete.css"/>
   933     </resources>
   935     <content ignorekeys="true" level="top" consumeoutsideclicks="false">
   936       <xul:richlistbox anonid="richlistbox" class="autocomplete-richlistbox" flex="1"/>
   937       <xul:hbox>
   938         <children/>
   939       </xul:hbox>
   940     </content>
   942     <implementation implements="nsIAutoCompletePopup">
   943       <field name="_currentIndex">0</field>
   944       <field name="_rowHeight">0</field>
   946       <!-- =================== nsIAutoCompletePopup =================== -->
   948       <property name="selectedIndex"
   949                 onget="return this.richlistbox.selectedIndex;">
   950         <setter>
   951           <![CDATA[
   952           this.richlistbox.selectedIndex = val;
   954           // when clearing the selection (val == -1, so selectedItem will be
   955           // null), we want to scroll back to the top.  see bug #406194
   956           this.richlistbox.ensureElementIsVisible(
   957             this.richlistbox.selectedItem || this.richlistbox.firstChild);
   959           return val;
   960         ]]>
   961         </setter>
   962       </property>
   964       <method name="openAutocompletePopup">
   965         <parameter name="aInput"/>
   966         <parameter name="aElement"/>
   967         <body>
   968           <![CDATA[
   969           // until we have "baseBinding", (see bug #373652) this allows
   970           // us to override openAutocompletePopup(), but still call
   971           // the method on the base class
   972           this._openAutocompletePopup(aInput, aElement);
   973         ]]>
   974         </body>
   975       </method>
   977       <method name="_openAutocompletePopup">
   978         <parameter name="aInput"/>
   979         <parameter name="aElement"/>
   980         <body>
   981           <![CDATA[
   982           if (!this.mPopupOpen) {
   983             this.mInput = aInput;
   984             // clear any previous selection, see bugs 400671 and 488357
   985             this.selectedIndex = -1;
   987             var width = aElement.getBoundingClientRect().width;
   988             this.setAttribute("width", width > 100 ? width : 100);
   989             // invalidate() depends on the width attribute
   990             this._invalidate();
   992             this.openPopup(aElement, "after_start", 0, 0, false, false);
   993           }
   994         ]]>
   995         </body>
   996       </method>
   998       <method name="invalidate">
   999         <body>
  1000           <![CDATA[
  1001           // Don't bother doing work if we're not even showing
  1002           if (!this.mPopupOpen)
  1003             return;
  1005           this._invalidate();
  1006           ]]>
  1007         </body>
  1008       </method>
  1010       <method name="_invalidate">
  1011         <body>
  1012           <![CDATA[
  1013           if (!this.hasAttribute("height")) {
  1014             // collapsed if no matches
  1015             this.richlistbox.collapsed = (this._matchCount == 0);
  1017             // Dynamically update height until richlistbox.rows works (bug 401939)
  1018             // Adjust the height immediately and after the row contents update
  1019             this.adjustHeight();
  1020             setTimeout(function(self) self.adjustHeight(), 0, this);
  1023           // make sure to collapse any existing richlistitems
  1024           // that aren't going to be used
  1025           var existingItemsCount = this.richlistbox.childNodes.length;
  1026           for (var i = this._matchCount; i < existingItemsCount; i++)
  1027             this.richlistbox.childNodes[i].collapsed = true;
  1029           this._currentIndex = 0;
  1030           this._appendCurrentResult();
  1031         ]]>
  1032         </body>
  1033       </method>
  1035       <property name="maxResults" readonly="true">
  1036         <getter>
  1037           <![CDATA[
  1038             // this is how many richlistitems will be kept around
  1039             // (note, this getter may be overridden)
  1040             return 20;
  1041           ]]>
  1042         </getter>
  1043       </property>
  1045       <property name="_matchCount" readonly="true">
  1046         <getter>
  1047           <![CDATA[
  1048           return Math.min(this.mInput.controller.matchCount, this.maxResults);
  1049           ]]>
  1050         </getter>
  1051       </property>
  1053       <method name="adjustHeight">
  1054         <body>
  1055           <![CDATA[
  1056           // Figure out how many rows to show
  1057           let rows = this.richlistbox.childNodes;
  1058           let numRows = Math.min(this._matchCount, this.maxRows, rows.length);
  1060           // Default the height to 0 if we have no rows to show
  1061           let height = 0;
  1062           if (numRows) {
  1063             if (!this._rowHeight) {
  1064               let firstRowRect = rows[0].getBoundingClientRect();
  1065               this._rowHeight = firstRowRect.height;
  1068             // Calculate the height to have the first row to last row shown
  1069             height = this._rowHeight * numRows;
  1072           // Only update the height if we have a non-zero height and if it
  1073           // changed (the richlistbox is collapsed if there are no results)
  1074           if (height && height != this.richlistbox.height)
  1075             this.richlistbox.height = height;
  1076           ]]>
  1077         </body>
  1078       </method>
  1080       <method name="_appendCurrentResult">
  1081         <body>
  1082           <![CDATA[
  1083           var controller = this.mInput.controller;
  1084           var matchCount = this._matchCount;
  1085           var existingItemsCount = this.richlistbox.childNodes.length;
  1087           // Process maxRows per chunk to improve performance and user experience
  1088           for (let i = 0; i < this.maxRows; i++) {
  1089             if (this._currentIndex >= matchCount)
  1090               return;
  1092             var item;
  1094             // trim the leading/trailing whitespace
  1095             var trimmedSearchString = controller.searchString.replace(/^\s+/, "").replace(/\s+$/, "");
  1097             // Unescape the URI spec for showing as an entry in the popup
  1098             let url = Components.classes["@mozilla.org/intl/texttosuburi;1"].
  1099               getService(Components.interfaces.nsITextToSubURI).
  1100               unEscapeURIForUI("UTF-8", controller.getValueAt(this._currentIndex));
  1102             if (typeof this.input.trimValue == "function")
  1103               url = this.input.trimValue(url);
  1105             if (this._currentIndex < existingItemsCount) {
  1106               // re-use the existing item
  1107               item = this.richlistbox.childNodes[this._currentIndex];
  1109               // Completely re-use the existing richlistitem if it's the same
  1110               if (item.getAttribute("text") == trimmedSearchString &&
  1111                   item.getAttribute("url") == url) {
  1112                 item.collapsed = false;
  1113                 this._currentIndex++;
  1114                 continue;
  1117             else {
  1118               // need to create a new item
  1119               item = document.createElementNS("http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul", "richlistitem");
  1122             // set these attributes before we set the class
  1123             // so that we can use them from the constructor
  1124             item.setAttribute("image", controller.getImageAt(this._currentIndex));
  1125             item.setAttribute("url", url);
  1126             item.setAttribute("title", controller.getCommentAt(this._currentIndex));
  1127             item.setAttribute("type", controller.getStyleAt(this._currentIndex));
  1128             item.setAttribute("text", trimmedSearchString);
  1130             if (this._currentIndex < existingItemsCount) {
  1131               // re-use the existing item
  1132               item._adjustAcItem();
  1133               item.collapsed = false;
  1135             else {
  1136               // set the class at the end so we can use the attributes
  1137               // in the xbl constructor
  1138               item.className = "autocomplete-richlistitem";
  1139               this.richlistbox.appendChild(item);
  1142             this._currentIndex++;
  1145           // yield after each batch of items so that typing the url bar is responsive
  1146           setTimeout(function (self) { self._appendCurrentResult(); }, 0, this);
  1147         ]]>
  1148         </body>
  1149       </method>
  1151       <method name="selectBy">
  1152         <parameter name="aReverse"/>
  1153         <parameter name="aPage"/>
  1154         <body>
  1155           <![CDATA[
  1156           try {
  1157             var amount = aPage ? 5 : 1;
  1159             // because we collapsed unused items, we can't use this.richlistbox.getRowCount(), we need to use the matchCount
  1160             this.selectedIndex = this.getNextIndex(aReverse, amount, this.selectedIndex, this._matchCount - 1);
  1161             if (this.selectedIndex == -1) {
  1162               this.input._focus();
  1164           } catch (ex) {
  1165             // do nothing - occasionally timer-related js errors happen here
  1166             // e.g. "this.selectedIndex has no properties", when you type fast and hit a
  1167             // navigation key before this popup has opened
  1169             ]]>
  1170         </body>
  1171       </method>
  1173       <field name="richlistbox">
  1174         document.getAnonymousElementByAttribute(this, "anonid", "richlistbox");
  1175       </field>
  1177       <property name="view"
  1178                 onget="return this.mInput.controller;"
  1179                 onset="return val;"/>
  1181     </implementation>
  1182   </binding>
  1184   <binding id="autocomplete-richlistitem" extends="chrome://global/content/bindings/richlistbox.xml#richlistitem">
  1185     <content>
  1186       <xul:hbox align="center" class="ac-title-box">
  1187         <xul:image xbl:inherits="src=image" class="ac-site-icon"/>
  1188         <xul:hbox anonid="title-box" class="ac-title" flex="1"
  1189                   onunderflow="_doUnderflow('_title');"
  1190                   onoverflow="_doOverflow('_title');">
  1191           <xul:description anonid="title" class="ac-normal-text ac-comment" xbl:inherits="selected"/>
  1192         </xul:hbox>
  1193         <xul:label anonid="title-overflow-ellipsis" xbl:inherits="selected"
  1194                    class="ac-ellipsis-after ac-comment"/>
  1195         <xul:hbox anonid="extra-box" class="ac-extra" align="center" hidden="true">
  1196           <xul:image class="ac-result-type-tag"/>
  1197           <xul:label class="ac-normal-text ac-comment" xbl:inherits="selected" value=":"/>
  1198           <xul:description anonid="extra" class="ac-normal-text ac-comment" xbl:inherits="selected"/>
  1199         </xul:hbox>
  1200         <xul:image anonid="type-image" class="ac-type-icon"/>
  1201       </xul:hbox>
  1202       <xul:hbox align="center" class="ac-url-box">
  1203         <xul:spacer class="ac-site-icon"/>
  1204         <xul:image class="ac-action-icon"/>
  1205         <xul:hbox anonid="url-box" class="ac-url" flex="1"
  1206                   onunderflow="_doUnderflow('_url');"
  1207                   onoverflow="_doOverflow('_url');">
  1208           <xul:description anonid="url" class="ac-normal-text ac-url-text"
  1209                            xbl:inherits="selected type"/>
  1210           <xul:description anonid="action" class="ac-normal-text ac-action-text"
  1211                            xbl:inherits="selected type"/>
  1212         </xul:hbox>
  1213         <xul:label anonid="url-overflow-ellipsis" xbl:inherits="selected"
  1214                    class="ac-ellipsis-after ac-url-text"/>
  1215         <xul:spacer class="ac-type-icon"/>
  1216       </xul:hbox>
  1217     </content>
  1218     <implementation implements="nsIDOMXULSelectControlItemElement">
  1219       <constructor>
  1220         <![CDATA[
  1221             let ellipsis = "\u2026";
  1222             try {
  1223               ellipsis = Components.classes["@mozilla.org/preferences-service;1"].
  1224                 getService(Components.interfaces.nsIPrefBranch).
  1225                 getComplexValue("intl.ellipsis",
  1226                   Components.interfaces.nsIPrefLocalizedString).data;
  1227             } catch (ex) {
  1228               // Do nothing.. we already have a default
  1231             this._urlOverflowEllipsis = document.getAnonymousElementByAttribute(this, "anonid", "url-overflow-ellipsis");
  1232             this._titleOverflowEllipsis = document.getAnonymousElementByAttribute(this, "anonid", "title-overflow-ellipsis");
  1234             this._urlOverflowEllipsis.value = ellipsis;
  1235             this._titleOverflowEllipsis.value = ellipsis;
  1237             this._typeImage = document.getAnonymousElementByAttribute(this, "anonid", "type-image");
  1239             this._urlBox = document.getAnonymousElementByAttribute(this, "anonid", "url-box");
  1240             this._url = document.getAnonymousElementByAttribute(this, "anonid", "url");
  1241             this._action = document.getAnonymousElementByAttribute(this, "anonid", "action");
  1243             this._titleBox = document.getAnonymousElementByAttribute(this, "anonid", "title-box");
  1244             this._title = document.getAnonymousElementByAttribute(this, "anonid", "title");
  1246             this._extraBox = document.getAnonymousElementByAttribute(this, "anonid", "extra-box");
  1247             this._extra = document.getAnonymousElementByAttribute(this, "anonid", "extra");
  1249             this._adjustAcItem();
  1250           ]]>
  1251       </constructor>
  1253       <property name="label" readonly="true">
  1254         <getter>
  1255           <![CDATA[
  1256             var title = this.getAttribute("title");
  1257             var url = this.getAttribute("url");
  1258             var panel = this.parentNode.parentNode;
  1260             // allow consumers that have extended popups to override
  1261             // the label values for the richlistitems
  1262             if (panel.createResultLabel)
  1263               return panel.createResultLabel(title, url, this.getAttribute("type"));
  1265             // aType (ex: "ac-result-type-<aType>") is related to the class of the image,
  1266             // and is not "visible" text so don't use it for the label (for accessibility).
  1267             return title + " " + url;
  1268           ]]>
  1269         </getter>
  1270       </property>
  1272       <field name="_boundaryCutoff">null</field>
  1274       <property name="boundaryCutoff" readonly="true">
  1275         <getter>
  1276           <![CDATA[
  1277           if (!this._boundaryCutoff) {
  1278             this._boundaryCutoff =
  1279               Components.classes["@mozilla.org/preferences-service;1"].
  1280               getService(Components.interfaces.nsIPrefBranch).
  1281               getIntPref("toolkit.autocomplete.richBoundaryCutoff");
  1283           return this._boundaryCutoff;
  1284           ]]>
  1285         </getter>
  1286       </property>
  1288       <method name="_getBoundaryIndices">
  1289         <parameter name="aText"/>
  1290         <parameter name="aSearchTokens"/>
  1291         <body>
  1292           <![CDATA[
  1293           // Short circuit for empty search ([""] == "")
  1294           if (aSearchTokens == "")
  1295             return [0, aText.length];
  1297           // Find which regions of text match the search terms
  1298           let regions = [];
  1299           for each (let search in aSearchTokens) {
  1300             let matchIndex;
  1301             let startIndex = 0;
  1302             let searchLen = search.length;
  1304             // Find all matches of the search terms, but stop early for perf
  1305             let lowerText = aText.toLowerCase().substr(0, this.boundaryCutoff);
  1306             while ((matchIndex = lowerText.indexOf(search, startIndex)) >= 0) {
  1307               // Start the next search from where this one finished
  1308               startIndex = matchIndex + searchLen;
  1309               regions.push([matchIndex, startIndex]);
  1313           // Sort the regions by start position then end position
  1314           regions = regions.sort(function(a, b) let (start = a[0] - b[0])
  1315             start == 0 ? a[1] - b[1] : start);
  1317           // Generate the boundary indices from each region
  1318           let start = 0;
  1319           let end = 0;
  1320           let boundaries = [];
  1321           let len = regions.length;
  1322           for (let i = 0; i < len; i++) {
  1323             // We have a new boundary if the start of the next is past the end
  1324             let region = regions[i];
  1325             if (region[0] > end) {
  1326               // First index is the beginning of match
  1327               boundaries.push(start);
  1328               // Second index is the beginning of non-match
  1329               boundaries.push(end);
  1331               // Track the new region now that we've stored the previous one
  1332               start = region[0];
  1335             // Push back the end index for the current or new region
  1336             end = Math.max(end, region[1]);
  1339           // Add the last region
  1340           boundaries.push(start);
  1341           boundaries.push(end);
  1343           // Put on the end boundary if necessary
  1344           if (end < aText.length)
  1345             boundaries.push(aText.length);
  1347           // Skip the first item because it's always 0
  1348           return boundaries.slice(1);
  1349           ]]>
  1350         </body>
  1351       </method>
  1353       <method name="_getSearchTokens">
  1354         <parameter name="aSearch"/>
  1355         <body>
  1356           <![CDATA[
  1357           let search = aSearch.toLowerCase();
  1358           return search.split(/\s+/);
  1359           ]]>
  1360         </body>
  1361       </method>
  1363       <method name="_setUpDescription">
  1364         <parameter name="aDescriptionElement"/>
  1365         <parameter name="aText"/>
  1366         <parameter name="aNoEmphasis"/>
  1367         <body>
  1368           <![CDATA[
  1369           // Get rid of all previous text
  1370           while (aDescriptionElement.hasChildNodes())
  1371             aDescriptionElement.removeChild(aDescriptionElement.firstChild);
  1373           // If aNoEmphasis is specified, don't add any emphasis
  1374           if (aNoEmphasis) {
  1375             aDescriptionElement.appendChild(document.createTextNode(aText));
  1376             return;
  1379           // Get the indices that separate match and non-match text
  1380           let search = this.getAttribute("text");
  1381           let tokens = this._getSearchTokens(search);
  1382           let indices = this._getBoundaryIndices(aText, tokens);
  1384           let next;
  1385           let start = 0;
  1386           let len = indices.length;
  1387           // Even indexed boundaries are matches, so skip the 0th if it's empty
  1388           for (let i = indices[0] == 0 ? 1 : 0; i < len; i++) {
  1389             next = indices[i];
  1390             let text = aText.substr(start, next - start);
  1391             start = next;
  1393             if (i % 2 == 0) {
  1394               // Emphasize the text for even indices
  1395               let span = aDescriptionElement.appendChild(
  1396                 document.createElementNS("http://www.w3.org/1999/xhtml", "span"));
  1397               span.className = "ac-emphasize-text";
  1398               span.textContent = text;
  1399             } else {
  1400               // Otherwise, it's plain text
  1401               aDescriptionElement.appendChild(document.createTextNode(text));
  1404           ]]>
  1405         </body>
  1406       </method>
  1408       <method name="_adjustAcItem">
  1409         <body>
  1410           <![CDATA[
  1411           var url = this.getAttribute("url");
  1412           var title = this.getAttribute("title");
  1413           var type = this.getAttribute("type");
  1415           this.removeAttribute("actiontype");
  1417           // The ellipses are hidden via their visibility so that they always
  1418           // take up space and don't pop in on top of text when shown.  For
  1419           // keyword searches, however, the title ellipsis should not take up
  1420           // space when hidden.  Setting the hidden property accomplishes that.
  1421           this._titleOverflowEllipsis.hidden = false;
  1423           // If the type includes an action, set up the item appropriately.
  1424           var types = type.split(/\s+/);
  1425           var actionIndex = types.indexOf("action");
  1426           if (actionIndex >= 0) {
  1427             let [,action, param] = url.match(/^moz-action:([^,]+),(.*)$/);
  1428             this.setAttribute("actiontype", action);
  1429             url = param;
  1430             let desc = "]]>&action.switchToTab.label;<![CDATA[";
  1431             this._setUpDescription(this._action, desc, true);
  1433             // Remove the "action" substring so that the correct style, if any,
  1434             // is applied below.
  1435             types.splice(actionIndex, 1);
  1436             type = types.join(" ");
  1439           // If we have a tag match, show the tags and icon
  1440           if (type == "tag") {
  1441             // Configure the extra box for tags display
  1442             this._extraBox.hidden = false;
  1443             this._extraBox.childNodes[0].hidden = false;
  1444             this._extraBox.childNodes[1].hidden = true;
  1445             this._extraBox.pack = "end";
  1446             this._titleBox.flex = 1;
  1448             // The title is separated from the tags by an endash
  1449             let tags;
  1450             [, title, tags] = title.match(/^(.+) \u2013 (.+)$/);
  1452             // Each tag is split by a comma in an undefined order, so sort it
  1453             let sortedTags = tags.split(",").sort().join(", ");
  1455             // Emphasize the matching text in the tags
  1456             this._setUpDescription(this._extra, sortedTags);
  1458             // Treat tagged matches as bookmarks for the star
  1459             type = "bookmark";
  1460           } else if (type == "keyword") {
  1461             // Configure the extra box for keyword display
  1462             this._extraBox.hidden = false;
  1463             this._extraBox.childNodes[0].hidden = true;
  1464             this._extraBox.childNodes[1].hidden = false;
  1465             this._extraBox.pack = "start";
  1466             this._titleBox.flex = 0;
  1468             // Hide the ellipsis so it doesn't take up space.
  1469             this._titleOverflowEllipsis.hidden = true;
  1471             // Put the parameters next to the title if we have any
  1472             let search = this.getAttribute("text");
  1473             let params = "";
  1474             let paramsIndex = search.indexOf(' ');
  1475             if (paramsIndex != -1)
  1476               params = search.substr(paramsIndex + 1);
  1478             // Emphasize the keyword parameters
  1479             this._setUpDescription(this._extra, params);
  1481             // Don't emphasize keyword searches in the title or url
  1482             this.setAttribute("text", "");
  1483           } else {
  1484             // Hide the title's extra box if we don't need extra stuff
  1485             this._extraBox.hidden = true;
  1486             this._titleBox.flex = 1;
  1489           // Give the image the icon style and a special one for the type
  1490           this._typeImage.className = "ac-type-icon" +
  1491             (type ? " ac-result-type-" + type : "");
  1493           // Show the url as the title if we don't have a title
  1494           if (title == "")
  1495             title = url;
  1497           // Emphasize the matching search terms for the description
  1498           this._setUpDescription(this._title, title);
  1499           this._setUpDescription(this._url, url);
  1501           // Set up overflow on a timeout because the contents of the box
  1502           // might not have a width yet even though we just changed them
  1503           setTimeout(this._setUpOverflow, 0, this._titleBox, this._titleOverflowEllipsis);
  1504           setTimeout(this._setUpOverflow, 0, this._urlBox, this._urlOverflowEllipsis);
  1505           ]]>
  1506         </body>
  1507       </method>
  1509       <method name="_setUpOverflow">
  1510         <parameter name="aParentBox"/>
  1511         <parameter name="aEllipsis"/>
  1512         <body>
  1513           <![CDATA[
  1514           // Hide the ellipsis incase there's just enough to not underflow
  1515           aEllipsis.style.visibility = "hidden";
  1517           // Start with the parent's width and subtract off its children
  1518           let tooltip = [];
  1519           let children = aParentBox.childNodes;
  1520           let widthDiff = aParentBox.boxObject.width;
  1522           for (let i = 0; i < children.length; i++) {
  1523             // Only consider a child if it actually takes up space
  1524             let childWidth = children[i].boxObject.width;
  1525             if (childWidth > 0) {
  1526               // Subtract a little less to account for subpixel rounding
  1527               widthDiff -= childWidth - .5;
  1529               // Add to the tooltip if it's not hidden and has text
  1530               let childText = children[i].textContent;
  1531               if (childText)
  1532                 tooltip.push(childText);
  1536           // If the children take up more space than the parent.. overflow!
  1537           if (widthDiff < 0) {
  1538             // Re-show the ellipsis now that we know it's needed
  1539             aEllipsis.style.visibility = "visible";
  1541             // Separate text components with a ndash --
  1542             aParentBox.tooltipText = tooltip.join(" \u2013 ");
  1544           ]]>
  1545         </body>
  1546       </method>
  1548       <method name="_doUnderflow">
  1549         <parameter name="aName"/>
  1550         <body>
  1551           <![CDATA[
  1552           // Hide the ellipsis right when we know we're underflowing instead of
  1553           // waiting for the timeout to trigger the _setUpOverflow calculations
  1554           this[aName + "Box"].tooltipText = "";
  1555           this[aName + "OverflowEllipsis"].style.visibility = "hidden";
  1556           ]]>
  1557         </body>
  1558       </method>
  1560       <method name="_doOverflow">
  1561         <parameter name="aName"/>
  1562         <body>
  1563           <![CDATA[
  1564           this._setUpOverflow(this[aName + "Box"],
  1565                               this[aName + "OverflowEllipsis"]);
  1566           ]]>
  1567         </body>
  1568       </method>
  1570     </implementation>
  1571   </binding>
  1573   <binding id="autocomplete-tree" extends="chrome://global/content/bindings/tree.xml#tree">
  1574     <content>
  1575       <children includes="treecols"/>
  1576       <xul:treerows class="autocomplete-treerows tree-rows" xbl:inherits="hidescrollbar" flex="1">
  1577         <children/>
  1578       </xul:treerows>
  1579     </content>
  1580   </binding>
  1582   <binding id="autocomplete-richlistbox" extends="chrome://global/content/bindings/richlistbox.xml#richlistbox">
  1583     <implementation>
  1584       <field name="mLastMoveTime">Date.now()</field>
  1585     </implementation>
  1586     <handlers>
  1587       <handler event="mouseup">
  1588         <![CDATA[
  1589         // don't call onPopupClick for the scrollbar buttons, thumb, slider, etc.
  1590         var item = event.originalTarget;
  1592         while (item && item.localName != "richlistitem")
  1593           item = item.parentNode;
  1595         if (!item)
  1596           return;
  1598         this.parentNode.onPopupClick(event);
  1599       ]]>
  1600       </handler>
  1602       <handler event="mousemove">
  1603         <![CDATA[
  1604         if (Date.now() - this.mLastMoveTime > 30) {
  1605          var item = event.target;
  1607          while (item && item.localName != "richlistitem")
  1608            item = item.parentNode;
  1610          if (!item)
  1611            return;
  1613          var rc = this.getIndexOfItem(item);
  1614          if (rc != this.selectedIndex)
  1615             this.selectedIndex = rc;
  1617          this.mLastMoveTime = Date.now();
  1619       ]]>
  1620       </handler>
  1621     </handlers>
  1622   </binding>
  1624   <binding id="autocomplete-treebody">
  1625     <implementation>
  1626       <field name="mLastMoveTime">Date.now()</field>
  1627     </implementation>
  1629     <handlers>
  1630       <handler event="mouseup" action="this.parentNode.parentNode.onPopupClick(event);"/>
  1632       <handler event="mousedown"><![CDATA[
  1633          var rc = this.parentNode.treeBoxObject.getRowAt(event.clientX, event.clientY);
  1634          if (rc != this.parentNode.currentIndex)
  1635             this.parentNode.view.selection.select(rc);
  1636       ]]></handler>
  1638       <handler event="mousemove"><![CDATA[
  1639         if (Date.now() - this.mLastMoveTime > 30) {
  1640          var rc = this.parentNode.treeBoxObject.getRowAt(event.clientX, event.clientY);
  1641          if (rc != this.parentNode.currentIndex)
  1642             this.parentNode.view.selection.select(rc);
  1643          this.mLastMoveTime = Date.now();
  1645       ]]></handler>
  1646     </handlers>
  1647   </binding>
  1649   <binding id="autocomplete-treerows">
  1650     <content>
  1651       <xul:hbox flex="1" class="tree-bodybox">
  1652         <children/>
  1653       </xul:hbox>
  1654       <xul:scrollbar xbl:inherits="collapsed=hidescrollbar" orient="vertical" class="tree-scrollbar"/>
  1655     </content>
  1656   </binding>
  1658   <binding id="history-dropmarker" extends="chrome://global/content/bindings/general.xml#dropmarker">
  1659     <implementation>
  1660       <method name="showPopup">
  1661         <body><![CDATA[
  1662           var textbox = document.getBindingParent(this);
  1663           textbox.showHistoryPopup();
  1664         ]]></body>
  1665       </method>
  1666     </implementation>
  1668     <handlers>
  1669       <handler event="mousedown" button="0"><![CDATA[
  1670         this.showPopup();
  1671       ]]></handler>
  1672     </handlers>
  1673   </binding>
  1675 </bindings>

mercurial