зеркало из https://github.com/mozilla/gecko-dev.git
2300 строки
83 KiB
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>
|