toolkit/content/widgets/autocomplete.xml
author Matthew Noorenberghe <mozilla@noorenberghe.ca>
Fri, 08 Feb 2019 17:46:51 -0800
changeset 458980 047455dcfc2037b755cd2c23434f0b8767b7b352
parent 456527 8641112b948793530189b1b1fff705d145301f57
child 459039 d09c7dc93f20d0db02cfe7db965b178673f32687
permissions -rw-r--r--
Bug 1439023 - Don't reset autocomplete controller state when autofill fell back to form history. r=jaws Otherwise the cached value won't be used for filling by satchel and _fillFromAutocompleteRow won't fill a result that isn't a `autofill-profile` style: https://searchfox.org/mozilla-central/rev/03ebbdab952409640c6857d835d3040bf6f9e2db/browser/extensions/formautofill/FormAutofillContent.jsm#283,295 Differential Revision: https://phabricator.services.mozilla.com/D19249

<?xml version="1.0"?>
<!-- This Source Code Form is subject to the terms of the Mozilla Public
   - License, v. 2.0. If a copy of the MPL was not distributed with this
   - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->

<bindings id="autocompleteBindings"
          xmlns="http://www.mozilla.org/xbl"
          xmlns:html="http://www.w3.org/1999/xhtml"
          xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
          xmlns:xbl="http://www.mozilla.org/xbl">

  <binding id="autocomplete"
           extends="chrome://global/content/bindings/textbox.xml#textbox">
    <content sizetopopup="pref">
      <children includes="image|box"/>

      <xul:moz-input-box anonid="moz-input-box" flex="1">
        <children/>
        <html:input anonid="input" class="textbox-input"
                    allowevents="true"
                    autocomplete="off"
                    xbl:inherits="value,type=inputtype,maxlength,disabled,size,readonly,placeholder,tabindex,accesskey,mozactionhint"/>
      </xul:moz-input-box>
      <children includes="hbox"/>

      <xul:popupset anonid="popupset" class="autocomplete-result-popupset"/>
    </content>

    <implementation implements="nsIAutoCompleteInput, nsIDOMXULMenuListElement">
      <field name="mController">null</field>
      <field name="mSearchNames">null</field>
      <field name="mIgnoreInput">false</field>

      <field name="_searchBeginHandler">null</field>
      <field name="_searchCompleteHandler">null</field>
      <field name="_textEnteredHandler">null</field>
      <field name="_textRevertedHandler">null</field>

      <constructor><![CDATA[
        this.mController = Cc["@mozilla.org/autocomplete/controller;1"].
          getService(Ci.nsIAutoCompleteController);

        this._searchBeginHandler = this.initEventHandler("searchbegin");
        this._searchCompleteHandler = this.initEventHandler("searchcomplete");
        this._textEnteredHandler = this.initEventHandler("textentered");
        this._textRevertedHandler = this.initEventHandler("textreverted");

        // For security reasons delay searches on pasted values.
        this.inputField.controllers.insertControllerAt(0, this._pasteController);
      ]]></constructor>

      <destructor><![CDATA[
        this.inputField.controllers.removeController(this._pasteController);
      ]]></destructor>

      <!-- =================== nsIAutoCompleteInput =================== -->

      <field name="_popup">null</field>
      <property name="popup" readonly="true">
        <getter><![CDATA[
          // Memoize the result in a field rather than replacing this property,
          // so that it can be reset along with the binding.
          if (this._popup) {
            return this._popup;
          }

          let popup = null;
          let popupId = this.getAttribute("autocompletepopup");
          if (popupId) {
            popup = document.getElementById(popupId);
          }
          if (!popup) {
            popup = document.createXULElement("panel");
            popup.setAttribute("type", "autocomplete-richlistbox");
            popup.setAttribute("noautofocus", "true");

            let popupset = document.getAnonymousElementByAttribute(this, "anonid", "popupset");
            popupset.appendChild(popup);
          }
          popup.mInput = this;

          return this._popup = popup;
        ]]></getter>
      </property>

      <property name="controller" onget="return this.mController;" readonly="true"/>

      <property name="popupOpen"
                onget="return this.popup.popupOpen;"
                onset="if (val) this.openPopup(); else this.closePopup();"/>

      <property name="disableAutoComplete"
                onset="this.setAttribute('disableautocomplete', val); return val;"
                onget="return this.getAttribute('disableautocomplete') == 'true';"/>

      <property name="completeDefaultIndex"
                onset="this.setAttribute('completedefaultindex', val); return val;"
                onget="return this.getAttribute('completedefaultindex') == 'true';"/>

      <property name="completeSelectedIndex"
                onset="this.setAttribute('completeselectedindex', val); return val;"
                onget="return this.getAttribute('completeselectedindex') == 'true';"/>

      <property name="forceComplete"
                onset="this.setAttribute('forcecomplete', val); return val;"
                onget="return this.getAttribute('forcecomplete') == 'true';"/>

      <property name="minResultsForPopup"
                onset="this.setAttribute('minresultsforpopup', val); return val;"
                onget="var m = parseInt(this.getAttribute('minresultsforpopup')); return isNaN(m) ? 1 : m;"/>

      <property name="timeout"
                onset="this.setAttribute('timeout', val); return val;">
        <getter><![CDATA[
          // For security reasons delay searches on pasted values.
          if (this._valueIsPasted) {
            let t = parseInt(this.getAttribute("pastetimeout"));
            return isNaN(t) ? 1000 : t;
          }

          let t = parseInt(this.getAttribute("timeout"));
          return isNaN(t) ? 50 : t;
        ]]></getter>
      </property>

      <property name="searchParam"
                onget="return this.getAttribute('autocompletesearchparam') || '';"
                onset="this.setAttribute('autocompletesearchparam', val); return val;"/>

      <property name="searchCount" readonly="true"
                onget="this.initSearchNames(); return this.mSearchNames.length;"/>

      <property name="PrivateBrowsingUtils" readonly="true">
        <getter><![CDATA[
          let module = {};
          ChromeUtils.import("resource://gre/modules/PrivateBrowsingUtils.jsm", module);
          Object.defineProperty(this, "PrivateBrowsingUtils", {
            configurable: true,
            enumerable: true,
            writable: true,
            value: module.PrivateBrowsingUtils,
          });
          return module.PrivateBrowsingUtils;
        ]]></getter>
      </property>

      <property name="inPrivateContext" readonly="true"
                onget="return this.PrivateBrowsingUtils.isWindowPrivate(window);"/>

      <property name="noRollupOnCaretMove" readonly="true"
                onget="return this.popup.getAttribute('norolluponanchor') == 'true'"/>

      <!-- This is the maximum number of drop-down rows we get when we
            hit the drop marker beside fields that have it (like the URLbar).-->
      <field name="maxDropMarkerRows" readonly="true">14</field>

      <method name="getSearchAt">
        <parameter name="aIndex"/>
        <body><![CDATA[
          this.initSearchNames();
          return this.mSearchNames[aIndex];
        ]]></body>
      </method>

      <method name="setTextValueWithReason">
        <parameter name="aValue"/>
        <parameter name="aReason"/>
        <body><![CDATA[
          if (aReason == Ci.nsIAutoCompleteInput
                           .TEXTVALUE_REASON_COMPLETEDEFAULT) {
            this._textValueSetByCompleteDefault = true;
          }
          this.textValue = aValue;
          this._textValueSetByCompleteDefault = false;
        ]]></body>
      </method>

      <property name="textValue">
        <getter><![CDATA[
          if (typeof this.onBeforeTextValueGet == "function") {
            let result = this.onBeforeTextValueGet();
            if (result) {
              return result.value;
            }
          }
          return this.value;
        ]]></getter>
        <setter><![CDATA[
          if (typeof this.onBeforeTextValueSet == "function" &&
              !this._textValueSetByCompleteDefault) {
            val = this.onBeforeTextValueSet(val);
          }

          // "input" event is automatically dispatched by the editor if
          // necessary.
          this._setValueInternal(val, true);

          return this.value;
        ]]></setter>
      </property>

      <method name="selectTextRange">
        <parameter name="aStartIndex"/>
        <parameter name="aEndIndex"/>
        <body><![CDATA[
          this.inputField.setSelectionRange(aStartIndex, aEndIndex);
        ]]></body>
      </method>

      <method name="onSearchBegin">
        <body><![CDATA[
          if (this.popup && typeof this.popup.onSearchBegin == "function")
            this.popup.onSearchBegin();
          if (this._searchBeginHandler)
            this._searchBeginHandler();
        ]]></body>
      </method>

      <method name="onSearchComplete">
        <body><![CDATA[
          if (this.mController.matchCount == 0)
            this.setAttribute("nomatch", "true");
          else
            this.removeAttribute("nomatch");

          if (this.ignoreBlurWhileSearching && !this.focused) {
            this.handleEnter();
            this.detachController();
          }

          if (this._searchCompleteHandler)
            this._searchCompleteHandler();
        ]]></body>
      </method>

      <method name="onTextEntered">
        <parameter name="event"/>
        <body><![CDATA[
          let rv = false;
          if (this._textEnteredHandler) {
            rv = this._textEnteredHandler(event);
          }
          return rv;
        ]]></body>
      </method>

      <method name="onTextReverted">
        <body><![CDATA[
          if (this._textRevertedHandler)
            return this._textRevertedHandler();
          return false;
        ]]></body>
      </method>

      <!-- =================== nsIDOMXULMenuListElement =================== -->

      <property name="editable" readonly="true"
                onget="return true;" />

      <property name="crop"
                onset="this.setAttribute('crop',val); return val;"
                onget="return this.getAttribute('crop');"/>

      <property name="open"
                onget="return this.getAttribute('open') == 'true';">
        <setter><![CDATA[
          if (val)
            this.showHistoryPopup();
          else
            this.closePopup();
        ]]></setter>
      </property>

      <!-- =================== PUBLIC MEMBERS =================== -->

      <field name="valueIsTyped">false</field>
      <field name="_textValueSetByCompleteDefault">false</field>
      <property name="value"
                onset="return this._setValueInternal(val, false);">
        <getter><![CDATA[
          if (typeof this.onBeforeValueGet == "function") {
            var result = this.onBeforeValueGet();
            if (result)
              return result.value;
          }
          return this.inputField.value;
        ]]></getter>
      </property>

      <property name="focused" readonly="true"
                onget="return this.getAttribute('focused') == 'true';"/>

      <!-- maximum number of rows to display at a time -->
      <property name="maxRows"
                onset="this.setAttribute('maxrows', val); return val;"
                onget="return parseInt(this.getAttribute('maxrows')) || 0;"/>

      <!-- option to allow scrolling through the list via the tab key, rather than
           tab moving focus out of the textbox -->
      <property name="tabScrolling"
                onset="this.setAttribute('tabscrolling', val); return val;"
                onget="return this.getAttribute('tabscrolling') == 'true';"/>

      <!-- option to completely ignore any blur events while searches are
           still going on. -->
      <property name="ignoreBlurWhileSearching"
                onset="this.setAttribute('ignoreblurwhilesearching', val); return val;"
                onget="return this.getAttribute('ignoreblurwhilesearching') == 'true';"/>

      <!-- option to highlight entries that don't have any matches -->
      <property name="highlightNonMatches"
                onset="this.setAttribute('highlightnonmatches', val); return val;"
                onget="return this.getAttribute('highlightnonmatches') == 'true';"/>

      <!-- =================== PRIVATE MEMBERS =================== -->

      <!-- ::::::::::::: autocomplete controller ::::::::::::: -->

      <method name="attachController">
        <body><![CDATA[
          this.mController.input = this;
        ]]></body>
      </method>

      <method name="detachController">
        <body><![CDATA[
          if (this.mController.input == this)
            this.mController.input = null;
        ]]></body>
      </method>

      <!-- ::::::::::::: popup opening ::::::::::::: -->

      <method name="openPopup">
        <body><![CDATA[
          if (this.focused)
            this.popup.openAutocompletePopup(this, this);
        ]]></body>
      </method>

      <method name="closePopup">
        <body><![CDATA[
          this.popup.closePopup();
        ]]></body>
      </method>

      <method name="showHistoryPopup">
        <body><![CDATA[
          // Store our "normal" maxRows on the popup, so that it can reset the
          // value when the popup is hidden.
          this.popup._normalMaxRows = this.maxRows;

          // Increase our maxRows temporarily, since we want the dropdown to
          // be bigger in this case. The popup's popupshowing/popuphiding
          // handlers will take care of resetting this.
          this.maxRows = this.maxDropMarkerRows;

          // Ensure that we have focus.
          if (!this.focused)
            this.focus();
          this.attachController();
          this.mController.startSearch("");
        ]]></body>
      </method>

      <method name="toggleHistoryPopup">
        <body><![CDATA[
          if (!this.popup.popupOpen)
            this.showHistoryPopup();
          else
            this.closePopup();
        ]]></body>
      </method>

      <!-- ::::::::::::: event dispatching ::::::::::::: -->

      <method name="initEventHandler">
        <parameter name="aEventType"/>
        <body><![CDATA[
          let handlerString = this.getAttribute("on" + aEventType);
          if (handlerString) {
            return (new Function("eventType", "param", handlerString)).bind(this, aEventType);
          }
          return null;
        ]]></body>
      </method>

      <!-- ::::::::::::: key handling ::::::::::::: -->

      <field name="_selectionDetails">null</field>
      <method name="onKeyPress">
        <parameter name="aEvent"/>
        <body><![CDATA[
          return this.handleKeyPress(aEvent);
        ]]></body>
      </method>

      <method name="handleKeyPress">
        <parameter name="aEvent"/>
        <parameter name="aOptions"/>
        <body><![CDATA[
          if (aEvent.target.localName != "textbox")
            return true; // Let child buttons of autocomplete take input

          // Re: urlbarDeferred, see the comment in urlbarBindings.xml.
          if (aEvent.defaultPrevented && !aEvent.urlbarDeferred) {
            return false;
          }

          const isMac = /Mac/.test(navigator.platform);
          var cancel = false;

          // Catch any keys that could potentially move the caret. Ctrl can be
          // used in combination with these keys on Windows and Linux; and Alt
          // can be used on OS X, so make sure the unused one isn't used.
          let metaKey = isMac ? aEvent.ctrlKey : aEvent.altKey;
          if (!metaKey) {
            switch (aEvent.keyCode) {
              case KeyEvent.DOM_VK_LEFT:
              case KeyEvent.DOM_VK_RIGHT:
              case KeyEvent.DOM_VK_HOME:
                cancel = this.mController.handleKeyNavigation(aEvent.keyCode);
                break;
            }
          }

          // Handle keys that are not part of a keyboard shortcut (no Ctrl or Alt)
          if (!aEvent.ctrlKey && !aEvent.altKey) {
            switch (aEvent.keyCode) {
              case KeyEvent.DOM_VK_TAB:
                if (this.tabScrolling && this.popup.popupOpen)
                  cancel = this.mController.handleKeyNavigation(aEvent.shiftKey ?
                                                                KeyEvent.DOM_VK_UP :
                                                                KeyEvent.DOM_VK_DOWN);
                else if (this.forceComplete && this.mController.matchCount >= 1)
                  this.mController.handleTab();
                break;
              case KeyEvent.DOM_VK_UP:
              case KeyEvent.DOM_VK_DOWN:
              case KeyEvent.DOM_VK_PAGE_UP:
              case KeyEvent.DOM_VK_PAGE_DOWN:
                cancel = this.mController.handleKeyNavigation(aEvent.keyCode);
                break;
            }
          }

          // Handle readline/emacs-style navigation bindings on Mac.
          if (isMac &&
              this.popup.popupOpen &&
              aEvent.ctrlKey &&
              (aEvent.key === "n" || aEvent.key === "p")) {
            const effectiveKey = (aEvent.key === "p") ?
                                 KeyEvent.DOM_VK_UP :
                                 KeyEvent.DOM_VK_DOWN;
            cancel = this.mController.handleKeyNavigation(effectiveKey);
          }

          // Handle keys we know aren't part of a shortcut, even with Alt or
          // Ctrl.
          switch (aEvent.keyCode) {
            case KeyEvent.DOM_VK_ESCAPE:
              cancel = this.mController.handleEscape();
              break;
            case KeyEvent.DOM_VK_RETURN:
              if (isMac) {
                // Prevent the default action, since it will beep on Mac
                if (aEvent.metaKey)
                  aEvent.preventDefault();
              }
              if (this.popup.selectedIndex >= 0) {
                this._selectionDetails = {
                  index: this.popup.selectedIndex,
                  kind: "key",
                };
              }
              cancel = this.handleEnter(aEvent, aOptions);
              break;
            case KeyEvent.DOM_VK_DELETE:
              if (isMac && !aEvent.shiftKey) {
                break;
              }
              cancel = this.handleDelete();
              break;
            case KeyEvent.DOM_VK_BACK_SPACE:
              if (isMac && aEvent.shiftKey) {
                cancel = this.handleDelete();
              }
              break;
            case KeyEvent.DOM_VK_DOWN:
            case KeyEvent.DOM_VK_UP:
              if (aEvent.altKey)
                this.toggleHistoryPopup();
              break;
            case KeyEvent.DOM_VK_F4:
              if (!isMac) {
                this.toggleHistoryPopup();
              }
              break;
          }

          if (cancel) {
            aEvent.stopPropagation();
            aEvent.preventDefault();
          }

          return true;
        ]]></body>
      </method>

      <method name="handleEnter">
        <parameter name="event"/>
        <body><![CDATA[
          return this.mController.handleEnter(false, event || null);
        ]]></body>
      </method>

      <method name="handleDelete">
        <body><![CDATA[
          return this.mController.handleDelete();
        ]]></body>
      </method>

      <!-- ::::::::::::: miscellaneous ::::::::::::: -->

      <method name="initSearchNames">
        <body><![CDATA[
          if (!this.mSearchNames) {
            var names = this.getAttribute("autocompletesearch");
            if (!names)
              this.mSearchNames = [];
            else
              this.mSearchNames = names.split(" ");
          }
        ]]></body>
      </method>

      <method name="_focus">
        <!-- doesn't reset this.mController -->
        <body><![CDATA[
          this._dontBlur = true;
          this.focus();
          this._dontBlur = false;
        ]]></body>
      </method>

      <method name="resetActionType">
        <body><![CDATA[
          if (this.mIgnoreInput)
            return;
          this.removeAttribute("actiontype");
        ]]></body>
      </method>

      <field name="_valueIsPasted">false</field>
      <field name="_pasteController"><![CDATA[
        ({
          _autocomplete: this,
          _kGlobalClipboard: Ci.nsIClipboard.kGlobalClipboard,
          supportsCommand: aCommand => aCommand == "cmd_paste",
          doCommand(aCommand) {
            this._autocomplete._valueIsPasted = true;
            this._autocomplete.editor.paste(this._kGlobalClipboard);
            this._autocomplete._valueIsPasted = false;
          },
          isCommandEnabled(aCommand) {
            return this._autocomplete.editor.isSelectionEditable &&
                   this._autocomplete.editor.canPaste(this._kGlobalClipboard);
          },
          onEvent() {},
        })
      ]]></field>

      <method name="_setValueInternal">
        <parameter name="aValue"/>
        <parameter name="aIsUserInput"/>
        <body><![CDATA[
          this.mIgnoreInput = true;

          if (typeof this.onBeforeValueSet == "function")
            aValue = this.onBeforeValueSet(aValue);

          if (typeof this.trimValue == "function" &&
              !this._textValueSetByCompleteDefault)
            aValue = this.trimValue(aValue);

          this.valueIsTyped = false;
          if (aIsUserInput) {
            this.inputField.setUserInput(aValue);
          } else {
            this.inputField.value = aValue;
          }

          if (typeof this.formatValue == "function")
            this.formatValue();

          this.mIgnoreInput = false;
          var event = document.createEvent("Events");
          event.initEvent("ValueChange", true, true);
          this.inputField.dispatchEvent(event);
          return aValue;
        ]]></body>
      </method>

      <method name="onInput">
        <parameter name="aEvent"/>
        <body><![CDATA[
          if (!this.mIgnoreInput && this.mController.input == this) {
            this.valueIsTyped = true;
            this.mController.handleText();
          }
          this.resetActionType();
        ]]></body>
      </method>
    </implementation>

    <handlers>
      <handler event="input"><![CDATA[
        this.onInput(event);
      ]]></handler>

      <handler event="keypress" phase="capturing" group="system"
               action="return this.onKeyPress(event);"/>

      <handler event="compositionstart" phase="capturing"
               action="if (this.mController.input == this) this.mController.handleStartComposition();"/>

      <handler event="compositionend" phase="capturing"
               action="if (this.mController.input == this) this.mController.handleEndComposition();"/>

      <handler event="focus" phase="capturing"><![CDATA[
        this.attachController();
        if (window.gBrowser && window.gBrowser.selectedBrowser.hasAttribute("usercontextid")) {
          this.userContextId = parseInt(window.gBrowser.selectedBrowser.getAttribute("usercontextid"));
        } else {
          this.userContextId = 0;
        }
      ]]></handler>

      <handler event="blur" phase="capturing"><![CDATA[
        if (!this._dontBlur) {
          if (this.forceComplete && this.mController.matchCount >= 1) {
            // If forceComplete is requested, we need to call the enter processing
            // on blur so the input will be forced to the closest match.
            // Thunderbird is the only consumer of forceComplete and this is used
            // to force an recipient's email to the exact address book entry.
            this.mController.handleEnter(true);
          }
          if (!this.ignoreBlurWhileSearching)
            this.detachController();
        }
      ]]></handler>
    </handlers>
  </binding>

  <binding id="autocomplete-rich-result-popup" extends="chrome://global/content/bindings/popup.xml#popup">
    <content ignorekeys="true" level="top" consumeoutsideclicks="never">
      <xul:richlistbox anonid="richlistbox" class="autocomplete-richlistbox" flex="1"/>
      <xul:hbox>
        <children/>
      </xul:hbox>
    </content>

    <implementation implements="nsIAutoCompletePopup">
      <field name="mInput">null</field>
      <field name="mPopupOpen">false</field>
      <field name="_currentIndex">0</field>

      <constructor><![CDATA[
        if (!this.listEvents) {
          this.listEvents = {
            handleEvent: event => {
              if (!this.parentNode) {
                return;
              }

              switch (event.type) {
                case "mouseup":
                  // Don't call onPopupClick for the scrollbar buttons, thumb,
                  // slider, etc. If we hit the richlistbox and not a
                  // richlistitem, we ignore the event.
                  if (event.target.closest("richlistbox,richlistitem")
                                  .localName == "richlistitem") {
                    this.onPopupClick(event);
                  }
                  break;
                case "mousemove":
                  if (Date.now() - this.mLastMoveTime <= 30) {
                    return;
                  }

                  let item = event.target.closest("richlistbox,richlistitem");

                  // If we hit the richlistbox and not a richlistitem, we ignore
                  // the event.
                  if (item.localName == "richlistbox") {
                    return;
                  }

                  let index = this.richlistbox.getIndexOfItem(item);

                  this.mousedOverIndex = index;

                  if (item.selectedByMouseOver) {
                    this.richlistbox.selectedIndex = index;
                  }

                  this.mLastMoveTime = Date.now();
                  break;
              }
            },
          };
          this.richlistbox.addEventListener("mouseup", this.listEvents);
          this.richlistbox.addEventListener("mousemove", this.listEvents);
        }
      ]]></constructor>

      <destructor><![CDATA[
        if (this.listEvents) {
          this.richlistbox.removeEventListener("mouseup", this.listEvents);
          this.richlistbox.removeEventListener("mousemove", this.listEvents);
          delete this.listEvents;
        }
      ]]></destructor>

      <!-- =================== nsIAutoCompletePopup =================== -->

      <property name="input" readonly="true"
                onget="return this.mInput"/>

      <property name="overrideValue" readonly="true"
                onget="return null;"/>

      <property name="popupOpen" readonly="true"
                onget="return this.mPopupOpen;"/>

      <method name="closePopup">
        <body>
          <![CDATA[
          if (this.mPopupOpen) {
            this.hidePopup();
            this.removeAttribute("width");
          }
        ]]>
        </body>
      </method>

      <!-- This is the default number of rows that we give the autocomplete
           popup when the textbox doesn't have a "maxrows" attribute
           for us to use. -->
      <field name="defaultMaxRows" readonly="true">6</field>

      <!-- In some cases (e.g. when the input's dropmarker button is clicked),
           the input wants to display a popup with more rows. In that case, it
           should increase its maxRows property and store the "normal" maxRows
           in this field. When the popup is hidden, we restore the input's
           maxRows to the value stored in this field.

           This field is set to -1 between uses so that we can tell when it's
           been set by the input and when we need to set it in the popupshowing
           handler. -->
      <field name="_normalMaxRows">-1</field>

      <property name="maxRows" readonly="true">
        <getter>
          <![CDATA[
          return (this.mInput && this.mInput.maxRows) || this.defaultMaxRows;
        ]]>
        </getter>
      </property>

      <method name="getNextIndex">
        <parameter name="aReverse"/>
        <parameter name="aAmount"/>
        <parameter name="aIndex"/>
        <parameter name="aMaxRow"/>
        <body><![CDATA[
          if (aMaxRow < 0)
            return -1;

          var newIdx = aIndex + (aReverse ? -1 : 1) * aAmount;
          if (aReverse && aIndex == -1 || newIdx > aMaxRow && aIndex != aMaxRow)
            newIdx = aMaxRow;
          else if (!aReverse && aIndex == -1 || newIdx < 0 && aIndex != 0)
            newIdx = 0;

          if (newIdx < 0 && aIndex == 0 || newIdx > aMaxRow && aIndex == aMaxRow)
            aIndex = -1;
          else
            aIndex = newIdx;

          return aIndex;
        ]]></body>
      </method>

      <method name="onPopupClick">
        <parameter name="aEvent"/>
        <body><![CDATA[
          this.input.controller.handleEnter(true, aEvent);
        ]]></body>
      </method>

      <property name="selectedIndex"
                onget="return this.richlistbox.selectedIndex;">
        <setter>
          <![CDATA[
          if (val != this.richlistbox.selectedIndex) {
            this._previousSelectedIndex = this.richlistbox.selectedIndex;
          }
          this.richlistbox.selectedIndex = val;
          // Since ensureElementIsVisible may cause an expensive Layout flush,
          // invoke it only if there may be a scrollbar, so if we could fetch
          // more results than we can show at once.
          // maxResults is the maximum number of fetched results, maxRows is the
          // maximum number of rows we show at once, without a scrollbar.
          if (this.mPopupOpen && this.maxResults > this.maxRows) {
            // when clearing the selection (val == -1, so selectedItem will be
            // null), we want to scroll back to the top.  see bug #406194
            this.richlistbox.ensureElementIsVisible(
              this.richlistbox.selectedItem || this.richlistbox.firstElementChild);
          }
          return val;
        ]]>
        </setter>
      </property>

      <field name="_previousSelectedIndex">-1</field>
      <field name="mLastMoveTime">Date.now()</field>
      <field name="mousedOverIndex">-1</field>

      <method name="onSearchBegin">
        <body><![CDATA[
          this.mousedOverIndex = -1;

          if (typeof this._onSearchBegin == "function") {
            this._onSearchBegin();
          }
        ]]></body>
      </method>

      <method name="openAutocompletePopup">
        <parameter name="aInput"/>
        <parameter name="aElement"/>
        <body>
          <![CDATA[
          // until we have "baseBinding", (see bug #373652) this allows
          // us to override openAutocompletePopup(), but still call
          // the method on the base class
          this._openAutocompletePopup(aInput, aElement);
        ]]>
        </body>
      </method>

      <method name="_openAutocompletePopup">
        <parameter name="aInput"/>
        <parameter name="aElement"/>
        <body>
          <![CDATA[
          if (!this.mPopupOpen) {
            // It's possible that the panel is hidden initially
            // to avoid impacting startup / new window performance
            aInput.popup.hidden = false;

            this.mInput = aInput;
            // clear any previous selection, see bugs 400671 and 488357
            this.selectedIndex = -1;

            var width = aElement.getBoundingClientRect().width;
            this.setAttribute("width", width > 100 ? width : 100);
            // invalidate() depends on the width attribute
            this._invalidate();

            this.openPopup(aElement, "after_start", 0, 0, false, false);
          }
        ]]>
        </body>
      </method>

      <method name="invalidate">
        <parameter name="reason"/>
        <body>
          <![CDATA[
          // Don't bother doing work if we're not even showing
          if (!this.mPopupOpen)
            return;

          this._invalidate(reason);
          ]]>
        </body>
      </method>

      <method name="_invalidate">
        <parameter name="reason"/>
        <body>
          <![CDATA[
          // collapsed if no matches
          this.richlistbox.collapsed = (this.matchCount == 0);

          // Update the richlistbox height.
          if (this._adjustHeightRAFToken) {
            cancelAnimationFrame(this._adjustHeightRAFToken);
            this._adjustHeightRAFToken = null;
          }

          if (this.mPopupOpen) {
            delete this._adjustHeightOnPopupShown;
            this._adjustHeightRAFToken = requestAnimationFrame(() => this.adjustHeight());
          } else {
            this._adjustHeightOnPopupShown = true;
          }

          this._currentIndex = 0;
          if (this._appendResultTimeout) {
            clearTimeout(this._appendResultTimeout);
          }
          this._appendCurrentResult(reason);
        ]]>
        </body>
      </method>

      <property name="maxResults" readonly="true">
        <getter>
          <![CDATA[
            // This is how many richlistitems will be kept around.
            // Note, this getter may be overridden, or instances
            // can have the nomaxresults attribute set to have no
            // limit.
            if (this.getAttribute("nomaxresults") == "true") {
              return Infinity;
            }

            return 20;
          ]]>
        </getter>
      </property>

      <property name="matchCount" readonly="true">
        <getter>
          <![CDATA[
          return Math.min(this.mInput.controller.matchCount, this.maxResults);
          ]]>
        </getter>
      </property>

      <method name="_collapseUnusedItems">
        <body>
          <![CDATA[
            let existingItemsCount = this.richlistbox.children.length;
            for (let i = this.matchCount; i < existingItemsCount; ++i) {
              let item = this.richlistbox.children[i];

              item.collapsed = true;
              if (typeof item._onCollapse == "function") {
                item._onCollapse();
              }
            }
          ]]>
        </body>
      </method>

      <method name="adjustHeight">
        <body>
          <![CDATA[
          // Figure out how many rows to show
          let rows = this.richlistbox.children;
          let numRows = Math.min(this.matchCount, this.maxRows, rows.length);

          // Default the height to 0 if we have no rows to show
          let height = 0;
          if (numRows) {
            let firstRowRect = rows[0].getBoundingClientRect();
            if (this._rlbPadding == undefined) {
              let style = window.getComputedStyle(this.richlistbox);
              let paddingTop = parseInt(style.paddingTop) || 0;
              let paddingBottom = parseInt(style.paddingBottom) || 0;
              this._rlbPadding = paddingTop + paddingBottom;
            }

            // The class `forceHandleUnderflow` is for the item might need to
            // handle OverUnderflow or Overflow when the height of an item will
            // be changed dynamically.
            for (let i = 0; i < numRows; i++) {
              if (rows[i].classList.contains("forceHandleUnderflow")) {
                rows[i].handleOverUnderflow();
              }
            }

            let lastRowRect = rows[numRows - 1].getBoundingClientRect();
            // Calculate the height to have the first row to last row shown
            height = lastRowRect.bottom - firstRowRect.top +
                     this._rlbPadding;
          }

          let currentHeight = this.richlistbox.getBoundingClientRect().height;
          if (height <= currentHeight) {
            this._collapseUnusedItems();
          }
          this.richlistbox.style.removeProperty("height");
          // We need to get the ceiling of the calculated value to ensure that the box fully contains
          // all of its contents and doesn't cause a scrollbar since nsIBoxObject only expects a
          // `long`. e.g. if `height` is 99.5 the richlistbox would render at height 99px with a
          // scrollbar for the extra 0.5px.
          this.richlistbox.height = Math.ceil(height);
          ]]>
        </body>
      </method>

      <method name="_appendCurrentResult">
        <parameter name="invalidateReason"/>
        <body>
          <![CDATA[
          var controller = this.mInput.controller;
          var matchCount = this.matchCount;
          var existingItemsCount = this.richlistbox.children.length;

          // Process maxRows per chunk to improve performance and user experience
          for (let i = 0; i < this.maxRows; i++) {
            if (this._currentIndex >= matchCount) {
              break;
            }
            let item;
            let itemExists = this._currentIndex < existingItemsCount;

            let originalValue, originalText, originalType;
            let style = controller.getStyleAt(this._currentIndex);
            let value =
              style && style.includes("autofill") ?
              controller.getFinalCompleteValueAt(this._currentIndex) :
              controller.getValueAt(this._currentIndex);
            let label = controller.getLabelAt(this._currentIndex);
            let comment = controller.getCommentAt(this._currentIndex);
            let image = controller.getImageAt(this._currentIndex);
            // trim the leading/trailing whitespace
            let trimmedSearchString = controller.searchString.replace(/^\s+/, "").replace(/\s+$/, "");

            let reusable = false;
            if (itemExists) {
              item = this.richlistbox.children[this._currentIndex];

              // Url may be a modified version of value, see _adjustAcItem().
              originalValue = item.getAttribute("url") || item.getAttribute("ac-value");
              originalText = item.getAttribute("ac-text");
              originalType = item.getAttribute("originaltype");

              // The styles on the list which have different <content> structure and overrided
              // _adjustAcItem() are unreusable.
              const UNREUSEABLE_STYLES = [
                "autofill-profile",
                "autofill-footer",
                "autofill-clear-button",
                "autofill-insecureWarning",
              ];
              // Reuse the item when its style is exactly equal to the previous style or
              // neither of their style are in the UNREUSEABLE_STYLES.
              reusable = originalType === style ||
                !(UNREUSEABLE_STYLES.includes(style) || UNREUSEABLE_STYLES.includes(originalType));
            }

            // If no reusable item available, then create a new item.
            if (!reusable) {
              let options = null;
              switch (style) {
                case "autofill-profile":
                case "autofill-footer":
                case "autofill-clear-button":
                case "autofill-insecureWarning":
                  // implemented via XBL bindings, no CE for them
                  break;
                case "insecureWarning":
                  options = { is: "autocomplete-richlistitem-insecure-warning" };
                  break;
                default:
                  options = { is: "autocomplete-richlistitem" };
              }
              item = document.createXULElement("richlistitem", options);
              item.className = "autocomplete-richlistitem";
            }

            item.setAttribute("dir", this.style.direction);
            item.setAttribute("ac-image", image);
            item.setAttribute("ac-value", value);
            item.setAttribute("ac-label", label);
            item.setAttribute("ac-comment", comment);
            item.setAttribute("ac-text", trimmedSearchString);

            // Completely reuse the existing richlistitem for invalidation
            // due to new results, but only when: the item is the same, *OR*
            // we are about to replace the currently moused-over item, to
            // avoid surprising the user.
            let iface = Ci.nsIAutoCompletePopup;
            if (reusable &&
                originalText == trimmedSearchString &&
                invalidateReason == iface.INVALIDATE_REASON_NEW_RESULT &&
                (originalValue == value ||
                 this.mousedOverIndex === this._currentIndex)) {
              // try to re-use the existing item
              let reused = item._reuseAcItem();
              if (reused) {
                this._currentIndex++;
                continue;
              }
            } else {
              if (typeof item._cleanup == "function") {
                item._cleanup();
              }
              item.setAttribute("originaltype", style);
            }

            if (reusable) {
              // Adjust only when the result's type is reusable for existing
              // item's. Otherwise, we might insensibly call old _adjustAcItem()
              // as new binding has not been attached yet.
              // We don't need to worry about switching to new binding, since
              // _adjustAcItem() will fired by its own constructor accordingly.
              item._adjustAcItem();
              item.collapsed = false;
            } else if (itemExists) {
              let oldItem = this.richlistbox.children[this._currentIndex];
              this.richlistbox.replaceChild(item, oldItem);
            } else {
              this.richlistbox.appendChild(item);
            }

            this._currentIndex++;
          }

          if (typeof this.onResultsAdded == "function") {
            // The items bindings may not be attached yet, so we must delay this
            // before we can properly handle items properly without breaking
            // the richlistbox.
            Services.tm.dispatchToMainThread(() => this.onResultsAdded());
          }

          if (this._currentIndex < matchCount) {
            // yield after each batch of items so that typing the url bar is
            // responsive
            this._appendResultTimeout = setTimeout(() => this._appendCurrentResult(), 0);
          }
        ]]>
        </body>
      </method>

      <property name="overflowPadding"
                onget="return Number(this.getAttribute('overflowpadding'))"
                readonly="true" />

      <method name="selectBy">
        <parameter name="aReverse"/>
        <parameter name="aPage"/>
        <body>
          <![CDATA[
          try {
            var amount = aPage ? 5 : 1;

            // because we collapsed unused items, we can't use this.richlistbox.getRowCount(), we need to use the matchCount
            this.selectedIndex = this.getNextIndex(aReverse, amount, this.selectedIndex, this.matchCount - 1);
            if (this.selectedIndex == -1) {
              this.input._focus();
            }
          } catch (ex) {
            // do nothing - occasionally timer-related js errors happen here
            // e.g. "this.selectedIndex has no properties", when you type fast and hit a
            // navigation key before this popup has opened
          }
            ]]>
        </body>
      </method>

      <field name="richlistbox">
        document.getAnonymousElementByAttribute(this, "anonid", "richlistbox");
      </field>

      <property name="view"
                onget="return this.mInput.controller;"
                onset="return val;"/>

    </implementation>
    <handlers>
      <handler event="popupshowing"><![CDATA[
        // If normalMaxRows wasn't already set by the input, then set it here
        // so that we restore the correct number when the popup is hidden.

        // Null-check this.mInput; see bug 1017914
        if (this._normalMaxRows < 0 && this.mInput) {
          this._normalMaxRows = this.mInput.maxRows;
        }

        // Set an attribute for styling the popup based on the input.
        let inputID = "";
        if (this.mInput && this.mInput.ownerDocument &&
            this.mInput.ownerDocument.documentURIObject.schemeIs("chrome")) {
          inputID = this.mInput.id;
          // Take care of elements with no id that are inside xbl bindings
          if (!inputID) {
            let bindingParent = this.mInput.ownerDocument.getBindingParent(this.mInput);
            if (bindingParent) {
              inputID = bindingParent.id;
            }
          }
        }
        this.setAttribute("autocompleteinput", inputID);

        this.mPopupOpen = true;
      ]]></handler>

      <handler event="popupshown">
        <![CDATA[
          if (this._adjustHeightOnPopupShown) {
            delete this._adjustHeightOnPopupShown;
            this.adjustHeight();
          }
      ]]>
      </handler>

      <handler event="popuphiding"><![CDATA[
        var isListActive = true;
        if (this.selectedIndex == -1)
          isListActive = false;
        this.input.controller.stopSearch();

        this.removeAttribute("autocompleteinput");
        this.mPopupOpen = false;

        // Reset the maxRows property to the cached "normal" value (if there's
        // any), and reset normalMaxRows so that we can detect whether it was set
        // by the input when the popupshowing handler runs.

        // Null-check this.mInput; see bug 1017914
        if (this.mInput && this._normalMaxRows > 0) {
          this.mInput.maxRows = this._normalMaxRows;
        }
        this._normalMaxRows = -1;
        // If the list was being navigated and then closed, make sure
        // we fire accessible focus event back to textbox

        // Null-check this.mInput; see bug 1017914
        if (isListActive && this.mInput) {
          this.mInput.mIgnoreFocus = true;
          this.mInput._focus();
          this.mInput.mIgnoreFocus = false;
        }
      ]]></handler>
    </handlers>
  </binding>
</bindings>