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 * A factory to generate FormLike objects that represent a set of related fields
7 * which aren't necessarily marked up with a <form> element. FormLike's emulate
8 * the properties of an HTMLFormElement which are relevant to form tasks.
10 export let FormLikeFactory = {
11 _propsFromForm: ["action", "autocomplete", "ownerDocument"],
14 * Create a FormLike object from a <form>.
16 * @param {HTMLFormElement} aForm
18 * @throws Error if aForm isn't an HTMLFormElement
20 createFromForm(aForm) {
21 if (!HTMLFormElement.isInstance(aForm)) {
22 throw new Error("createFromForm: aForm must be a HTMLFormElement");
26 elements: [...aForm.elements],
30 for (let prop of this._propsFromForm) {
31 formLike[prop] = aForm[prop];
34 this._addToJSONProperty(formLike);
40 * Create a FormLike object from an <input>/<select> in a document.
42 * If the field is in a <form>, construct the FormLike from the form.
43 * Otherwise, create a FormLike with a rootElement (wrapper) according to
44 * heuristics. Currently all <input>/<select> not in a <form> are one FormLike
45 * but this shouldn't be relied upon as the heuristics may change to detect
46 * multiple "forms" (e.g. registration and login) on one page with a <form>.
48 * Note that two FormLikes created from the same field won't return the same FormLike object.
49 * Use the `rootElement` property on the FormLike as a key instead.
51 * @param {HTMLInputElement|HTMLSelectElement} aField - an <input> or <select> field in a document
53 * @throws Error if aField isn't a password or username field in a document
55 createFromField(aField) {
57 (!HTMLInputElement.isInstance(aField) &&
58 !HTMLSelectElement.isInstance(aField)) ||
61 throw new Error("createFromField requires a field in a document");
64 let rootElement = this.findRootForField(aField);
65 if (HTMLFormElement.isInstance(rootElement)) {
66 return this.createFromForm(rootElement);
69 let doc = aField.ownerDocument;
78 // FormLikes can be created when fields are inserted into the DOM. When
79 // many, many fields are inserted one after the other, we create many
80 // FormLikes, and computing the elements list becomes more and more
81 // expensive. Making the elements list lazy means that it'll only
82 // be computed when it's eventually needed (if ever).
83 ChromeUtils.defineLazyGetter(formLike, "elements", function () {
85 for (let el of this.rootElement.querySelectorAll("input, select")) {
86 // Exclude elements inside the rootElement that are already in a <form> as
87 // they will be handled by their own FormLike.
96 this._addToJSONProperty(formLike);
101 * Find the closest <form> if any when aField is inside a ShadowRoot.
103 * @param {HTMLInputElement} aField - a password or username field in a document
104 * @return {HTMLFormElement|null}
106 closestFormIgnoringShadowRoots(aField) {
107 let form = aField.closest("form");
108 let current = aField;
110 let shadowRoot = current.getRootNode();
111 if (!ShadowRoot.isInstance(shadowRoot)) {
114 let host = shadowRoot.host;
115 form = host.closest("form");
122 * Determine the Element that encapsulates the related fields. For example, if
123 * a page contains a login form and a checkout form which are "submitted"
124 * separately, and the username field is passed in, ideally this would return
125 * an ancestor Element of the username and password fields which doesn't
126 * include any of the checkout fields.
128 * @param {HTMLInputElement|HTMLSelectElement} aField - a field in a document
129 * @return {HTMLElement} - the root element surrounding related fields
131 findRootForField(aField) {
132 let form = aField.form || this.closestFormIgnoringShadowRoots(aField);
137 return aField.ownerDocument.documentElement;
141 * Add a `toJSON` property to a FormLike so logging which ends up going
142 * through dump doesn't include usless garbage from DOM objects.
144 _addToJSONProperty(aFormLike) {
145 function prettyElementOutput(aElement) {
146 let idText = aElement.id ? "#" + aElement.id : "";
148 for (let className of aElement.classList) {
149 classText += "." + className;
151 return `<${aElement.nodeName + idText + classText}>`;
154 Object.defineProperty(aFormLike, "toJSON", {
157 for (let key of Object.keys(aFormLike)) {
158 let value = aFormLike[key];
159 let cleansedValue = value;
164 for (let element of value) {
165 cleansedValue.push(prettyElementOutput(element));
170 case "ownerDocument": {
173 href: value.location.href,
179 case "rootElement": {
180 cleansedValue = prettyElementOutput(value);
185 cleansed[key] = cleansedValue;