summaryrefslogtreecommitdiffstats
path: root/toolkit/content/widgets/autocomplete.xml
diff options
context:
space:
mode:
Diffstat (limited to 'toolkit/content/widgets/autocomplete.xml')
-rw-r--r--toolkit/content/widgets/autocomplete.xml2515
1 files changed, 2515 insertions, 0 deletions
diff --git a/toolkit/content/widgets/autocomplete.xml b/toolkit/content/widgets/autocomplete.xml
new file mode 100644
index 000000000..da2bf678d
--- /dev/null
+++ b/toolkit/content/widgets/autocomplete.xml
@@ -0,0 +1,2515 @@
+<?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" role="xul:combobox"
+ extends="chrome://global/content/bindings/textbox.xml#textbox">
+ <resources>
+ <stylesheet src="chrome://global/content/autocomplete.css"/>
+ <stylesheet src="chrome://global/skin/autocomplete.css"/>
+ </resources>
+
+ <content sizetopopup="pref">
+ <xul:hbox class="autocomplete-textbox-container" flex="1" xbl:inherits="focused">
+ <children includes="image|deck|stack|box">
+ <xul:image class="autocomplete-icon" allowevents="true"/>
+ </children>
+
+ <xul:hbox anonid="textbox-input-box" class="textbox-input-box" flex="1" xbl:inherits="tooltiptext=inputtooltiptext">
+ <children/>
+ <html:input anonid="input" class="autocomplete-textbox textbox-input"
+ allowevents="true"
+ xbl:inherits="tooltiptext=inputtooltiptext,value,type=inputtype,maxlength,disabled,size,readonly,placeholder,tabindex,accesskey,mozactionhint"/>
+ </xul:hbox>
+ <children includes="hbox"/>
+ </xul:hbox>
+
+ <xul:dropmarker anonid="historydropmarker" class="autocomplete-history-dropmarker"
+ allowevents="true"
+ xbl:inherits="open,enablehistory,parentfocused=focused"/>
+
+ <xul:popupset anonid="popupset" class="autocomplete-result-popupset"/>
+
+ <children includes="toolbarbutton"/>
+ </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 = Components.classes["@mozilla.org/autocomplete/controller;1"].
+ getService(Components.interfaces.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.createElement("panel");
+ popup.setAttribute("type", "autocomplete");
+ 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="showCommentColumn"
+ onset="this.setAttribute('showcommentcolumn', val); return val;"
+ onget="return this.getAttribute('showcommentcolumn') == 'true';"/>
+
+ <property name="showImageColumn"
+ onset="this.setAttribute('showimagecolumn', val); return val;"
+ onget="return this.getAttribute('showimagecolumn') == 'true';"/>
+
+ <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;"/>
+
+ <field name="shrinkDelay" readonly="true">
+ parseInt(this.getAttribute("shrinkdelay")) || 0
+ </field>
+
+ <property name="PrivateBrowsingUtils" readonly="true">
+ <getter><![CDATA[
+ let module = {};
+ Components.utils.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 == Components.interfaces.nsIAutoCompleteInput
+ .TEXTVALUE_REASON_COMPLETEDEFAULT) {
+ this._disableTrim = true;
+ }
+ this.textValue = aValue;
+ this._disableTrim = 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")
+ val = this.onBeforeTextValueSet(val);
+
+ this.value = val;
+
+ // Completing a result should simulate the user typing the result, so
+ // fire an input event.
+ let evt = document.createEvent("UIEvents");
+ evt.initUIEvent("input", true, false, window, 0);
+ this.mIgnoreInput = true;
+ this.dispatchEvent(evt);
+ this.mIgnoreInput = false;
+
+ 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="_disableTrim">false</field>
+ <property name="value">
+ <getter><![CDATA[
+ if (typeof this.onBeforeValueGet == "function") {
+ var result = this.onBeforeValueGet();
+ if (result)
+ return result.value;
+ }
+ return this.inputField.value;
+ ]]></getter>
+ <setter><![CDATA[
+ this.mIgnoreInput = true;
+
+ if (typeof this.onBeforeValueSet == "function")
+ val = this.onBeforeValueSet(val);
+
+ if (typeof this.trimValue == "function" && !this._disableTrim)
+ val = this.trimValue(val);
+
+ this.valueIsTyped = false;
+ this.inputField.value = val;
+
+ 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 val;
+ ]]></setter>
+ </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';"/>
+
+ <!-- disable key navigation handling in the popup results -->
+ <property name="disableKeyNavigation"
+ onset="this.setAttribute('disablekeynavigation', val); return val;"
+ onget="return this.getAttribute('disablekeynavigation') == '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[
+ // history dropmarker pushed state
+ function cleanup(popup) {
+ popup.removeEventListener("popupshowing", onShow, false);
+ }
+ function onShow(event) {
+ var popup = event.target, input = popup.input;
+ cleanup(popup);
+ input.setAttribute("open", "true");
+ function onHide() {
+ input.removeAttribute("open");
+ popup.removeEventListener("popuphiding", onHide, false);
+ }
+ popup.addEventListener("popuphiding", onHide, false);
+ }
+ this.popup.addEventListener("popupshowing", onShow, false);
+ setTimeout(cleanup, 1000, this.popup);
+
+ // 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 method is called on the same event tick as the popup gets
+ // hidden, do nothing to avoid re-opening the popup when the drop
+ // marker is clicked while the popup is still open.
+ if (!this.popup.isPopupHidingTick && !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"/>
+ <body><![CDATA[
+ if (aEvent.target.localName != "textbox")
+ return true; // Let child buttons of autocomplete take input
+
+ // XXXpch this is so bogus...
+ if (aEvent.defaultPrevented)
+ return false;
+
+ 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 = /Mac/.test(navigator.platform) ? aEvent.ctrlKey : aEvent.altKey;
+ if (!this.disableKeyNavigation && !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 (!this.disableKeyNavigation && !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 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 (/Mac/.test(navigator.platform)) {
+ // Prevent the default action, since it will beep on Mac
+ if (aEvent.metaKey)
+ aEvent.preventDefault();
+ }
+ if (this.mController.selection) {
+ this._selectionDetails = {
+ index: this.mController.selection.currentIndex,
+ kind: "key"
+ };
+ }
+ cancel = this.handleEnter(aEvent);
+ break;
+ case KeyEvent.DOM_VK_DELETE:
+ if (/Mac/.test(navigator.platform) && !aEvent.shiftKey) {
+ break;
+ }
+ cancel = this.handleDelete();
+ break;
+ case KeyEvent.DOM_VK_BACK_SPACE:
+ if (/Mac/.test(navigator.platform) && 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 (!/Mac/.test(navigator.platform)) {
+ 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: Components.interfaces.nsIClipboard.kGlobalClipboard,
+ supportsCommand: aCommand => aCommand == "cmd_paste",
+ doCommand: function(aCommand) {
+ this._autocomplete._valueIsPasted = true;
+ this._autocomplete.editor.paste(this._kGlobalClipboard);
+ this._autocomplete._valueIsPasted = false;
+ },
+ isCommandEnabled: function(aCommand) {
+ return this._autocomplete.editor.isSelectionEditable &&
+ this._autocomplete.editor.canPaste(this._kGlobalClipboard);
+ },
+ onEvent: function() {}
+ })
+ ]]></field>
+
+ <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"
+ 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) {
+ // mousemove sets selected index. Don't blindly use that selected
+ // index in this blur handler since if the popup is open you can
+ // easily "select" another match just by moving the mouse over it.
+ let filledVal = this.value.replace(/.+ >> /, "").toLowerCase();
+ let selectedVal = null;
+ if (this.popup.selectedIndex >= 0) {
+ selectedVal = this.mController.getFinalCompleteValueAt(
+ this.popup.selectedIndex);
+ }
+ if (selectedVal && filledVal != selectedVal.toLowerCase()) {
+ for (let i = 0; i < this.mController.matchCount; i++) {
+ let matchVal = this.mController.getFinalCompleteValueAt(i);
+ if (matchVal.toLowerCase() == filledVal) {
+ this.popup.selectedIndex = i;
+ break;
+ }
+ }
+ }
+ this.mController.handleEnter(false);
+ }
+ if (!this.ignoreBlurWhileSearching)
+ this.detachController();
+ }
+ ]]></handler>
+ </handlers>
+ </binding>
+
+ <binding id="autocomplete-result-popup" extends="chrome://global/content/bindings/autocomplete.xml#autocomplete-base-popup">
+ <resources>
+ <stylesheet src="chrome://global/content/autocomplete.css"/>
+ <stylesheet src="chrome://global/skin/tree.css"/>
+ <stylesheet src="chrome://global/skin/autocomplete.css"/>
+ </resources>
+
+ <content ignorekeys="true" level="top" consumeoutsideclicks="never">
+ <xul:tree anonid="tree" class="autocomplete-tree plain" hidecolumnpicker="true" flex="1" seltype="single">
+ <xul:treecols anonid="treecols">
+ <xul:treecol id="treecolAutoCompleteValue" class="autocomplete-treecol" flex="1" overflow="true"/>
+ </xul:treecols>
+ <xul:treechildren class="autocomplete-treebody"/>
+ </xul:tree>
+ </content>
+
+ <implementation>
+ <field name="mShowCommentColumn">false</field>
+ <field name="mShowImageColumn">false</field>
+
+ <property name="showCommentColumn"
+ onget="return this.mShowCommentColumn;">
+ <setter>
+ <![CDATA[
+ if (!val && this.mShowCommentColumn) {
+ // reset the flex on the value column and remove the comment column
+ document.getElementById("treecolAutoCompleteValue").setAttribute("flex", 1);
+ this.removeColumn("treecolAutoCompleteComment");
+ } else if (val && !this.mShowCommentColumn) {
+ // reset the flex on the value column and add the comment column
+ document.getElementById("treecolAutoCompleteValue").setAttribute("flex", 2);
+ this.addColumn({id: "treecolAutoCompleteComment", flex: 1});
+ }
+ this.mShowCommentColumn = val;
+ return val;
+ ]]>
+ </setter>
+ </property>
+
+ <property name="showImageColumn"
+ onget="return this.mShowImageColumn;">
+ <setter>
+ <![CDATA[
+ if (!val && this.mShowImageColumn) {
+ // remove the image column
+ this.removeColumn("treecolAutoCompleteImage");
+ } else if (val && !this.mShowImageColumn) {
+ // add the image column
+ this.addColumn({id: "treecolAutoCompleteImage", flex: 1});
+ }
+ this.mShowImageColumn = val;
+ return val;
+ ]]>
+ </setter>
+ </property>
+
+
+ <method name="addColumn">
+ <parameter name="aAttrs"/>
+ <body>
+ <![CDATA[
+ var col = document.createElement("treecol");
+ col.setAttribute("class", "autocomplete-treecol");
+ for (var name in aAttrs)
+ col.setAttribute(name, aAttrs[name]);
+ this.treecols.appendChild(col);
+ return col;
+ ]]>
+ </body>
+ </method>
+
+ <method name="removeColumn">
+ <parameter name="aColId"/>
+ <body>
+ <![CDATA[
+ return this.treecols.removeChild(document.getElementById(aColId));
+ ]]>
+ </body>
+ </method>
+
+ <property name="selectedIndex"
+ onget="return this.tree.currentIndex;">
+ <setter>
+ <![CDATA[
+ this.tree.view.selection.select(val);
+ if (this.tree.treeBoxObject.height > 0)
+ this.tree.treeBoxObject.ensureRowIsVisible(val < 0 ? 0 : val);
+ // Fire select event on xul:tree so that accessibility API
+ // support layer can fire appropriate accessibility events.
+ var event = document.createEvent('Events');
+ event.initEvent("select", true, true);
+ this.tree.dispatchEvent(event);
+ return val;
+ ]]></setter>
+ </property>
+
+ <method name="adjustHeight">
+ <body>
+ <![CDATA[
+ // detect the desired height of the tree
+ var bx = this.tree.treeBoxObject;
+ var view = this.tree.view;
+ if (!view)
+ return;
+ var rows = this.maxRows;
+ if (!view.rowCount || (rows && view.rowCount < rows))
+ rows = view.rowCount;
+
+ var height = rows * bx.rowHeight;
+
+ if (height == 0) {
+ this.tree.setAttribute("collapsed", "true");
+ } else {
+ if (this.tree.hasAttribute("collapsed"))
+ this.tree.removeAttribute("collapsed");
+
+ this.tree.setAttribute("height", height);
+ }
+ this.tree.setAttribute("hidescrollbar", view.rowCount <= rows);
+ ]]>
+ </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) {
+ this.mInput = aInput;
+ this.view = aInput.controller.QueryInterface(Components.interfaces.nsITreeView);
+ this.invalidate();
+
+ this.showCommentColumn = this.mInput.showCommentColumn;
+ this.showImageColumn = this.mInput.showImageColumn;
+
+ var rect = aElement.getBoundingClientRect();
+ var nav = aElement.ownerDocument.defaultView.QueryInterface(Components.interfaces.nsIInterfaceRequestor)
+ .getInterface(Components.interfaces.nsIWebNavigation);
+ var docShell = nav.QueryInterface(Components.interfaces.nsIDocShell);
+ var docViewer = docShell.contentViewer;
+ var width = (rect.right - rect.left) * docViewer.fullZoom;
+ this.setAttribute("width", width > 100 ? width : 100);
+
+ // Adjust the direction of the autocomplete popup list based on the textbox direction, bug 649840
+ var popupDirection = aElement.ownerDocument.defaultView.getComputedStyle(aElement).direction;
+ this.style.direction = popupDirection;
+
+ this.openPopup(aElement, "after_start", 0, 0, false, false);
+ }
+ ]]></body>
+ </method>
+
+ <method name="invalidate">
+ <body><![CDATA[
+ this.adjustHeight();
+ this.tree.treeBoxObject.invalidate();
+ ]]></body>
+ </method>
+
+ <method name="selectBy">
+ <parameter name="aReverse"/>
+ <parameter name="aPage"/>
+ <body><![CDATA[
+ try {
+ var amount = aPage ? 5 : 1;
+ this.selectedIndex = this.getNextIndex(aReverse, amount, this.selectedIndex, this.tree.view.rowCount-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>
+
+ <!-- =================== PUBLIC MEMBERS =================== -->
+
+ <field name="tree">
+ document.getAnonymousElementByAttribute(this, "anonid", "tree");
+ </field>
+
+ <field name="treecols">
+ document.getAnonymousElementByAttribute(this, "anonid", "treecols");
+ </field>
+
+ <property name="view"
+ onget="return this.mView;">
+ <setter><![CDATA[
+ // We must do this by hand because the tree binding may not be ready yet
+ this.mView = val;
+ this.tree.boxObject.view = val;
+ ]]></setter>
+ </property>
+
+ </implementation>
+ </binding>
+
+ <binding id="autocomplete-base-popup" role="none"
+extends="chrome://global/content/bindings/popup.xml#popup">
+ <implementation implements="nsIAutoCompletePopup">
+ <field name="mInput">null</field>
+ <field name="mPopupOpen">false</field>
+ <field name="mIsPopupHidingTick">false</field>
+
+ <!-- =================== 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;"/>
+
+ <property name="isPopupHidingTick" readonly="true"
+ onget="return this.mIsPopupHidingTick;"/>
+
+ <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[
+ var controller = this.view.QueryInterface(Components.interfaces.nsIAutoCompleteController);
+ controller.handleEnter(true, aEvent);
+ ]]></body>
+ </method>
+ </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="popuphiding"><![CDATA[
+ var isListActive = true;
+ if (this.selectedIndex == -1)
+ isListActive = false;
+ var controller = this.view.QueryInterface(Components.interfaces.nsIAutoCompleteController);
+ controller.stopSearch();
+
+ this.removeAttribute("autocompleteinput");
+ this.mPopupOpen = false;
+
+ // Prevent opening popup from historydropmarker mousedown handler
+ // on the same event tick the popup is hidden by the same mousedown
+ // event.
+ this.mIsPopupHidingTick = true;
+ setTimeout(() => {
+ this.mIsPopupHidingTick = false;
+ }, 0);
+
+ // Reset the maxRows property to the cached "normal" value, 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.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>
+
+ <binding id="autocomplete-rich-result-popup" extends="chrome://global/content/bindings/autocomplete.xml#autocomplete-base-popup">
+ <resources>
+ <stylesheet src="chrome://global/content/autocomplete.css"/>
+ <stylesheet src="chrome://global/skin/autocomplete.css"/>
+ </resources>
+
+ <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="_currentIndex">0</field>
+ <field name="_rlbAnimated">false</field>
+
+ <!-- =================== nsIAutoCompletePopup =================== -->
+
+ <property name="selectedIndex"
+ onget="return this.richlistbox.selectedIndex;">
+ <setter>
+ <![CDATA[
+ this.richlistbox.selectedIndex = val;
+
+ // 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.firstChild);
+
+ return val;
+ ]]>
+ </setter>
+ </property>
+
+ <method name="onSearchBegin">
+ <body><![CDATA[
+ this.richlistbox.mouseSelectedIndex = -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._adjustHeightTimeout) {
+ clearTimeout(this._adjustHeightTimeout);
+ }
+ if (this._shrinkTimeout) {
+ clearTimeout(this._shrinkTimeout);
+ }
+
+ if (this.mPopupOpen) {
+ delete this._adjustHeightOnPopupShown;
+ this._adjustHeightTimeout = setTimeout(() => this.adjustHeight(), 0);
+ } 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)
+ 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.childNodes.length;
+ for (let i = this._matchCount; i < existingItemsCount; ++i) {
+ this.richlistbox.childNodes[i].collapsed = true;
+ }
+ ]]>
+ </body>
+ </method>
+
+ <method name="adjustHeight">
+ <body>
+ <![CDATA[
+ // Figure out how many rows to show
+ let rows = this.richlistbox.childNodes;
+ let numRows = Math.min(this._matchCount, this.maxRows, rows.length);
+
+ this.removeAttribute("height");
+
+ // 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 transition = style.transitionProperty;
+ this._rlbAnimated = transition && transition != "none";
+
+ let paddingTop = parseInt(style.paddingTop) || 0;
+ let paddingBottom = parseInt(style.paddingBottom) || 0;
+ this._rlbPadding = paddingTop + paddingBottom;
+ }
+
+ if (numRows > this.maxRows) {
+ // Set a fixed max-height to avoid flicker when growing the panel.
+ let lastVisibleRowRect = rows[this.maxRows - 1].getBoundingClientRect();
+ let visibleHeight = lastVisibleRowRect.bottom - firstRowRect.top;
+ this.richlistbox.style.maxHeight =
+ visibleHeight + this._rlbPadding + "px";
+ }
+
+ // 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 animate = this._rlbAnimated &&
+ this.getAttribute("dontanimate") != "true";
+ let currentHeight = this.richlistbox.getBoundingClientRect().height;
+ if (height > currentHeight) {
+ // Grow immediately.
+ if (animate) {
+ this.richlistbox.removeAttribute("height");
+ this.richlistbox.style.height = height + "px";
+ } else {
+ this.richlistbox.style.removeProperty("height");
+ this.richlistbox.height = height;
+ }
+ } else {
+ // Delay shrinking to avoid flicker.
+ this._shrinkTimeout = setTimeout(() => {
+ this._collapseUnusedItems();
+ if (animate) {
+ this.richlistbox.removeAttribute("height");
+ this.richlistbox.style.height = height + "px";
+ } else {
+ this.richlistbox.style.removeProperty("height");
+ this.richlistbox.height = height;
+ }
+ }, this.mInput.shrinkDelay);
+ }
+ ]]>
+ </body>
+ </method>
+
+ <method name="_appendCurrentResult">
+ <parameter name="invalidateReason"/>
+ <body>
+ <![CDATA[
+ var controller = this.mInput.controller;
+ var matchCount = this._matchCount;
+ var existingItemsCount = this.richlistbox.childNodes.length;
+
+ // Process maxRows per chunk to improve performance and user experience
+ for (let i = 0; i < this.maxRows; i++) {
+ if (this._currentIndex >= matchCount)
+ break;
+
+ var item;
+
+ // trim the leading/trailing whitespace
+ var trimmedSearchString = controller.searchString.replace(/^\s+/, "").replace(/\s+$/, "");
+
+ let url = controller.getValueAt(this._currentIndex);
+
+ if (this._currentIndex < existingItemsCount) {
+ // re-use the existing item
+ item = this.richlistbox.childNodes[this._currentIndex];
+ item.setAttribute("dir", this.style.direction);
+
+ // 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 mouse-selected item, to
+ // avoid surprising the user.
+ let iface = Components.interfaces.nsIAutoCompletePopup;
+ if (item.getAttribute("text") == trimmedSearchString &&
+ invalidateReason == iface.INVALIDATE_REASON_NEW_RESULT &&
+ (item.getAttribute("url") == url ||
+ this.richlistbox.mouseSelectedIndex === this._currentIndex)) {
+ // Additionally, if the item is a searchengine action, then it
+ // should only be reused if the engine name is the same as the
+ // popup's override engine name, if any.
+ let action = item._parseActionUrl(url);
+ if (!action ||
+ action.type != "searchengine" ||
+ !this.overrideSearchEngineName ||
+ action.params.engineName == this.overrideSearchEngineName) {
+ item.collapsed = false;
+ // Call adjustSiteIconStart only after setting collapsed=
+ // false. The calculations it does may be wrong otherwise.
+ item.adjustSiteIconStart(this._siteIconStart);
+ // The popup may have changed size between now and the last
+ // time the item was shown, so always handle over/underflow.
+ item.handleOverUnderflow();
+ this._currentIndex++;
+ continue;
+ }
+ }
+ }
+ else {
+ // need to create a new item
+ item = document.createElementNS("http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul", "richlistitem");
+ item.setAttribute("dir", this.style.direction);
+ }
+
+ // set these attributes before we set the class
+ // so that we can use them from the constructor
+ let iconURI = controller.getImageAt(this._currentIndex);
+ item.setAttribute("image", iconURI);
+ item.setAttribute("url", url);
+ item.setAttribute("title", controller.getCommentAt(this._currentIndex));
+ item.setAttribute("originaltype", controller.getStyleAt(this._currentIndex));
+ item.setAttribute("text", trimmedSearchString);
+
+ if (this._currentIndex < existingItemsCount) {
+ // re-use the existing item
+ item._adjustAcItem();
+ item.collapsed = false;
+ }
+ else {
+ // set the class at the end so we can use the attributes
+ // in the xbl constructor
+ item.className = "autocomplete-richlistitem";
+ this.richlistbox.appendChild(item);
+ }
+
+ // The binding may have not been applied yet.
+ setTimeout(() => {
+ let changed = item.adjustSiteIconStart(this._siteIconStart);
+ if (changed) {
+ item.handleOverUnderflow();
+ }
+ }, 0);
+
+ this._currentIndex++;
+ }
+
+ if (typeof this.onResultsAdded == "function")
+ 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>
+
+ <!-- The x-coordinate relative to the leading edge of the window of the
+ items' site icons (favicons). -->
+ <property name="siteIconStart"
+ onget="return this._siteIconStart;">
+ <setter>
+ <![CDATA[
+ if (val != this._siteIconStart) {
+ this._siteIconStart = val;
+ for (let item of this.richlistbox.childNodes) {
+ let changed = item.adjustSiteIconStart(val);
+ if (changed) {
+ item.handleOverUnderflow();
+ }
+ }
+ }
+ return val;
+ ]]>
+ </setter>
+ </property>
+
+ <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="popupshown">
+ <![CDATA[
+ if (this._adjustHeightOnPopupShown) {
+ delete this._adjustHeightOnPopupShown;
+ this.adjustHeight();
+ }
+ ]]>
+ </handler>
+ </handlers>
+ </binding>
+
+ <binding id="autocomplete-richlistitem-insecure-field" extends="chrome://global/content/bindings/autocomplete.xml#autocomplete-richlistitem">
+ <content align="center"
+ onoverflow="this._onOverflow();"
+ onunderflow="this._onUnderflow();">
+ <xul:image anonid="type-icon"
+ class="ac-type-icon"
+ xbl:inherits="selected,current,type"/>
+ <xul:image anonid="site-icon"
+ class="ac-site-icon"
+ xbl:inherits="src=image,selected,type"/>
+ <xul:vbox class="ac-title"
+ align="left"
+ xbl:inherits="">
+ <xul:description class="ac-text-overflow-container">
+ <xul:description anonid="title-text"
+ class="ac-title-text"
+ xbl:inherits="selected"/>
+ </xul:description>
+ </xul:vbox>
+ <xul:hbox anonid="tags"
+ class="ac-tags"
+ align="center"
+ xbl:inherits="selected">
+ <xul:description class="ac-text-overflow-container">
+ <xul:description anonid="tags-text"
+ class="ac-tags-text"
+ xbl:inherits="selected"/>
+ </xul:description>
+ </xul:hbox>
+ <xul:hbox anonid="separator"
+ class="ac-separator"
+ align="center"
+ xbl:inherits="selected,actiontype,type">
+ <xul:description class="ac-separator-text">—</xul:description>
+ </xul:hbox>
+ <xul:hbox class="ac-url"
+ align="center"
+ xbl:inherits="selected,actiontype">
+ <xul:description class="ac-text-overflow-container">
+ <xul:description anonid="url-text"
+ class="ac-url-text"
+ xbl:inherits="selected"/>
+ </xul:description>
+ </xul:hbox>
+ <xul:hbox class="ac-action"
+ align="center"
+ xbl:inherits="selected,actiontype">
+ <xul:description class="ac-text-overflow-container">
+ <xul:description anonid="action-text"
+ class="ac-action-text"
+ xbl:inherits="selected"/>
+ </xul:description>
+ </xul:hbox>
+ </content>
+
+ <handlers>
+ <handler event="click" button="0"><![CDATA[
+ let baseURL = Services.urlFormatter.formatURLPref("app.support.baseURL");
+ window.openUILinkIn(baseURL + "insecure-password", "tab", {
+ relatedToCurrent: true,
+ });
+ ]]></handler>
+ </handlers>
+
+ <implementation>
+ <constructor><![CDATA[
+ // Unlike other autocomplete items, the height of the insecure warning
+ // increases by wrapping. So "forceHandleUnderflow" is for container to
+ // recalculate an item's height and width.
+ this.classList.add("forceHandleUnderflow");
+ ]]></constructor>
+
+ <property name="_learnMoreString">
+ <getter><![CDATA[
+ if (!this.__learnMoreString) {
+ this.__learnMoreString =
+ Services.strings.createBundle("chrome://passwordmgr/locale/passwordmgr.properties").
+ GetStringFromName("insecureFieldWarningLearnMore");
+ }
+ return this.__learnMoreString;
+ ]]></getter>
+ </property>
+
+ <method name="_getSearchTokens">
+ <parameter name="aSearch"/>
+ <body>
+ <![CDATA[
+ return [this._learnMoreString.toLowerCase()];
+ ]]>
+ </body>
+ </method>
+
+ </implementation>
+ </binding>
+
+ <binding id="autocomplete-richlistitem" extends="chrome://global/content/bindings/richlistbox.xml#richlistitem">
+
+ <content align="center"
+ onoverflow="this._onOverflow();"
+ onunderflow="this._onUnderflow();">
+ <xul:image anonid="type-icon"
+ class="ac-type-icon"
+ xbl:inherits="selected,current,type"/>
+ <xul:image anonid="site-icon"
+ class="ac-site-icon"
+ xbl:inherits="src=image,selected,type"/>
+ <xul:hbox class="ac-title"
+ align="center"
+ xbl:inherits="selected">
+ <xul:description class="ac-text-overflow-container">
+ <xul:description anonid="title-text"
+ class="ac-title-text"
+ xbl:inherits="selected"/>
+ </xul:description>
+ </xul:hbox>
+ <xul:hbox anonid="tags"
+ class="ac-tags"
+ align="center"
+ xbl:inherits="selected">
+ <xul:description class="ac-text-overflow-container">
+ <xul:description anonid="tags-text"
+ class="ac-tags-text"
+ xbl:inherits="selected"/>
+ </xul:description>
+ </xul:hbox>
+ <xul:hbox anonid="separator"
+ class="ac-separator"
+ align="center"
+ xbl:inherits="selected,actiontype,type">
+ <xul:description class="ac-separator-text">—</xul:description>
+ </xul:hbox>
+ <xul:hbox class="ac-url"
+ align="center"
+ xbl:inherits="selected,actiontype">
+ <xul:description class="ac-text-overflow-container">
+ <xul:description anonid="url-text"
+ class="ac-url-text"
+ xbl:inherits="selected"/>
+ </xul:description>
+ </xul:hbox>
+ <xul:hbox class="ac-action"
+ align="center"
+ xbl:inherits="selected,actiontype">
+ <xul:description class="ac-text-overflow-container">
+ <xul:description anonid="action-text"
+ class="ac-action-text"
+ xbl:inherits="selected"/>
+ </xul:description>
+ </xul:hbox>
+ </content>
+
+ <implementation implements="nsIDOMXULSelectControlItemElement">
+ <constructor>
+ <![CDATA[
+ this._typeIcon = document.getAnonymousElementByAttribute(
+ this, "anonid", "type-icon"
+ );
+ this._siteIcon = document.getAnonymousElementByAttribute(
+ this, "anonid", "site-icon"
+ );
+ this._titleText = document.getAnonymousElementByAttribute(
+ this, "anonid", "title-text"
+ );
+ this._tags = document.getAnonymousElementByAttribute(
+ this, "anonid", "tags"
+ );
+ this._tagsText = document.getAnonymousElementByAttribute(
+ this, "anonid", "tags-text"
+ );
+ this._separator = document.getAnonymousElementByAttribute(
+ this, "anonid", "separator"
+ );
+ this._urlText = document.getAnonymousElementByAttribute(
+ this, "anonid", "url-text"
+ );
+ this._actionText = document.getAnonymousElementByAttribute(
+ this, "anonid", "action-text"
+ );
+ this._adjustAcItem();
+ ]]>
+ </constructor>
+
+ <property name="label" readonly="true">
+ <getter>
+ <![CDATA[
+ // This property is a string that is read aloud by screen readers,
+ // so it must not contain anything that should not be user-facing.
+
+ let parts = [
+ this.getAttribute("title"),
+ this.getAttribute("displayurl"),
+ ];
+ let label = parts.filter(str => str).join(" ")
+
+ // allow consumers that have extended popups to override
+ // the label values for the richlistitems
+ let panel = this.parentNode.parentNode;
+ if (panel.createResultLabel) {
+ return panel.createResultLabel(this, label);
+ }
+
+ return label;
+ ]]>
+ </getter>
+ </property>
+
+ <property name="_stringBundle">
+ <getter><![CDATA[
+ if (!this.__stringBundle) {
+ this.__stringBundle = Services.strings.createBundle("chrome://global/locale/autocomplete.properties");
+ }
+ return this.__stringBundle;
+ ]]></getter>
+ </property>
+
+ <field name="_boundaryCutoff">null</field>
+
+ <property name="boundaryCutoff" readonly="true">
+ <getter>
+ <![CDATA[
+ if (!this._boundaryCutoff) {
+ this._boundaryCutoff =
+ Components.classes["@mozilla.org/preferences-service;1"].
+ getService(Components.interfaces.nsIPrefBranch).
+ getIntPref("toolkit.autocomplete.richBoundaryCutoff");
+ }
+ return this._boundaryCutoff;
+ ]]>
+ </getter>
+ </property>
+
+ <field name="_inOverflow">false</field>
+
+ <method name="_onOverflow">
+ <body>
+ <![CDATA[
+ this._inOverflow = true;
+ this._handleOverflow();
+ ]]>
+ </body>
+ </method>
+
+ <method name="_onUnderflow">
+ <body>
+ <![CDATA[
+ this._inOverflow = false;
+ this._handleOverflow();
+ ]]>
+ </body>
+ </method>
+
+ <method name="_getBoundaryIndices">
+ <parameter name="aText"/>
+ <parameter name="aSearchTokens"/>
+ <body>
+ <![CDATA[
+ // Short circuit for empty search ([""] == "")
+ if (aSearchTokens == "")
+ return [0, aText.length];
+
+ // Find which regions of text match the search terms
+ let regions = [];
+ for (let search of Array.prototype.slice.call(aSearchTokens)) {
+ let matchIndex = -1;
+ let searchLen = search.length;
+
+ // Find all matches of the search terms, but stop early for perf
+ let lowerText = aText.substr(0, this.boundaryCutoff).toLowerCase();
+ while ((matchIndex = lowerText.indexOf(search, matchIndex + 1)) >= 0) {
+ regions.push([matchIndex, matchIndex + searchLen]);
+ }
+ }
+
+ // Sort the regions by start position then end position
+ regions = regions.sort((a, b) => {
+ let start = a[0] - b[0];
+ return (start == 0) ? a[1] - b[1] : start;
+ });
+
+ // Generate the boundary indices from each region
+ let start = 0;
+ let end = 0;
+ let boundaries = [];
+ let len = regions.length;
+ for (let i = 0; i < len; i++) {
+ // We have a new boundary if the start of the next is past the end
+ let region = regions[i];
+ if (region[0] > end) {
+ // First index is the beginning of match
+ boundaries.push(start);
+ // Second index is the beginning of non-match
+ boundaries.push(end);
+
+ // Track the new region now that we've stored the previous one
+ start = region[0];
+ }
+
+ // Push back the end index for the current or new region
+ end = Math.max(end, region[1]);
+ }
+
+ // Add the last region
+ boundaries.push(start);
+ boundaries.push(end);
+
+ // Put on the end boundary if necessary
+ if (end < aText.length)
+ boundaries.push(aText.length);
+
+ // Skip the first item because it's always 0
+ return boundaries.slice(1);
+ ]]>
+ </body>
+ </method>
+
+ <method name="_getSearchTokens">
+ <parameter name="aSearch"/>
+ <body>
+ <![CDATA[
+ let search = aSearch.toLowerCase();
+ return search.split(/\s+/);
+ ]]>
+ </body>
+ </method>
+
+ <method name="_setUpDescription">
+ <parameter name="aDescriptionElement"/>
+ <parameter name="aText"/>
+ <parameter name="aNoEmphasis"/>
+ <body>
+ <![CDATA[
+ // Get rid of all previous text
+ if (!aDescriptionElement) {
+ return;
+ }
+ while (aDescriptionElement.hasChildNodes())
+ aDescriptionElement.removeChild(aDescriptionElement.firstChild);
+
+ // If aNoEmphasis is specified, don't add any emphasis
+ if (aNoEmphasis) {
+ aDescriptionElement.appendChild(document.createTextNode(aText));
+ return;
+ }
+
+ // Get the indices that separate match and non-match text
+ let search = this.getAttribute("text");
+ let tokens = this._getSearchTokens(search);
+ let indices = this._getBoundaryIndices(aText, tokens);
+
+ this._appendDescriptionSpans(indices, aText, aDescriptionElement,
+ aDescriptionElement);
+ ]]>
+ </body>
+ </method>
+
+ <method name="_appendDescriptionSpans">
+ <parameter name="indices"/>
+ <parameter name="text"/>
+ <parameter name="spansParentElement"/>
+ <parameter name="descriptionElement"/>
+ <body>
+ <![CDATA[
+ let next;
+ let start = 0;
+ let len = indices.length;
+ // Even indexed boundaries are matches, so skip the 0th if it's empty
+ for (let i = indices[0] == 0 ? 1 : 0; i < len; i++) {
+ next = indices[i];
+ let spanText = text.substr(start, next - start);
+ start = next;
+
+ if (i % 2 == 0) {
+ // Emphasize the text for even indices
+ let span = spansParentElement.appendChild(
+ document.createElementNS("http://www.w3.org/1999/xhtml", "span"));
+ this._setUpEmphasisSpan(span, descriptionElement);
+ span.textContent = spanText;
+ } else {
+ // Otherwise, it's plain text
+ spansParentElement.appendChild(document.createTextNode(spanText));
+ }
+ }
+ ]]>
+ </body>
+ </method>
+
+ <method name="_setUpTags">
+ <parameter name="tags"/>
+ <body>
+ <![CDATA[
+ while (this._tagsText.hasChildNodes()) {
+ this._tagsText.firstChild.remove();
+ }
+
+ let anyTagsMatch = false;
+
+ // Include only tags that match the search string.
+ for (let tag of tags) {
+ // Check if the tag matches the search string.
+ let search = this.getAttribute("text");
+ let tokens = this._getSearchTokens(search);
+ let indices = this._getBoundaryIndices(tag, tokens);
+
+ if (indices.length == 2 &&
+ indices[0] == 0 &&
+ indices[1] == tag.length) {
+ // The tag doesn't match the search string, so don't include it.
+ continue;
+ }
+
+ anyTagsMatch = true;
+
+ let tagSpan =
+ document.createElementNS("http://www.w3.org/1999/xhtml", "span");
+ tagSpan.classList.add("ac-tag");
+ this._tagsText.appendChild(tagSpan);
+
+ this._appendDescriptionSpans(indices, tag, tagSpan, this._tagsText);
+ }
+
+ return anyTagsMatch;
+ ]]>
+ </body>
+ </method>
+
+ <method name="_setUpEmphasisSpan">
+ <parameter name="aSpan"/>
+ <parameter name="aDescriptionElement"/>
+ <body>
+ <![CDATA[
+ aSpan.classList.add("ac-emphasize-text");
+ switch (aDescriptionElement) {
+ case this._titleText:
+ aSpan.classList.add("ac-emphasize-text-title");
+ break;
+ case this._tagsText:
+ aSpan.classList.add("ac-emphasize-text-tag");
+ break;
+ case this._urlText:
+ aSpan.classList.add("ac-emphasize-text-url");
+ break;
+ case this._actionText:
+ aSpan.classList.add("ac-emphasize-text-action");
+ break;
+ }
+ ]]>
+ </body>
+ </method>
+
+ <!--
+ This will generate an array of emphasis pairs for use with
+ _setUpEmphasisedSections(). Each pair is a tuple (array) that
+ represents a block of text - containing the text of that block, and a
+ boolean for whether that block should have an emphasis styling applied
+ to it.
+
+ These pairs are generated by parsing a localised string (aSourceString)
+ with parameters, in the format that is used by
+ nsIStringBundle.formatStringFromName():
+
+ "textA %1$S textB textC %2$S"
+
+ Or:
+
+ "textA %S"
+
+ Where "%1$S", "%2$S", and "%S" are intended to be replaced by provided
+ replacement strings. These are specified an array of tuples
+ (aReplacements), each containing the replacement text and a boolean for
+ whether that text should have an emphasis styling applied. This is used
+ as a 1-based array - ie, "%1$S" is replaced by the item in the first
+ index of aReplacements, "%2$S" by the second, etc. "%S" will always
+ match the first index.
+ -->
+ <method name="_generateEmphasisPairs">
+ <parameter name="aSourceString"/>
+ <parameter name="aReplacements"/>
+ <body>
+ <![CDATA[
+ let pairs = [];
+
+ // Split on %S, %1$S, %2$S, etc. ie:
+ // "textA %S"
+ // becomes ["textA ", "%S"]
+ // "textA %1$S textB textC %2$S"
+ // becomes ["textA ", "%1$S", " textB textC ", "%2$S"]
+ let parts = aSourceString.split(/(%(?:[0-9]+\$)?S)/);
+
+ for (let part of parts) {
+ // The above regex will actually give us an empty string at the
+ // end - we don't want that, as we don't want to later generate an
+ // empty text node for it.
+ if (part.length === 0)
+ continue;
+
+ // Determine if this token is a replacement token or a normal text
+ // token. If it is a replacement token, we want to extract the
+ // numerical number. However, we still want to match on "$S".
+ let match = part.match(/^%(?:([0-9]+)\$)?S$/);
+
+ if (match) {
+ // "%S" doesn't have a numerical number in it, but will always
+ // be assumed to be 1. Furthermore, the input string specifies
+ // these with a 1-based index, but we want a 0-based index.
+ let index = (match[1] || 1) - 1;
+
+ if (index >= 0 && index < aReplacements.length) {
+ pairs.push([...aReplacements[index]]);
+ }
+ } else {
+ pairs.push([part]);
+ }
+ }
+
+ return pairs;
+ ]]>
+ </body>
+ </method>
+
+ <!--
+ _setUpEmphasisedSections() has the same use as _setUpDescription,
+ except instead of taking a string and highlighting given tokens, it takes
+ an array of pairs generated by _generateEmphasisPairs(). This allows
+ control over emphasising based on specific blocks of text, rather than
+ search for substrings.
+ -->
+ <method name="_setUpEmphasisedSections">
+ <parameter name="aDescriptionElement"/>
+ <parameter name="aTextPairs"/>
+ <body>
+ <![CDATA[
+ // Get rid of all previous text
+ while (aDescriptionElement.hasChildNodes())
+ aDescriptionElement.firstChild.remove();
+
+ for (let [text, emphasise] of aTextPairs) {
+ if (emphasise) {
+ let span = aDescriptionElement.appendChild(
+ document.createElementNS("http://www.w3.org/1999/xhtml", "span"));
+ span.textContent = text;
+ switch (emphasise) {
+ case "match":
+ this._setUpEmphasisSpan(span, aDescriptionElement);
+ break;
+ }
+ } else {
+ aDescriptionElement.appendChild(document.createTextNode(text));
+ }
+ }
+ ]]>
+ </body>
+ </method>
+
+ <field name="_textToSubURI">null</field>
+ <method name="_unescapeUrl">
+ <parameter name="url"/>
+ <body>
+ <![CDATA[
+ if (!this._textToSubURI) {
+ this._textToSubURI =
+ Components.classes["@mozilla.org/intl/texttosuburi;1"]
+ .getService(Components.interfaces.nsITextToSubURI);
+ }
+ return this._textToSubURI.unEscapeURIForUI("UTF-8", url);
+ ]]>
+ </body>
+ </method>
+
+ <method name="_adjustAcItem">
+ <body>
+ <![CDATA[
+ let popup = this.parentNode.parentNode;
+ if (!popup.popupOpen) {
+ // Removing the max-width and resetting it later when overflow is
+ // handled is jarring when the item is visible, so skip this when
+ // the popup is open.
+ this._removeMaxWidths();
+ }
+
+ let title = this.getAttribute("title");
+ let titleLooksLikeUrl = false;
+
+ let displayUrl;
+ let originalUrl = this.getAttribute("url");
+ let emphasiseUrl = true;
+
+ let type = this.getAttribute("originaltype");
+ let types = new Set(type.split(/\s+/));
+ let initialTypes = new Set(types);
+ // Remove types that should ultimately not be in the `type` string.
+ types.delete("action");
+ types.delete("autofill");
+ types.delete("heuristic");
+ type = [...types][0] || "";
+
+ let action;
+
+ if (initialTypes.has("autofill")) {
+ // Treat autofills as visiturl actions.
+ action = {
+ type: "visiturl",
+ params: {
+ url: originalUrl,
+ },
+ };
+ }
+
+ this.removeAttribute("actiontype");
+ this.classList.remove("overridable-action");
+
+ // If the type includes an action, set up the item appropriately.
+ if (initialTypes.has("action") || action) {
+ action = action || this._parseActionUrl(originalUrl);
+ this.setAttribute("actiontype", action.type);
+
+ if (action.type == "switchtab") {
+ this.classList.add("overridable-action");
+ displayUrl = this._unescapeUrl(action.params.url);
+ let desc = this._stringBundle.GetStringFromName("switchToTab2");
+ this._setUpDescription(this._actionText, desc, true);
+ } else if (action.type == "remotetab") {
+ displayUrl = this._unescapeUrl(action.params.url);
+ let desc = action.params.deviceName;
+ this._setUpDescription(this._actionText, desc, true);
+ } else if (action.type == "searchengine") {
+ emphasiseUrl = false;
+
+ // The order here is not localizable, we default to appending
+ // "- Search with Engine" to the search string, to be able to
+ // properly generate emphasis pairs. That said, no localization
+ // changed the order while it was possible, so doesn't look like
+ // there's a strong need for that.
+ let {engineName, searchSuggestion, searchQuery} = action.params;
+
+ // Override the engine name if the popup defines an override.
+ let override = popup.overrideSearchEngineName;
+ if (override && override != engineName) {
+ engineName = override;
+ action.params.engineName = override;
+ let newURL =
+ PlacesUtils.mozActionURI(action.type, action.params);
+ this.setAttribute("url", newURL);
+ }
+
+ let engineStr =
+ this._stringBundle.formatStringFromName("searchWithEngine",
+ [engineName], 1);
+ this._setUpDescription(this._actionText, engineStr, true);
+
+ // Make the title by generating an array of pairs and its
+ // corresponding interpolation string (e.g., "%1$S") to pass to
+ // _generateEmphasisPairs.
+ let pairs;
+ if (searchSuggestion) {
+ // Check if the search query appears in the suggestion. It may
+ // not. If it does, then emphasize the query in the suggestion
+ // and otherwise just include the suggestion without emphasis.
+ let idx = searchSuggestion.indexOf(searchQuery);
+ if (idx >= 0) {
+ pairs = [
+ [searchSuggestion.substring(0, idx), ""],
+ [searchQuery, "match"],
+ [searchSuggestion.substring(idx + searchQuery.length), ""],
+ ];
+ } else {
+ pairs = [
+ [searchSuggestion, ""],
+ ];
+ }
+ } else {
+ pairs = [
+ [searchQuery, ""],
+ ];
+ }
+ let interpStr = pairs.map((pair, i) => `%${i + 1}$S`).join("");
+ title = this._generateEmphasisPairs(interpStr, pairs);
+
+ // If this is a default search match, we remove the image so we
+ // can style it ourselves with a generic search icon.
+ // We don't do this when matching an aliased search engine,
+ // because the icon helps with recognising which engine will be
+ // used (when using the default engine, we don't need that
+ // recognition).
+ if (!action.params.alias && !initialTypes.has("favicon")) {
+ this.removeAttribute("image");
+ }
+ } else if (action.type == "visiturl") {
+ emphasiseUrl = false;
+ displayUrl = this._unescapeUrl(action.params.url);
+ title = displayUrl;
+ titleLooksLikeUrl = true;
+ let visitStr = this._stringBundle.GetStringFromName("visit");
+ this._setUpDescription(this._actionText, visitStr, true);
+ } else if (action.type == "extension") {
+ let content = action.params.content;
+ displayUrl = content;
+ this._setUpDescription(this._actionText, content, true);
+ }
+ }
+
+ if (!displayUrl) {
+ let input = popup.input;
+ let url = typeof(input.trimValue) == "function" ?
+ input.trimValue(originalUrl) :
+ originalUrl;
+ displayUrl = this._unescapeUrl(url);
+ }
+ // For performance reasons we may want to limit the displayUrl size.
+ if (popup.textRunsMaxLen) {
+ displayUrl = displayUrl.substr(0, popup.textRunsMaxLen);
+ }
+ this.setAttribute("displayurl", displayUrl);
+
+ // Show the domain as the title if we don't have a title.
+ if (!title) {
+ title = displayUrl;
+ titleLooksLikeUrl = true;
+ try {
+ let uri = Services.io.newURI(originalUrl, null, null);
+ // Not all valid URLs have a domain.
+ if (uri.host)
+ title = uri.host;
+ } catch (e) {}
+ }
+
+ this._tags.setAttribute("empty", "true");
+
+ if (type == "tag" || type == "bookmark-tag") {
+ // The title is separated from the tags by an endash
+ let tags;
+ [, title, tags] = title.match(/^(.+) \u2013 (.+)$/);
+
+ // Each tag is split by a comma in an undefined order, so sort it
+ let sortedTags = tags.split(/\s*,\s*/).sort((a, b) => {
+ return a.localeCompare(a);
+ });
+
+ let anyTagsMatch = this._setUpTags(sortedTags);
+ if (anyTagsMatch) {
+ this._tags.removeAttribute("empty");
+ }
+ if (type == "bookmark-tag") {
+ type = "bookmark";
+ }
+ } else if (type == "keyword") {
+ // Note that this is a moz-action with action.type == keyword.
+ emphasiseUrl = false;
+ let keywordArg = this.getAttribute("text").replace(/^[^\s]+\s*/, "");
+ if (!keywordArg) {
+ // Treat keyword searches without arguments as visiturl actions.
+ type = "visiturl";
+ this.setAttribute("actiontype", "visiturl");
+ let visitStr = this._stringBundle.GetStringFromName("visit");
+ this._setUpDescription(this._actionText, visitStr, true);
+ } else {
+ let pairs = [[title, ""], [keywordArg, "match"]];
+ let interpStr =
+ this._stringBundle.GetStringFromName("bookmarkKeywordSearch");
+ title = this._generateEmphasisPairs(interpStr, pairs);
+ // The action box will be visible since this is a moz-action, but
+ // we want it to appear as if it were not visible, so set its text
+ // to the empty string.
+ this._setUpDescription(this._actionText, "", false);
+ }
+ }
+
+ this.setAttribute("type", type);
+
+ if (titleLooksLikeUrl) {
+ this._titleText.setAttribute("lookslikeurl", "true");
+ } else {
+ this._titleText.removeAttribute("lookslikeurl");
+ }
+
+ if (Array.isArray(title)) {
+ // For performance reasons we may want to limit the title size.
+ if (popup.textRunsMaxLen) {
+ title = title.map(t => t.substr(0, popup.textRunsMaxLen));
+ }
+ this._setUpEmphasisedSections(this._titleText, title);
+ } else {
+ // For performance reasons we may want to limit the title size.
+ if (popup.textRunsMaxLen) {
+ title = title.substr(0, popup.textRunsMaxLen);
+ }
+ this._setUpDescription(this._titleText, title, false);
+ }
+ this._setUpDescription(this._urlText, displayUrl, !emphasiseUrl);
+
+ if (this._inOverflow) {
+ this._handleOverflow();
+ }
+ ]]>
+ </body>
+ </method>
+
+ <method name="_removeMaxWidths">
+ <body>
+ <![CDATA[
+ this._titleText.style.removeProperty("max-width");
+ this._tagsText.style.removeProperty("max-width");
+ this._urlText.style.removeProperty("max-width");
+ this._actionText.style.removeProperty("max-width");
+ ]]>
+ </body>
+ </method>
+
+ <!-- Sets the x-coordinate of the leading edge of the site icon (favicon)
+ relative the the leading edge of the window.
+ @param newStart The new x-coordinate, relative to the leading edge of
+ the window. Pass undefined to reset the icon's position to
+ whatever is specified in CSS.
+ @return True if the icon's position changed, false if not. -->
+ <method name="adjustSiteIconStart">
+ <parameter name="newStart"/>
+ <body>
+ <![CDATA[
+ if (typeof(newStart) != "number") {
+ this._typeIcon.style.removeProperty("margin-inline-start");
+ return true;
+ }
+ let rect = this._siteIcon.getBoundingClientRect();
+ let dir = this.getAttribute("dir");
+ let delta = dir == "rtl" ? rect.right - newStart
+ : newStart - rect.left;
+ let px = this._typeIcon.style.marginInlineStart;
+ if (!px) {
+ // Allow margin-inline-start not to be specified in CSS initially.
+ let style = window.getComputedStyle(this._typeIcon);
+ px = dir == "rtl" ? style.marginRight : style.marginLeft;
+ }
+ let typeIconStart = Number(px.substr(0, px.length - 2));
+ this._typeIcon.style.marginInlineStart = (typeIconStart + delta) + "px";
+ return delta > 0;
+ ]]>
+ </body>
+ </method>
+
+ <!-- This method truncates the displayed strings as necessary. -->
+ <method name="_handleOverflow">
+ <body><![CDATA[
+ let itemRect = this.parentNode.getBoundingClientRect();
+ let titleRect = this._titleText.getBoundingClientRect();
+ let tagsRect = this._tagsText.getBoundingClientRect();
+ let separatorRect = this._separator.getBoundingClientRect();
+ let urlRect = this._urlText.getBoundingClientRect();
+ let actionRect = this._actionText.getBoundingClientRect();
+ let separatorURLActionWidth =
+ separatorRect.width + Math.max(urlRect.width, actionRect.width);
+
+ // Total width for the title and URL/action is the width of the item
+ // minus the start of the title text minus a little optional extra padding.
+ // This extra padding amount is basically arbitrary but keeps the text
+ // from getting too close to the popup's edge.
+ let dir = this.getAttribute("dir");
+ let titleStart = dir == "rtl" ? itemRect.right - titleRect.right
+ : titleRect.left - itemRect.left;
+
+ let popup = this.parentNode.parentNode;
+ let itemWidth = itemRect.width - titleStart - popup.overflowPadding;
+
+ if (this._tags.hasAttribute("empty")) {
+ // The tags box is not displayed in this case.
+ tagsRect.width = 0;
+ }
+
+ let titleTagsWidth = titleRect.width + tagsRect.width;
+ if (titleTagsWidth + separatorURLActionWidth > itemWidth) {
+ // Title + tags + URL/action overflows the item width.
+
+ // The percentage of the item width allocated to the title and tags.
+ let titleTagsPct = 0.66;
+
+ let titleTagsAvailable = itemWidth - separatorURLActionWidth;
+ let titleTagsMaxWidth = Math.max(
+ titleTagsAvailable,
+ itemWidth * titleTagsPct
+ );
+ if (titleTagsWidth > titleTagsMaxWidth) {
+ // Title + tags overflows the max title + tags width.
+
+ // The percentage of the title + tags width allocated to the
+ // title.
+ let titlePct = 0.33;
+
+ let titleAvailable = titleTagsMaxWidth - tagsRect.width;
+ let titleMaxWidth = Math.max(
+ titleAvailable,
+ titleTagsMaxWidth * titlePct
+ );
+ let tagsAvailable = titleTagsMaxWidth - titleRect.width;
+ let tagsMaxWidth = Math.max(
+ tagsAvailable,
+ titleTagsMaxWidth * (1 - titlePct)
+ );
+ this._titleText.style.maxWidth = titleMaxWidth + "px";
+ this._tagsText.style.maxWidth = tagsMaxWidth + "px";
+ }
+ let urlActionMaxWidth = Math.max(
+ itemWidth - titleTagsWidth,
+ itemWidth * (1 - titleTagsPct)
+ );
+ urlActionMaxWidth -= separatorRect.width;
+ this._urlText.style.maxWidth = urlActionMaxWidth + "px";
+ this._actionText.style.maxWidth = urlActionMaxWidth + "px";
+ }
+ ]]></body>
+ </method>
+
+ <method name="handleOverUnderflow">
+ <body>
+ <![CDATA[
+ this._removeMaxWidths();
+ this._handleOverflow();
+ ]]>
+ </body>
+ </method>
+
+ <method name="_parseActionUrl">
+ <parameter name="aUrl"/>
+ <body><![CDATA[
+ if (!aUrl.startsWith("moz-action:"))
+ return null;
+
+ // URL is in the format moz-action:ACTION,PARAMS
+ // Where PARAMS is a JSON encoded object.
+ let [, type, params] = aUrl.match(/^moz-action:([^,]+),(.*)$/);
+
+ let action = {
+ type: type,
+ };
+
+ try {
+ action.params = JSON.parse(params);
+ for (let key in action.params) {
+ action.params[key] = decodeURIComponent(action.params[key]);
+ }
+ } catch (e) {
+ // If this failed, we assume that params is not a JSON object, and
+ // is instead just a flat string. This may happen for legacy
+ // search components.
+ action.params = {
+ url: params,
+ }
+ }
+
+ return action;
+ ]]></body>
+ </method>
+ </implementation>
+ </binding>
+
+ <binding id="autocomplete-tree" extends="chrome://global/content/bindings/tree.xml#tree">
+ <content>
+ <children includes="treecols"/>
+ <xul:treerows class="autocomplete-treerows tree-rows" xbl:inherits="hidescrollbar" flex="1">
+ <children/>
+ </xul:treerows>
+ </content>
+ </binding>
+
+ <binding id="autocomplete-richlistbox" extends="chrome://global/content/bindings/richlistbox.xml#richlistbox">
+ <implementation>
+ <field name="mLastMoveTime">Date.now()</field>
+ <field name="mouseSelectedIndex">-1</field>
+ </implementation>
+ <handlers>
+ <handler event="mouseup">
+ <![CDATA[
+ // don't call onPopupClick for the scrollbar buttons, thumb, slider, etc.
+ let item = event.originalTarget;
+ while (item && item.localName != "richlistitem") {
+ item = item.parentNode;
+ }
+
+ if (!item)
+ return;
+
+ this.parentNode.onPopupClick(event);
+ ]]>
+ </handler>
+
+ <handler event="mousemove">
+ <![CDATA[
+ if (Date.now() - this.mLastMoveTime > 30) {
+ let item = event.target;
+ while (item && item.localName != "richlistitem") {
+ item = item.parentNode;
+ }
+
+ if (!item)
+ return;
+
+ let index = this.getIndexOfItem(item);
+ if (index != this.selectedIndex) {
+ this.mouseSelectedIndex = this.selectedIndex = index;
+ }
+
+ this.mLastMoveTime = Date.now();
+ }
+ ]]>
+ </handler>
+ </handlers>
+ </binding>
+
+ <binding id="autocomplete-treebody">
+ <implementation>
+ <field name="mLastMoveTime">Date.now()</field>
+ </implementation>
+
+ <handlers>
+ <handler event="mouseup" action="this.parentNode.parentNode.onPopupClick(event);"/>
+
+ <handler event="mousedown"><![CDATA[
+ var rc = this.parentNode.treeBoxObject.getRowAt(event.clientX, event.clientY);
+ if (rc != this.parentNode.currentIndex)
+ this.parentNode.view.selection.select(rc);
+ ]]></handler>
+
+ <handler event="mousemove"><![CDATA[
+ if (Date.now() - this.mLastMoveTime > 30) {
+ var rc = this.parentNode.treeBoxObject.getRowAt(event.clientX, event.clientY);
+ if (rc != this.parentNode.currentIndex)
+ this.parentNode.view.selection.select(rc);
+ this.mLastMoveTime = Date.now();
+ }
+ ]]></handler>
+ </handlers>
+ </binding>
+
+ <binding id="autocomplete-treerows">
+ <content>
+ <xul:hbox flex="1" class="tree-bodybox">
+ <children/>
+ </xul:hbox>
+ <xul:scrollbar xbl:inherits="collapsed=hidescrollbar" orient="vertical" class="tree-scrollbar"/>
+ </content>
+ </binding>
+
+ <binding id="history-dropmarker" extends="chrome://global/content/bindings/general.xml#dropmarker">
+ <handlers>
+ <handler event="mousedown" button="0"><![CDATA[
+ document.getBindingParent(this).toggleHistoryPopup();
+ ]]></handler>
+ </handlers>
+ </binding>
+
+</bindings>