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/. */
6 * This is a utility object to work with HTML labels in web pages,
7 * including finding label elements and label text extraction.
9 export const LabelUtils = {
10 // The tag name list is from Chromium except for "STYLE":
11 // eslint-disable-next-line max-len
12 // https://cs.chromium.org/chromium/src/components/autofill/content/renderer/form_autofill_util.cc?l=216&rcl=d33a171b7c308a64dc3372fac3da2179c63b419e
13 EXCLUDED_TAGS: ["SCRIPT", "NOSCRIPT", "OPTION", "STYLE"],
15 // A map object, whose keys are the id's of form fields and each value is an
16 // array consisting of label elements correponding to the id.
17 // @type {Map<string, array>}
20 // An array consisting of label elements whose correponding form field doesn't
21 // have an id attribute.
22 // @type {Array<[HTMLLabelElement, HTMLElement]>}
23 _unmappedLabelControls: null,
25 // A weak map consisting of label element and extracted strings pairs.
26 // @type {WeakMap<HTMLLabelElement, array>}
30 * Extract all strings of an element's children to an array.
31 * "element.textContent" is a string which is merged of all children nodes,
32 * and this function provides an array of the strings contains in an element.
34 * @param {object} element
35 * A DOM element to be extracted.
37 * All strings in an element.
39 extractLabelStrings(element) {
40 if (this._labelStrings.has(element)) {
41 return this._labelStrings.get(element);
44 let _extractLabelStrings = el => {
45 if (this.EXCLUDED_TAGS.includes(el.tagName)) {
49 if (el.nodeType == el.TEXT_NODE || !el.childNodes.length) {
50 let trimmedText = el.textContent.trim();
52 strings.push(trimmedText);
57 for (let node of el.childNodes) {
58 let nodeType = node.nodeType;
59 if (nodeType != node.ELEMENT_NODE && nodeType != node.TEXT_NODE) {
62 _extractLabelStrings(node);
65 _extractLabelStrings(element);
66 this._labelStrings.set(element, strings);
70 generateLabelMap(doc) {
71 this._mappedLabels = new Map();
72 this._unmappedLabelControls = [];
73 this._labelStrings = new WeakMap();
75 for (let label of doc.querySelectorAll("label")) {
76 let id = label.htmlFor;
79 control = label.control;
81 // If the label has no control, yet there is a single control
82 // adjacent to the label, assume that is meant to be the control.
83 let nodes = label.parentNode.querySelectorAll(
84 ":scope > :is(input,select)"
86 if (nodes.length == 1) {
95 let labels = this._mappedLabels.get(id);
99 this._mappedLabels.set(id, [label]);
102 // control must be non-empty here
103 this._unmappedLabelControls.push({ label, control });
109 this._mappedLabels = null;
110 this._unmappedLabelControls = null;
111 this._labelStrings = null;
114 findLabelElements(element) {
115 if (!this._mappedLabels) {
116 this.generateLabelMap(element.ownerDocument);
121 return this._unmappedLabelControls
122 .filter(lc => lc.control == element)
123 .map(lc => lc.label);
125 return this._mappedLabels.get(id) || [];
129 export default LabelUtils;