diff options
Diffstat (limited to 'toolkit/modules/FormLikeFactory.jsm')
-rw-r--r-- | toolkit/modules/FormLikeFactory.jsm | 166 |
1 files changed, 166 insertions, 0 deletions
diff --git a/toolkit/modules/FormLikeFactory.jsm b/toolkit/modules/FormLikeFactory.jsm new file mode 100644 index 000000000..45f25187c --- /dev/null +++ b/toolkit/modules/FormLikeFactory.jsm @@ -0,0 +1,166 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +this.EXPORTED_SYMBOLS = ["FormLikeFactory"]; + +const { classes: Cc, interfaces: Ci, results: Cr, utils: Cu } = Components; + +/** + * A factory to generate FormLike objects that represent a set of related fields + * which aren't necessarily marked up with a <form> element. FormLike's emulate + * the properties of an HTMLFormElement which are relevant to form tasks. + */ +let FormLikeFactory = { + _propsFromForm: [ + "action", + "autocomplete", + "ownerDocument", + ], + + /** + * Create a FormLike object from a <form>. + * + * @param {HTMLFormElement} aForm + * @return {FormLike} + * @throws Error if aForm isn't an HTMLFormElement + */ + createFromForm(aForm) { + if (!(aForm instanceof Ci.nsIDOMHTMLFormElement)) { + throw new Error("createFromForm: aForm must be a nsIDOMHTMLFormElement"); + } + + let formLike = { + elements: [...aForm.elements], + rootElement: aForm, + }; + + for (let prop of this._propsFromForm) { + formLike[prop] = aForm[prop]; + } + + this._addToJSONProperty(formLike); + + return formLike; + }, + + /** + * Create a FormLike object from an <input> in a document. + * + * If the field is in a <form>, construct the FormLike from the form. + * Otherwise, create a FormLike with a rootElement (wrapper) according to + * heuristics. Currently all <input> not in a <form> are one FormLike but this + * shouldn't be relied upon as the heuristics may change to detect multiple + * "forms" (e.g. registration and login) on one page with a <form>. + * + * Note that two FormLikes created from the same field won't return the same FormLike object. + * Use the `rootElement` property on the FormLike as a key instead. + * + * @param {HTMLInputElement} aField - a field in a document + * @return {FormLike} + * @throws Error if aField isn't a password or username field in a document + */ + createFromField(aField) { + if (!(aField instanceof Ci.nsIDOMHTMLInputElement) || + !aField.ownerDocument) { + throw new Error("createFromField requires a field in a document"); + } + + let rootElement = this.findRootForField(aField); + if (rootElement instanceof Ci.nsIDOMHTMLFormElement) { + return this.createFromForm(rootElement); + } + + let doc = aField.ownerDocument; + let elements = []; + for (let el of rootElement.querySelectorAll("input")) { + // Exclude elements inside the rootElement that are already in a <form> as + // they will be handled by their own FormLike. + if (!el.form) { + elements.push(el); + } + } + let formLike = { + action: doc.baseURI, + autocomplete: "on", + elements, + ownerDocument: doc, + rootElement, + }; + + this._addToJSONProperty(formLike); + return formLike; + }, + + /** + * Determine the Element that encapsulates the related fields. For example, if + * a page contains a login form and a checkout form which are "submitted" + * separately, and the username field is passed in, ideally this would return + * an ancestor Element of the username and password fields which doesn't + * include any of the checkout fields. + * + * @param {HTMLInputElement} aField - a field in a document + * @return {HTMLElement} - the root element surrounding related fields + */ + findRootForField(aField) { + if (aField.form) { + return aField.form; + } + + return aField.ownerDocument.documentElement; + }, + + /** + * Add a `toJSON` property to a FormLike so logging which ends up going + * through dump doesn't include usless garbage from DOM objects. + */ + _addToJSONProperty(aFormLike) { + function prettyElementOutput(aElement) { + let idText = aElement.id ? "#" + aElement.id : ""; + let classText = ""; + for (let className of aElement.classList) { + classText += "." + className; + } + return `<${aElement.nodeName + idText + classText}>`; + } + + Object.defineProperty(aFormLike, "toJSON", { + value: () => { + let cleansed = {}; + for (let key of Object.keys(aFormLike)) { + let value = aFormLike[key]; + let cleansedValue = value; + + switch (key) { + case "elements": { + cleansedValue = []; + for (let element of value) { + cleansedValue.push(prettyElementOutput(element)); + } + break; + } + + case "ownerDocument": { + cleansedValue = { + location: { + href: value.location.href, + }, + }; + break; + } + + case "rootElement": { + cleansedValue = prettyElementOutput(value); + break; + } + } + + cleansed[key] = cleansedValue; + } + return cleansed; + } + }); + }, +}; |