@ -15,10 +15,12 @@ import {
@@ -15,10 +15,12 @@ import {
elementIsLabelElement ,
elementIsSelectElement ,
elementIsSpanElement ,
nodeIsFormElement ,
nodeIsElement ,
elementIsInputElement ,
elementIsTextAreaElement ,
nodeIsFormElement ,
nodeIsInputElement ,
sendExtensionMessage ,
} from "../utils" ;
import { AutofillOverlayContentService } from "./abstractions/autofill-overlay-content.service" ;
@ -42,7 +44,10 @@ class CollectAutofillContentService implements CollectAutofillContentServiceInte
@@ -42,7 +44,10 @@ class CollectAutofillContentService implements CollectAutofillContentServiceInte
private elementInitializingIntersectionObserver : Set < Element > = new Set ( ) ;
private mutationObserver : MutationObserver ;
private updateAutofillElementsAfterMutationTimeout : number | NodeJS . Timeout ;
private mutationsQueue : MutationRecord [ ] [ ] = [ ] ;
private readonly updateAfterMutationTimeoutDelay = 1000 ;
private readonly formFieldQueryString ;
private readonly nonInputFormFieldTags = new Set ( [ "textarea" , "select" ] ) ;
private readonly ignoredInputTypes = new Set ( [
"hidden" ,
"submit" ,
@ -51,6 +56,7 @@ class CollectAutofillContentService implements CollectAutofillContentServiceInte
@@ -51,6 +56,7 @@ class CollectAutofillContentService implements CollectAutofillContentServiceInte
"image" ,
"file" ,
] ) ;
private useTreeWalkerStrategyFlagSet = false ;
constructor (
domElementVisibilityService : DomElementVisibilityService ,
@ -58,6 +64,17 @@ class CollectAutofillContentService implements CollectAutofillContentServiceInte
@@ -58,6 +64,17 @@ class CollectAutofillContentService implements CollectAutofillContentServiceInte
) {
this . domElementVisibilityService = domElementVisibilityService ;
this . autofillOverlayContentService = autofillOverlayContentService ;
let inputQuery = "input:not([data-bwignore])" ;
for ( const type of this . ignoredInputTypes ) {
inputQuery += ` :not([type=" ${ type } "]) ` ;
}
this . formFieldQueryString = ` ${ inputQuery } , textarea:not([data-bwignore]), select:not([data-bwignore]), span[data-bwautofill] ` ;
void sendExtensionMessage ( "getUseTreeWalkerApiForPageDetailsCollectionFeatureFlag" ) . then (
( useTreeWalkerStrategyFlag ) = >
( this . useTreeWalkerStrategyFlagSet = ! ! useTreeWalkerStrategyFlag ? . result ) ,
) ;
}
/ * *
@ -136,28 +153,86 @@ class CollectAutofillContentService implements CollectAutofillContentServiceInte
@@ -136,28 +153,86 @@ class CollectAutofillContentService implements CollectAutofillContentServiceInte
}
/ * *
* Queries the DOM for all the nodes that match the given filter callback
* and returns a collection of nodes .
* @param { Node } rootNode
* @param { Function } filterCallback
* @param { boolean } isObservingShadowRoo t
* @returns { Node [ ] }
* Queries all elements in the DOM that match the given query string .
* Also , recursively queries all shadow roots for the element .
*
* @param root - The root element to start the query from
* @param queryString - The query string to match elements agains t
* @param isObservingShadowRoot - Determines whether to observe shadow roots
* /
queryAllTreeWalkerNodes (
rootNode : Node ,
filterCallback : CallableFunction ,
isObservingShadowRoot = true ,
) : Node [ ] {
const treeWalkerQueryResults : Node [ ] = [ ] ;
deepQueryElements < T > (
root : Document | ShadowRoot | Element ,
queryString : string ,
isObservingShadowRoot = false ,
) : T [ ] {
let elements = this . queryElements < T > ( root , queryString ) ;
const shadowRoots = this . recursivelyQueryShadowRoots ( root , isObservingShadowRoot ) ;
for ( let index = 0 ; index < shadowRoots . length ; index ++ ) {
const shadowRoot = shadowRoots [ index ] ;
elements = elements . concat ( this . queryElements < T > ( shadowRoot , queryString ) ) ;
}
return elements ;
}
this . buildTreeWalkerNodesQueryResults (
rootNode ,
treeWalkerQueryResults ,
filterCallback ,
isObservingShadowRoot ,
) ;
/ * *
* Queries the DOM for elements based on the given query string .
*
* @param root - The root element to start the query from
* @param queryString - The query string to match elements against
* /
private queryElements < T > ( root : Document | ShadowRoot | Element , queryString : string ) : T [ ] {
if ( ! root . querySelector ( queryString ) ) {
return [ ] ;
}
return treeWalkerQueryResults ;
return Array . from ( root . querySelectorAll ( queryString ) ) as T [ ] ;
}
/ * *
* Recursively queries all shadow roots found within the given root element .
* Will also set up a mutation observer on the shadow root if the
* ` isObservingShadowRoot ` parameter is set to true .
*
* @param root - The root element to start the query from
* @param isObservingShadowRoot - Determines whether to observe shadow roots
* /
private recursivelyQueryShadowRoots (
root : Document | ShadowRoot | Element ,
isObservingShadowRoot = false ,
) : ShadowRoot [ ] {
let shadowRoots = this . queryShadowRoots ( root ) ;
for ( let index = 0 ; index < shadowRoots . length ; index ++ ) {
const shadowRoot = shadowRoots [ index ] ;
shadowRoots = shadowRoots . concat ( this . recursivelyQueryShadowRoots ( shadowRoot ) ) ;
if ( isObservingShadowRoot ) {
this . mutationObserver . observe ( shadowRoot , {
attributes : true ,
childList : true ,
subtree : true ,
} ) ;
}
}
return shadowRoots ;
}
/ * *
* Queries any immediate shadow roots found within the given root element .
*
* @param root - The root element to start the query from
* /
private queryShadowRoots ( root : Document | ShadowRoot | Element ) : ShadowRoot [ ] {
const shadowRoots : ShadowRoot [ ] = [ ] ;
const potentialShadowRoots = root . querySelectorAll ( ":defined" ) ;
for ( let index = 0 ; index < potentialShadowRoots . length ; index ++ ) {
const shadowRoot = this . getShadowRoot ( potentialShadowRoots [ index ] ) ;
if ( shadowRoot ) {
shadowRoots . push ( shadowRoot ) ;
}
}
return shadowRoots ;
}
/ * *
@ -294,11 +369,12 @@ class CollectAutofillContentService implements CollectAutofillContentServiceInte
@@ -294,11 +369,12 @@ class CollectAutofillContentService implements CollectAutofillContentServiceInte
fieldsLimit? : number ,
previouslyFoundFormFieldElements? : FormFieldElement [ ] ,
) : FormFieldElement [ ] {
const formFieldElements =
previouslyFoundFormFieldElements ||
( this . queryAllTreeWalkerNodes ( document . documentElement , ( node : Node ) = >
this . isNodeFormFieldElement ( node ) ,
) as FormFieldElement [ ] ) ;
let formFieldElements = previouslyFoundFormFieldElements ;
if ( ! formFieldElements ) {
formFieldElements = this . useTreeWalkerStrategyFlagSet
? this . queryTreeWalkerForAutofillFormFieldElements ( )
: this . deepQueryElements ( document , this . formFieldQueryString , true ) ;
}
if ( ! fieldsLimit || formFieldElements . length <= fieldsLimit ) {
return formFieldElements ;
@ -371,7 +447,7 @@ class CollectAutofillContentService implements CollectAutofillContentServiceInte
@@ -371,7 +447,7 @@ class CollectAutofillContentService implements CollectAutofillContentServiceInte
if ( ! autofillFieldBase . viewable ) {
this . elementInitializingIntersectionObserver . add ( element ) ;
this . intersectionObserver . observe ( element ) ;
this . intersectionObserver ? . observe ( element ) ;
}
if ( elementIsSpanElement ( element ) ) {
@ -864,28 +940,33 @@ class CollectAutofillContentService implements CollectAutofillContentServiceInte
@@ -864,28 +940,33 @@ class CollectAutofillContentService implements CollectAutofillContentServiceInte
* Queries all potential form and field elements from the DOM and returns
* a collection of form and field elements . Leverages the TreeWalker API
* to deep query Shadow DOM elements .
* @returns { { formElements : Node [ ] , formFieldElements : Node [ ] } }
* @private
* /
private queryAutofillFormAndFieldElements ( ) : {
formElements : Node [ ] ;
formFieldElements : Node [ ] ;
formElements : HTMLFormElement [ ] ;
formFieldElements : FormFieldElement [ ] ;
} {
const formElements : Node [ ] = [ ] ;
const formFieldElements : Node [ ] = [ ] ;
this . queryAllTreeWalkerNodes ( document . documentElement , ( node : Node ) = > {
if ( nodeIsFormElement ( node ) ) {
formElements . push ( node ) ;
return true ;
}
if ( this . useTreeWalkerStrategyFlagSet ) {
return this . queryTreeWalkerForAutofillFormAndFieldElements ( ) ;
}
if ( this . isNodeFormFieldElement ( node ) ) {
formFieldElements . push ( node ) ;
return true ;
const queriedElements = this . deepQueryElements < HTMLElement > (
document ,
` form, ${ this . formFieldQueryString } ` ,
true ,
) ;
const formElements : HTMLFormElement [ ] = [ ] ;
const formFieldElements : FormFieldElement [ ] = [ ] ;
for ( let index = 0 ; index < queriedElements . length ; index ++ ) {
const element = queriedElements [ index ] ;
if ( elementIsFormElement ( element ) ) {
formElements . push ( element ) ;
continue ;
}
return false ;
} ) ;
if ( this . isNodeFormFieldElement ( element ) ) {
formFieldElements . push ( element ) ;
}
}
return { formElements , formFieldElements } ;
}
@ -916,7 +997,7 @@ class CollectAutofillContentService implements CollectAutofillContentServiceInte
@@ -916,7 +997,7 @@ class CollectAutofillContentService implements CollectAutofillContentServiceInte
return true ;
}
return [ "textarea" , "select" ] . include s( nodeTagName ) && ! nodeHasBwIgnoreAttribute ;
return this . nonInputFormFieldTags . ha s( nodeTagName ) && ! nodeHasBwIgnoreAttribute ;
}
/ * *
@ -928,7 +1009,7 @@ class CollectAutofillContentService implements CollectAutofillContentServiceInte
@@ -928,7 +1009,7 @@ class CollectAutofillContentService implements CollectAutofillContentServiceInte
* @param { Node } node
* /
private getShadowRoot ( node : Node ) : ShadowRoot | null {
if ( ! nodeIsElement ( node ) || node . childNodes . length !== 0 ) {
if ( ! nodeIsElement ( node ) ) {
return null ;
}
@ -947,51 +1028,6 @@ class CollectAutofillContentService implements CollectAutofillContentServiceInte
@@ -947,51 +1028,6 @@ class CollectAutofillContentService implements CollectAutofillContentServiceInte
return ( node as any ) . openOrClosedShadowRoot ;
}
/ * *
* Recursively builds a collection of nodes that match the given filter callback .
* If a node has a ShadowRoot , it will be observed for mutations .
* @param { Node } rootNode
* @param { Node [ ] } treeWalkerQueryResults
* @param { Function } filterCallback
* @param { boolean } isObservingShadowRoot
* @private
* /
private buildTreeWalkerNodesQueryResults (
rootNode : Node ,
treeWalkerQueryResults : Node [ ] ,
filterCallback : CallableFunction ,
isObservingShadowRoot : boolean ,
) {
const treeWalker = document ? . createTreeWalker ( rootNode , NodeFilter . SHOW_ELEMENT ) ;
let currentNode = treeWalker ? . currentNode ;
while ( currentNode ) {
if ( filterCallback ( currentNode ) ) {
treeWalkerQueryResults . push ( currentNode ) ;
}
const nodeShadowRoot = this . getShadowRoot ( currentNode ) ;
if ( nodeShadowRoot ) {
if ( isObservingShadowRoot ) {
this . mutationObserver . observe ( nodeShadowRoot , {
attributes : true ,
childList : true ,
subtree : true ,
} ) ;
}
this . buildTreeWalkerNodesQueryResults (
nodeShadowRoot ,
treeWalkerQueryResults ,
filterCallback ,
isObservingShadowRoot ,
) ;
}
currentNode = treeWalker ? . nextNode ( ) ;
}
}
/ * *
* Sets up a mutation observer on the body of the document . Observes changes to
* DOM elements to ensure we have an updated set of autofill field data .
@ -1020,29 +1056,10 @@ class CollectAutofillContentService implements CollectAutofillContentServiceInte
@@ -1020,29 +1056,10 @@ class CollectAutofillContentService implements CollectAutofillContentServiceInte
return ;
}
for ( let mutationsIndex = 0 ; mutationsIndex < mutations . length ; mutationsIndex ++ ) {
const mutation = mutations [ mutationsIndex ] ;
if (
mutation . type === "childList" &&
( this . isAutofillElementNodeMutated ( mutation . removedNodes , true ) ||
this . isAutofillElementNodeMutated ( mutation . addedNodes ) )
) {
this . domRecentlyMutated = true ;
if ( this . autofillOverlayContentService ) {
this . autofillOverlayContentService . pageDetailsUpdateRequired = true ;
}
this . noFieldsFound = false ;
continue ;
}
if ( mutation . type === "attributes" ) {
this . handleAutofillElementAttributeMutation ( mutation ) ;
}
}
if ( this . domRecentlyMutated ) {
this . updateAutofillElementsAfterMutation ( ) ;
if ( ! this . mutationsQueue . length ) {
globalThis . requestIdleCallback ( this . processMutations , { timeout : 500 } ) ;
}
this . mutationsQueue . push ( mutations ) ;
} ;
/ * *
@ -1065,12 +1082,54 @@ class CollectAutofillContentService implements CollectAutofillContentServiceInte
@@ -1065,12 +1082,54 @@ class CollectAutofillContentService implements CollectAutofillContentServiceInte
this . updateAutofillElementsAfterMutation ( ) ;
}
/ * *
* Handles the processing of all mutations in the mutations queue . Will trigger
* within an idle callback to help with performance and prevent excessive updates .
* /
private processMutations = ( ) = > {
for ( let queueIndex = 0 ; queueIndex < this . mutationsQueue . length ; queueIndex ++ ) {
this . processMutationRecord ( this . mutationsQueue [ queueIndex ] ) ;
}
if ( this . domRecentlyMutated ) {
this . updateAutofillElementsAfterMutation ( ) ;
}
this . mutationsQueue = [ ] ;
} ;
/ * *
* Processes a mutation record and updates the autofill elements if necessary .
*
* @param mutations - The mutation record to process
* /
private processMutationRecord ( mutations : MutationRecord [ ] ) {
for ( let mutationIndex = 0 ; mutationIndex < mutations . length ; mutationIndex ++ ) {
const mutation = mutations [ mutationIndex ] ;
if (
mutation . type === "childList" &&
( this . isAutofillElementNodeMutated ( mutation . removedNodes , true ) ||
this . isAutofillElementNodeMutated ( mutation . addedNodes ) )
) {
this . domRecentlyMutated = true ;
if ( this . autofillOverlayContentService ) {
this . autofillOverlayContentService . pageDetailsUpdateRequired = true ;
}
this . noFieldsFound = false ;
continue ;
}
if ( mutation . type === "attributes" ) {
this . handleAutofillElementAttributeMutation ( mutation ) ;
}
}
}
/ * *
* Checks if the passed nodes either contain or are autofill elements .
* @param { NodeList } nodes
* @param { boolean } isRemovingNodes
* @returns { boolean }
* @private
*
* @param nodes - The nodes to check
* @param isRemovingNodes - Whether the nodes are being removed
* /
private isAutofillElementNodeMutated ( nodes : NodeList , isRemovingNodes = false ) : boolean {
if ( ! nodes . length ) {
@ -1078,34 +1137,41 @@ class CollectAutofillContentService implements CollectAutofillContentServiceInte
@@ -1078,34 +1137,41 @@ class CollectAutofillContentService implements CollectAutofillContentServiceInte
}
let isElementMutated = false ;
const mutatedElements : Node [ ] = [ ] ;
let mutatedElements : HTMLElement [ ] = [ ] ;
for ( let index = 0 ; index < nodes . length ; index ++ ) {
const node = nodes [ index ] ;
if ( ! nodeIsElement ( node ) ) {
continue ;
}
const autofillElementNodes = this . queryAllTreeWalkerNodes (
node ,
( walkerNode : Node ) = >
nodeIsFormElement ( walkerNode ) || this . isNodeFormFieldElement ( walkerNode ) ,
) as HTMLElement [ ] ;
if (
! this . useTreeWalkerStrategyFlagSet &&
( nodeIsFormElement ( node ) || this . isNodeFormFieldElement ( node ) )
) {
mutatedElements . push ( node as HTMLElement ) ;
}
const autofillElements = this . useTreeWalkerStrategyFlagSet
? this . queryTreeWalkerForMutatedElements ( node )
: this . deepQueryElements < HTMLElement > ( node , ` form, ${ this . formFieldQueryString } ` , true ) ;
if ( autofillElements . length ) {
mutatedElements = mutatedElements . concat ( autofillElements ) ;
}
if ( autofillElementNodes . length ) {
if ( mutatedElement s. length ) {
isElementMutated = true ;
mutatedElements . push ( . . . autofillElementNodes ) ;
}
}
if ( isRemovingNodes ) {
for ( let elementIndex = 0 ; elementIndex < mutatedElements . length ; elementIndex ++ ) {
const node = mutatedElements [ elementIndex ] ;
const element = mutatedElements [ elementIndex ] ;
this . deleteCachedAutofillElement (
node as ElementWithOpId < HTMLFormElement > | ElementWithOpId < FormFieldElement > ,
element as ElementWithOpId < HTMLFormElement > | ElementWithOpId < FormFieldElement > ,
) ;
}
} else if ( this . autofillOverlayContentService ) {
setTimeout ( ( ) = > this . setupOverlayListenersOnMutatedElements ( mutatedElements ) , 1000 ) ;
this . setupOverlayListenersOnMutatedElements ( mutatedElements ) ;
}
return isElementMutated ;
@ -1122,15 +1188,18 @@ class CollectAutofillContentService implements CollectAutofillContentServiceInte
@@ -1122,15 +1188,18 @@ class CollectAutofillContentService implements CollectAutofillContentServiceInte
for ( let elementIndex = 0 ; elementIndex < mutatedElements . length ; elementIndex ++ ) {
const node = mutatedElements [ elementIndex ] ;
if (
this . isNodeFormFieldElement ( node ) &&
! this . autofillFieldElements . get ( node as ElementWithOpId < FormFieldElement > )
! this . isNodeFormFieldElement ( node ) ||
this . autofillFieldElements . get ( node as ElementWithOpId < FormFieldElement > )
) {
continue ;
}
globalThis . requestIdleCallback (
// We are setting this item to a -1 index because we do not know its position in the DOM.
// This value should be updated with the next call to collect page details.
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
// eslint-disable-next-line @typescript-eslint/no-floating-promises
this . buildAutofillFieldItem ( node as ElementWithOpId < FormFieldElement > , - 1 ) ;
}
( ) = > void this . buildAutofillFieldItem ( node as ElementWithOpId < FormFieldElement > , - 1 ) ,
{ timeout : 1000 } ,
) ;
}
}
@ -1360,7 +1429,7 @@ class CollectAutofillContentService implements CollectAutofillContentServiceInte
@@ -1360,7 +1429,7 @@ class CollectAutofillContentService implements CollectAutofillContentServiceInte
cachedAutofillFieldElement ,
) ;
this . intersectionObserver . unobserve ( entry . target ) ;
this . intersectionObserver ? . unobserve ( entry . target ) ;
}
} ;
@ -1375,6 +1444,150 @@ class CollectAutofillContentService implements CollectAutofillContentServiceInte
@@ -1375,6 +1444,150 @@ class CollectAutofillContentService implements CollectAutofillContentServiceInte
this . mutationObserver ? . disconnect ( ) ;
this . intersectionObserver ? . disconnect ( ) ;
}
/ * *
* Queries the DOM for all the nodes that match the given filter callback
* and returns a collection of nodes .
* @param rootNode
* @param filterCallback
* @param isObservingShadowRoot
*
* @deprecated - This method remains as a fallback in the case that the deepQuery implementation fails .
* /
private queryAllTreeWalkerNodes (
rootNode : Node ,
filterCallback : CallableFunction ,
isObservingShadowRoot = true ,
) : Node [ ] {
const treeWalkerQueryResults : Node [ ] = [ ] ;
this . buildTreeWalkerNodesQueryResults (
rootNode ,
treeWalkerQueryResults ,
filterCallback ,
isObservingShadowRoot ,
) ;
return treeWalkerQueryResults ;
}
/ * *
* Recursively builds a collection of nodes that match the given filter callback .
* If a node has a ShadowRoot , it will be observed for mutations .
*
* @param rootNode
* @param treeWalkerQueryResults
* @param filterCallback
*
* @deprecated - This method remains as a fallback in the case that the deepQuery implementation fails .
* /
private buildTreeWalkerNodesQueryResults (
rootNode : Node ,
treeWalkerQueryResults : Node [ ] ,
filterCallback : CallableFunction ,
isObservingShadowRoot : boolean ,
) {
const treeWalker = document ? . createTreeWalker ( rootNode , NodeFilter . SHOW_ELEMENT ) ;
let currentNode = treeWalker ? . currentNode ;
while ( currentNode ) {
if ( filterCallback ( currentNode ) ) {
treeWalkerQueryResults . push ( currentNode ) ;
}
const nodeShadowRoot = this . getShadowRoot ( currentNode ) ;
if ( nodeShadowRoot ) {
if ( isObservingShadowRoot ) {
this . mutationObserver . observe ( nodeShadowRoot , {
attributes : true ,
childList : true ,
subtree : true ,
} ) ;
}
this . buildTreeWalkerNodesQueryResults (
nodeShadowRoot ,
treeWalkerQueryResults ,
filterCallback ,
isObservingShadowRoot ,
) ;
}
currentNode = treeWalker ? . nextNode ( ) ;
}
}
/ * *
* @deprecated - This method remains as a fallback in the case that the deepQuery implementation fails .
* /
private queryTreeWalkerForAutofillFormAndFieldElements ( ) : {
formElements : HTMLFormElement [ ] ;
formFieldElements : FormFieldElement [ ] ;
} {
const formElements : HTMLFormElement [ ] = [ ] ;
const formFieldElements : FormFieldElement [ ] = [ ] ;
this . queryAllTreeWalkerNodes ( document . documentElement , ( node : Node ) = > {
if ( nodeIsFormElement ( node ) ) {
formElements . push ( node ) ;
return true ;
}
if ( this . isNodeFormFieldElement ( node ) ) {
formFieldElements . push ( node as FormFieldElement ) ;
return true ;
}
return false ;
} ) ;
return { formElements , formFieldElements } ;
}
/ * *
* @deprecated - This method remains as a fallback in the case that the deepQuery implementation fails .
* /
private queryTreeWalkerForAutofillFormFieldElements ( ) : FormFieldElement [ ] {
return this . queryAllTreeWalkerNodes ( document . documentElement , ( node : Node ) = >
this . isNodeFormFieldElement ( node ) ,
) as FormFieldElement [ ] ;
}
/ * *
* @deprecated - This method remains as a fallback in the case that the deepQuery implementation fails .
*
* @param node - The node to query
* /
private queryTreeWalkerForMutatedElements ( node : Node ) : HTMLElement [ ] {
return this . queryAllTreeWalkerNodes (
node ,
( walkerNode : Node ) = >
nodeIsFormElement ( walkerNode ) || this . isNodeFormFieldElement ( walkerNode ) ,
) as HTMLElement [ ] ;
}
/ * *
* @deprecated - This method remains as a fallback in the case that the deepQuery implementation fails .
* /
private queryTreeWalkerForPasswordElements ( ) : HTMLElement [ ] {
return this . queryAllTreeWalkerNodes (
document . documentElement ,
( node : Node ) = > nodeIsInputElement ( node ) && node . type === "password" ,
false ,
) as HTMLElement [ ] ;
}
/ * *
* This is a temporary method to maintain a fallback strategy for the tree walker API
*
* @deprecated - This method remains as a fallback in the case that the deepQuery implementation fails .
* /
isPasswordFieldWithinDocument ( ) : boolean {
if ( this . useTreeWalkerStrategyFlagSet ) {
return Boolean ( this . queryTreeWalkerForPasswordElements ( ) ? . length ) ;
}
return Boolean ( this . deepQueryElements ( document , ` input[type="password"] ` ) ? . length ) ;
}
}
export default CollectAutofillContentService ;