Source code

Revision control

Copy as Markdown

Other Tools

/* vim: set ts=4 sts=4 sw=4 et tw=80: */
/* 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/. */
import { sendFillRequestToParent } from "resource://gre/modules/FillHelpers.sys.mjs";
const formFillController = Cc[
"@mozilla.org/satchel/form-fill-controller;1"
].getService(Ci.nsIFormFillController);
/**
* This autocomplete result combines 3 arrays of entries, fixedEntries and
* externalEntries.
* Entries are Form History entries, they can be removed.
* Fixed entries are "appended" to entries, they are used for datalist items,
* search suggestions and extra items from integrations.
* External entries are meant for integrations, like Firefox Relay.
* Internally entries and fixed entries are kept separated so we can
* reuse and filter them.
*
* @implements {nsIAutoCompleteResult}
*/
export class FormHistoryAutoCompleteResult {
constructor(input, entries, inputName, searchString) {
this.input = input;
this.entries = entries;
this.inputName = inputName;
this.searchString = searchString;
}
QueryInterface = ChromeUtils.generateQI([
"nsIAutoCompleteResult",
"nsISupportsWeakReference",
]);
// private
input = null;
entries = null;
inputName = null;
#fixedEntries = [];
externalEntries = [];
set fixedEntries(value) {
this.#fixedEntries = value;
this.removeDuplicateHistoryEntries();
}
/**
* Remove items from history list that are already present in fixed list.
* We do this rather than the opposite ( i.e. remove items from fixed list)
* to reflect the order that is specified in the fixed list.
*/
removeDuplicateHistoryEntries() {
this.entries = this.entries.filter(entry =>
this.#fixedEntries.every(
fixed => entry.text != (fixed.label || fixed.value)
)
);
}
getAt(index) {
for (const group of [
this.entries,
this.#fixedEntries,
this.externalEntries,
]) {
if (index < group.length) {
return group[index];
}
index -= group.length;
}
throw Components.Exception(
"Index out of range.",
Cr.NS_ERROR_ILLEGAL_VALUE
);
}
// Allow autoCompleteSearch to get at the JS object so it can
// modify some readonly properties for internal use.
get wrappedJSObject() {
return this;
}
// Interfaces from idl...
searchString = "";
errorDescription = "";
get defaultIndex() {
return this.matchCount ? 0 : -1;
}
get searchResult() {
return this.matchCount
? Ci.nsIAutoCompleteResult.RESULT_SUCCESS
: Ci.nsIAutoCompleteResult.RESULT_NOMATCH;
}
get matchCount() {
return (
this.entries.length +
this.#fixedEntries.length +
this.externalEntries.length
);
}
getValueAt(index) {
const item = this.getAt(index);
return item.text || item.value;
}
getLabelAt(index) {
const item = this.getAt(index);
return item.text || item.label || item.value;
}
getCommentAt(index) {
return this.getAt(index).comment ?? "";
}
getStyleAt(index) {
const itemStyle = this.getAt(index).style;
if (itemStyle) {
return itemStyle;
}
if (index >= 0) {
if (index < this.entries.length) {
return "fromhistory";
}
if (index > 0 && index == this.entries.length) {
return "datalist-first";
}
}
return "";
}
getImageAt(_index) {
return "";
}
getFinalCompleteValueAt(index) {
return this.getValueAt(index);
}
isRemovableAt(index) {
return this.#isFormHistoryEntry(index) || this.getAt(index).removable;
}
removeValueAt(index) {
if (this.#isFormHistoryEntry(index)) {
const [removedEntry] = this.entries.splice(index, 1);
const actor =
this.input.ownerGlobal.windowGlobalChild.getActor("FormHistory");
actor.sendAsyncMessage("FormHistory:RemoveEntry", {
inputName: this.inputName,
value: removedEntry.text,
guid: removedEntry.guid,
});
}
}
#isFormHistoryEntry(index) {
return index >= 0 && index < this.entries.length;
}
}
export class FormHistoryAutoComplete {
constructor() {
Services.obs.addObserver(this, "autocomplete-will-enter-text");
}
classID = Components.ID("{23530265-31d1-4ee9-864c-c081975fb7bc}");
QueryInterface = ChromeUtils.generateQI([
"nsIFormHistoryAutoComplete",
"nsISupportsWeakReference",
]);
// AutoCompleteE10S needs to be able to call autoCompleteSearchAsync without
// going through IDL in order to pass a mock DOM object field.
get wrappedJSObject() {
return this;
}
async observe(subject, topic, data) {
switch (topic) {
case "autocomplete-will-enter-text": {
if (subject && subject == formFillController.controller?.input) {
await sendFillRequestToParent("FormHistory", subject, data);
}
break;
}
}
}
}