/* 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} _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} _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;