Backed out changeset f594e6f00208 (bug 1940883) for causing crashes in bug 1941164.
[gecko.git] / toolkit / components / formautofill / shared / LabelUtils.sys.mjs
blobdfa58ce69333f2b24b2e3c9b6e9120463074475c
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 /**
6  * This is a utility object to work with HTML labels in web pages,
7  * including finding label elements and label text extraction.
8  */
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>}
18   _mappedLabels: null,
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>}
27   _labelStrings: null,
29   /**
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.
33    *
34    * @param  {object} element
35    *         A DOM element to be extracted.
36    * @returns {Array}
37    *          All strings in an element.
38    */
39   extractLabelStrings(element) {
40     if (this._labelStrings.has(element)) {
41       return this._labelStrings.get(element);
42     }
43     let strings = [];
44     let _extractLabelStrings = el => {
45       if (this.EXCLUDED_TAGS.includes(el.tagName)) {
46         return;
47       }
49       if (el.nodeType == el.TEXT_NODE || !el.childNodes.length) {
50         let trimmedText = el.textContent.trim();
51         if (trimmedText) {
52           strings.push(trimmedText);
53         }
54         return;
55       }
57       for (let node of el.childNodes) {
58         let nodeType = node.nodeType;
59         if (nodeType != node.ELEMENT_NODE && nodeType != node.TEXT_NODE) {
60           continue;
61         }
62         _extractLabelStrings(node);
63       }
64     };
65     _extractLabelStrings(element);
66     this._labelStrings.set(element, strings);
67     return strings;
68   },
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;
77       let control;
78       if (!id) {
79         control = label.control;
80         if (!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)"
85           );
86           if (nodes.length == 1) {
87             control = nodes[0];
88           } else {
89             continue;
90           }
91         }
92         id = control.id;
93       }
94       if (id) {
95         let labels = this._mappedLabels.get(id);
96         if (labels) {
97           labels.push(label);
98         } else {
99           this._mappedLabels.set(id, [label]);
100         }
101       } else {
102         // control must be non-empty here
103         this._unmappedLabelControls.push({ label, control });
104       }
105     }
106   },
108   clearLabelMap() {
109     this._mappedLabels = null;
110     this._unmappedLabelControls = null;
111     this._labelStrings = null;
112   },
114   findLabelElements(element) {
115     if (!this._mappedLabels) {
116       this.generateLabelMap(element.ownerDocument);
117     }
119     let id = element.id;
120     if (!id) {
121       return this._unmappedLabelControls
122         .filter(lc => lc.control == element)
123         .map(lc => lc.label);
124     }
125     return this._mappedLabels.get(id) || [];
126   },
129 export default LabelUtils;