1
0
Fork 0
firefox/toolkit/components/formautofill/shared/LabelUtils.sys.mjs
Daniel Baumann 5e9a113729
Adding upstream version 140.0.
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
2025-06-25 09:37:52 +02:00

230 lines
7.2 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/. */
const lazy = {};
ChromeUtils.defineESModuleGetters(lazy, {
FormAutofillUtils: "resource://gre/modules/shared/FormAutofillUtils.sys.mjs",
});
/**
* This is a utility object to work with HTML labels in web pages,
* including finding label elements and label text extraction.
*/
export const LabelUtils = {
// The tag name list is from Chromium except for "STYLE":
// eslint-disable-next-line max-len
// https://cs.chromium.org/chromium/src/components/autofill/content/renderer/form_autofill_util.cc?l=216&rcl=d33a171b7c308a64dc3372fac3da2179c63b419e
EXCLUDED_TAGS: ["SCRIPT", "NOSCRIPT", "OPTION", "STYLE"],
// A map object, whose keys are the id's of form fields and each value is an
// array consisting of label elements correponding to the id. This map only
// contains those labels with an id that matches a form element.
// @type {Map<string, array>}
_mappedLabels: null,
// A weak map consisting of label element and extracted strings pairs.
// @type {WeakMap<HTMLLabelElement, array>}
_labelStrings: null,
/**
* Extract all strings of an element's children to an array.
* "element.textContent" is a string which is merged of all children nodes,
* and this function provides an array of the strings contains in an element.
*
* @param {object} element
* A DOM element to be extracted.
* @returns {Array}
* All strings in an element.
*/
extractLabelStrings(element) {
if (this._labelStrings.has(element)) {
return this._labelStrings.get(element);
}
let strings = [];
let _extractLabelStrings = el => {
if (this.EXCLUDED_TAGS.includes(el.tagName)) {
return;
}
if (el.nodeType == el.TEXT_NODE || !el.childNodes.length) {
let trimmedText = el.textContent.trim();
if (trimmedText) {
strings.push(trimmedText);
}
return;
}
for (let node of el.childNodes) {
let nodeType = node.nodeType;
if (nodeType != node.ELEMENT_NODE && nodeType != node.TEXT_NODE) {
continue;
}
_extractLabelStrings(node);
}
};
_extractLabelStrings(element);
this._labelStrings.set(element, strings);
return strings;
},
/**
* From a starting label element, find a nearby input or select element
* by traversing the nodes in document order, but don't search past another
* related element or outside the form.
*/
findAdjacentControl(labelElement, potentialLabels) {
// First, look for an form element after the label.
let foundElementAfter = this.findNextFormControl(
labelElement,
false,
potentialLabels
);
// If the control has the same parent as the label, return it.
if (foundElementAfter?.parentNode == labelElement.parentNode) {
return foundElementAfter;
}
// Otherwise, look for a form control with the same parent backwards
// in the document.
let foundElementBefore = this.findNextFormControl(
labelElement,
true,
potentialLabels
);
if (foundElementBefore?.parentNode == labelElement.parentNode) {
return foundElementBefore;
}
// If there is no form control with the same parent forward or backward,
// return the form control nearest forward, if any, even though it doesn't
// have the same parent.
return foundElementAfter;
},
/**
* Find the next form control in the document tree after a starting label that
* could correspond to the label. If the form control is in potentialLabels, then
* it has already been possibly matched to another label so should be ignored.
*
* @param {HTMLLabelElement} element
* starting <label> element
* @param {boolean} reverse
* true to search backwards or false to search forwards
* @param {Map} potentialLabels
* map of form controls that have already potentially matched
*/
findNextFormControl(element, reverse, potentialLabels) {
// Ignore elements and stop searching for elements that are already potentially
// labelled or are form elements that cannot be autofilled.
while ((element = this.nextElementInOrder(element, reverse))) {
if (potentialLabels.has(element)) {
break;
} else if (
lazy.FormAutofillUtils.isCreditCardOrAddressFieldType(element)
) {
return element;
} else if (
[
"button",
"input",
"label",
"meter",
"output",
"progress",
"select",
"textarea",
].includes(element.localName)
) {
break;
}
}
return null;
},
nextElementInOrder(element, reverse) {
let result = reverse ? element.lastElementChild : element.firstElementChild;
if (result) {
return result;
}
while (element) {
result = reverse
? element.previousElementSibling
: element.nextElementSibling;
if (result) {
return result;
}
element = element.parentNode;
if (
!element ||
element.localName == "form" ||
element.localName == "fieldset"
) {
break;
}
}
return null;
},
generateLabelMap(doc) {
this._mappedLabels = new Map();
this._labelStrings = new WeakMap();
// A map of potential label -> control for labels that don't have an id or
// control associated with them. Labels that have ids or associated controls
// will be placed in _mappedLabels.
let potentialLabels = new Map();
for (let label of doc.querySelectorAll("label")) {
let control = label.control;
if (control) {
const controlId = lazy.FormAutofillUtils.getElementIdentifier(control);
let labels = this._mappedLabels.get(controlId);
if (labels) {
labels.push(label);
} else {
this._mappedLabels.set(controlId, [label]);
}
} else {
// If the label has no control, look for the next input or select
// element in the document and add that to the potentialLabels list.
control = this.findAdjacentControl(label, potentialLabels);
if (control) {
potentialLabels.set(control, label);
}
}
}
// Now check the potentialLabels list. If any of the labels match form controls
// that are not bound to a label, add them. This allows a label to match a form
// control that is nearby even when it has no for attribute or doesn't match an id.
if (potentialLabels.size) {
for (let label of potentialLabels) {
const elementId = lazy.FormAutofillUtils.getElementIdentifier(label[0]);
if (!this._mappedLabels.has(elementId)) {
this._mappedLabels.set(elementId, [label[1]]);
}
}
}
},
clearLabelMap() {
this._mappedLabels = null;
this._labelStrings = null;
},
findLabelElements(element) {
if (!this._mappedLabels) {
this.generateLabelMap(element.ownerDocument);
}
let id = lazy.FormAutofillUtils.getElementIdentifier(element);
return this._mappedLabels.get(id) || [];
},
};
export default LabelUtils;