diff options
Diffstat (limited to 'toolkit/content/widgets/autocomplete.xml')
-rw-r--r-- | toolkit/content/widgets/autocomplete.xml | 2515 |
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> |