summaryrefslogtreecommitdiffstats
path: root/toolkit/modules/FormLikeFactory.sys.mjs
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--toolkit/modules/FormLikeFactory.sys.mjs193
1 files changed, 193 insertions, 0 deletions
diff --git a/toolkit/modules/FormLikeFactory.sys.mjs b/toolkit/modules/FormLikeFactory.sys.mjs
new file mode 100644
index 0000000000..9c175241aa
--- /dev/null
+++ b/toolkit/modules/FormLikeFactory.sys.mjs
@@ -0,0 +1,193 @@
+/* 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/. */
+
+import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs";
+
+/**
+ * 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.
+ */
+export 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 (!HTMLFormElement.isInstance(aForm)) {
+ throw new Error("createFromForm: aForm must be a HTMLFormElement");
+ }
+
+ 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>/<select> 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>/<select> 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|HTMLSelectElement} aField - an <input> or <select> field in a document
+ * @return {FormLike}
+ * @throws Error if aField isn't a password or username field in a document
+ */
+ createFromField(aField) {
+ if (
+ (!HTMLInputElement.isInstance(aField) &&
+ !HTMLSelectElement.isInstance(aField)) ||
+ !aField.ownerDocument
+ ) {
+ throw new Error("createFromField requires a field in a document");
+ }
+
+ let rootElement = this.findRootForField(aField);
+ if (HTMLFormElement.isInstance(rootElement)) {
+ return this.createFromForm(rootElement);
+ }
+
+ let doc = aField.ownerDocument;
+
+ let formLike = {
+ action: doc.baseURI,
+ autocomplete: "on",
+ ownerDocument: doc,
+ rootElement,
+ };
+
+ // FormLikes can be created when fields are inserted into the DOM. When
+ // many, many fields are inserted one after the other, we create many
+ // FormLikes, and computing the elements list becomes more and more
+ // expensive. Making the elements list lazy means that it'll only
+ // be computed when it's eventually needed (if ever).
+ XPCOMUtils.defineLazyGetter(formLike, "elements", function () {
+ let elements = [];
+ for (let el of this.rootElement.querySelectorAll("input, select")) {
+ // 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);
+ }
+ }
+
+ return elements;
+ });
+
+ this._addToJSONProperty(formLike);
+ return formLike;
+ },
+
+ /**
+ * Find the closest <form> if any when aField is inside a ShadowRoot.
+ *
+ * @param {HTMLInputElement} aField - a password or username field in a document
+ * @return {HTMLFormElement|null}
+ */
+ closestFormIgnoringShadowRoots(aField) {
+ let form = aField.closest("form");
+ let current = aField;
+ while (!form) {
+ let shadowRoot = current.getRootNode();
+ if (!ShadowRoot.isInstance(shadowRoot)) {
+ break;
+ }
+ let host = shadowRoot.host;
+ form = host.closest("form");
+ current = host;
+ }
+ return form;
+ },
+
+ /**
+ * 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|HTMLSelectElement} aField - a field in a document
+ * @return {HTMLElement} - the root element surrounding related fields
+ */
+ findRootForField(aField) {
+ let form = aField.form || this.closestFormIgnoringShadowRoots(aField);
+ if (form) {
+ return 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;
+ },
+ });
+ },
+};