gecko-dev/toolkit/content/widgets/autocomplete.xml

2300 строки
83 KiB
XML

<?xml version="1.0"?>
<!-- This Source Code Form is subject to the terms of the Mozilla Public
- License, v. 2.0. If a copy of the MPL was not distributed with this
- file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
<bindings id="autocompleteBindings"
xmlns="http://www.mozilla.org/xbl"
xmlns:html="http://www.w3.org/1999/xhtml"
xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
xmlns:xbl="http://www.mozilla.org/xbl">
<binding id="autocomplete"
extends="chrome://global/content/bindings/textbox.xml#textbox">
<content sizetopopup="pref">
<children includes="image|box"/>
<xul:moz-input-box anonid="moz-input-box" flex="1">
<children/>
<html:input anonid="input" class="textbox-input"
allowevents="true"
xbl:inherits="value,type=inputtype,maxlength,disabled,size,readonly,placeholder,tabindex,accesskey,mozactionhint"/>
</xul:moz-input-box>
<children includes="hbox"/>
<xul:popupset anonid="popupset" class="autocomplete-result-popupset"/>
</content>
<implementation implements="nsIAutoCompleteInput, nsIDOMXULMenuListElement">
<field name="mController">null</field>
<field name="mSearchNames">null</field>
<field name="mIgnoreInput">false</field>
<field name="_searchBeginHandler">null</field>
<field name="_searchCompleteHandler">null</field>
<field name="_textEnteredHandler">null</field>
<field name="_textRevertedHandler">null</field>
<constructor><![CDATA[
this.mController = Cc["@mozilla.org/autocomplete/controller;1"].
getService(Ci.nsIAutoCompleteController);
this._searchBeginHandler = this.initEventHandler("searchbegin");
this._searchCompleteHandler = this.initEventHandler("searchcomplete");
this._textEnteredHandler = this.initEventHandler("textentered");
this._textRevertedHandler = this.initEventHandler("textreverted");
// For security reasons delay searches on pasted values.
this.inputField.controllers.insertControllerAt(0, this._pasteController);
]]></constructor>
<destructor><![CDATA[
this.inputField.controllers.removeController(this._pasteController);
]]></destructor>
<!-- =================== nsIAutoCompleteInput =================== -->
<field name="_popup">null</field>
<property name="popup" readonly="true">
<getter><![CDATA[
// Memoize the result in a field rather than replacing this property,
// so that it can be reset along with the binding.
if (this._popup) {
return this._popup;
}
let popup = null;
let popupId = this.getAttribute("autocompletepopup");
if (popupId) {
popup = document.getElementById(popupId);
}
if (!popup) {
popup = document.createXULElement("panel");
popup.setAttribute("type", "autocomplete-richlistbox");
popup.setAttribute("noautofocus", "true");
let popupset = document.getAnonymousElementByAttribute(this, "anonid", "popupset");
popupset.appendChild(popup);
}
popup.mInput = this;
return this._popup = popup;
]]></getter>
</property>
<property name="controller" onget="return this.mController;" readonly="true"/>
<property name="popupOpen"
onget="return this.popup.popupOpen;"
onset="if (val) this.openPopup(); else this.closePopup();"/>
<property name="disableAutoComplete"
onset="this.setAttribute('disableautocomplete', val); return val;"
onget="return this.getAttribute('disableautocomplete') == 'true';"/>
<property name="completeDefaultIndex"
onset="this.setAttribute('completedefaultindex', val); return val;"
onget="return this.getAttribute('completedefaultindex') == 'true';"/>
<property name="completeSelectedIndex"
onset="this.setAttribute('completeselectedindex', val); return val;"
onget="return this.getAttribute('completeselectedindex') == 'true';"/>
<property name="forceComplete"
onset="this.setAttribute('forcecomplete', val); return val;"
onget="return this.getAttribute('forcecomplete') == 'true';"/>
<property name="minResultsForPopup"
onset="this.setAttribute('minresultsforpopup', val); return val;"
onget="var m = parseInt(this.getAttribute('minresultsforpopup')); return isNaN(m) ? 1 : m;"/>
<property name="timeout"
onset="this.setAttribute('timeout', val); return val;">
<getter><![CDATA[
// For security reasons delay searches on pasted values.
if (this._valueIsPasted) {
let t = parseInt(this.getAttribute("pastetimeout"));
return isNaN(t) ? 1000 : t;
}
let t = parseInt(this.getAttribute("timeout"));
return isNaN(t) ? 50 : t;
]]></getter>
</property>
<property name="searchParam"
onget="return this.getAttribute('autocompletesearchparam') || '';"
onset="this.setAttribute('autocompletesearchparam', val); return val;"/>
<property name="searchCount" readonly="true"
onget="this.initSearchNames(); return this.mSearchNames.length;"/>
<property name="PrivateBrowsingUtils" readonly="true">
<getter><![CDATA[
let module = {};
ChromeUtils.import("resource://gre/modules/PrivateBrowsingUtils.jsm", module);
Object.defineProperty(this, "PrivateBrowsingUtils", {
configurable: true,
enumerable: true,
writable: true,
value: module.PrivateBrowsingUtils,
});
return module.PrivateBrowsingUtils;
]]></getter>
</property>
<property name="inPrivateContext" readonly="true"
onget="return this.PrivateBrowsingUtils.isWindowPrivate(window);"/>
<property name="noRollupOnCaretMove" readonly="true"
onget="return this.popup.getAttribute('norolluponanchor') == 'true'"/>
<!-- This is the maximum number of drop-down rows we get when we
hit the drop marker beside fields that have it (like the URLbar).-->
<field name="maxDropMarkerRows" readonly="true">14</field>
<method name="getSearchAt">
<parameter name="aIndex"/>
<body><![CDATA[
this.initSearchNames();
return this.mSearchNames[aIndex];
]]></body>
</method>
<method name="setTextValueWithReason">
<parameter name="aValue"/>
<parameter name="aReason"/>
<body><![CDATA[
if (aReason == Ci.nsIAutoCompleteInput
.TEXTVALUE_REASON_COMPLETEDEFAULT) {
this._textValueSetByCompleteDefault = true;
}
this.textValue = aValue;
this._textValueSetByCompleteDefault = false;
]]></body>
</method>
<property name="textValue">
<getter><![CDATA[
if (typeof this.onBeforeTextValueGet == "function") {
let result = this.onBeforeTextValueGet();
if (result) {
return result.value;
}
}
return this.value;
]]></getter>
<setter><![CDATA[
if (typeof this.onBeforeTextValueSet == "function" &&
!this._textValueSetByCompleteDefault) {
val = this.onBeforeTextValueSet(val);
}
// "input" event is automatically dispatched by the editor if
// necessary.
this._setValueInternal(val, true);
return this.value;
]]></setter>
</property>
<method name="selectTextRange">
<parameter name="aStartIndex"/>
<parameter name="aEndIndex"/>
<body><![CDATA[
this.inputField.setSelectionRange(aStartIndex, aEndIndex);
]]></body>
</method>
<method name="onSearchBegin">
<body><![CDATA[
if (this.popup && typeof this.popup.onSearchBegin == "function")
this.popup.onSearchBegin();
if (this._searchBeginHandler)
this._searchBeginHandler();
]]></body>
</method>
<method name="onSearchComplete">
<body><![CDATA[
if (this.mController.matchCount == 0)
this.setAttribute("nomatch", "true");
else
this.removeAttribute("nomatch");
if (this.ignoreBlurWhileSearching && !this.focused) {
this.handleEnter();
this.detachController();
}
if (this._searchCompleteHandler)
this._searchCompleteHandler();
]]></body>
</method>
<method name="onTextEntered">
<parameter name="event"/>
<body><![CDATA[
let rv = false;
if (this._textEnteredHandler) {
rv = this._textEnteredHandler(event);
}
return rv;
]]></body>
</method>
<method name="onTextReverted">
<body><![CDATA[
if (this._textRevertedHandler)
return this._textRevertedHandler();
return false;
]]></body>
</method>
<!-- =================== nsIDOMXULMenuListElement =================== -->
<property name="editable" readonly="true"
onget="return true;" />
<property name="crop"
onset="this.setAttribute('crop',val); return val;"
onget="return this.getAttribute('crop');"/>
<property name="open"
onget="return this.getAttribute('open') == 'true';">
<setter><![CDATA[
if (val)
this.showHistoryPopup();
else
this.closePopup();
]]></setter>
</property>
<!-- =================== PUBLIC MEMBERS =================== -->
<field name="valueIsTyped">false</field>
<field name="_textValueSetByCompleteDefault">false</field>
<property name="value"
onset="return this._setValueInternal(val, false);">
<getter><![CDATA[
if (typeof this.onBeforeValueGet == "function") {
var result = this.onBeforeValueGet();
if (result)
return result.value;
}
return this.inputField.value;
]]></getter>
</property>
<property name="focused" readonly="true"
onget="return this.getAttribute('focused') == 'true';"/>
<!-- maximum number of rows to display at a time -->
<property name="maxRows"
onset="this.setAttribute('maxrows', val); return val;"
onget="return parseInt(this.getAttribute('maxrows')) || 0;"/>
<!-- option to allow scrolling through the list via the tab key, rather than
tab moving focus out of the textbox -->
<property name="tabScrolling"
onset="this.setAttribute('tabscrolling', val); return val;"
onget="return this.getAttribute('tabscrolling') == 'true';"/>
<!-- option to completely ignore any blur events while searches are
still going on. -->
<property name="ignoreBlurWhileSearching"
onset="this.setAttribute('ignoreblurwhilesearching', val); return val;"
onget="return this.getAttribute('ignoreblurwhilesearching') == 'true';"/>
<!-- option to highlight entries that don't have any matches -->
<property name="highlightNonMatches"
onset="this.setAttribute('highlightnonmatches', val); return val;"
onget="return this.getAttribute('highlightnonmatches') == 'true';"/>
<!-- =================== PRIVATE MEMBERS =================== -->
<!-- ::::::::::::: autocomplete controller ::::::::::::: -->
<method name="attachController">
<body><![CDATA[
this.mController.input = this;
]]></body>
</method>
<method name="detachController">
<body><![CDATA[
if (this.mController.input == this)
this.mController.input = null;
]]></body>
</method>
<!-- ::::::::::::: popup opening ::::::::::::: -->
<method name="openPopup">
<body><![CDATA[
if (this.focused)
this.popup.openAutocompletePopup(this, this);
]]></body>
</method>
<method name="closePopup">
<body><![CDATA[
this.popup.closePopup();
]]></body>
</method>
<method name="showHistoryPopup">
<body><![CDATA[
// Store our "normal" maxRows on the popup, so that it can reset the
// value when the popup is hidden.
this.popup._normalMaxRows = this.maxRows;
// Increase our maxRows temporarily, since we want the dropdown to
// be bigger in this case. The popup's popupshowing/popuphiding
// handlers will take care of resetting this.
this.maxRows = this.maxDropMarkerRows;
// Ensure that we have focus.
if (!this.focused)
this.focus();
this.attachController();
this.mController.startSearch("");
]]></body>
</method>
<method name="toggleHistoryPopup">
<body><![CDATA[
if (!this.popup.popupOpen)
this.showHistoryPopup();
else
this.closePopup();
]]></body>
</method>
<!-- ::::::::::::: event dispatching ::::::::::::: -->
<method name="initEventHandler">
<parameter name="aEventType"/>
<body><![CDATA[
let handlerString = this.getAttribute("on" + aEventType);
if (handlerString) {
return (new Function("eventType", "param", handlerString)).bind(this, aEventType);
}
return null;
]]></body>
</method>
<!-- ::::::::::::: key handling ::::::::::::: -->
<field name="_selectionDetails">null</field>
<method name="onKeyPress">
<parameter name="aEvent"/>
<body><![CDATA[
return this.handleKeyPress(aEvent);
]]></body>
</method>
<method name="handleKeyPress">
<parameter name="aEvent"/>
<parameter name="aOptions"/>
<body><![CDATA[
if (aEvent.target.localName != "textbox")
return true; // Let child buttons of autocomplete take input
// Re: urlbarDeferred, see the comment in urlbarBindings.xml.
if (aEvent.defaultPrevented && !aEvent.urlbarDeferred) {
return false;
}
const isMac = /Mac/.test(navigator.platform);
var cancel = false;
// Catch any keys that could potentially move the caret. Ctrl can be
// used in combination with these keys on Windows and Linux; and Alt
// can be used on OS X, so make sure the unused one isn't used.
let metaKey = isMac ? aEvent.ctrlKey : aEvent.altKey;
if (!metaKey) {
switch (aEvent.keyCode) {
case KeyEvent.DOM_VK_LEFT:
case KeyEvent.DOM_VK_RIGHT:
case KeyEvent.DOM_VK_HOME:
cancel = this.mController.handleKeyNavigation(aEvent.keyCode);
break;
}
}
// Handle keys that are not part of a keyboard shortcut (no Ctrl or Alt)
if (!aEvent.ctrlKey && !aEvent.altKey) {
switch (aEvent.keyCode) {
case KeyEvent.DOM_VK_TAB:
if (this.tabScrolling && this.popup.popupOpen)
cancel = this.mController.handleKeyNavigation(aEvent.shiftKey ?
KeyEvent.DOM_VK_UP :
KeyEvent.DOM_VK_DOWN);
else if (this.forceComplete && this.mController.matchCount >= 1)
this.mController.handleTab();
break;
case KeyEvent.DOM_VK_UP:
case KeyEvent.DOM_VK_DOWN:
case KeyEvent.DOM_VK_PAGE_UP:
case KeyEvent.DOM_VK_PAGE_DOWN:
cancel = this.mController.handleKeyNavigation(aEvent.keyCode);
break;
}
}
// Handle readline/emacs-style navigation bindings on Mac.
if (isMac &&
this.popup.popupOpen &&
aEvent.ctrlKey &&
(aEvent.key === "n" || aEvent.key === "p")) {
const effectiveKey = (aEvent.key === "p") ?
KeyEvent.DOM_VK_UP :
KeyEvent.DOM_VK_DOWN;
cancel = this.mController.handleKeyNavigation(effectiveKey);
}
// Handle keys we know aren't part of a shortcut, even with Alt or
// Ctrl.
switch (aEvent.keyCode) {
case KeyEvent.DOM_VK_ESCAPE:
cancel = this.mController.handleEscape();
break;
case KeyEvent.DOM_VK_RETURN:
if (isMac) {
// Prevent the default action, since it will beep on Mac
if (aEvent.metaKey)
aEvent.preventDefault();
}
if (this.popup.selectedIndex >= 0) {
this._selectionDetails = {
index: this.popup.selectedIndex,
kind: "key",
};
}
cancel = this.handleEnter(aEvent, aOptions);
break;
case KeyEvent.DOM_VK_DELETE:
if (isMac && !aEvent.shiftKey) {
break;
}
cancel = this.handleDelete();
break;
case KeyEvent.DOM_VK_BACK_SPACE:
if (isMac && aEvent.shiftKey) {
cancel = this.handleDelete();
}
break;
case KeyEvent.DOM_VK_DOWN:
case KeyEvent.DOM_VK_UP:
if (aEvent.altKey)
this.toggleHistoryPopup();
break;
case KeyEvent.DOM_VK_F4:
if (!isMac) {
this.toggleHistoryPopup();
}
break;
}
if (cancel) {
aEvent.stopPropagation();
aEvent.preventDefault();
}
return true;
]]></body>
</method>
<method name="handleEnter">
<parameter name="event"/>
<body><![CDATA[
return this.mController.handleEnter(false, event || null);
]]></body>
</method>
<method name="handleDelete">
<body><![CDATA[
return this.mController.handleDelete();
]]></body>
</method>
<!-- ::::::::::::: miscellaneous ::::::::::::: -->
<method name="initSearchNames">
<body><![CDATA[
if (!this.mSearchNames) {
var names = this.getAttribute("autocompletesearch");
if (!names)
this.mSearchNames = [];
else
this.mSearchNames = names.split(" ");
}
]]></body>
</method>
<method name="_focus">
<!-- doesn't reset this.mController -->
<body><![CDATA[
this._dontBlur = true;
this.focus();
this._dontBlur = false;
]]></body>
</method>
<method name="resetActionType">
<body><![CDATA[
if (this.mIgnoreInput)
return;
this.removeAttribute("actiontype");
]]></body>
</method>
<field name="_valueIsPasted">false</field>
<field name="_pasteController"><![CDATA[
({
_autocomplete: this,
_kGlobalClipboard: Ci.nsIClipboard.kGlobalClipboard,
supportsCommand: aCommand => aCommand == "cmd_paste",
doCommand(aCommand) {
this._autocomplete._valueIsPasted = true;
this._autocomplete.editor.paste(this._kGlobalClipboard);
this._autocomplete._valueIsPasted = false;
},
isCommandEnabled(aCommand) {
return this._autocomplete.editor.isSelectionEditable &&
this._autocomplete.editor.canPaste(this._kGlobalClipboard);
},
onEvent() {},
})
]]></field>
<method name="_setValueInternal">
<parameter name="aValue"/>
<parameter name="aIsUserInput"/>
<body><![CDATA[
this.mIgnoreInput = true;
if (typeof this.onBeforeValueSet == "function")
aValue = this.onBeforeValueSet(aValue);
if (typeof this.trimValue == "function" &&
!this._textValueSetByCompleteDefault)
aValue = this.trimValue(aValue);
this.valueIsTyped = false;
if (aIsUserInput) {
this.inputField.setUserInput(aValue);
} else {
this.inputField.value = aValue;
}
if (typeof this.formatValue == "function")
this.formatValue();
this.mIgnoreInput = false;
var event = document.createEvent("Events");
event.initEvent("ValueChange", true, true);
this.inputField.dispatchEvent(event);
return aValue;
]]></body>
</method>
<method name="onInput">
<parameter name="aEvent"/>
<body><![CDATA[
if (!this.mIgnoreInput && this.mController.input == this) {
this.valueIsTyped = true;
this.mController.handleText();
}
this.resetActionType();
]]></body>
</method>
</implementation>
<handlers>
<handler event="input"><![CDATA[
this.onInput(event);
]]></handler>
<handler event="keypress" phase="capturing" group="system"
action="return this.onKeyPress(event);"/>
<handler event="compositionstart" phase="capturing"
action="if (this.mController.input == this) this.mController.handleStartComposition();"/>
<handler event="compositionend" phase="capturing"
action="if (this.mController.input == this) this.mController.handleEndComposition();"/>
<handler event="focus" phase="capturing"><![CDATA[
this.attachController();
if (window.gBrowser && window.gBrowser.selectedBrowser.hasAttribute("usercontextid")) {
this.userContextId = parseInt(window.gBrowser.selectedBrowser.getAttribute("usercontextid"));
} else {
this.userContextId = 0;
}
]]></handler>
<handler event="blur" phase="capturing"><![CDATA[
if (!this._dontBlur) {
if (this.forceComplete && this.mController.matchCount >= 1) {
// If forceComplete is requested, we need to call the enter processing
// on blur so the input will be forced to the closest match.
// Thunderbird is the only consumer of forceComplete and this is used
// to force an recipient's email to the exact address book entry.
this.mController.handleEnter(true);
}
if (!this.ignoreBlurWhileSearching)
this.detachController();
}
]]></handler>
</handlers>
</binding>
<binding id="autocomplete-rich-result-popup" extends="chrome://global/content/bindings/popup.xml#popup">
<content ignorekeys="true" level="top" consumeoutsideclicks="never">
<xul:richlistbox anonid="richlistbox" class="autocomplete-richlistbox" flex="1"/>
<xul:hbox>
<children/>
</xul:hbox>
</content>
<implementation implements="nsIAutoCompletePopup">
<field name="mInput">null</field>
<field name="mPopupOpen">false</field>
<field name="_currentIndex">0</field>
<constructor><![CDATA[
if (!this.listEvents) {
this.listEvents = {
handleEvent: event => {
if (!this.parentNode) {
return;
}
switch (event.type) {
case "mouseup":
// Don't call onPopupClick for the scrollbar buttons, thumb,
// slider, etc. If we hit the richlistbox and not a
// richlistitem, we ignore the event.
if (event.target.closest("richlistbox,richlistitem")
.localName == "richlistitem") {
this.onPopupClick(event);
}
break;
case "mousemove":
if (Date.now() - this.mLastMoveTime <= 30) {
return;
}
let item = event.target.closest("richlistbox,richlistitem");
// If we hit the richlistbox and not a richlistitem, we ignore
// the event.
if (item.localName == "richlistbox") {
return;
}
let index = this.richlistbox.getIndexOfItem(item);
this.mousedOverIndex = index;
if (item.selectedByMouseOver) {
this.richlistbox.selectedIndex = index;
}
this.mLastMoveTime = Date.now();
break;
}
},
};
this.richlistbox.addEventListener("mouseup", this.listEvents);
this.richlistbox.addEventListener("mousemove", this.listEvents);
}
]]></constructor>
<destructor><![CDATA[
if (this.listEvents) {
this.richlistbox.removeEventListener("mouseup", this.listEvents);
this.richlistbox.removeEventListener("mousemove", this.listEvents);
delete this.listEvents;
}
]]></destructor>
<!-- =================== nsIAutoCompletePopup =================== -->
<property name="input" readonly="true"
onget="return this.mInput"/>
<property name="overrideValue" readonly="true"
onget="return null;"/>
<property name="popupOpen" readonly="true"
onget="return this.mPopupOpen;"/>
<method name="closePopup">
<body>
<![CDATA[
if (this.mPopupOpen) {
this.hidePopup();
this.removeAttribute("width");
}
]]>
</body>
</method>
<!-- This is the default number of rows that we give the autocomplete
popup when the textbox doesn't have a "maxrows" attribute
for us to use. -->
<field name="defaultMaxRows" readonly="true">6</field>
<!-- In some cases (e.g. when the input's dropmarker button is clicked),
the input wants to display a popup with more rows. In that case, it
should increase its maxRows property and store the "normal" maxRows
in this field. When the popup is hidden, we restore the input's
maxRows to the value stored in this field.
This field is set to -1 between uses so that we can tell when it's
been set by the input and when we need to set it in the popupshowing
handler. -->
<field name="_normalMaxRows">-1</field>
<property name="maxRows" readonly="true">
<getter>
<![CDATA[
return (this.mInput && this.mInput.maxRows) || this.defaultMaxRows;
]]>
</getter>
</property>
<method name="getNextIndex">
<parameter name="aReverse"/>
<parameter name="aAmount"/>
<parameter name="aIndex"/>
<parameter name="aMaxRow"/>
<body><![CDATA[
if (aMaxRow < 0)
return -1;
var newIdx = aIndex + (aReverse ? -1 : 1) * aAmount;
if (aReverse && aIndex == -1 || newIdx > aMaxRow && aIndex != aMaxRow)
newIdx = aMaxRow;
else if (!aReverse && aIndex == -1 || newIdx < 0 && aIndex != 0)
newIdx = 0;
if (newIdx < 0 && aIndex == 0 || newIdx > aMaxRow && aIndex == aMaxRow)
aIndex = -1;
else
aIndex = newIdx;
return aIndex;
]]></body>
</method>
<method name="onPopupClick">
<parameter name="aEvent"/>
<body><![CDATA[
this.input.controller.handleEnter(true, aEvent);
]]></body>
</method>
<property name="selectedIndex"
onget="return this.richlistbox.selectedIndex;">
<setter>
<![CDATA[
if (val != this.richlistbox.selectedIndex) {
this._previousSelectedIndex = this.richlistbox.selectedIndex;
}
this.richlistbox.selectedIndex = val;
// Since ensureElementIsVisible may cause an expensive Layout flush,
// invoke it only if there may be a scrollbar, so if we could fetch
// more results than we can show at once.
// maxResults is the maximum number of fetched results, maxRows is the
// maximum number of rows we show at once, without a scrollbar.
if (this.mPopupOpen && this.maxResults > this.maxRows) {
// when clearing the selection (val == -1, so selectedItem will be
// null), we want to scroll back to the top. see bug #406194
this.richlistbox.ensureElementIsVisible(
this.richlistbox.selectedItem || this.richlistbox.firstElementChild);
}
return val;
]]>
</setter>
</property>
<field name="_previousSelectedIndex">-1</field>
<field name="mLastMoveTime">Date.now()</field>
<field name="mousedOverIndex">-1</field>
<method name="onSearchBegin">
<body><![CDATA[
this.mousedOverIndex = -1;
if (typeof this._onSearchBegin == "function") {
this._onSearchBegin();
}
]]></body>
</method>
<method name="openAutocompletePopup">
<parameter name="aInput"/>
<parameter name="aElement"/>
<body>
<![CDATA[
// until we have "baseBinding", (see bug #373652) this allows
// us to override openAutocompletePopup(), but still call
// the method on the base class
this._openAutocompletePopup(aInput, aElement);
]]>
</body>
</method>
<method name="_openAutocompletePopup">
<parameter name="aInput"/>
<parameter name="aElement"/>
<body>
<![CDATA[
if (!this.mPopupOpen) {
// It's possible that the panel is hidden initially
// to avoid impacting startup / new window performance
aInput.popup.hidden = false;
this.mInput = aInput;
// clear any previous selection, see bugs 400671 and 488357
this.selectedIndex = -1;
var width = aElement.getBoundingClientRect().width;
this.setAttribute("width", width > 100 ? width : 100);
// invalidate() depends on the width attribute
this._invalidate();
this.openPopup(aElement, "after_start", 0, 0, false, false);
}
]]>
</body>
</method>
<method name="invalidate">
<parameter name="reason"/>
<body>
<![CDATA[
// Don't bother doing work if we're not even showing
if (!this.mPopupOpen)
return;
this._invalidate(reason);
]]>
</body>
</method>
<method name="_invalidate">
<parameter name="reason"/>
<body>
<![CDATA[
// collapsed if no matches
this.richlistbox.collapsed = (this.matchCount == 0);
// Update the richlistbox height.
if (this._adjustHeightRAFToken) {
cancelAnimationFrame(this._adjustHeightRAFToken);
this._adjustHeightRAFToken = null;
}
if (this.mPopupOpen) {
delete this._adjustHeightOnPopupShown;
this._adjustHeightRAFToken = requestAnimationFrame(() => this.adjustHeight());
} else {
this._adjustHeightOnPopupShown = true;
}
this._currentIndex = 0;
if (this._appendResultTimeout) {
clearTimeout(this._appendResultTimeout);
}
this._appendCurrentResult(reason);
]]>
</body>
</method>
<property name="maxResults" readonly="true">
<getter>
<![CDATA[
// This is how many richlistitems will be kept around.
// Note, this getter may be overridden, or instances
// can have the nomaxresults attribute set to have no
// limit.
if (this.getAttribute("nomaxresults") == "true") {
return Infinity;
}
return 20;
]]>
</getter>
</property>
<property name="matchCount" readonly="true">
<getter>
<![CDATA[
return Math.min(this.mInput.controller.matchCount, this.maxResults);
]]>
</getter>
</property>
<method name="_collapseUnusedItems">
<body>
<![CDATA[
let existingItemsCount = this.richlistbox.children.length;
for (let i = this.matchCount; i < existingItemsCount; ++i) {
let item = this.richlistbox.children[i];
item.collapsed = true;
if (typeof item._onCollapse == "function") {
item._onCollapse();
}
}
]]>
</body>
</method>
<method name="adjustHeight">
<body>
<![CDATA[
// Figure out how many rows to show
let rows = this.richlistbox.children;
let numRows = Math.min(this.matchCount, this.maxRows, rows.length);
// Default the height to 0 if we have no rows to show
let height = 0;
if (numRows) {
let firstRowRect = rows[0].getBoundingClientRect();
if (this._rlbPadding == undefined) {
let style = window.getComputedStyle(this.richlistbox);
let paddingTop = parseInt(style.paddingTop) || 0;
let paddingBottom = parseInt(style.paddingBottom) || 0;
this._rlbPadding = paddingTop + paddingBottom;
}
// The class `forceHandleUnderflow` is for the item might need to
// handle OverUnderflow or Overflow when the height of an item will
// be changed dynamically.
for (let i = 0; i < numRows; i++) {
if (rows[i].classList.contains("forceHandleUnderflow")) {
rows[i].handleOverUnderflow();
}
}
let lastRowRect = rows[numRows - 1].getBoundingClientRect();
// Calculate the height to have the first row to last row shown
height = lastRowRect.bottom - firstRowRect.top +
this._rlbPadding;
}
let currentHeight = this.richlistbox.getBoundingClientRect().height;
if (height <= currentHeight) {
this._collapseUnusedItems();
}
this.richlistbox.style.removeProperty("height");
this.richlistbox.height = height;
]]>
</body>
</method>
<method name="_appendCurrentResult">
<parameter name="invalidateReason"/>
<body>
<![CDATA[
var controller = this.mInput.controller;
var matchCount = this.matchCount;
var existingItemsCount = this.richlistbox.children.length;
// Process maxRows per chunk to improve performance and user experience
for (let i = 0; i < this.maxRows; i++) {
if (this._currentIndex >= matchCount) {
break;
}
let item;
let reusable = false;
let itemExists = this._currentIndex < existingItemsCount;
let originalValue, originalText, originalType;
let style = controller.getStyleAt(this._currentIndex);
let value =
style && style.includes("autofill") ?
controller.getFinalCompleteValueAt(this._currentIndex) :
controller.getValueAt(this._currentIndex);
let label = controller.getLabelAt(this._currentIndex);
let comment = controller.getCommentAt(this._currentIndex);
let image = controller.getImageAt(this._currentIndex);
// trim the leading/trailing whitespace
let trimmedSearchString = controller.searchString.replace(/^\s+/, "").replace(/\s+$/, "");
if (itemExists) {
item = this.richlistbox.children[this._currentIndex];
// Url may be a modified version of value, see _adjustAcItem().
originalValue = item.getAttribute("url") || item.getAttribute("ac-value");
originalText = item.getAttribute("ac-text");
originalType = item.getAttribute("originaltype");
// The styles on the list which have different <content> structure and overrided
// _adjustAcItem() are unreusable.
const UNREUSEABLE_STYLES = [
"autofill-profile",
"autofill-footer",
"autofill-clear-button",
"autofill-insecureWarning",
];
// Reuse the item when its style is exactly equal to the previous style or
// neither of their style are in the UNREUSEABLE_STYLES.
reusable = originalType === style ||
!(UNREUSEABLE_STYLES.includes(style) || UNREUSEABLE_STYLES.includes(originalType));
} else {
// need to create a new item
item = document.createXULElement("richlistitem");
}
item.setAttribute("dir", this.style.direction);
item.setAttribute("ac-image", image);
item.setAttribute("ac-value", value);
item.setAttribute("ac-label", label);
item.setAttribute("ac-comment", comment);
item.setAttribute("ac-text", trimmedSearchString);
// Completely reuse the existing richlistitem for invalidation
// due to new results, but only when: the item is the same, *OR*
// we are about to replace the currently moused-over item, to
// avoid surprising the user.
let iface = Ci.nsIAutoCompletePopup;
if (reusable &&
originalText == trimmedSearchString &&
invalidateReason == iface.INVALIDATE_REASON_NEW_RESULT &&
(originalValue == value ||
this.mousedOverIndex === this._currentIndex)) {
// try to re-use the existing item
let reused = item._reuseAcItem();
if (reused) {
this._currentIndex++;
continue;
}
} else {
if (typeof item._cleanup == "function") {
item._cleanup();
}
item.setAttribute("originaltype", style);
}
if (itemExists) {
// Adjust only when the result's type is reusable for existing
// item's. Otherwise, we might insensibly call old _adjustAcItem()
// as new binding has not been attached yet.
// We don't need to worry about switching to new binding, since
// _adjustAcItem() will fired by its own constructor accordingly.
if (reusable) {
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);
}
this._currentIndex++;
}
if (typeof this.onResultsAdded == "function") {
// The items bindings may not be attached yet, so we must delay this
// before we can properly handle items properly without breaking
// the richlistbox.
Services.tm.dispatchToMainThread(() => this.onResultsAdded());
}
if (this._currentIndex < matchCount) {
// yield after each batch of items so that typing the url bar is
// responsive
this._appendResultTimeout = setTimeout(() => this._appendCurrentResult(), 0);
}
]]>
</body>
</method>
<property name="overflowPadding"
onget="return Number(this.getAttribute('overflowpadding'))"
readonly="true" />
<method name="selectBy">
<parameter name="aReverse"/>
<parameter name="aPage"/>
<body>
<![CDATA[
try {
var amount = aPage ? 5 : 1;
// because we collapsed unused items, we can't use this.richlistbox.getRowCount(), we need to use the matchCount
this.selectedIndex = this.getNextIndex(aReverse, amount, this.selectedIndex, this.matchCount - 1);
if (this.selectedIndex == -1) {
this.input._focus();
}
} catch (ex) {
// do nothing - occasionally timer-related js errors happen here
// e.g. "this.selectedIndex has no properties", when you type fast and hit a
// navigation key before this popup has opened
}
]]>
</body>
</method>
<field name="richlistbox">
document.getAnonymousElementByAttribute(this, "anonid", "richlistbox");
</field>
<property name="view"
onget="return this.mInput.controller;"
onset="return val;"/>
</implementation>
<handlers>
<handler event="popupshowing"><![CDATA[
// If normalMaxRows wasn't already set by the input, then set it here
// so that we restore the correct number when the popup is hidden.
// Null-check this.mInput; see bug 1017914
if (this._normalMaxRows < 0 && this.mInput) {
this._normalMaxRows = this.mInput.maxRows;
}
// Set an attribute for styling the popup based on the input.
let inputID = "";
if (this.mInput && this.mInput.ownerDocument &&
this.mInput.ownerDocument.documentURIObject.schemeIs("chrome")) {
inputID = this.mInput.id;
// Take care of elements with no id that are inside xbl bindings
if (!inputID) {
let bindingParent = this.mInput.ownerDocument.getBindingParent(this.mInput);
if (bindingParent) {
inputID = bindingParent.id;
}
}
}
this.setAttribute("autocompleteinput", inputID);
this.mPopupOpen = true;
]]></handler>
<handler event="popupshown">
<![CDATA[
if (this._adjustHeightOnPopupShown) {
delete this._adjustHeightOnPopupShown;
this.adjustHeight();
}
]]>
</handler>
<handler event="popuphiding"><![CDATA[
var isListActive = true;
if (this.selectedIndex == -1)
isListActive = false;
this.input.controller.stopSearch();
this.removeAttribute("autocompleteinput");
this.mPopupOpen = false;
// Reset the maxRows property to the cached "normal" value (if there's
// any), and reset normalMaxRows so that we can detect whether it was set
// by the input when the popupshowing handler runs.
// Null-check this.mInput; see bug 1017914
if (this.mInput && this._normalMaxRows > 0) {
this.mInput.maxRows = this._normalMaxRows;
}
this._normalMaxRows = -1;
// If the list was being navigated and then closed, make sure
// we fire accessible focus event back to textbox
// Null-check this.mInput; see bug 1017914
if (isListActive && this.mInput) {
this.mInput.mIgnoreFocus = true;
this.mInput._focus();
this.mInput.mIgnoreFocus = false;
}
]]></handler>
</handlers>
</binding>
<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 = this.Services.urlFormatter.formatURLPref("app.support.baseURL");
window.openTrustedLinkIn(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 =
this.Services.strings.createBundle("chrome://passwordmgr/locale/passwordmgr.properties").
GetStringFromName("insecureFieldWarningLearnMore");
}
return this.__learnMoreString;
]]></getter>
</property>
<!-- Override _getSearchTokens to have the Learn More text emphasized -->
<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="Services" readonly="true">
<getter><![CDATA[
let module = {};
if (window.Services) {
module.Services = window.Services;
} else {
ChromeUtils.import("resource://gre/modules/Services.jsm", module);
}
Object.defineProperty(this, "Services", {
configurable: true,
enumerable: true,
writable: true,
value: module.Services,
});
return module.Services;
]]></getter>
</property>
<method name="_cleanup">
<body>
<![CDATA[
this.removeAttribute("url");
this.removeAttribute("image");
this.removeAttribute("title");
this.removeAttribute("text");
this.removeAttribute("displayurl");
]]>
</body>
</method>
<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 = this.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 =
Cc["@mozilla.org/preferences-service;1"].
getService(Ci.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.firstChild.remove();
// 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>
<method name="_unescapeUrl">
<parameter name="url"/>
<body>
<![CDATA[
return this.Services.textToSubURI.unEscapeURIForUI("UTF-8", url);
]]>
</body>
</method>
<method name="_reuseAcItem">
<body>
<![CDATA[
let action = this._parseActionUrl(this.getAttribute("url"));
let popup = this.parentNode.parentNode;
// 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.
if (!action ||
action.type != "searchengine" ||
!popup.overrideSearchEngineName ||
action.params.engineName == popup.overrideSearchEngineName) {
this.collapsed = false;
// The popup may have changed size between now and the last
// time the item was shown, so always handle over/underflow.
let dwu = window.windowUtils;
let popupWidth = dwu.getBoundsWithoutFlushing(this.parentNode).width;
if (!this._previousPopupWidth || this._previousPopupWidth != popupWidth) {
this._previousPopupWidth = popupWidth;
this.handleOverUnderflow();
}
return true;
}
return false;
]]>
</body>
</method>
<method name="_adjustAcItem">
<body>
<![CDATA[
let originalUrl = this.getAttribute("ac-value");
let title = this.getAttribute("ac-comment");
this.setAttribute("url", originalUrl);
this.setAttribute("image", this.getAttribute("ac-image"));
this.setAttribute("title", title);
this.setAttribute("text", this.getAttribute("ac-text"));
let popup = this.parentNode.parentNode;
let titleLooksLikeUrl = false;
let displayUrl = originalUrl;
let emphasiseUrl = true;
let trimDisplayUrl = 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") && !initialTypes.has("action")) {
// Treat autofills as visiturl actions, unless they are already also
// actions.
action = {
type: "visiturl",
params: { url: title },
};
}
this.removeAttribute("actiontype");
this.classList.remove(
"overridable-action",
"emptySearchQuery",
"aliasOffer"
);
// 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);
switch (action.type) {
case "switchtab": {
this.classList.add("overridable-action");
displayUrl = action.params.url;
let desc = this._stringBundle.GetStringFromName("switchToTab2");
this._setUpDescription(this._actionText, desc, true);
break;
}
case "remotetab": {
displayUrl = action.params.url;
let desc = action.params.deviceName;
this._setUpDescription(this._actionText, desc, true);
break;
}
case "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,
alias,
} = 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 if (alias &&
!searchQuery.trim() &&
!initialTypes.has("heuristic")) {
// For non-heuristic alias results that have an empty query, we
// want to show "@engine -- Search with Engine" to make it clear
// that the user can search by selecting the result and using
// the alias. Normally we hide the "Search with Engine" part
// until the result is selected or moused over, but not here.
// Add the aliasOffer class so we can detect this in the CSS.
this.classList.add("aliasOffer");
pairs = [
[alias, ""],
];
} else {
// Add the emptySearchQuery class if the search query is the
// empty string. We use it to hide .ac-separator in CSS.
if (!searchQuery.trim()) {
this.classList.add("emptySearchQuery");
}
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");
}
break;
}
case "visiturl": {
emphasiseUrl = false;
displayUrl = action.params.url;
titleLooksLikeUrl = true;
let visitStr = this._stringBundle.GetStringFromName("visit");
this._setUpDescription(this._actionText, visitStr, true);
break;
}
case "extension": {
let content = action.params.content;
displayUrl = content;
trimDisplayUrl = false;
this._setUpDescription(this._actionText, content, true);
break;
}
}
}
if (trimDisplayUrl) {
let input = popup.input;
if (typeof input.trimValue == "function")
displayUrl = input.trimValue(displayUrl);
displayUrl = this._unescapeUrl(displayUrl);
}
// For performance reasons we may want to limit the displayUrl size.
if (popup.textRunsMaxLen && displayUrl) {
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) {
titleLooksLikeUrl = true;
try {
let uri = this.Services.io.newURI(originalUrl);
// Not all valid URLs have a domain.
if (uri.host)
title = uri.host;
} catch (e) {}
if (!title)
title = displayUrl;
}
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.forEach(t => {
// Limit all the even items.
for (let i = 0; i < t.length; i += 2) {
t[i] = t[i].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 = title.substr(0, popup.textRunsMaxLen);
}
this._setUpDescription(this._titleText, title, false);
}
this._setUpDescription(this._urlText, displayUrl, !emphasiseUrl);
// Removing the max-width may be jarring when the item is visible, but
// we have no other choice to properly crop the text.
// Removing max-widths may cause overflow or underflow events, that
// will set the _inOverflow property. In case both the old and the new
// text are overflowing, the overflow event won't happen, and we must
// enforce an _handleOverflow() call to update the max-widths.
let wasInOverflow = this._inOverflow;
this._removeMaxWidths();
if (wasInOverflow && this._inOverflow) {
this._handleOverflow();
}
]]>
</body>
</method>
<method name="_removeMaxWidths">
<body>
<![CDATA[
if (this._hasMaxWidths) {
this._titleText.style.removeProperty("max-width");
this._tagsText.style.removeProperty("max-width");
this._urlText.style.removeProperty("max-width");
this._actionText.style.removeProperty("max-width");
this._hasMaxWidths = false;
}
]]>
</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 -
(popup.margins ? popup.margins.end : 0);
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";
this._hasMaxWidths = true;
}
]]></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,
};
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>
<handlers>
<!--
This overrides listitem's mousedown handler because we want to set the
selected item even when the shift or accel keys are pressed.
-->
<handler event="mousedown"><![CDATA[
// Call this.control only once since it's not a simple getter.
let control = this.control;
if (!control || control.disabled) {
return;
}
if (!this.selected) {
control.selectItem(this);
}
control.currentItem = this;
]]></handler>
<handler event="mouseover"><![CDATA[
// The point of implementing this handler is to allow drags to change
// the selected item. If the user mouses down on an item, it becomes
// selected. If they then drag the mouse to another item, select it.
// Handle all three primary mouse buttons: right, left, and wheel, since
// all three change the selection on mousedown.
let mouseDown = event.buttons & 0b111;
if (!mouseDown) {
return;
}
// Call this.control only once since it's not a simple getter.
let control = this.control;
if (!control || control.disabled) {
return;
}
if (!this.selected) {
control.selectItem(this);
}
control.currentItem = this;
]]></handler>
</handlers>
</binding>
</bindings>