Bug 1611178 [wpt PR 21377] - Update wpt metadata, a=testonly
[gecko.git] / toolkit / modules / FormLikeFactory.jsm
bloba4d65cbe49c7563af06a107be68846b2b39610e7
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 "use strict";
7 var EXPORTED_SYMBOLS = ["FormLikeFactory"];
9 const { XPCOMUtils } = ChromeUtils.import(
10   "resource://gre/modules/XPCOMUtils.jsm"
13 /**
14  * A factory to generate FormLike objects that represent a set of related fields
15  * which aren't necessarily marked up with a <form> element. FormLike's emulate
16  * the properties of an HTMLFormElement which are relevant to form tasks.
17  */
18 let FormLikeFactory = {
19   _propsFromForm: ["action", "autocomplete", "ownerDocument"],
21   /**
22    * Create a FormLike object from a <form>.
23    *
24    * @param {HTMLFormElement} aForm
25    * @return {FormLike}
26    * @throws Error if aForm isn't an HTMLFormElement
27    */
28   createFromForm(aForm) {
29     if (ChromeUtils.getClassName(aForm) !== "HTMLFormElement") {
30       throw new Error("createFromForm: aForm must be a HTMLFormElement");
31     }
33     let formLike = {
34       elements: [...aForm.elements],
35       rootElement: aForm,
36     };
38     for (let prop of this._propsFromForm) {
39       formLike[prop] = aForm[prop];
40     }
42     this._addToJSONProperty(formLike);
44     return formLike;
45   },
47   /**
48    * Create a FormLike object from an <input>/<select> in a document.
49    *
50    * If the field is in a <form>, construct the FormLike from the form.
51    * Otherwise, create a FormLike with a rootElement (wrapper) according to
52    * heuristics. Currently all <input>/<select> not in a <form> are one FormLike
53    * but this shouldn't be relied upon as the heuristics may change to detect
54    * multiple "forms" (e.g. registration and login) on one page with a <form>.
55    *
56    * Note that two FormLikes created from the same field won't return the same FormLike object.
57    * Use the `rootElement` property on the FormLike as a key instead.
58    *
59    * @param {HTMLInputElement|HTMLSelectElement} aField - an <input> or <select> field in a document
60    * @return {FormLike}
61    * @throws Error if aField isn't a password or username field in a document
62    */
63   createFromField(aField) {
64     if (
65       (ChromeUtils.getClassName(aField) !== "HTMLInputElement" &&
66         ChromeUtils.getClassName(aField) !== "HTMLSelectElement") ||
67       !aField.ownerDocument
68     ) {
69       throw new Error("createFromField requires a field in a document");
70     }
72     let rootElement = this.findRootForField(aField);
73     if (ChromeUtils.getClassName(rootElement) === "HTMLFormElement") {
74       return this.createFromForm(rootElement);
75     }
77     let doc = aField.ownerDocument;
79     let formLike = {
80       action: doc.baseURI,
81       autocomplete: "on",
82       ownerDocument: doc,
83       rootElement,
84     };
86     // FormLikes can be created when fields are inserted into the DOM. When
87     // many, many fields are inserted one after the other, we create many
88     // FormLikes, and computing the elements list becomes more and more
89     // expensive. Making the elements list lazy means that it'll only
90     // be computed when it's eventually needed (if ever).
91     XPCOMUtils.defineLazyGetter(formLike, "elements", function() {
92       let elements = [];
93       for (let el of this.rootElement.querySelectorAll("input, select")) {
94         // Exclude elements inside the rootElement that are already in a <form> as
95         // they will be handled by their own FormLike.
96         if (!el.form) {
97           elements.push(el);
98         }
99       }
101       return elements;
102     });
104     this._addToJSONProperty(formLike);
105     return formLike;
106   },
108   /**
109    * Determine the Element that encapsulates the related fields. For example, if
110    * a page contains a login form and a checkout form which are "submitted"
111    * separately, and the username field is passed in, ideally this would return
112    * an ancestor Element of the username and password fields which doesn't
113    * include any of the checkout fields.
114    *
115    * @param {HTMLInputElement|HTMLSelectElement} aField - a field in a document
116    * @return {HTMLElement} - the root element surrounding related fields
117    */
118   findRootForField(aField) {
119     if (aField.form) {
120       return aField.form;
121     }
123     return aField.ownerDocument.documentElement;
124   },
126   /**
127    * Add a `toJSON` property to a FormLike so logging which ends up going
128    * through dump doesn't include usless garbage from DOM objects.
129    */
130   _addToJSONProperty(aFormLike) {
131     function prettyElementOutput(aElement) {
132       let idText = aElement.id ? "#" + aElement.id : "";
133       let classText = "";
134       for (let className of aElement.classList) {
135         classText += "." + className;
136       }
137       return `<${aElement.nodeName + idText + classText}>`;
138     }
140     Object.defineProperty(aFormLike, "toJSON", {
141       value: () => {
142         let cleansed = {};
143         for (let key of Object.keys(aFormLike)) {
144           let value = aFormLike[key];
145           let cleansedValue = value;
147           switch (key) {
148             case "elements": {
149               cleansedValue = [];
150               for (let element of value) {
151                 cleansedValue.push(prettyElementOutput(element));
152               }
153               break;
154             }
156             case "ownerDocument": {
157               cleansedValue = {
158                 location: {
159                   href: value.location.href,
160                 },
161               };
162               break;
163             }
165             case "rootElement": {
166               cleansedValue = prettyElementOutput(value);
167               break;
168             }
169           }
171           cleansed[key] = cleansedValue;
172         }
173         return cleansed;
174       },
175     });
176   },