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/. */
7 var EXPORTED_SYMBOLS = ["FormLikeFactory"];
9 const { XPCOMUtils } = ChromeUtils.import(
10 "resource://gre/modules/XPCOMUtils.jsm"
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.
18 let FormLikeFactory = {
19 _propsFromForm: ["action", "autocomplete", "ownerDocument"],
22 * Create a FormLike object from a <form>.
24 * @param {HTMLFormElement} aForm
26 * @throws Error if aForm isn't an HTMLFormElement
28 createFromForm(aForm) {
29 if (ChromeUtils.getClassName(aForm) !== "HTMLFormElement") {
30 throw new Error("createFromForm: aForm must be a HTMLFormElement");
34 elements: [...aForm.elements],
38 for (let prop of this._propsFromForm) {
39 formLike[prop] = aForm[prop];
42 this._addToJSONProperty(formLike);
48 * Create a FormLike object from an <input>/<select> in a document.
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>.
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.
59 * @param {HTMLInputElement|HTMLSelectElement} aField - an <input> or <select> field in a document
61 * @throws Error if aField isn't a password or username field in a document
63 createFromField(aField) {
65 (ChromeUtils.getClassName(aField) !== "HTMLInputElement" &&
66 ChromeUtils.getClassName(aField) !== "HTMLSelectElement") ||
69 throw new Error("createFromField requires a field in a document");
72 let rootElement = this.findRootForField(aField);
73 if (ChromeUtils.getClassName(rootElement) === "HTMLFormElement") {
74 return this.createFromForm(rootElement);
77 let doc = aField.ownerDocument;
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() {
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.
104 this._addToJSONProperty(formLike);
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.
115 * @param {HTMLInputElement|HTMLSelectElement} aField - a field in a document
116 * @return {HTMLElement} - the root element surrounding related fields
118 findRootForField(aField) {
123 return aField.ownerDocument.documentElement;
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.
130 _addToJSONProperty(aFormLike) {
131 function prettyElementOutput(aElement) {
132 let idText = aElement.id ? "#" + aElement.id : "";
134 for (let className of aElement.classList) {
135 classText += "." + className;
137 return `<${aElement.nodeName + idText + classText}>`;
140 Object.defineProperty(aFormLike, "toJSON", {
143 for (let key of Object.keys(aFormLike)) {
144 let value = aFormLike[key];
145 let cleansedValue = value;
150 for (let element of value) {
151 cleansedValue.push(prettyElementOutput(element));
156 case "ownerDocument": {
159 href: value.location.href,
165 case "rootElement": {
166 cleansedValue = prettyElementOutput(value);
171 cleansed[key] = cleansedValue;