@@ -7,6 +7,26 @@ export interface InjectUtils {
77 */
88 waitForElement ( selector : string ) : Promise < Element > ;
99
10+ /**
11+ * Inserts an element into the DOM at the specified selectors.
12+ * Re-inserts element whenever it gets removed
13+ * Returns a promise that is resolved the first time the element is inserted.
14+ *
15+ * @param element The element to insert.
16+ * @param options The insertion options.
17+ * @param options.parentSelector The parent selector to insert the element into.
18+ * @param options.insertBeforeSelector The sibling selector to insert the element before.
19+ * @param callback Optional callback to be called after every insertion.
20+ */
21+ insertElement (
22+ element : Element ,
23+ options : {
24+ parentSelector : string ;
25+ insertBeforeSelector : string | null ;
26+ } ,
27+ callback ?: ( ) => void ,
28+ ) : Promise < void > ;
29+
1030 /**
1131 * Function which can be set inside injected javascript code. This function is
1232 * called on page change, when experiments are re-evaluated.
@@ -19,21 +39,32 @@ export interface InjectUtils {
1939 remove : ( ( ) => void ) | undefined ;
2040}
2141
22- export const getInjectUtils = ( ) : InjectUtils =>
42+ export const getInjectUtils = ( state : { cancelled : boolean } ) : InjectUtils =>
2343 ( {
2444 async waitForElement ( selector : string ) : Promise < Element > {
45+ let observer : MutationObserver | undefined = undefined ;
46+
47+ const findElement = ( ) => {
48+ if ( state . cancelled ) {
49+ observer ?. disconnect ( ) ;
50+ return ;
51+ }
52+
53+ return document . querySelector ( selector ) ;
54+ } ;
55+
2556 // If selector found in DOM, then return directly.
26- const elem = document . querySelector ( selector ) ;
57+ const elem = findElement ( ) ;
2758 if ( elem ) {
2859 return elem ;
2960 }
3061
3162 return new Promise < Element > ( ( resolve ) => {
3263 // An observer that is listening for all DOM mutation events.
33- const observer = new MutationObserver ( ( ) => {
34- const elem = document . querySelector ( selector ) ;
64+ observer = new MutationObserver ( ( ) => {
65+ const elem = findElement ( ) ;
3566 if ( elem ) {
36- observer . disconnect ( ) ;
67+ observer ? .disconnect ( ) ;
3768 resolve ( elem ) ;
3869 }
3970 } ) ;
@@ -46,4 +77,62 @@ export const getInjectUtils = (): InjectUtils =>
4677 } ) ;
4778 } ) ;
4879 } ,
80+
81+ insertElement (
82+ element : Element ,
83+ options : {
84+ parentSelector : string ;
85+ insertBeforeSelector : string | null ;
86+ } ,
87+ callback ?: ( ) => void ,
88+ ) : Promise < void > {
89+ return new Promise ( ( resolve ) => {
90+ let rateLimit = 0 ;
91+ let observer : MutationObserver | undefined = undefined ;
92+
93+ const checkElementInserted = ( ) => {
94+ if ( state . cancelled ) {
95+ observer ?. disconnect ( ) ;
96+ return ;
97+ }
98+
99+ if ( element . isConnected && element . ownerDocument === document ) {
100+ return ; // element was already inserted
101+ }
102+
103+ if ( rateLimit >= 10 ) {
104+ return ;
105+ }
106+ rateLimit ++ ;
107+ setTimeout ( ( ) => rateLimit -- , 1000 ) ;
108+
109+ const parent = document . querySelector ( options . parentSelector ) ;
110+ if ( ! parent ) {
111+ return ;
112+ }
113+ const sibling = options . insertBeforeSelector
114+ ? parent . querySelector ( options . insertBeforeSelector )
115+ : null ;
116+
117+ if ( options . insertBeforeSelector && ! sibling ) {
118+ // wait until matching sibling is found before inserting
119+ return ;
120+ }
121+
122+ parent . insertBefore ( element , sibling ) ;
123+ callback ?.( ) ;
124+ resolve ( ) ;
125+ } ;
126+
127+ checkElementInserted ( ) ;
128+ observer = new MutationObserver ( checkElementInserted ) ;
129+
130+ // Observe on all document changes.
131+ observer . observe ( document . documentElement , {
132+ childList : true ,
133+ subtree : true ,
134+ attributes : true ,
135+ } ) ;
136+ } ) ;
137+ } ,
49138 } as InjectUtils ) ;
0 commit comments