1 /* This Source Code Form is subject to the terms of the Mozilla Public
2 * License, v. 2.0. If a copy of the MPL was not distributed with this
3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
5 import { FormAutofill } from "resource://autofill/FormAutofill.sys.mjs";
6 import { FormAutofillUtils } from "resource://gre/modules/shared/FormAutofillUtils.sys.mjs";
9 ChromeUtils.defineESModuleGetters(lazy, {
10 AddressParser: "resource://gre/modules/shared/AddressParser.sys.mjs",
12 "resource://gre/modules/shared/AutofillFormFactory.sys.mjs",
13 CreditCard: "resource://gre/modules/CreditCard.sys.mjs",
14 FieldDetail: "resource://gre/modules/shared/FieldScanner.sys.mjs",
15 FormAutofillHeuristics:
16 "resource://gre/modules/shared/FormAutofillHeuristics.sys.mjs",
17 FormAutofillNameUtils:
18 "resource://gre/modules/shared/FormAutofillNameUtils.sys.mjs",
19 LabelUtils: "resource://gre/modules/shared/LabelUtils.sys.mjs",
22 const { FIELD_STATES } = FormAutofillUtils;
25 * Handles profile autofill for a DOM Form element.
27 export class FormAutofillHandler {
28 // The window to which this form belongs
31 // DOM Form element to which this object is attached
34 // Keeps track of filled state for all identified elements
35 #filledStateByElement = new WeakMap();
37 // An object that caches the current selected option, keyed by element.
38 #matchingSelectOption = null;
41 * Array of collected data about relevant form fields. Each item is an object
42 * storing the identifying details of the field and a reference to the
43 * originally associated element from the form.
45 * The "section", "addressType", "contactType", and "fieldName" values are
46 * used to identify the exact field when the serializable data is received
47 * from the backend. There cannot be multiple fields which have
48 * the same exact combination of these values.
50 * A direct reference to the associated element cannot be sent to the user
51 * interface because processing may be done in the parent process.
56 * Initialize the form from `FormLike` object to handle the section or form
59 * @param {FormLike} form Form that need to be auto filled
60 * @param {Function} onFilledModifiedCallback Function that can be invoked
61 * when we want to suggest autofill on a form.
63 constructor(form, onFilledModifiedCallback = () => {}) {
64 this._updateForm(form);
66 this.window = this.form.rootElement.ownerGlobal;
68 this.onFilledModifiedCallback = onFilledModifiedCallback;
70 // The identifier generated via ContentDOMReference for the root element.
71 this.rootElementId = FormAutofillUtils.getElementIdentifier(
75 ChromeUtils.defineLazyGetter(this, "log", () =>
76 FormAutofill.defineLogGetter(this, "FormAutofillHandler")
81 * Retrieves the 'fieldDetails' property, ensuring it has been initialized by
82 * `setIdentifiedFieldDetails`. Throws an error if accessed before initialization.
84 * This is because 'fieldDetail'' contains information that need to be computed
85 * in the parent side first.
87 * @throws {Error} If `setIdentifiedFieldDetails` has not been called.
88 * @returns {Array<FieldDetail>}
89 * The list of autofillable field details for this form.
92 if (!this.#fieldDetails) {
94 `Should only use 'fieldDetails' after 'setIdentifiedFieldDetails' is called`
97 return this.#fieldDetails;
101 * Sets the list of 'FieldDetail' objects for autofillable fields within the form.
103 * @param {Array<FieldDetail>} fieldDetails
104 * An array of field details that has been computed on the parent side.
105 * This method should be called before accessing `fieldDetails`.
107 setIdentifiedFieldDetails(fieldDetails) {
108 this.#fieldDetails = fieldDetails;
112 * Determines whether 'setIdentifiedFieldDetails' has been called and the
113 * `fieldDetails` have been initialized.
116 * True if 'fieldDetails' has been initialized; otherwise, False.
118 hasIdentifiedFields() {
119 return !!this.#fieldDetails;
123 switch (event.type) {
125 if (!event.isTrusted) {
129 // This uses the #filledStateByElement map instead of
130 // autofillState as the state has already been cleared by the time
131 // the input event fires.
132 const fieldDetail = this.getFieldDetailByElement(event.target);
133 const previousState = this.getFilledStateByElement(event.target);
134 const newState = FIELD_STATES.NORMAL;
136 if (previousState != newState) {
137 this.changeFieldState(fieldDetail, newState);
140 this.onFilledModifiedCallback?.(fieldDetail, previousState, newState);
145 getFieldDetailByName(fieldName) {
146 return this.fieldDetails.find(detail => detail.fieldName == fieldName);
149 getFieldDetailByElement(element) {
150 return this.fieldDetails.find(detail => detail.element == element);
153 getFieldDetailByElementId(elementId) {
154 return this.fieldDetails.find(detail => detail.elementId == elementId);
158 * Only use this API within handleEvent
160 getFilledStateByElement(element) {
161 return this.#filledStateByElement.get(element);
165 * Check the form is necessary to be updated. This function should be able to
166 * detect any changes including all control elements in the form.
168 * @param {HTMLElement} element The element supposed to be in the form.
169 * @returns {boolean} FormAutofillHandler.form is updated or not.
171 updateFormIfNeeded(element) {
172 // When the following condition happens, FormAutofillHandler.form should be
174 // * The count of form controls is changed.
175 // * When the element can not be found in the current form.
177 // However, we should improve the function to detect the element changes.
178 // e.g. a tel field is changed from type="hidden" to type="tel".
181 const getFormLike = () => {
183 _formLike = lazy.AutofillFormFactory.createFromField(element);
188 const currentForm = getFormLike();
189 if (currentForm.elements.length != this.form.elements.length) {
190 this.log.debug("The count of form elements is changed.");
191 this._updateForm(getFormLike());
195 if (!this.form.elements.includes(element)) {
196 this.log.debug("The element can not be found in the current form.");
197 this._updateForm(getFormLike());
205 * Update the form with a new FormLike, and the related fields should be
206 * updated or clear to ensure the data consistency.
208 * @param {FormLike} form a new FormLike to replace the original one.
213 this.#fieldDetails = null;
217 * Collect <input>, <select>, and <iframe> elements from the specified form
218 * and return the correspond 'FieldDetail' objects.
220 * @param {formLike} formLike
221 * The form that we collect information from.
222 * @param {boolean} includeIframe
223 * True to add <iframe> to the returned FieldDetails array.
224 * @param {boolean} ignoreInvisibleInput
225 * True to NOT run heuristics on invisible <input> fields.
227 * @returns {Array<FieldDeail>}
228 * An array containing eliglble fields for autofill, also
231 static collectFormFieldDetails(
234 ignoreInvisibleInput = true
237 lazy.FormAutofillHeuristics.getFormInfo(formLike, ignoreInvisibleInput) ??
240 // 'FormLike' only contains <input> & <select>, so in order to include <iframe>
241 // in the list of 'FieldDetails', we need to search for <iframe> in the form.
242 if (!includeIframe) {
246 // Insert <iframe> elements into the fieldDetails array, maintaining the element order.
247 const fieldDetailsIncludeIframe = [];
249 const elements = formLike.rootElement.querySelectorAll(
250 "input, select, iframe"
252 for (const element of elements) {
253 if (fieldDetails[index]?.element == element) {
254 fieldDetailsIncludeIframe.push(fieldDetails[index]);
257 element.localName == "iframe" &&
258 FormAutofillUtils.isFieldVisible(element)
260 // Add the <iframe> only if it is under the `formLike` element.
261 // While we use formLike.rootElement.querySelectorAll, it is still possible
262 // we find an <iframe> inside a <form> within this rootElement. In this
263 // case, we don't want to include the <iframe> in the field list.
265 lazy.AutofillFormFactory.findRootForField(element) ==
268 const iframeFd = lazy.FieldDetail.create(element, formLike, "iframe");
269 fieldDetailsIncludeIframe.push(iframeFd);
273 return fieldDetailsIncludeIframe;
277 * Change the state of a field to correspond with different presentations.
279 * @param {object} fieldDetail
280 * A fieldDetail of which its element is about to update the state.
281 * @param {string} state
282 * The state to apply.
284 changeFieldState(fieldDetail, state) {
285 const element = fieldDetail.element;
288 fieldDetail.fieldName,
289 "is unreachable while changing state"
294 if (!Object.values(FIELD_STATES).includes(state)) {
296 fieldDetail.fieldName,
297 "is trying to change to an invalid state"
302 element.autofillState = state;
303 this.#filledStateByElement.set(element, state);
305 if (state == FIELD_STATES.AUTO_FILLED) {
306 element.addEventListener("input", this, { mozSystemGroup: true });
311 * Populates result to the preview layers with given profile.
313 * @param {Array} elementIds
314 * @param {object} profile
315 * A profile to be previewed with
317 previewFields(elementIds, profile) {
318 this.getAdaptedProfiles([profile]);
320 for (const fieldDetail of this.fieldDetails) {
321 const element = fieldDetail.element;
323 // Skip the field if it is null or readonly or disabled
325 !elementIds.includes(fieldDetail.elementId) ||
326 !FormAutofillUtils.isFieldAutofillable(element)
331 let value = this.getFilledValueFromProfile(fieldDetail, profile);
333 this.changeFieldState(fieldDetail, FIELD_STATES.NORMAL);
337 if (HTMLInputElement.isInstance(element)) {
338 if (element.value && element.value != element.defaultValue) {
339 // Skip the field if the user has already entered text and that text
340 // is not the site prefilled value.
343 } else if (HTMLSelectElement.isInstance(element)) {
344 // Unlike text input, select element is always previewed even if
345 // the option is already selected.
346 const option = this.matchSelectOptions(fieldDetail, profile);
347 value = option?.text ?? "";
352 element.previewValue = value?.toString().replaceAll("*", "•");
353 this.changeFieldState(fieldDetail, FIELD_STATES.PREVIEW);
358 * Processes form fields that can be autofilled, and populates them with the
359 * profile provided by backend.
361 * @param {string} focusedId
362 * The id of the element that triggers autofilling.
363 * @param {Array} elementIds
364 * An array of IDs for the elements that should be autofilled.
365 * @param {object} profile
366 * The data profile containing the values to be autofilled into the form fields.
368 fillFields(focusedId, elementIds, profile) {
369 this.getAdaptedProfiles([profile]);
371 for (const fieldDetail of this.fieldDetails) {
372 const { element, elementId } = fieldDetail;
375 !elementIds.includes(elementId) ||
376 !FormAutofillUtils.isFieldAutofillable(element)
381 element.previewValue = "";
383 if (HTMLInputElement.isInstance(element)) {
384 // Bug 1687679: Since profile appears to be presentation ready data, we need to utilize the "x-formatted" field
385 // that is generated when presentation ready data doesn't fit into the autofilling element.
386 // For example, autofilling expiration month into an input element will not work as expected if
387 // the month is less than 10, since the input is expected a zero-padded string.
388 // See Bug 1722941 for follow up.
389 const value = this.getFilledValueFromProfile(fieldDetail, profile);
394 // For the focused input element, it will be filled with a valid value
396 // For the others, the fields should be only filled when their values are empty
397 // or their values are equal to the site prefill value
398 // or are the result of an earlier auto-fill.
400 elementId == focusedId ||
402 element.value == element.defaultValue ||
403 element.autofillState == FIELD_STATES.AUTO_FILLED
405 FormAutofillHandler.fillFieldValue(element, value);
406 this.changeFieldState(fieldDetail, FIELD_STATES.AUTO_FILLED);
408 } else if (HTMLSelectElement.isInstance(element)) {
409 const option = this.matchSelectOptions(fieldDetail, profile);
414 // Do not change value or dispatch events if the option is already selected.
415 // Use case for multiple select is not considered here.
416 if (!option.selected) {
417 option.selected = true;
418 FormAutofillHandler.fillFieldValue(element, option.value);
420 // Autofill highlight appears regardless if value is changed or not
421 this.changeFieldState(fieldDetail, FIELD_STATES.AUTO_FILLED);
427 FormAutofillUtils.getElementByIdentifier(focusedId)?.focus({
431 this.registerFormChangeHandler();
434 registerFormChangeHandler() {
435 if (this.onChangeHandler) {
439 this.log.debug("register change handler for filled form:", this.form);
441 this.onChangeHandler = e => {
445 if (e.type == "reset") {
446 for (const fieldDetail of this.fieldDetails) {
447 const element = fieldDetail.element;
448 element.removeEventListener("input", this, { mozSystemGroup: true });
449 this.changeFieldState(fieldDetail, FIELD_STATES.NORMAL);
453 // Unregister listeners once no field is in AUTO_FILLED state.
455 this.fieldDetails.every(
456 detail => detail.element.autofillState != FIELD_STATES.AUTO_FILLED
459 this.form.rootElement.removeEventListener(
461 this.onChangeHandler,
463 mozSystemGroup: true,
466 this.form.rootElement.removeEventListener(
468 this.onChangeHandler,
470 mozSystemGroup: true,
473 this.onChangeHandler = null;
477 // Handle the highlight style resetting caused by user's correction afterward.
478 this.log.debug("register change handler for filled form:", this.form);
479 this.form.rootElement.addEventListener("input", this.onChangeHandler, {
480 mozSystemGroup: true,
482 this.form.rootElement.addEventListener("reset", this.onChangeHandler, {
483 mozSystemGroup: true,
487 computeFillingValue(fieldDetail) {
488 const element = fieldDetail.element;
493 let value = element.value.trim();
494 switch (fieldDetail.fieldName) {
495 case "address-level1":
496 if (HTMLSelectElement.isInstance(element)) {
497 // Don't save the record when the option value is empty *OR* there
498 // are multiple options being selected. The empty option is usually
499 // assumed to be default along with a meaningless text to users.
500 if (!value || element.selectedOptions.length != 1) {
501 // Keep the property and preserve more information for address updating
504 const text = element.selectedOptions[0].text.trim();
506 FormAutofillUtils.getAbbreviatedSubregionName([value, text]) ||
512 // This is a temporary fix. Ideally we should have either case-insensitive comparison of country codes
513 // or handle this elsewhere see Bug 1889234 for more context.
514 value = value.toUpperCase();
518 HTMLSelectElement.isInstance(element) &&
519 !lazy.CreditCard.isValidNetwork(value)
521 // Don't save the record when the option value is empty *OR* there
522 // are multiple options being selected. The empty option is usually
523 // assumed to be default along with a meaningless text to users.
524 if (value && element.selectedOptions.length == 1) {
525 const selectedOption = element.selectedOptions[0];
527 lazy.CreditCard.getNetworkFromName(selectedOption.text) ??
528 lazy.CreditCard.getNetworkFromName(selectedOption.value);
541 * Apply both address and credit card related transformers.
543 * @param {Object} profile
544 * A profile for adjusting credit card related value.
547 applyTransformers(profile) {
548 this.addressTransformer(profile);
549 this.telTransformer(profile);
550 this.creditCardExpiryDateTransformer(profile);
551 this.creditCardExpMonthAndYearTransformer(profile);
552 this.creditCardNameTransformer(profile);
553 this.adaptFieldMaxLength(profile);
556 getAdaptedProfiles(originalProfiles) {
557 for (let profile of originalProfiles) {
558 this.applyTransformers(profile);
560 return originalProfiles;
564 * Match the select option for a field if we autofill with the given profile.
565 * This function caches the matching result in the `#matchingSelectionOption`
568 * @param {FieldDetail} fieldDetail
569 * The field information of the matching element.
570 * @param {object} profile
571 * The profile used for autofill.
574 * The matched option, or undefined if no matching option is found.
576 matchSelectOptions(fieldDetail, profile) {
577 if (!this.#matchingSelectOption) {
578 this.#matchingSelectOption = new WeakMap();
581 const { element, fieldName } = fieldDetail;
582 if (!HTMLSelectElement.isInstance(element)) {
586 const cache = this.#matchingSelectOption.get(element) || {};
587 const value = profile[fieldName];
589 let option = cache[value]?.deref();
591 option = FormAutofillUtils.findSelectOption(element, profile, fieldName);
594 cache[value] = new WeakRef(option);
595 this.#matchingSelectOption.set(element, cache);
596 } else if (cache[value]) {
598 this.#matchingSelectOption.set(element, cache);
605 adaptFieldMaxLength(profile) {
606 for (let key in profile) {
607 let detail = this.getFieldDetailByName(key);
608 if (!detail || detail.part) {
612 let element = detail.element;
617 let maxLength = element.maxLength;
619 maxLength === undefined ||
621 profile[key].toString().length <= maxLength
627 switch (typeof profile[key]) {
629 // If this is an expiration field and our previous
630 // adaptations haven't resulted in a string that is
631 // short enough to satisfy the field length, and the
632 // field is constrained to a length of 4 or 5, then we
633 // assume it is intended to hold an expiration of the
634 // form "MMYY" or "MM/YY".
635 if (key == "cc-exp" && (maxLength == 4 || maxLength == 5)) {
636 const month2Digits = (
637 "0" + profile["cc-exp-month"].toString()
639 const year2Digits = profile["cc-exp-year"].toString().slice(-2);
640 const separator = maxLength == 5 ? "/" : "";
641 profile[key] = `${month2Digits}${separator}${year2Digits}`;
642 } else if (key == "cc-number") {
643 // We want to show the last four digits of credit card so that
644 // the masked credit card previews correctly and appears correctly
645 // in the autocomplete menu
646 profile[key] = profile[key].substr(
647 profile[key].length - maxLength
650 profile[key] = profile[key].substr(0, maxLength);
654 // There's no way to truncate a number smaller than a
659 // The only numbers we store are expiration month/year,
660 // and if they truncate, we want the final digits, not
662 profile[key] = profile[key] % Math.pow(10, maxLength);
668 delete profile[`${key}-formatted`];
674 * Handles credit card expiry date transformation when
675 * the expiry date exists in a cc-exp field.
677 * @param {object} profile
679 creditCardExpiryDateTransformer(profile) {
680 if (!profile["cc-exp"]) {
684 const element = this.getFieldDetailByName("cc-exp")?.element;
689 function updateExpiry(_string, _month, _year) {
690 // Bug 1687681: This is a short term fix to other locales having
691 // different characters to represent year.
692 // - FR locales may use "A" to represent year.
693 // - DE locales may use "J" to represent year.
694 // - PL locales may use "R" to represent year.
695 // This approach will not scale well and should be investigated in a follow up bug.
696 const monthChars = "m";
697 const yearChars = "yy|aa|jj|rr";
698 const expiryDateFormatRegex = (firstChars, secondChars) =>
702 "]{2}){1,2})\\s*([\\-/])\\s*((?:[" +
704 "]{2}){1,2})(?:\\b|$)",
708 // If the month first check finds a result, where placeholder is "mm - yyyy",
709 // the result will be structured as such: ["mm - yyyy", "mm", "-", "yyyy"]
710 let result = expiryDateFormatRegex(monthChars, yearChars).exec(_string);
713 _month.padStart(result[1].length, "0") +
715 _year.substr(-1 * result[3].length)
719 // If the year first check finds a result, where placeholder is "yyyy mm",
720 // the result will be structured as such: ["yyyy mm", "yyyy", " ", "mm"]
721 result = expiryDateFormatRegex(yearChars, monthChars).exec(_string);
724 _year.substr(-1 * result[1].length) +
726 _month.padStart(result[3].length, "0")
732 let newExpiryString = null;
733 const month = profile["cc-exp-month"].toString();
734 const year = profile["cc-exp-year"].toString();
735 if (element.localName == "input") {
736 // Use the placeholder or label to determine the expiry string format.
737 const possibleExpiryStrings = [];
738 if (element.placeholder) {
739 possibleExpiryStrings.push(element.placeholder);
741 const labels = lazy.LabelUtils.findLabelElements(element);
743 // Not consider multiple lable for now.
744 possibleExpiryStrings.push(element.labels[0]?.textContent);
746 if (element.previousElementSibling?.localName == "label") {
747 possibleExpiryStrings.push(element.previousElementSibling.textContent);
750 possibleExpiryStrings.some(string => {
751 newExpiryString = updateExpiry(string, month, year);
752 return !!newExpiryString;
756 // Bug 1688576: Change YYYY-MM to MM/YYYY since MM/YYYY is the
757 // preferred presentation format for credit card expiry dates.
758 profile["cc-exp"] = newExpiryString ?? `${month.padStart(2, "0")}/${year}`;
762 * Handles credit card expiry date transformation when the expiry date exists in
763 * the separate cc-exp-month and cc-exp-year fields
765 * @param {object} profile
767 creditCardExpMonthAndYearTransformer(profile) {
768 const getInputElementByField = (field, self) => {
772 const detail = self.getFieldDetailByName(field);
776 const element = detail.element;
777 return element.localName === "input" ? element : null;
779 const month = getInputElementByField("cc-exp-month", this);
781 // Transform the expiry month to MM since this is a common format needed for filling.
782 profile["cc-exp-month-formatted"] = profile["cc-exp-month"]
786 const year = getInputElementByField("cc-exp-year", this);
787 // If the expiration year element is an input,
788 // then we examine any placeholder to see if we should format the expiration year
789 // as a zero padded string in order to autofill correctly.
791 const placeholder = year.placeholder;
793 // Checks for 'YY'|'AA'|'JJ'|'RR' placeholder and converts the year to a two digit string using the last two digits.
794 const result = /\b(yy|aa|jj|rr)\b/i.test(placeholder);
796 profile["cc-exp-year-formatted"] = profile["cc-exp-year"]
804 * Handles credit card name transformation when the name exists in
805 * the separate cc-given-name, cc-middle-name, and cc-family name fields
807 * @param {object} profile
809 creditCardNameTransformer(profile) {
810 const name = profile["cc-name"];
815 const given = this.getFieldDetailByName("cc-given-name");
816 const middle = this.getFieldDetailByName("cc-middle-name");
817 const family = this.getFieldDetailByName("cc-family-name");
818 if (given || middle || family) {
819 const nameParts = lazy.FormAutofillNameUtils.splitName(name);
820 if (given && nameParts.given) {
821 profile["cc-given-name"] = nameParts.given;
823 if (middle && nameParts.middle) {
824 profile["cc-middle-name"] = nameParts.middle;
826 if (family && nameParts.family) {
827 profile["cc-family-name"] = nameParts.family;
832 addressTransformer(profile) {
833 if (profile["street-address"]) {
834 // "-moz-street-address-one-line" is used by the labels in
835 // ProfileAutoCompleteResult.
836 profile["-moz-street-address-one-line"] =
837 FormAutofillUtils.toOneLineAddress(profile["street-address"]);
838 let streetAddressDetail = this.getFieldDetailByName("street-address");
840 streetAddressDetail &&
841 HTMLInputElement.isInstance(streetAddressDetail.element)
843 profile["street-address"] = profile["-moz-street-address-one-line"];
846 let waitForConcat = [];
847 for (let f of ["address-line3", "address-line2", "address-line1"]) {
848 waitForConcat.unshift(profile[f]);
849 if (this.getFieldDetailByName(f)) {
850 if (waitForConcat.length > 1) {
851 profile[f] = FormAutofillUtils.toOneLineAddress(waitForConcat);
858 // If a house number field exists, split the address up into house number
860 if (this.getFieldDetailByName("address-housenumber")) {
861 let address = lazy.AddressParser.parseStreetAddress(
862 profile["street-address"]
865 profile["address-housenumber"] = address.street_number;
866 let field = this.getFieldDetailByName("address-line1")
869 profile[field] = address.street_name;
875 * Replace tel with tel-national if tel violates the input element's
878 * @param {object} profile
879 * A profile to be converted.
881 telTransformer(profile) {
882 if (!profile.tel || !profile["tel-national"]) {
886 let detail = this.getFieldDetailByName("tel");
891 let element = detail.element;
893 let testPattern = str => {
895 // The pattern has to match the entire value.
896 _pattern = new RegExp("^(?:" + element.pattern + ")$", "u");
898 return _pattern.test(str);
900 if (element.pattern) {
901 if (testPattern(profile.tel)) {
904 } else if (element.maxLength) {
906 detail.reason == "autocomplete" &&
907 profile.tel.length <= element.maxLength
913 if (detail.reason != "autocomplete") {
914 // Since we only target people living in US and using en-US websites in
915 // MVP, it makes more sense to fill `tel-national` instead of `tel`
916 // if the field is identified by heuristics and no other clues to
917 // determine which one is better.
918 // TODO: [Bug 1407545] This should be improved once more countries are
920 profile.tel = profile["tel-national"];
921 } else if (element.pattern) {
922 if (testPattern(profile["tel-national"])) {
923 profile.tel = profile["tel-national"];
925 } else if (element.maxLength) {
926 if (profile["tel-national"].length <= element.maxLength) {
927 profile.tel = profile["tel-national"];
934 * @param {object} fieldDetail A fieldDetail of the related element.
935 * @param {object} profile The profile to fill.
936 * @returns {string} The value to fill for the given field.
938 getFilledValueFromProfile(fieldDetail, profile) {
940 profile[`${fieldDetail.fieldName}-formatted`] ||
941 profile[fieldDetail.fieldName];
943 if (fieldDetail.fieldName == "cc-number" && fieldDetail.part != null) {
944 const part = fieldDetail.part;
945 return value.slice((part - 1) * 4, part * 4);
950 * Fills the provided element with the specified value.
952 * @param {HTMLInputElement| HTMLSelectElement} element - The form field element to be filled.
953 * @param {string} value - The value to be filled into the form field.
955 static fillFieldValue(element, value) {
956 if (FormAutofillUtils.focusOnAutofill) {
957 element.focus({ preventScroll: true });
959 if (HTMLInputElement.isInstance(element)) {
960 element.setUserInput(value);
961 } else if (HTMLSelectElement.isInstance(element)) {
962 // Set the value of the select element so that web event handlers can react accordingly
963 element.value = value;
964 element.dispatchEvent(
965 new element.ownerGlobal.Event("input", { bubbles: true })
967 element.dispatchEvent(
968 new element.ownerGlobal.Event("change", { bubbles: true })
973 clearPreviewedFields(elementIds) {
974 for (const elementId of elementIds) {
975 const fieldDetail = this.getFieldDetailByElementId(elementId);
976 const element = fieldDetail?.element;
978 this.log.warn(fieldDetail.fieldName, "is unreachable");
982 element.previewValue = "";
983 if (element.autofillState == FIELD_STATES.AUTO_FILLED) {
986 this.changeFieldState(fieldDetail, FIELD_STATES.NORMAL);
990 clearFilledFields(focusedId, elementIds) {
991 const fieldDetails = elementIds.map(id =>
992 this.getFieldDetailByElementId(id)
994 for (const fieldDetail of fieldDetails) {
995 const element = fieldDetail?.element;
997 this.log.warn(fieldDetail?.fieldName, "is unreachable");
1001 if (element.autofillState == FIELD_STATES.AUTO_FILLED) {
1003 if (HTMLSelectElement.isInstance(element)) {
1004 if (!element.options.length) {
1007 // Resets a <select> element to its selected option or the first
1008 // option if there is none selected.
1009 const selected = [...element.options].find(option =>
1010 option.hasAttribute("selected")
1012 value = selected ? selected.value : element.options[0].value;
1014 FormAutofillHandler.fillFieldValue(element, value);
1018 let focusedElement = FormAutofillUtils.getElementByIdentifier(focusedId);
1019 if (FormAutofillUtils.focusOnAutofill && focusedElement) {
1020 focusedElement.focus({ preventScroll: true });
1025 * Return the record that is keyed by element id and value is the normalized value
1026 * done by computeFillingValue
1028 * @returns {object} An object keyed by element id, and the value is
1029 * an object that includes the following properties:
1030 * filledState: The autofill state of the element
1031 * filledvalue: The value of the element
1033 collectFormFilledData() {
1034 const filledData = new Map();
1036 for (const fieldDetail of this.fieldDetails) {
1037 const element = fieldDetail.element;
1038 filledData.set(fieldDetail.elementId, {
1039 filledState: element.autofillState,
1040 filledValue: this.computeFillingValue(fieldDetail),
1046 isFieldAutofillable(fieldDetail, profile) {
1047 if (HTMLInputElement.isInstance(fieldDetail.element)) {
1048 return !!profile[fieldDetail.fieldName];
1050 return !!this.matchSelectOptions(fieldDetail, profile);