Bug 1900009 - Dispatch DOMPossibleUsernameInputAdded event for valid text and email inputs. r=dimi,edgar

Differential Revision: https://phabricator.services.mozilla.com/D214821
This commit is contained in:
Ryan Safaeian
2024-07-12 17:38:48 +00:00
parent 52f40ac5fd
commit ccd363c347
12 changed files with 290 additions and 132 deletions

View File

@@ -164,7 +164,7 @@ NS_IMPL_ISUPPORTS_CYCLE_COLLECTION_INHERITED_0(HTMLFormElement,
void HTMLFormElement::AsyncEventRunning(AsyncEventDispatcher* aEvent) {
if (aEvent->mEventType == u"DOMFormHasPassword"_ns) {
mHasPendingPasswordEvent = false;
} else if (aEvent->mEventType == u"DOMFormHasPossibleUsername"_ns) {
} else if (aEvent->mEventType == u"DOMPossibleUsernameInputAdded"_ns) {
mHasPendingPossibleUsernameEvent = false;
}
}

View File

@@ -57,7 +57,8 @@ class HTMLFormElement final : public nsGenericHTMLElement {
/** Whether we already dispatched a DOMFormHasPassword event or not */
bool mHasPendingPasswordEvent = false;
/** Whether we already dispatched a DOMFormHasPossibleUsername event or not */
/** Whether we already dispatched a DOMPossibleUsernameInputAdded event or not
*/
bool mHasPendingPossibleUsernameEvent = false;
// nsIContent

View File

@@ -4415,7 +4415,7 @@ void HTMLInputElement::MaybeDispatchLoginManagerEvents(HTMLFormElement* aForm) {
}
nsString eventType;
Element* target = nullptr;
EventTarget* target = nullptr;
if (mType == FormControlType::InputPassword) {
// Don't fire another event if we have a pending event.
@@ -4426,28 +4426,40 @@ void HTMLInputElement::MaybeDispatchLoginManagerEvents(HTMLFormElement* aForm) {
// TODO(Bug 1864404): Use one event for formless and form inputs.
eventType = aForm ? u"DOMFormHasPassword"_ns : u"DOMInputPasswordAdded"_ns;
target = aForm ? static_cast<Element*>(aForm) : this;
if (aForm) {
target = aForm;
aForm->mHasPendingPasswordEvent = true;
} else {
target = this;
}
} else if (mType == FormControlType::InputEmail ||
mType == FormControlType::InputText) {
// Don't fire a username event if:
// - <input> is not part of a form
// - we have a pending event
// - username only forms are not supported
if (!aForm || aForm->mHasPendingPossibleUsernameEvent ||
!StaticPrefs::signon_usernameOnlyForm_enabled()) {
// fire event if we have a username field without a form with the
// autcomplete value of username
if (!StaticPrefs::signon_usernameOnlyForm_enabled()) {
return;
}
eventType = u"DOMFormHasPossibleUsername"_ns;
target = aForm;
aForm->mHasPendingPossibleUsernameEvent = true;
if (aForm) {
if (aForm->mHasPendingPossibleUsernameEvent) {
return;
}
aForm->mHasPendingPossibleUsernameEvent = true;
target = aForm;
} else {
nsAutoString autocompleteValue;
GetAutocomplete(autocompleteValue);
if (!autocompleteValue.EqualsASCII("username")) {
return;
}
target = GetComposedDoc();
}
eventType = u"DOMPossibleUsernameInputAdded"_ns;
} else {
return;
}

View File

@@ -49,18 +49,33 @@ export const LoginFormFactory = {
let formLike = lazy.FormLikeFactory.createFromForm(aForm);
formLike.action = lazy.LoginHelper.getFormActionOrigin(aForm);
let rootElementsSet = this.getRootElementsWeakSetForDocument(
formLike.ownerDocument
);
rootElementsSet.add(formLike.rootElement);
this._addLoginFormToRootElementsSet(formLike);
return formLike;
},
/**
* Create a LoginForm object from the HTMLHtmlElement that is the root of the document
*
* Currently all <input> not in a <form> are one LoginForm 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>.
*
* @param {HTMLHtmlElement} aDocumentRoot
* @return {LoginForm}
* @throws Error if aDocumentRoot isn't an HTMLHtmlElement
*/
createFromDocumentRoot(aDocumentRoot) {
const formLike = lazy.FormLikeFactory.createFromDocumentRoot(aDocumentRoot);
formLike.action = lazy.LoginHelper.getLoginOrigin(aDocumentRoot.baseURI);
lazy.log.debug(
"adding",
formLike.rootElement,
"to root elements for",
formLike.ownerDocument
"Created non-form LoginForm for rootElement:",
aDocumentRoot
);
this._loginFormsByRootElement.set(formLike.rootElement, formLike);
this._addLoginFormToRootElementsSet(formLike);
return formLike;
},
@@ -113,19 +128,7 @@ export const LoginFormFactory = {
aField.ownerDocument.documentElement
);
let rootElementsSet = this.getRootElementsWeakSetForDocument(
formLike.ownerDocument
);
rootElementsSet.add(formLike.rootElement);
lazy.log.debug(
"adding",
formLike.rootElement,
"to root elements for",
formLike.ownerDocument
);
this._loginFormsByRootElement.set(formLike.rootElement, formLike);
this._addLoginFormToRootElementsSet(formLike);
return formLike;
},
@@ -145,4 +148,19 @@ export const LoginFormFactory = {
setForRootElement(aRootElement, aLoginForm) {
return this._loginFormsByRootElement.set(aRootElement, aLoginForm);
},
_addLoginFormToRootElementsSet(formLike) {
let rootElementsSet = this.getRootElementsWeakSetForDocument(
formLike.ownerDocument
);
rootElementsSet.add(formLike.rootElement);
lazy.log.debug(
"adding",
formLike.rootElement,
"to root elements for",
formLike.ownerDocument
);
this._loginFormsByRootElement.set(formLike.rootElement, formLike);
},
};

View File

@@ -771,21 +771,21 @@ export class LoginFormState {
* 2. Only contains one input field whose type is username compatible.
* 3. The username compatible input field looks like a username field
* or the form itself looks like a sign-in or sign-up form.
* Additionally, if an input is formless and its autocomplete attribute is
* set to 'username' (this check is done in the DOM to avoid firing excessive events),
* we construct a FormLike object using this input and perform the same logic
* described above to determine if the new FormLike object is username-only.
*
* @param {Element} formElement
* @param {FormLike} form
* the form to check.
* @param {Object} recipe=null
* A relevant field override recipe to use.
* @returns {Element} The username field or null (if the form is not a
* username-only form).
*/
getUsernameFieldFromUsernameOnlyForm(formElement, recipe = null) {
if (!HTMLFormElement.isInstance(formElement)) {
return null;
}
getUsernameFieldFromUsernameOnlyForm(form, recipe = null) {
let candidate = null;
for (let element of formElement.elements) {
for (let element of form.elements) {
// We are looking for a username-only form, so if there is a password
// field in the form, this is NOT a username-only form.
if (element.hasBeenTypePassword) {
@@ -811,10 +811,9 @@ export class LoginFormState {
}
candidate = element;
}
if (
candidate &&
this.#isProbablyAUsernameLoginForm(formElement, candidate)
this.#isProbablyAUsernameLoginForm(form.rootElement, candidate)
) {
return candidate;
}
@@ -1109,7 +1108,7 @@ export class LoginFormState {
}
usernameField = this.getUsernameFieldFromUsernameOnlyForm(
form.rootElement,
form,
fieldOverrideRecipe
);
@@ -1525,8 +1524,8 @@ export class LoginManagerChild extends JSWindowActorChild {
lazy.InsecurePasswordUtils.reportInsecurePasswords(formLike);
break;
}
case "DOMFormHasPossibleUsername": {
this.#onDOMFormHasPossibleUsername(event);
case "DOMPossibleUsernameInputAdded": {
this.#onDOMPossibleUsernameInputAdded(event);
break;
}
case "DOMInputPasswordAdded": {
@@ -1797,19 +1796,31 @@ export class LoginManagerChild extends JSWindowActorChild {
this._fetchLoginsFromParentAndFillForm(formLike);
}
#onDOMFormHasPossibleUsername(event) {
#onDOMPossibleUsernameInputAdded(event) {
if (!event.isTrusted) {
return;
}
const isPrimaryPasswordSet = this.#getIsPrimaryPasswordSet();
let document = event.target.ownerDocument;
let document;
let formLike;
if (HTMLFormElement.isInstance(event.target)) {
document = event.target.ownerDocument;
formLike = lazy.LoginFormFactory.createFromForm(event.target);
} else {
document = event.target;
formLike = lazy.LoginFormFactory.createFromDocumentRoot(
document.documentElement
);
}
lazy.log(
`#onDOMFormHasPossibleUsername: visibilityState: ${document.visibilityState}, isPrimaryPasswordSet: ${isPrimaryPasswordSet}.`
`#onDomPossibleUsernameInputAdded: visibilityState: ${document.visibilityState}, isPrimaryPasswordSet: ${isPrimaryPasswordSet}.`
);
// For simplicity, the result of the telemetry is stacked. This means if a
// document receives two `DOMFormHasPossibleEvent`, we add one counter to both
// document receives two `DOMPossibleUsernameInputAdded`, we add one counter to both
// bucket 1 & 2.
let docState = this.stateForDocument(document);
@@ -1823,19 +1834,16 @@ export class LoginManagerChild extends JSWindowActorChild {
}
if (document.visibilityState == "visible" || isPrimaryPasswordSet) {
this._processDOMFormHasPossibleUsernameEvent(event);
this._processDOMPossibleUsernameInputAddedEvent(formLike);
} else {
// wait until the document becomes visible before handling this event
this._deferHandlingEventUntilDocumentVisible(event, document, () => {
this._processDOMFormHasPossibleUsernameEvent(event);
this._processDOMPossibleUsernameInputAddedEvent(formLike);
});
}
}
_processDOMFormHasPossibleUsernameEvent(event) {
let form = event.target;
let formLike = lazy.LoginFormFactory.createFromForm(form);
_processDOMPossibleUsernameInputAddedEvent(formLike) {
// If the form contains a passoword field, `getUsernameFieldFromUsernameOnlyForm` returns
// null, so we don't trigger autofill for those forms here. In this function,
// we only care about username-only forms. For forms contain a password, they'll be handled
@@ -1844,8 +1852,12 @@ export class LoginManagerChild extends JSWindowActorChild {
// We specifically set the recipe to empty here to avoid loading site recipes during page loads.
// This is okay because if we end up finding a username-only form that should be ignore by
// the site recipe, the form will be skipped while autofilling later.
let docState = this.stateForDocument(form.ownerDocument);
let usernameField = docState.getUsernameFieldFromUsernameOnlyForm(form, {});
let docState = this.stateForDocument(formLike.ownerDocument);
let usernameField = docState.getUsernameFieldFromUsernameOnlyForm(
formLike,
{}
);
if (usernameField) {
// Autofill the username-only form.
lazy.log("A username-only form is found.");

View File

@@ -6,7 +6,7 @@ const ids = {
INPUT_TYPE: "",
};
function task({ contentIds, expected }) {
function task({ contentIds, expected, hasForm = true }) {
let resolve;
let promise = new Promise(r => {
resolve = r;
@@ -27,14 +27,17 @@ function task({ contentIds, expected }) {
removeEventListener("load", tabLoad, true);
gDoc = content.document;
gDoc.addEventListener("DOMFormHasPossibleUsername", unexpectedContentEvent);
addEventListener("DOMFormHasPossibleUsername", unexpectedContentEvent);
gDoc.addEventListener(
"DOMPossibleUsernameInputAdded",
unexpectedContentEvent
);
addEventListener("DOMPossibleUsernameInputAdded", unexpectedContentEvent);
gDoc.defaultView.setTimeout(test_inputAdd, 0);
}
function test_inputAdd() {
if (expected) {
addEventListener("DOMFormHasPossibleUsername", test_inputAddHandler, {
addEventListener("DOMPossibleUsernameInputAdded", test_inputAddHandler, {
once: true,
capture: true,
});
@@ -45,26 +48,46 @@ function task({ contentIds, expected }) {
input.setAttribute("type", contentIds.INPUT_TYPE);
input.setAttribute("id", contentIds.INPUT_ID);
input.setAttribute("data-test", "unique-attribute");
gDoc.getElementById(contentIds.FORM1_ID).appendChild(input);
if (hasForm) {
gDoc.getElementById(contentIds.FORM1_ID).appendChild(input);
} else {
input.setAttribute("autocomplete", "username");
gDoc.querySelector("body").appendChild(input);
}
}
function test_inputAddHandler(evt) {
if (expected) {
evt.stopPropagation();
Assert.equal(
evt.target.id,
contentIds.FORM1_ID,
evt.type +
" event targets correct form element (added possible username element)"
);
if (hasForm) {
Assert.equal(
evt.target.id,
contentIds.FORM1_ID,
evt.type + " event targets correct username element"
);
} else {
Assert.ok(
HTMLDocument.isInstance(evt.target),
evt.type + " event targets document"
);
}
}
gDoc.defaultView.setTimeout(test_inputChangeForm, 0);
let nextTask;
if (hasForm) {
nextTask = test_inputChangeForm;
} else if (!hasForm && contentIds.INPUT_TYPE !== "text") {
nextTask = test_inputChangesType;
} else {
nextTask = finish;
}
gDoc.defaultView.setTimeout(nextTask, 0);
}
function test_inputChangeForm() {
if (expected) {
addEventListener(
"DOMFormHasPossibleUsername",
"DOMPossibleUsernameInputAdded",
test_inputChangeFormHandler,
{ once: true, capture: true }
);
@@ -81,7 +104,7 @@ function task({ contentIds, expected }) {
Assert.equal(
evt.target.id,
contentIds.FORM2_ID,
evt.type + " event targets correct form element (changed form)"
evt.type + " event targets correct username element"
);
}
// TODO(Bug 1864405): Refactor this test to not expect a DOM event
@@ -96,7 +119,7 @@ function task({ contentIds, expected }) {
function test_inputChangesType() {
if (expected) {
addEventListener(
"DOMFormHasPossibleUsername",
"DOMPossibleUsernameInputAdded",
test_inputChangesTypeHandler,
{ once: true, capture: true }
);
@@ -110,19 +133,29 @@ function task({ contentIds, expected }) {
function test_inputChangesTypeHandler(evt) {
if (expected) {
evt.stopPropagation();
Assert.equal(
evt.target.id,
contentIds.FORM1_ID,
evt.type + " event targets correct form element (changed type)"
);
if (hasForm) {
Assert.equal(
evt.target.id,
contentIds.FORM1_ID,
evt.type + " event targets correct input element (changed type)"
);
} else {
Assert.ok(
HTMLDocument.isInstance(evt.target),
evt.type + " event targets document"
);
}
}
gDoc.defaultView.setTimeout(finish, 0);
}
function finish() {
removeEventListener("DOMFormHasPossibleUsername", unexpectedContentEvent);
removeEventListener(
"DOMPossibleUsernameInputAdded",
unexpectedContentEvent
);
gDoc.removeEventListener(
"DOMFormHasPossibleUsername",
"DOMPossibleUsernameInputAdded",
unexpectedContentEvent
);
resolve();
@@ -148,20 +181,20 @@ add_task(async function test_disconnectedInputs() {
);
};
addEventListener("DOMFormHasPossibleUsername", unexpectedEvent);
addEventListener("DOMPossibleUsernameInputAdded", unexpectedEvent);
const form = content.document.createElement("form");
const textInput = content.document.createElement("input");
textInput.setAttribute("type", "text");
form.appendChild(textInput);
// Delay the execution for a bit to allow time for any asynchronously
// dispatched 'DOMFormHasPossibleUsername' events to be processed.
// dispatched 'DOMPossibleUsernameInputAdded' events to be processed.
// This is necessary because such events might not be triggered immediately,
// and we want to ensure that if they are dispatched, they are captured
// before we remove the event listener.
// eslint-disable-next-line mozilla/no-arbitrary-setTimeout
await new Promise(resolve => setTimeout(resolve, 50));
removeEventListener("DOMFormHasPossibleUsername", unexpectedEvent);
removeEventListener("DOMPossibleUsernameInputAdded", unexpectedEvent);
});
Assert.ok(true, "Test completed");
@@ -195,6 +228,30 @@ add_task(async function test_usernameOnlyForm() {
}
});
add_task(async function test_formlessUsernameInput() {
for (let type of ["text", "email"]) {
let tab = (gBrowser.selectedTab = BrowserTestUtils.addTab(gBrowser));
ids.INPUT_TYPE = type;
let promise = ContentTask.spawn(
tab.linkedBrowser,
{ contentIds: ids, expected: true, hasForm: false },
task
);
BrowserTestUtils.startLoadingURIString(
tab.linkedBrowser,
`data:text/html;charset=utf-8,
<html><body>
<input id="${ids.CHANGE_INPUT_ID}" autocomplete="username">
</body></html>`
);
await promise;
Assert.ok(true, "Test completed");
gBrowser.removeCurrentTab();
}
});
add_task(async function test_nonSupportedInputType() {
for (let type of ["url", "tel", "number"]) {
let tab = (gBrowser.selectedTab = BrowserTestUtils.addTab(gBrowser));

View File

@@ -421,10 +421,10 @@ async function checkUnmodifiedFormInFrame(bc, formNum) {
return SpecialPowers.spawn(bc, [formNum], formNumF => {
let form = this.content.document.getElementById(`form${formNumF}`);
ok(form, "Locating form " + formNumF);
const elements =
form.nodeName === "form" ? form.elements : form.querySelectorAll("input");
for (var i = 0; i < form.elements.length; i++) {
var ele = form.elements[i];
for (const ele of elements) {
// No point in checking form submit/reset buttons.
if (ele.type == "submit" || ele.type == "reset") {
continue;
@@ -466,10 +466,15 @@ async function checkLoginFormInFrameWithElementValues(
let numToCheck = arguments.length - 1;
const elements =
form.nodeName === "form"
? form.elements
: form.querySelectorAll("input");
if (!numToCheck--) {
return;
}
e = form.elements[0];
e = elements[0];
if (val1F == null) {
is(
e.value,
@@ -488,7 +493,7 @@ async function checkLoginFormInFrameWithElementValues(
return;
}
e = form.elements[1];
e = elements[1];
if (val2F == null) {
is(
e.value,
@@ -506,7 +511,7 @@ async function checkLoginFormInFrameWithElementValues(
if (!numToCheck--) {
return;
}
e = form.elements[2];
e = elements[2];
if (val3F == null) {
is(
e.value,

View File

@@ -13,7 +13,8 @@ Test autofill on username-form
<script>
add_setup(async () => {
await setStoredLoginsAsync(
[window.location.origin, "https://autofill", null, "user1", "pass1"]
[window.location.origin, "https://autofill", null, "user1", "pass1"],
[window.location.origin, "http://mochi.test", null, "user2", "pass1"]
);
});
@@ -28,10 +29,9 @@ add_task(async function test_autofill_username_only_form() {
let win = window.open("about:blank");
SimpleTest.registerCleanupFunction(() => win.close());
// 5 out of the 7 forms should be autofilled
// 6 out of the 10 forms should be autofilled
await loadFormIntoWindow(window.location.origin, `
<!-- no password field, 1 username field -->
<form id='form1' action='https://autofill'> 1
<!-- no password field, 1 username field --> <form id='form1' action='https://autofill'> 1
<input type='text' name='uname' autocomplete='username' value=''>
<button type='submit'>Submit</button>
@@ -85,7 +85,24 @@ add_task(async function test_autofill_username_only_form() {
<button type='submit'>Submit</button>
<button type='reset'> Reset </button>
</form>`, win, 5);
</form>
<!-- no form, one text input field (without autocomplete == 'username'), should be ignored -->
<div id="form8"> 8
<input id='un1' name='shouldnotfill' placeholder=username value= ''>
</div>
<!-- no password field, 1 username field without autocomplete-->
<form id='form9' action='https://autofill'> 9
<input type="text" id="username" name='uname' value=''>
</form>
<!-- no form, two text input fields with autcomplete == 'username', should be ignored -->
<div id='form10'> 10
<input name='shouldnotfill' autocomplete=username value = ''>
<input name='shouldnotfill' autocomplete=username value = ''>
</div>`, win, 6);
await checkLoginFormInFrameWithElementValues(win, 1, "user1");
await checkLoginFormInFrameWithElementValues(win, 2, "someuser");
@@ -94,7 +111,21 @@ add_task(async function test_autofill_username_only_form() {
await checkUnmodifiedFormInFrame(win, 5);
await checkUnmodifiedFormInFrame(win, 6);
await checkUnmodifiedFormInFrame(win, 7);
await checkUnmodifiedFormInFrame(win, 8);
await checkLoginFormInFrameWithElementValues(win, 9, "user1");
await checkUnmodifiedFormInFrame(win, 10);
// We must close and reopen the window since multiple formless inputs in one document are grouped together,
// which won't trigger autofill.
win.close()
win = window.open("about:blank");
await loadFormIntoWindow(window.location.origin, `
<!-- no form, 1 username field with autocomplete, without a value set -->
<div id='form1'> 1
<input type='text' name='uname' autocomplete='username' value=''>
</div>`, win, 1);
await checkLoginFormInFrameWithElementValues(win, 1, "user2");
await resetRecipes();
});
</script>

View File

@@ -102,7 +102,7 @@ const TESTCASES = [
{
description: "1 username field outside of a <form>",
document: `<input id="un1" autocomplete=username>`,
returnedFieldIDs: [null, null, null],
returnedFieldIDs: ["un1", null, null],
},
{
description: "username with type=user",

View File

@@ -8,6 +8,10 @@ const { LoginManagerChild } = ChromeUtils.importESModule(
"resource://gre/modules/LoginManagerChild.sys.mjs"
);
const { FormLikeFactory } = ChromeUtils.importESModule(
"resource://gre/modules/FormLikeFactory.sys.mjs"
);
// expectation[0] tests cases when a form doesn't have a sign-in keyword.
// expectation[1] tests cases when a form has a sign-in keyword.
const TESTCASES = [
@@ -162,10 +166,11 @@ for (let tc of TESTCASES) {
form.setAttribute("name", "login");
}
const formLike = FormLikeFactory.createFromForm(form);
const lmc = new LoginManagerChild();
const docState = lmc.stateForDocument(form.ownerDocument);
const element = docState.getUsernameFieldFromUsernameOnlyForm(
form,
formLike,
testcase.fieldOverrideRecipe
);
Assert.strictEqual(

View File

@@ -379,7 +379,7 @@ let JSWINDOWACTORS = {
"form-submission-detected": { createActor: false },
"before-form-submission": { createActor: false },
DOMFormHasPassword: {},
DOMFormHasPossibleUsername: {},
DOMPossibleUsernameInputAdded: {},
DOMInputPasswordAdded: {},
},
},

View File

@@ -36,6 +36,53 @@ export let FormLikeFactory = {
return formLike;
},
/**
* Create a FormLike object from an HTMLHtmlElement that is the root of the document
*
* Currently all <input> not in a <form> are one LoginForm 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>.
*
* @param {HTMLHtmlElement} aDocumentRoot
* @return {FormLike}
* @throws Error if aDocumentRoot isn't an HTMLHtmlElement
*/
createFromDocumentRoot(aDocumentRoot) {
if (!HTMLHtmlElement.isInstance(aDocumentRoot)) {
throw new Error(
"createFromDocumentRoot: aDocumentRoot must be an HTMLHtmlElement"
);
}
let formLike = {
action: aDocumentRoot.baseURI,
autocomplete: "on",
ownerDocument: aDocumentRoot.ownerDocument,
rootElement: aDocumentRoot,
};
// 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).
ChromeUtils.defineLazyGetter(formLike, "elements", function () {
let elements = [];
for (let el of aDocumentRoot.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;
},
/**
* Create a FormLike object from an <input>/<select> in a document.
*
@@ -61,40 +108,10 @@ export let FormLikeFactory = {
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).
ChromeUtils.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;
const rootElement = this.findRootForField(aField);
return HTMLFormElement.isInstance(rootElement)
? this.createFromForm(rootElement)
: this.createFromDocumentRoot(rootElement);
},
/**