1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
|
/* 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/. */
/**
* 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.
// @type {Map<string, array>}
_mappedLabels: null,
// An array consisting of label elements whose correponding form field doesn't
// have an id attribute.
// @type {Array<[HTMLLabelElement, HTMLElement]>}
_unmappedLabelControls: 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;
},
generateLabelMap(doc) {
this._mappedLabels = new Map();
this._unmappedLabelControls = [];
this._labelStrings = new WeakMap();
for (let label of doc.querySelectorAll("label")) {
let id = label.htmlFor;
let control;
if (!id) {
control = label.control;
if (!control) {
continue;
}
id = control.id;
}
if (id) {
let labels = this._mappedLabels.get(id);
if (labels) {
labels.push(label);
} else {
this._mappedLabels.set(id, [label]);
}
} else {
// control must be non-empty here
this._unmappedLabelControls.push({ label, control });
}
}
},
clearLabelMap() {
this._mappedLabels = null;
this._unmappedLabelControls = null;
this._labelStrings = null;
},
findLabelElements(element) {
if (!this._mappedLabels) {
this.generateLabelMap(element.ownerDocument);
}
let id = element.id;
if (!id) {
return this._unmappedLabelControls
.filter(lc => lc.control == element)
.map(lc => lc.label);
}
return this._mappedLabels.get(id) || [];
},
};
export default LabelUtils;
|