forked from mirrors/gecko-dev
663 lines
20 KiB
JavaScript
663 lines
20 KiB
JavaScript
/* 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 { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs";
|
|
import { AppConstants } from "resource://gre/modules/AppConstants.sys.mjs";
|
|
|
|
const lazy = {};
|
|
|
|
ChromeUtils.defineESModuleGetters(lazy, {
|
|
AddressResult: "resource://autofill/ProfileAutoCompleteResult.sys.mjs",
|
|
AutoCompleteChild: "resource://gre/actors/AutoCompleteChild.sys.mjs",
|
|
AutofillTelemetry: "resource://gre/modules/shared/AutofillTelemetry.sys.mjs",
|
|
CreditCardResult: "resource://autofill/ProfileAutoCompleteResult.sys.mjs",
|
|
GenericAutocompleteItem: "resource://gre/modules/FillHelpers.sys.mjs",
|
|
InsecurePasswordUtils: "resource://gre/modules/InsecurePasswordUtils.sys.mjs",
|
|
FormAutofill: "resource://autofill/FormAutofill.sys.mjs",
|
|
FormAutofillContent: "resource://autofill/FormAutofillContent.sys.mjs",
|
|
FormAutofillUtils: "resource://gre/modules/shared/FormAutofillUtils.sys.mjs",
|
|
FormScenarios: "resource://gre/modules/FormScenarios.sys.mjs",
|
|
FormStateManager: "resource://gre/modules/shared/FormStateManager.sys.mjs",
|
|
PrivateBrowsingUtils: "resource://gre/modules/PrivateBrowsingUtils.sys.mjs",
|
|
setTimeout: "resource://gre/modules/Timer.sys.mjs",
|
|
FORM_SUBMISSION_REASON: "resource://gre/actors/FormHandlerChild.sys.mjs",
|
|
});
|
|
|
|
XPCOMUtils.defineLazyPreferenceGetter(
|
|
lazy,
|
|
"DELEGATE_AUTOCOMPLETE",
|
|
"toolkit.autocomplete.delegate",
|
|
false
|
|
);
|
|
|
|
const formFillController = Cc[
|
|
"@mozilla.org/satchel/form-fill-controller;1"
|
|
].getService(Ci.nsIFormFillController);
|
|
|
|
/**
|
|
* Handles content's interactions for the frame.
|
|
*/
|
|
export class FormAutofillChild extends JSWindowActorChild {
|
|
// Flag indicating whether the form is waiting to be filled by Autofill.
|
|
#autofillPending = false;
|
|
|
|
constructor() {
|
|
super();
|
|
|
|
this.log = lazy.FormAutofill.defineLogGetter(this, "FormAutofillChild");
|
|
this.debug("init");
|
|
|
|
this._nextHandleElement = null;
|
|
this._hasDOMContentLoadedHandler = false;
|
|
this._hasPendingTask = false;
|
|
|
|
/**
|
|
* @type {FormAutofillFieldDetailsManager} handling state management of current forms and handlers.
|
|
*/
|
|
this._fieldDetailsManager = new lazy.FormStateManager(
|
|
this.formSubmitted.bind(this),
|
|
this.formAutofilled.bind(this)
|
|
);
|
|
|
|
lazy.AutoCompleteChild.addPopupStateListener(this);
|
|
|
|
/**
|
|
* Tracks whether the last form submission was triggered by a form submit event,
|
|
* if so we'll ignore the page navigation that follows
|
|
*/
|
|
this.isFollowingSubmitEvent = false;
|
|
}
|
|
|
|
didDestroy() {
|
|
this._fieldDetailsManager.didDestroy();
|
|
|
|
lazy.AutoCompleteChild.removePopupStateListener(this);
|
|
}
|
|
|
|
popupStateChanged(messageName, _data, _target) {
|
|
if (!lazy.FormAutofill.isAutofillEnabled) {
|
|
return;
|
|
}
|
|
|
|
switch (messageName) {
|
|
case "AutoComplete:PopupClosed": {
|
|
this.onPopupClosed();
|
|
break;
|
|
}
|
|
case "AutoComplete:PopupOpened": {
|
|
this.onPopupOpened();
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Identifies and marks each autofill field
|
|
*/
|
|
identifyAutofillFields() {
|
|
if (this._hasPendingTask) {
|
|
return;
|
|
}
|
|
this._hasPendingTask = true;
|
|
|
|
lazy.setTimeout(() => {
|
|
const element = this._nextHandleElement;
|
|
this.debug(
|
|
`identifyAutofillFields: ${element.ownerDocument.location?.hostname}`
|
|
);
|
|
|
|
if (
|
|
lazy.DELEGATE_AUTOCOMPLETE ||
|
|
!lazy.FormAutofillContent.savedFieldNames
|
|
) {
|
|
this.debug("identifyAutofillFields: savedFieldNames are not known yet");
|
|
|
|
// Init can be asynchronous because we don't need anything from the parent
|
|
// at this point.
|
|
this.sendAsyncMessage("FormAutofill:InitStorage");
|
|
}
|
|
|
|
const validDetails =
|
|
this._fieldDetailsManager.identifyAutofillFields(element);
|
|
|
|
validDetails?.forEach(detail =>
|
|
this._markAsAutofillField(detail.element)
|
|
);
|
|
if (validDetails.length) {
|
|
this.manager
|
|
.getActor("FormHandler")
|
|
.registerFormSubmissionInterest(this, {
|
|
includesFormRemoval: lazy.FormAutofill.captureOnFormRemoval,
|
|
includesPageNavigation: lazy.FormAutofill.captureOnPageNavigation,
|
|
});
|
|
}
|
|
|
|
this._hasPendingTask = false;
|
|
this._nextHandleElement = null;
|
|
// This is for testing purpose only which sends a notification to indicate that the
|
|
// form has been identified, and ready to open popup.
|
|
this.sendAsyncMessage("FormAutofill:FieldsIdentified");
|
|
this.updateActiveInput();
|
|
});
|
|
}
|
|
|
|
/**
|
|
* We received a form-submission-detected event because
|
|
* the page was navigated.
|
|
*/
|
|
onPageNavigation() {
|
|
if (!lazy.FormAutofill.captureOnPageNavigation) {
|
|
return;
|
|
}
|
|
|
|
if (this.isFollowingSubmitEvent) {
|
|
// The next page navigation should be handled as form submission again
|
|
this.isFollowingSubmitEvent = false;
|
|
return;
|
|
}
|
|
let weakIdentifiedForms = ChromeUtils.nondeterministicGetWeakMapKeys(
|
|
this._fieldDetailsManager._formsDetails
|
|
);
|
|
const formSubmissionReason = lazy.FORM_SUBMISSION_REASON.PAGE_NAVIGATION;
|
|
|
|
for (const form of weakIdentifiedForms) {
|
|
// Disconnected forms are captured by the form removal heuristic
|
|
if (!form.isConnected) {
|
|
continue;
|
|
}
|
|
this.formSubmitted(form, formSubmissionReason);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* We received a form-submission-detected event because
|
|
* a form was removed from the DOM after a successful
|
|
* xhr/fetch request
|
|
*
|
|
* @param {Event} form form to be submitted
|
|
*/
|
|
onFormRemoval(form) {
|
|
if (!lazy.FormAutofill.captureOnFormRemoval) {
|
|
return;
|
|
}
|
|
|
|
const formSubmissionReason =
|
|
lazy.FORM_SUBMISSION_REASON.FORM_REMOVAL_AFTER_FETCH;
|
|
this.formSubmitted(form, formSubmissionReason);
|
|
this.manager.getActor("FormHandler").unregisterFormRemovalInterest(this);
|
|
}
|
|
|
|
shouldIgnoreFormAutofillEvent(event) {
|
|
let nodePrincipal = event.target.nodePrincipal;
|
|
return nodePrincipal.isSystemPrincipal || nodePrincipal.schemeIs("about");
|
|
}
|
|
|
|
handleEvent(evt) {
|
|
if (!evt.isTrusted) {
|
|
return;
|
|
}
|
|
if (this.shouldIgnoreFormAutofillEvent(evt)) {
|
|
return;
|
|
}
|
|
|
|
if (!this.windowContext) {
|
|
// !this.windowContext must not be null, because we need the
|
|
// windowContext and/or docShell to (un)register form submission listeners
|
|
return;
|
|
}
|
|
|
|
switch (evt.type) {
|
|
case "focusin": {
|
|
if (lazy.FormAutofill.isAutofillEnabled) {
|
|
this.onFocusIn(evt);
|
|
}
|
|
break;
|
|
}
|
|
case "form-submission-detected": {
|
|
if (lazy.FormAutofill.isAutofillEnabled) {
|
|
const formElement = evt.detail.form;
|
|
const formSubmissionReason = evt.detail.reason;
|
|
this.onFormSubmission(formElement, formSubmissionReason);
|
|
}
|
|
break;
|
|
}
|
|
|
|
default: {
|
|
throw new Error("Unexpected event type");
|
|
}
|
|
}
|
|
}
|
|
|
|
onFocusIn(evt) {
|
|
this.updateActiveInput();
|
|
|
|
const element = evt.target;
|
|
if (!lazy.FormAutofillUtils.isCreditCardOrAddressFieldType(element)) {
|
|
return;
|
|
}
|
|
|
|
this._nextHandleElement = element;
|
|
const doc = element.ownerDocument;
|
|
if (doc.readyState === "loading") {
|
|
// For auto-focused input, we might receive focus event before document becomes ready.
|
|
// When this happens, run field identification after receiving `DOMContentLoaded` event
|
|
if (!this._hasDOMContentLoadedHandler) {
|
|
this._hasDOMContentLoadedHandler = true;
|
|
doc.addEventListener(
|
|
"DOMContentLoaded",
|
|
() => this.identifyAutofillFields(),
|
|
{ once: true }
|
|
);
|
|
}
|
|
return;
|
|
}
|
|
|
|
this.identifyAutofillFields();
|
|
}
|
|
|
|
/**
|
|
* Handle form-submission-detected event (dispatched by FormHandlerChild)
|
|
*
|
|
* Depending on the heuristic that detected the form submission,
|
|
* the form that is submitted is retrieved differently
|
|
*
|
|
* @param {HTMLFormElement} form that is being submitted
|
|
* @param {string} reason heuristic that detected the form submission
|
|
* (see FormHandlerChild.FORM_SUBMISSION_REASON)
|
|
*/
|
|
onFormSubmission(form, reason) {
|
|
switch (reason) {
|
|
case lazy.FORM_SUBMISSION_REASON.PAGE_NAVIGATION:
|
|
this.onPageNavigation();
|
|
break;
|
|
case lazy.FORM_SUBMISSION_REASON.FORM_SUBMIT_EVENT:
|
|
this.formSubmitted(form, reason);
|
|
break;
|
|
case lazy.FORM_SUBMISSION_REASON.FORM_REMOVAL_AFTER_FETCH:
|
|
this.onFormRemoval(form);
|
|
break;
|
|
}
|
|
}
|
|
|
|
async receiveMessage(message) {
|
|
if (!lazy.FormAutofill.isAutofillEnabled) {
|
|
return;
|
|
}
|
|
|
|
switch (message.name) {
|
|
case "FormAutofill:PreviewProfile": {
|
|
this.previewProfile(message.data);
|
|
break;
|
|
}
|
|
case "FormAutofill:ClearForm": {
|
|
this.clearForm();
|
|
break;
|
|
}
|
|
case "FormAutofill:FillForm": {
|
|
await this.autofillFields(message.data);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
get activeFieldDetail() {
|
|
return this._fieldDetailsManager.activeFieldDetail;
|
|
}
|
|
|
|
get activeFormDetails() {
|
|
return this._fieldDetailsManager.activeFormDetails;
|
|
}
|
|
|
|
get activeInput() {
|
|
return this._fieldDetailsManager.activeInput;
|
|
}
|
|
|
|
get activeHandler() {
|
|
return this._fieldDetailsManager.activeHandler;
|
|
}
|
|
|
|
get activeSection() {
|
|
return this._fieldDetailsManager.activeSection;
|
|
}
|
|
|
|
/**
|
|
* Handle a form submission and early return when:
|
|
* 1. In private browsing mode.
|
|
* 2. Could not map any autofill handler by form element.
|
|
* 3. Number of filled fields is less than autofill threshold
|
|
*
|
|
* @param {HTMLElement} formElement Root element which receives submit event.
|
|
* @param {string} formSubmissionReason Reason for invoking the form submission
|
|
* (see options for FORM_SUBMISSION_REASON in FormAutofillUtils))
|
|
* @param {Window} domWin Content window; passed for unit tests and when
|
|
* invoked by the FormAutofillSection
|
|
* @param {object} handler FormAutofillHander, if known by caller
|
|
*/
|
|
formSubmitted(
|
|
formElement,
|
|
formSubmissionReason,
|
|
domWin = formElement.ownerGlobal,
|
|
handler = undefined
|
|
) {
|
|
this.debug(`Handling form submission - infered by ${formSubmissionReason}`);
|
|
|
|
lazy.AutofillTelemetry.recordFormSubmissionHeuristicCount(
|
|
formSubmissionReason
|
|
);
|
|
|
|
if (!lazy.FormAutofill.isAutofillEnabled) {
|
|
this.debug("Form Autofill is disabled");
|
|
return;
|
|
}
|
|
|
|
// The `domWin` truthiness test is used by unit tests to bypass this check.
|
|
if (domWin && lazy.PrivateBrowsingUtils.isContentWindowPrivate(domWin)) {
|
|
this.debug("Ignoring submission in a private window");
|
|
return;
|
|
}
|
|
|
|
handler = handler || this._fieldDetailsManager._getFormHandler(formElement);
|
|
const records = this._fieldDetailsManager.getRecords(formElement, handler);
|
|
|
|
if (!records || !handler) {
|
|
this.debug("Form element could not map to an existing handler");
|
|
return;
|
|
}
|
|
|
|
// After a form submit event follows (most likely) a page navigation, so we set this flag
|
|
// to not handle the following one as form submission in order to avoid re-submitting the same form.
|
|
// Ideally, we should keep a record of the last submitted form details and based on that we
|
|
// should decide if we want to submit a form (bug 1895437)
|
|
this.isFollowingSubmitEvent = true;
|
|
|
|
[records.address, records.creditCard].forEach((rs, idx) => {
|
|
lazy.AutofillTelemetry.recordSubmittedSectionCount(
|
|
idx == 0
|
|
? lazy.AutofillTelemetry.ADDRESS
|
|
: lazy.AutofillTelemetry.CREDIT_CARD,
|
|
rs?.length
|
|
);
|
|
|
|
rs?.forEach(r => {
|
|
lazy.AutofillTelemetry.recordFormInteractionEvent(
|
|
"submitted",
|
|
r.section,
|
|
{
|
|
record: r,
|
|
form: handler.form,
|
|
}
|
|
);
|
|
delete r.section;
|
|
});
|
|
});
|
|
|
|
this.sendAsyncMessage("FormAutofill:OnFormSubmit", records);
|
|
}
|
|
|
|
formAutofilled() {
|
|
lazy.FormAutofillContent.showPopup();
|
|
}
|
|
|
|
/**
|
|
* All active items should be updated according the active element of
|
|
* `formFillController.focusedInput`. All of them including element,
|
|
* handler, section, and field detail, can be retrieved by their own getters.
|
|
*
|
|
* @param {HTMLElement|null} element The active item should be updated based
|
|
* on this or `formFillController.focusedInput` will be taken.
|
|
*/
|
|
updateActiveInput(element) {
|
|
element = element || formFillController.focusedInput;
|
|
if (!element) {
|
|
this.debug("updateActiveElement: no element selected");
|
|
return;
|
|
}
|
|
lazy.FormAutofillContent.updateActiveAutofillChild(this);
|
|
|
|
this._fieldDetailsManager.updateActiveInput(element);
|
|
this.debug("updateActiveElement: checking for popup-on-focus");
|
|
// We know this element just received focus. If it's a credit card field,
|
|
// open its popup.
|
|
if (this.#autofillPending) {
|
|
this.debug("updateActiveElement: skipping check; autofill is imminent");
|
|
} else if (element.value?.length !== 0) {
|
|
this.debug(
|
|
`updateActiveElement: Not opening popup because field is not empty.`
|
|
);
|
|
} else {
|
|
this.debug(
|
|
"updateActiveElement: checking if empty field is cc-*: ",
|
|
this.activeFieldDetail?.fieldName
|
|
);
|
|
|
|
if (
|
|
this.activeFieldDetail?.fieldName?.startsWith("cc-") ||
|
|
AppConstants.platform === "android"
|
|
) {
|
|
lazy.FormAutofillContent.showPopup();
|
|
}
|
|
}
|
|
}
|
|
|
|
clearForm() {
|
|
if (!this.activeSection) {
|
|
return;
|
|
}
|
|
|
|
this.activeSection.clearPopulatedForm();
|
|
|
|
let fieldName = this.activeFieldDetail?.fieldName;
|
|
if (lazy.FormAutofillUtils.isCreditCardField(fieldName)) {
|
|
lazy.AutofillTelemetry.recordFormInteractionEvent(
|
|
"cleared",
|
|
this.activeSection,
|
|
{ fieldName }
|
|
);
|
|
}
|
|
}
|
|
|
|
get lastProfileAutoCompleteResult() {
|
|
return this.manager.getActor("AutoComplete")?.lastProfileAutoCompleteResult;
|
|
}
|
|
|
|
get lastProfileAutoCompleteFocusedInput() {
|
|
return this.manager.getActor("AutoComplete")
|
|
?.lastProfileAutoCompleteFocusedInput;
|
|
}
|
|
|
|
previewProfile(profile) {
|
|
if (profile && this.activeSection) {
|
|
const adaptedProfile = this.activeSection.getAdaptedProfiles([
|
|
profile,
|
|
])[0];
|
|
this.activeSection.previewFormFields(adaptedProfile);
|
|
} else {
|
|
this.activeSection.clearPreviewedFormFields();
|
|
}
|
|
}
|
|
|
|
async autofillFields(profile) {
|
|
this.#autofillPending = true;
|
|
Services.obs.notifyObservers(null, "autofill-fill-starting");
|
|
try {
|
|
Services.obs.notifyObservers(null, "autofill-fill-starting");
|
|
await this.activeHandler.autofillFormFields(profile);
|
|
Services.obs.notifyObservers(null, "autofill-fill-complete");
|
|
} finally {
|
|
this.#autofillPending = false;
|
|
}
|
|
}
|
|
|
|
onPopupClosed() {
|
|
this.debug("Popup has closed.");
|
|
this.activeSection?.clearPreviewedFormFields();
|
|
}
|
|
|
|
onPopupOpened() {
|
|
this.debug(
|
|
"Popup has opened, automatic =",
|
|
formFillController.passwordPopupAutomaticallyOpened
|
|
);
|
|
|
|
let fieldName = this.activeFieldDetail?.fieldName;
|
|
if (fieldName && this.activeSection) {
|
|
lazy.AutofillTelemetry.recordFormInteractionEvent(
|
|
"popup_shown",
|
|
this.activeSection,
|
|
{ fieldName }
|
|
);
|
|
}
|
|
}
|
|
|
|
_markAsAutofillField(field) {
|
|
// Since Form Autofill popup is only for input element, any non-Input
|
|
// element should be excluded here.
|
|
if (!HTMLInputElement.isInstance(field)) {
|
|
return;
|
|
}
|
|
|
|
this.manager
|
|
.getActor("AutoComplete")
|
|
?.markAsAutoCompletableField(field, this);
|
|
}
|
|
|
|
get actorName() {
|
|
return "FormAutofill";
|
|
}
|
|
|
|
/**
|
|
* Get the search options when searching for autocomplete entries in the parent
|
|
*
|
|
* @param {HTMLInputElement} input - The input element to search for autocompelte entries
|
|
* @returns {object} the search options for the input
|
|
*/
|
|
getAutoCompleteSearchOption(input) {
|
|
const fieldDetail = this._fieldDetailsManager
|
|
._getFormHandler(input)
|
|
?.getFieldDetailByElement(input);
|
|
|
|
const scenarioName = lazy.FormScenarios.detect({ input }).signUpForm
|
|
? "SignUpFormScenario"
|
|
: "";
|
|
return { fieldName: fieldDetail?.fieldName, scenarioName };
|
|
}
|
|
|
|
/**
|
|
* Ask the provider whether it might have autocomplete entry to show
|
|
* for the given input.
|
|
*
|
|
* @param {HTMLInputElement} input - The input element to search for autocompelte entries
|
|
* @returns {boolean} true if we shold search for autocomplete entries
|
|
*/
|
|
shouldSearchForAutoComplete(input) {
|
|
const fieldDetail = this._fieldDetailsManager
|
|
._getFormHandler(input)
|
|
?.getFieldDetailByElement(input);
|
|
if (!fieldDetail) {
|
|
return false;
|
|
}
|
|
const fieldName = fieldDetail.fieldName;
|
|
const isAddressField = lazy.FormAutofillUtils.isAddressField(fieldName);
|
|
const searchPermitted = isAddressField
|
|
? lazy.FormAutofill.isAutofillAddressesEnabled
|
|
: lazy.FormAutofill.isAutofillCreditCardsEnabled;
|
|
// If the specified autofill feature is pref off, do not search
|
|
if (!searchPermitted) {
|
|
return false;
|
|
}
|
|
|
|
// No profile can fill the currently-focused input.
|
|
if (!lazy.FormAutofillContent.savedFieldNames.has(fieldName)) {
|
|
return false;
|
|
}
|
|
|
|
// The current form has already been populated and the field is not
|
|
// an empty credit card field.
|
|
const isCreditCardField =
|
|
lazy.FormAutofillUtils.isCreditCardField(fieldName);
|
|
const isInputAutofilled =
|
|
input.autofillState == lazy.FormAutofillUtils.FIELD_STATES.AUTO_FILLED;
|
|
const filledRecordGUID = this.activeSection.filledRecordGUID;
|
|
if (
|
|
!isInputAutofilled &&
|
|
filledRecordGUID &&
|
|
!(isCreditCardField && this.activeInput.value === "")
|
|
) {
|
|
return false;
|
|
}
|
|
|
|
// (address only) less than 3 inputs are covered by all saved fields in the storage.
|
|
if (
|
|
isAddressField &&
|
|
this.activeSection.allFieldNames.filter(field =>
|
|
lazy.FormAutofillContent.savedFieldNames.has(field)
|
|
).length < lazy.FormAutofillUtils.AUTOFILL_FIELDS_THRESHOLD
|
|
) {
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Convert the search result to autocomplete results
|
|
*
|
|
* @param {string} searchString - The string to search for
|
|
* @param {HTMLInputElement} input - The input element to search for autocompelte entries
|
|
* @param {Array<object>} records - autocomplete records
|
|
* @returns {AutocompleteResult}
|
|
*/
|
|
searchResultToAutoCompleteResult(searchString, input, records) {
|
|
if (!records) {
|
|
return null;
|
|
}
|
|
|
|
const entries = records.records;
|
|
const externalEntries = records.externalEntries;
|
|
|
|
const fieldDetail = this._fieldDetailsManager
|
|
._getFormHandler(input)
|
|
?.getFieldDetailByElement(input);
|
|
if (!fieldDetail) {
|
|
return null;
|
|
}
|
|
|
|
const adaptedRecords = this.activeSection.getAdaptedProfiles(entries);
|
|
const isSecure = lazy.InsecurePasswordUtils.isFormSecure(
|
|
this.activeHandler.form
|
|
);
|
|
const isInputAutofilled =
|
|
input.autofillState == lazy.FormAutofillUtils.FIELD_STATES.AUTO_FILLED;
|
|
const allFieldNames = this.activeSection.allFieldNames;
|
|
|
|
const AutocompleteResult = lazy.FormAutofillUtils.isAddressField(
|
|
fieldDetail.fieldName
|
|
)
|
|
? lazy.AddressResult
|
|
: lazy.CreditCardResult;
|
|
|
|
const acResult = new AutocompleteResult(
|
|
searchString,
|
|
fieldDetail.fieldName,
|
|
allFieldNames,
|
|
adaptedRecords,
|
|
{ isSecure, isInputAutofilled }
|
|
);
|
|
|
|
acResult.externalEntries.push(
|
|
...externalEntries.map(
|
|
entry =>
|
|
new lazy.GenericAutocompleteItem(
|
|
entry.image,
|
|
entry.title,
|
|
entry.subtitle,
|
|
entry.fillMessageName,
|
|
entry.fillMessageData
|
|
)
|
|
)
|
|
);
|
|
|
|
return acResult;
|
|
}
|
|
}
|