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 element that is the root of the document
42 * Currently all <input> not in a <form> are one LoginForm but this
43 * shouldn't be relied upon as the heuristics may change to detect multiple
44 * "forms" (e.g. registration and login) on one page with a <form>.
46 * @param {HTMLElement} aDocumentRoot
47 * @param {Object} aOptions
48 * @param {boolean} [aOptions.ignoreForm = false]
49 * True to always use owner document as the `form`
51 * @throws Error if aDocumentRoot is null
53 createFromDocumentRoot(aDocumentRoot, aOptions = {}) {
55 throw new Error("createFromDocumentRoot: aDocumentRoot is null");
59 action: aDocumentRoot.baseURI,
61 ownerDocument: aDocumentRoot.ownerDocument,
62 rootElement: aDocumentRoot,
65 // FormLikes can be created when fields are inserted into the DOM. When
66 // many, many fields are inserted one after the other, we create many
67 // FormLikes, and computing the elements list becomes more and more
68 // expensive. Making the elements list lazy means that it'll only
69 // be computed when it's eventually needed (if ever).
70 ChromeUtils.defineLazyGetter(formLike, "elements", function () {
72 for (let el of aDocumentRoot.querySelectorAll("input, select")) {
73 // Exclude elements inside the rootElement that are already in a <form> as
74 // they will be handled by their own FormLike.
75 if (!el.form || aOptions.ignoreForm) {
83 this._addToJSONProperty(formLike);
88 * Create a FormLike object from an <input>/<select> in a document.
90 * If the field is in a <form>, construct the FormLike from the form.
91 * Otherwise, create a FormLike with a rootElement (wrapper) according to
92 * heuristics. Currently all <input>/<select> not in a <form> are one FormLike
93 * but this shouldn't be relied upon as the heuristics may change to detect
94 * multiple "forms" (e.g. registration and login) on one page with a <form>.
96 * Note that two FormLikes created from the same field won't return the same FormLike object.
97 * Use the `rootElement` property on the FormLike as a key instead.
99 * @param {HTMLInputElement|HTMLSelectElement} aField
100 * an <input>, <select> or <iframe> field in a document
101 * @param {Object} aOptions
102 * @param {boolean} [aOptions.ignoreForm = false]
103 * True to always use owner document as the `form`
105 * @throws Error if aField isn't a password or username field in a document
107 createFromField(aField, aOptions = {}) {
109 (!HTMLInputElement.isInstance(aField) &&
110 !HTMLIFrameElement.isInstance(aField) &&
111 !HTMLSelectElement.isInstance(aField)) ||
112 !aField.ownerDocument
114 throw new Error("createFromField requires a field in a document");
117 const rootElement = this.findRootForField(aField, aOptions);
118 return HTMLFormElement.isInstance(rootElement)
119 ? this.createFromForm(rootElement)
120 : this.createFromDocumentRoot(rootElement, aOptions);
124 * Find the closest <form> if any when aField is inside a ShadowRoot.
126 * @param {HTMLInputElement} aField - a password or username field in a document
127 * @return {HTMLFormElement|null}
129 closestFormIgnoringShadowRoots(aField) {
130 let form = aField.closest("form");
131 let current = aField;
133 let shadowRoot = current.getRootNode();
134 if (!ShadowRoot.isInstance(shadowRoot)) {
137 let host = shadowRoot.host;
138 form = host.closest("form");
145 * Determine the Element that encapsulates the related fields. For example, if
146 * a page contains a login form and a checkout form which are "submitted"
147 * separately, and the username field is passed in, ideally this would return
148 * an ancestor Element of the username and password fields which doesn't
149 * include any of the checkout fields.
151 * @param {HTMLInputElement|HTMLSelectElement} aField
152 * a field in a document
153 * @return {HTMLElement} - the root element surrounding related fields
155 findRootForField(aField, { ignoreForm = false } = {}) {
157 const form = aField.form || this.closestFormIgnoringShadowRoots(aField);
163 return aField.ownerDocument.documentElement;
167 * Add a `toJSON` property to a FormLike so logging which ends up going
168 * through dump doesn't include usless garbage from DOM objects.
170 _addToJSONProperty(aFormLike) {
171 function prettyElementOutput(aElement) {
172 let idText = aElement.id ? "#" + aElement.id : "";
174 for (let className of aElement.classList) {
175 classText += "." + className;
177 return `<${aElement.nodeName + idText + classText}>`;
180 Object.defineProperty(aFormLike, "toJSON", {
183 for (let key of Object.keys(aFormLike)) {
184 let value = aFormLike[key];
185 let cleansedValue = value;
190 for (let element of value) {
191 cleansedValue.push(prettyElementOutput(element));
196 case "ownerDocument": {
199 href: value.location.href,
205 case "rootElement": {
206 cleansedValue = prettyElementOutput(value);
211 cleansed[key] = cleansedValue;