/* 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",
});
}