summaryrefslogtreecommitdiff
path: root/toolkit/modules/FormLikeFactory.jsm
diff options
context:
space:
mode:
Diffstat (limited to 'toolkit/modules/FormLikeFactory.jsm')
-rw-r--r--toolkit/modules/FormLikeFactory.jsm166
1 files changed, 166 insertions, 0 deletions
diff --git a/toolkit/modules/FormLikeFactory.jsm b/toolkit/modules/FormLikeFactory.jsm
new file mode 100644
index 0000000000..45f25187c6
--- /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;
+ }
+ });
+ },
+};