Bug 1875768 - Call the appropriate postfork handler on MacOS r=glandium
[gecko.git] / toolkit / modules / FormLikeFactory.sys.mjs
blob4950ee0f8207558d9915871c49a09a90fc3636eb
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  * 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.
9  */
10 export let FormLikeFactory = {
11   _propsFromForm: ["action", "autocomplete", "ownerDocument"],
13   /**
14    * Create a FormLike object from a <form>.
15    *
16    * @param {HTMLFormElement} aForm
17    * @return {FormLike}
18    * @throws Error if aForm isn't an HTMLFormElement
19    */
20   createFromForm(aForm) {
21     if (!HTMLFormElement.isInstance(aForm)) {
22       throw new Error("createFromForm: aForm must be a HTMLFormElement");
23     }
25     let formLike = {
26       elements: [...aForm.elements],
27       rootElement: aForm,
28     };
30     for (let prop of this._propsFromForm) {
31       formLike[prop] = aForm[prop];
32     }
34     this._addToJSONProperty(formLike);
36     return formLike;
37   },
39   /**
40    * Create a FormLike object from an <input>/<select> in a document.
41    *
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>.
47    *
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.
50    *
51    * @param {HTMLInputElement|HTMLSelectElement} aField - an <input> or <select> field in a document
52    * @return {FormLike}
53    * @throws Error if aField isn't a password or username field in a document
54    */
55   createFromField(aField) {
56     if (
57       (!HTMLInputElement.isInstance(aField) &&
58         !HTMLSelectElement.isInstance(aField)) ||
59       !aField.ownerDocument
60     ) {
61       throw new Error("createFromField requires a field in a document");
62     }
64     let rootElement = this.findRootForField(aField);
65     if (HTMLFormElement.isInstance(rootElement)) {
66       return this.createFromForm(rootElement);
67     }
69     let doc = aField.ownerDocument;
71     let formLike = {
72       action: doc.baseURI,
73       autocomplete: "on",
74       ownerDocument: doc,
75       rootElement,
76     };
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 () {
84       let elements = [];
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.
88         if (!el.form) {
89           elements.push(el);
90         }
91       }
93       return elements;
94     });
96     this._addToJSONProperty(formLike);
97     return formLike;
98   },
100   /**
101    * Find the closest <form> if any when aField is inside a ShadowRoot.
102    *
103    * @param {HTMLInputElement} aField - a password or username field in a document
104    * @return {HTMLFormElement|null}
105    */
106   closestFormIgnoringShadowRoots(aField) {
107     let form = aField.closest("form");
108     let current = aField;
109     while (!form) {
110       let shadowRoot = current.getRootNode();
111       if (!ShadowRoot.isInstance(shadowRoot)) {
112         break;
113       }
114       let host = shadowRoot.host;
115       form = host.closest("form");
116       current = host;
117     }
118     return form;
119   },
121   /**
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.
127    *
128    * @param {HTMLInputElement|HTMLSelectElement} aField - a field in a document
129    * @return {HTMLElement} - the root element surrounding related fields
130    */
131   findRootForField(aField) {
132     let form = aField.form || this.closestFormIgnoringShadowRoots(aField);
133     if (form) {
134       return form;
135     }
137     return aField.ownerDocument.documentElement;
138   },
140   /**
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.
143    */
144   _addToJSONProperty(aFormLike) {
145     function prettyElementOutput(aElement) {
146       let idText = aElement.id ? "#" + aElement.id : "";
147       let classText = "";
148       for (let className of aElement.classList) {
149         classText += "." + className;
150       }
151       return `<${aElement.nodeName + idText + classText}>`;
152     }
154     Object.defineProperty(aFormLike, "toJSON", {
155       value: () => {
156         let cleansed = {};
157         for (let key of Object.keys(aFormLike)) {
158           let value = aFormLike[key];
159           let cleansedValue = value;
161           switch (key) {
162             case "elements": {
163               cleansedValue = [];
164               for (let element of value) {
165                 cleansedValue.push(prettyElementOutput(element));
166               }
167               break;
168             }
170             case "ownerDocument": {
171               cleansedValue = {
172                 location: {
173                   href: value.location.href,
174                 },
175               };
176               break;
177             }
179             case "rootElement": {
180               cleansedValue = prettyElementOutput(value);
181               break;
182             }
183           }
185           cleansed[key] = cleansedValue;
186         }
187         return cleansed;
188       },
189     });
190   },