Source code

Revision control

Copy as Markdown

Other Tools

/* 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/. */
"use strict";
// This is loaded into all XUL windows. Wrap in a block to prevent
// leaking to window scope.
{
const { AppConstants } = ChromeUtils.importESModule(
"resource://gre/modules/AppConstants.sys.mjs"
);
const { XPCOMUtils } = ChromeUtils.importESModule(
"resource://gre/modules/XPCOMUtils.sys.mjs"
);
class AutocompleteInput extends HTMLInputElement {
constructor() {
super();
this.popupSelectedIndex = -1;
ChromeUtils.defineESModuleGetters(this, {
PrivateBrowsingUtils:
"resource://gre/modules/PrivateBrowsingUtils.sys.mjs",
});
XPCOMUtils.defineLazyPreferenceGetter(
this,
"disablePopupAutohide",
"ui.popup.disable_autohide",
false
);
this.addEventListener("input", event => {
this.onInput(event);
});
this.addEventListener("keydown", event => this.handleKeyDown(event));
this.addEventListener(
"compositionstart",
() => {
if (
this.mController.input.wrappedJSObject == this.nsIAutocompleteInput
) {
this.mController.handleStartComposition();
}
},
true
);
this.addEventListener(
"compositionend",
() => {
if (
this.mController.input.wrappedJSObject == this.nsIAutocompleteInput
) {
this.mController.handleEndComposition();
}
},
true
);
this.addEventListener(
"focus",
() => {
this.attachController();
if (
window.gBrowser &&
window.gBrowser.selectedBrowser.hasAttribute("usercontextid")
) {
this.userContextId = parseInt(
window.gBrowser.selectedBrowser.getAttribute("usercontextid")
);
} else {
this.userContextId = 0;
}
},
true
);
this.addEventListener(
"blur",
() => {
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._dontClosePopup = this.disablePopupAutohide;
this.detachController();
}
}
},
true
);
}
connectedCallback() {
this.setAttribute("is", "autocomplete-input");
this.setAttribute("autocomplete", "off");
this.mController = Cc[
"@mozilla.org/autocomplete/controller;1"
].getService(Ci.nsIAutoCompleteController);
this.mSearchNames = null;
this.mIgnoreInput = false;
this.noRollupOnEmptySearch = false;
this._popup = null;
this.nsIAutocompleteInput = this.getCustomInterfaceCallback(
Ci.nsIAutoCompleteInput
);
this.valueIsTyped = false;
}
get popup() {
// 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);
}
/* This path is only used in tests, we have the <popupset> and <panel>
in document for other usages */
if (!popup) {
popup = document.createXULElement("panel", {
is: "autocomplete-richlistbox-popup",
});
popup.setAttribute("type", "autocomplete-richlistbox");
popup.setAttribute("noautofocus", "true");
if (!this._popupset) {
this._popupset = document.createXULElement("popupset");
document.documentElement.appendChild(this._popupset);
}
this._popupset.appendChild(popup);
}
popup.mInput = this;
return (this._popup = popup);
}
get popupElement() {
return this.popup;
}
get controller() {
return this.mController;
}
set popupOpen(val) {
if (val) {
this.openPopup();
} else {
this.closePopup();
}
}
get popupOpen() {
return this.popup.popupOpen;
}
set disableAutoComplete(val) {
this.setAttribute("disableautocomplete", val);
}
get disableAutoComplete() {
return this.getAttribute("disableautocomplete") == "true";
}
set completeDefaultIndex(val) {
this.setAttribute("completedefaultindex", val);
}
get completeDefaultIndex() {
return this.getAttribute("completedefaultindex") == "true";
}
set completeSelectedIndex(val) {
this.setAttribute("completeselectedindex", val);
}
get completeSelectedIndex() {
return this.getAttribute("completeselectedindex") == "true";
}
set forceComplete(val) {
this.setAttribute("forcecomplete", val);
}
get forceComplete() {
return this.getAttribute("forcecomplete") == "true";
}
set minResultsForPopup(val) {
this.setAttribute("minresultsforpopup", val);
}
get minResultsForPopup() {
var m = parseInt(this.getAttribute("minresultsforpopup"));
return isNaN(m) ? 1 : m;
}
set timeout(val) {
this.setAttribute("timeout", val);
}
get timeout() {
var t = parseInt(this.getAttribute("timeout"));
return isNaN(t) ? 50 : t;
}
set searchParam(val) {
this.setAttribute("autocompletesearchparam", val);
}
get searchParam() {
return this.getAttribute("autocompletesearchparam") || "";
}
get searchCount() {
this.initSearchNames();
return this.mSearchNames.length;
}
get inPrivateContext() {
return this.PrivateBrowsingUtils.isWindowPrivate(window);
}
get noRollupOnCaretMove() {
return this.popup.getAttribute("norolluponanchor") == "true";
}
set textValue(val) {
// "input" event is automatically dispatched by the editor if
// necessary.
this._setValueInternal(val, true);
}
get textValue() {
return this.value;
}
/**
* =================== nsIDOMXULMenuListElement ===================
*/
get editable() {
return true;
}
set open(val) {
if (val) {
this.showHistoryPopup();
} else {
this.closePopup();
}
}
get open() {
return this.getAttribute("open") == "true";
}
set value(val) {
this._setValueInternal(val, false);
}
get value() {
return super.value;
}
get focused() {
return this === document.activeElement;
}
/**
* maximum number of rows to display at a time when opening the popup normally
* (e.g., focus element and press the down arrow)
*/
set maxRows(val) {
this.setAttribute("maxrows", val);
}
get maxRows() {
return parseInt(this.getAttribute("maxrows")) || 0;
}
/**
* maximum number of rows to display at a time when opening the popup by
* clicking the dropmarker (for inputs that have one)
*/
set maxdropmarkerrows(val) {
this.setAttribute("maxdropmarkerrows", val);
}
get maxdropmarkerrows() {
return parseInt(this.getAttribute("maxdropmarkerrows"), 10) || 14;
}
/**
* option to allow scrolling through the list via the tab key, rather than
* tab moving focus out of the textbox
*/
set tabScrolling(val) {
this.setAttribute("tabscrolling", val);
}
get tabScrolling() {
return this.getAttribute("tabscrolling") == "true";
}
/**
* option to completely ignore any blur events while searches are
* still going on.
*/
set ignoreBlurWhileSearching(val) {
this.setAttribute("ignoreblurwhilesearching", val);
}
get ignoreBlurWhileSearching() {
return this.getAttribute("ignoreblurwhilesearching") == "true";
}
/**
* option to highlight entries that don't have any matches
*/
set highlightNonMatches(val) {
this.setAttribute("highlightnonmatches", val);
}
get highlightNonMatches() {
return this.getAttribute("highlightnonmatches") == "true";
}
getSearchAt(aIndex) {
this.initSearchNames();
return this.mSearchNames[aIndex];
}
selectTextRange(aStartIndex, aEndIndex) {
super.setSelectionRange(aStartIndex, aEndIndex);
}
onSearchBegin() {
if (this.popup && typeof this.popup.onSearchBegin == "function") {
this.popup.onSearchBegin();
}
}
onSearchComplete() {
if (this.mController.matchCount == 0) {
this.setAttribute("nomatch", "true");
} else {
this.removeAttribute("nomatch");
}
if (this.ignoreBlurWhileSearching && !this.focused) {
this.handleEnter();
this.detachController();
}
}
onTextEntered(event) {
if (this.getAttribute("notifylegacyevents") === "true") {
let e = new CustomEvent("textEntered", {
bubbles: false,
cancelable: true,
detail: { rootEvent: event },
});
return !this.dispatchEvent(e);
}
return false;
}
onTextReverted(event) {
if (this.getAttribute("notifylegacyevents") === "true") {
let e = new CustomEvent("textReverted", {
bubbles: false,
cancelable: true,
detail: { rootEvent: event },
});
return !this.dispatchEvent(e);
}
return false;
}
/**
* =================== PRIVATE MEMBERS ===================
*/
/*
* ::::::::::::: autocomplete controller :::::::::::::
*/
attachController() {
this.mController.input = this.nsIAutocompleteInput;
}
detachController() {
if (
this.mController.input &&
this.mController.input.wrappedJSObject == this.nsIAutocompleteInput
) {
this.mController.input = null;
}
}
/**
* ::::::::::::: popup opening :::::::::::::
*/
openPopup() {
if (this.focused) {
this.popup.openAutocompletePopup(this.nsIAutocompleteInput, this);
}
}
closePopup() {
if (this._dontClosePopup) {
delete this._dontClosePopup;
return;
}
this.popup.closePopup();
}
showHistoryPopup() {
// Store our "normal" maxRows on the popup, so that it can reset the
// value when the popup is hidden.
this.popup._normalMaxRows = this.maxRows;
// Temporarily change our maxRows, since we want the dropdown to be a
// different size 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("");
}
toggleHistoryPopup() {
if (!this.popup.popupOpen) {
this.showHistoryPopup();
} else {
this.closePopup();
}
}
handleKeyDown(aEvent) {
// Re: urlbarDeferred, see the comment in urlbarBindings.xml.
if (aEvent.defaultPrevented && !aEvent.urlbarDeferred) {
return false;
}
if (
typeof this.onBeforeHandleKeyDown == "function" &&
this.onBeforeHandleKeyDown(aEvent)
) {
return true;
}
const isMac = AppConstants.platform == "macosx";
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.popupSelectedIndex = this.popup.selectedIndex;
}
cancel = this.handleEnter(aEvent);
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;
}
handleEnter(event) {
return this.mController.handleEnter(false, event || null);
}
handleDelete() {
return this.mController.handleDelete();
}
/**
* ::::::::::::: miscellaneous :::::::::::::
*/
initSearchNames() {
if (!this.mSearchNames) {
var names = this.getAttribute("autocompletesearch");
if (!names) {
this.mSearchNames = [];
} else {
this.mSearchNames = names.split(" ");
}
}
}
_focus() {
this._dontBlur = true;
this.focus();
this._dontBlur = false;
}
resetActionType() {
if (this.mIgnoreInput) {
return;
}
this.removeAttribute("actiontype");
}
_setValueInternal(value, isUserInput) {
this.mIgnoreInput = true;
if (typeof this.onBeforeValueSet == "function") {
value = this.onBeforeValueSet(value);
}
this.valueIsTyped = false;
if (isUserInput) {
super.setUserInput(value);
} else {
super.value = value;
}
this.mIgnoreInput = false;
var event = document.createEvent("Events");
event.initEvent("ValueChange", true, true);
super.dispatchEvent(event);
return value;
}
onInput() {
if (
!this.mIgnoreInput &&
this.mController.input.wrappedJSObject == this.nsIAutocompleteInput
) {
this.valueIsTyped = true;
this.mController.handleText();
}
this.resetActionType();
}
}
MozHTMLElement.implementCustomInterface(AutocompleteInput, [
Ci.nsIAutoCompleteInput,
Ci.nsIDOMXULMenuListElement,
]);
customElements.define("autocomplete-input", AutocompleteInput, {
extends: "input",
});
}