mount-observer 0.0.111 → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (179) hide show
  1. package/Events.js +28 -26
  2. package/Events.ts +34 -30
  3. package/MountObserver.js +235 -520
  4. package/MountObserver.ts +281 -542
  5. package/README.md +149 -56
  6. package/attrCoordinates.js +93 -0
  7. package/attrCoordinates.ts +122 -0
  8. package/constants.js +6 -0
  9. package/constants.ts +7 -0
  10. package/index.js +3 -0
  11. package/index.ts +19 -0
  12. package/loadImports.js +47 -0
  13. package/loadImports.ts +56 -0
  14. package/package.json +8 -115
  15. package/playwright.config.ts +0 -1
  16. package/types.d.ts +86 -0
  17. package/whereAttr.js +174 -0
  18. package/whereAttr.ts +221 -0
  19. package/LICENSE +0 -21
  20. package/Newish.js +0 -145
  21. package/Newish.ts +0 -169
  22. package/ObsAttr.js +0 -18
  23. package/ObsAttr.ts +0 -18
  24. package/RootMutObs.js +0 -49
  25. package/RootMutObs.ts +0 -58
  26. package/Synthesizer.js +0 -125
  27. package/Synthesizer.ts +0 -130
  28. package/bindish.js +0 -15
  29. package/bindish.ts +0 -22
  30. package/compose.js +0 -148
  31. package/compose.ts +0 -164
  32. package/doCleanup.js +0 -31
  33. package/doCleanup.ts +0 -34
  34. package/getWhereAttrSelector.js +0 -83
  35. package/getWhereAttrSelector.ts +0 -92
  36. package/preloadContent.js +0 -44
  37. package/preloadContent.ts +0 -47
  38. package/readAttrs.ts +0 -60
  39. package/refid/README.md +0 -259
  40. package/refid/arr.js +0 -4
  41. package/refid/arr.ts +0 -4
  42. package/refid/camelToKebab.js +0 -4
  43. package/refid/camelToKebab.ts +0 -4
  44. package/refid/genIds.js +0 -190
  45. package/refid/genIds.ts +0 -177
  46. package/refid/getAdjRefs.js +0 -38
  47. package/refid/getAdjRefs.ts +0 -38
  48. package/refid/getContext.js +0 -13
  49. package/refid/getContext.ts +0 -14
  50. package/refid/getCount.js +0 -8
  51. package/refid/getCount.ts +0 -8
  52. package/refid/getIsh.js +0 -35
  53. package/refid/getIsh.ts +0 -37
  54. package/refid/hostish.js +0 -18
  55. package/refid/hostish.ts +0 -20
  56. package/refid/ism.js +0 -78
  57. package/refid/ism.ts +0 -81
  58. package/refid/itemprops.js +0 -60
  59. package/refid/itemprops.ts +0 -67
  60. package/refid/joinMatching.js +0 -56
  61. package/refid/joinMatching.ts +0 -54
  62. package/refid/nudge.js +0 -23
  63. package/refid/nudge.ts +0 -23
  64. package/refid/regIsh.js +0 -27
  65. package/refid/regIsh.ts +0 -31
  66. package/refid/secretKeys.js +0 -5
  67. package/refid/secretKeys.ts +0 -5
  68. package/refid/splitRefs.js +0 -6
  69. package/refid/splitRefs.ts +0 -6
  70. package/refid/stdVal.js +0 -15
  71. package/refid/stdVal.ts +0 -15
  72. package/refid/via.js +0 -114
  73. package/refid/via.ts +0 -113
  74. package/slotkin/affine.js +0 -39
  75. package/slotkin/affine.ts +0 -46
  76. package/slotkin/beKindred.js +0 -45
  77. package/slotkin/beKindred.ts +0 -55
  78. package/slotkin/getBreadth.js +0 -19
  79. package/slotkin/getBreadth.ts +0 -21
  80. package/slotkin/getFrag.js +0 -22
  81. package/slotkin/getFrag.ts +0 -21
  82. package/slotkin/toQuery.js +0 -12
  83. package/slotkin/toQuery.ts +0 -13
  84. package/slotkin/wrap.js +0 -13
  85. package/slotkin/wrap.ts +0 -18
  86. package/ts-refs/LICENSE +0 -21
  87. package/ts-refs/README.md +0 -18
  88. package/ts-refs/be-a-beacon/types.d.ts +0 -22
  89. package/ts-refs/be-alit/types.d.ts +0 -1
  90. package/ts-refs/be-based/types.d.ts +0 -32
  91. package/ts-refs/be-bound/types.d.ts +0 -65
  92. package/ts-refs/be-buttoned-up/types.d.ts +0 -21
  93. package/ts-refs/be-calculating/types.d.ts +0 -57
  94. package/ts-refs/be-clonable/types.d.ts +0 -28
  95. package/ts-refs/be-committed/types.d.ts +0 -26
  96. package/ts-refs/be-consoling/types.d.ts +0 -25
  97. package/ts-refs/be-counted/types.d.ts +0 -88
  98. package/ts-refs/be-delible/types.d.ts +0 -26
  99. package/ts-refs/be-directive/types.d.ts +0 -43
  100. package/ts-refs/be-dispatching/types.d.ts +0 -41
  101. package/ts-refs/be-elevating/types.d.ts +0 -55
  102. package/ts-refs/be-enhanced/types.d.ts +0 -32
  103. package/ts-refs/be-enhancing/types.d.ts +0 -31
  104. package/ts-refs/be-evanescent/types.d.ts +0 -20
  105. package/ts-refs/be-eventing/types.d.ts +0 -27
  106. package/ts-refs/be-exportable/types.d.ts +0 -26
  107. package/ts-refs/be-fetching/types.d.ts +0 -73
  108. package/ts-refs/be-flashy/types.d.ts +0 -27
  109. package/ts-refs/be-formalizing/types.d.ts +0 -29
  110. package/ts-refs/be-formidable/types.d.ts +0 -64
  111. package/ts-refs/be-giddy/types.d.ts +0 -26
  112. package/ts-refs/be-gingerly/types.d.ts +0 -19
  113. package/ts-refs/be-gone/types.d.ts +0 -24
  114. package/ts-refs/be-hashing-out/types.d.ts +0 -22
  115. package/ts-refs/be-hive/types.d.ts +0 -18
  116. package/ts-refs/be-imbued/types.d.ts +0 -30
  117. package/ts-refs/be-included/types.d.ts +0 -20
  118. package/ts-refs/be-inclusive/types.d.ts +0 -30
  119. package/ts-refs/be-intersectional/types.d.ts +0 -37
  120. package/ts-refs/be-intl/types.d.ts +0 -28
  121. package/ts-refs/be-invoking/types.d.ts +0 -28
  122. package/ts-refs/be-joining/types.d.ts +0 -26
  123. package/ts-refs/be-kvetching/types.d.ts +0 -24
  124. package/ts-refs/be-lazy/types.d.ts +0 -29
  125. package/ts-refs/be-literate/types.d.ts +0 -29
  126. package/ts-refs/be-mediating/types.d.ts +0 -34
  127. package/ts-refs/be-methodical/types.d.ts +0 -20
  128. package/ts-refs/be-modding/types.d.ts +0 -18
  129. package/ts-refs/be-observant/types.d.ts +0 -27
  130. package/ts-refs/be-observing/types.d.ts +0 -84
  131. package/ts-refs/be-parsed/types.d.ts +0 -19
  132. package/ts-refs/be-parsing/types.d.ts +0 -37
  133. package/ts-refs/be-persistent/types.d.ts +0 -66
  134. package/ts-refs/be-propagating/types.d.ts +0 -26
  135. package/ts-refs/be-reformable/types.d.ts +0 -48
  136. package/ts-refs/be-render-neutral/types.d.ts +0 -31
  137. package/ts-refs/be-scoped/types.d.ts +0 -24
  138. package/ts-refs/be-sharing/types.d.ts +0 -17
  139. package/ts-refs/be-switched/types.d.ts +0 -155
  140. package/ts-refs/be-typed/types.d.ts +0 -36
  141. package/ts-refs/be-value-added/types.d.ts +0 -34
  142. package/ts-refs/be-valued/types.d.ts +0 -22
  143. package/ts-refs/be-written/types.d.ts +0 -59
  144. package/ts-refs/css-charts/types.d.ts +0 -38
  145. package/ts-refs/css-echarts/types.d.ts +0 -13
  146. package/ts-refs/data-props/types.d.ts +0 -27
  147. package/ts-refs/do-inc/types.d.ts +0 -28
  148. package/ts-refs/do-invoke/types.d.ts +0 -28
  149. package/ts-refs/do-toggle/types.d.ts +0 -27
  150. package/ts-refs/em-bower/types.d.ts +0 -18
  151. package/ts-refs/fetch-for/types.d.ts +0 -37
  152. package/ts-refs/folder-picker/types.d.ts +0 -43
  153. package/ts-refs/for-fetch/doc.d.ts +0 -98
  154. package/ts-refs/for-fetch/types.d.ts +0 -83
  155. package/ts-refs/mount-observer/types.d.ts +0 -248
  156. package/ts-refs/mt-si/types.d.ts +0 -21
  157. package/ts-refs/per-each/types.d.ts +0 -51
  158. package/ts-refs/soak-up/types.d.ts +0 -36
  159. package/ts-refs/trans-render/XV/types.d.ts +0 -69
  160. package/ts-refs/trans-render/asmr/types.d.ts +0 -138
  161. package/ts-refs/trans-render/be/types.d.ts +0 -190
  162. package/ts-refs/trans-render/dss/types.d.ts +0 -57
  163. package/ts-refs/trans-render/froop/types.d.ts +0 -416
  164. package/ts-refs/trans-render/funions/types.d.ts +0 -12
  165. package/ts-refs/trans-render/lib/mixins/types.d.ts +0 -42
  166. package/ts-refs/trans-render/lib/prs/types.d.ts +0 -40
  167. package/ts-refs/trans-render/lib/types.d.ts +0 -489
  168. package/ts-refs/trans-render/types.d.ts +0 -583
  169. package/ts-refs/wc-info/SimpleWCInfo.d.ts +0 -15
  170. package/ts-refs/when-resolved/types.d.ts +0 -30
  171. package/ts-refs/xp-as/types.d.ts +0 -20
  172. package/ts-refs/xtal-element/types.d.ts +0 -43
  173. package/ts-refs/xtal-frappe-chart/types.d.ts +0 -193
  174. package/upShadowSearch.js +0 -25
  175. package/upShadowSearch.ts +0 -23
  176. package/waitForEvent.js +0 -12
  177. package/waitForEvent.ts +0 -13
  178. package/waitForIsh.js +0 -21
  179. package/waitForIsh.ts +0 -20
package/MountObserver.ts CHANGED
@@ -1,615 +1,354 @@
1
- import {MountInit, IMountObserver, AddMutationEventListener,
2
- MutationEvent, dismountEventName, mountEventName, IMountEvent, IDismountEvent,
3
- disconnectedEventName, IDisconnectEvent, IAttrChangeEvent, attrChangeEventName, AttrChangeInfo, loadEventName, ILoadEvent,
4
- AttrParts,
5
- MOSE, WeakDual,
1
+ import {
2
+ MountInit,
6
3
  MountObserverOptions,
7
- Assigner,
8
- RefType
9
- } from './ts-refs/mount-observer/types';
10
- import {RootMutObs} from './RootMutObs.js';
11
- import {bindish, bindishIt} from './bindish.js';
12
- import './refid/hostish.js'; // gets embedded even if not used
13
- export {MOSE} from './ts-refs/mount-observer/types';
14
- export const guid = '5Pv6bHOVH0ae07opRZ8N/g';
15
- export const wasItemReffed = Symbol.for('8aA6xB8+PkScmivaslBk5Q');
16
-
17
- export const mutationObserverLookup = new WeakMap<Node, RootMutObs>();
18
- const refCount = new WeakMap<Node, number>();
19
- export class MountObserver extends EventTarget implements IMountObserver{
20
-
21
- #mountInit: MountInit;
22
- #options: MountObserverOptions | undefined;
23
- //#rootMutObs: RootMutObs | undefined;
4
+ IMountObserver,
5
+ MountContext,
6
+ AttrChange
7
+ } from './types.js';
8
+ import {
9
+ MountEvent,
10
+ DismountEvent,
11
+ DisconnectEvent,
12
+ LoadEvent,
13
+ AttrChangeEvent
14
+ } from './Events.js';
15
+
16
+ export class MountObserver extends EventTarget implements IMountObserver {
17
+ #init: MountInit;
18
+ #options: MountObserverOptions;
24
19
  #abortController: AbortController;
25
- mountedElements: WeakDual<Element>;
26
- #mountedList: Array<WeakRef<Element>> | undefined;
27
- #disconnected: WeakSet<Element>;
28
- //#unmounted: WeakSet<Element>;
29
- #isComplex: boolean;
30
- objNde: WeakRef<Node> | undefined;
31
-
32
- constructor(init: MountInit){
20
+ #modules: any[] = [];
21
+ #mountedElements = new WeakSet<Element>();
22
+ #processedElements = new WeakSet<Element>();
23
+ #mutationObserver: MutationObserver | undefined;
24
+ #rootNode: WeakRef<Node> | undefined;
25
+ #importsLoaded = false;
26
+ #elementAttrStates = new WeakMap<Element, Map<string, string | null>>();
27
+ #matchesWhereAttrFn: ((element: Element, whereAttr: any) => boolean) | null = null;
28
+ #buildAttrCoordinateMapFn: ((whereAttr: any, isCustomElement: boolean) => any) | null = null;
29
+
30
+ constructor(init: MountInit, options: MountObserverOptions = {}) {
33
31
  super();
34
- const {on, whereElementIntersectsWith, whereMediaMatches} = init;
35
- let isComplex = false;
36
- //TODO: study this problem further. Starting to think this is basically not polyfillable
37
- if(on !== undefined){
38
- const reducedMatch = on.replaceAll(':not(', '');
39
- isComplex = reducedMatch.includes(' ') || (reducedMatch.includes(':') && reducedMatch.includes('('));
40
- }
41
- this.#isComplex = isComplex;
42
- if(whereElementIntersectsWith) throw 'NI'; //not implemented
43
- this.#mountInit = init;
32
+ this.#init = init;
33
+ this.#options = options;
44
34
  this.#abortController = new AbortController();
45
- this.mountedElements = {
46
- weakSet: new WeakSet(),
47
- setWeak: new Set(),
48
- };
49
- this.#disconnected = new WeakSet();
50
- //this.#unmounted = new WeakSet();
51
- }
52
-
53
- #calculatedSelector: string | undefined;
54
- #attrParts: Array<AttrParts> | undefined;
55
-
56
- #fullListOfEnhancementAttrs: Array<string> | undefined;
57
- async observedAttrs(){
58
- await this.#selector();
59
- return this.#fullListOfEnhancementAttrs;
60
- }
61
- //get #attrVals
62
- async #selector() : Promise<string>{
63
- if(this.#calculatedSelector !== undefined) return this.#calculatedSelector;
64
- const {on, whereAttr} = this.#mountInit;
65
- const withoutAttrs = on || '*';
66
- if(whereAttr === undefined) return withoutAttrs;
67
- const {getWhereAttrSelector} = await import('./getWhereAttrSelector.js');
68
- const info = await getWhereAttrSelector(whereAttr, withoutAttrs);
69
- const {fullListOfAttrs, calculatedSelector, partitionedAttrs} = info;
70
- this.#fullListOfEnhancementAttrs = fullListOfAttrs;
71
- this.#attrParts = partitionedAttrs;
72
- this.#calculatedSelector = calculatedSelector
73
- return this.#calculatedSelector;
74
- }
75
35
 
76
- //This method is called publicly from outside mount-observer -- keep it public
77
- async composeFragment(fragment: DocumentFragment, level: number){
78
- const bis = fragment.querySelectorAll(`${inclTemplQry}`) as NodeListOf<HTMLTemplateElement>;
79
- for(const bi of bis){
80
- if(bi.getAttribute('rel') === 'preload'){
81
- (await import('./preloadContent.js')).preloadContent(bi);
82
- }else{
83
- await this.#compose(bi, level);
84
- }
85
-
36
+ if (options.disconnectedSignal) {
37
+ options.disconnectedSignal.addEventListener('abort', () => {
38
+ this.disconnect();
39
+ });
86
40
  }
87
- }
88
-
89
41
 
90
- async #compose(el: HTMLTemplateElement, level: number){
91
- //[TODO]: load async, not used often
92
- const src = el.getAttribute('src');
93
- if(src === null || src.length < 2) return;
94
- const refType = src[0] as RefType;
95
- if(!['!', '#'].includes(refType)) return;
96
- const {compose} = await import('./compose.js');
97
- await compose(this, el, level, src.substring(1), refType);
98
-
42
+ // Preload whereAttr utilities if needed
43
+ if (init.whereAttr) {
44
+ this.#preloadWhereAttrUtilities();
45
+ }
99
46
 
100
- }
101
- #templLookUp: Map<string, HTMLElement> = new Map();
102
- #searchForComment(refName: string, fragment: Node){
103
- //get rid of
104
- const iterator = document.evaluate(
105
- `//comment()[.="${refName}"]`,
106
- fragment,
107
- null,
108
- XPathResult.ANY_TYPE,
109
- null
110
- );
111
- //console.log({xpathResult})
112
- try {
113
- let thisNode = iterator.iterateNext();
114
- return thisNode;
115
- }catch(e){
116
- return null;
47
+ // Start loading imports if eager
48
+ if (init.loadingEagerness === 'eager' && init.import) {
49
+ this.#loadImports();
117
50
  }
118
51
  }
119
- async findByID(
120
- //[TODO]: make external, not always used
121
- refName: string, fragment: DocumentFragment,
122
- refType: RefType): Promise<HTMLElement | DocumentFragment | null>{
123
- if(this.#templLookUp.has(refName)) return this.#templLookUp.get(refName)!;
124
- let templ: Node | null = null;
125
- templ = refType === '#' ? fragment.querySelector(`#${refName}`) : this.#searchForComment(refName, fragment);
126
- if(templ === null){
127
- let rootToSearchOutwardFrom = ((fragment.isConnected ? fragment.getRootNode() : this.#mountInit.withTargetShadowRoot) || document) as any;
128
- templ = refType === '#' ? rootToSearchOutwardFrom.getElementById(refName) : this.#searchForComment(refName, rootToSearchOutwardFrom);
129
- while(templ === null && rootToSearchOutwardFrom !== (document as any as DocumentFragment) ){
130
- rootToSearchOutwardFrom = (rootToSearchOutwardFrom.host || rootToSearchOutwardFrom).getRootNode() as DocumentFragment;
131
- templ = refType === '#' ? rootToSearchOutwardFrom.getElementById(refName) : this.#searchForComment(refName, rootToSearchOutwardFrom);
132
- }
52
+
53
+ async #preloadWhereAttrUtilities(): Promise<void> {
54
+ if (!this.#matchesWhereAttrFn) {
55
+ const { matchesWhereAttr } = await import('./whereAttr.js');
56
+ this.#matchesWhereAttrFn = matchesWhereAttr;
133
57
  }
134
-
135
-
136
- if(templ !== null) {
137
- if(!(templ instanceof HTMLTemplateElement)){
138
- const newTempl = document.createElement('template');
139
- const {getAdjRefs} = await import('./refid/getAdjRefs.js');
140
- const adjRefs = getAdjRefs(templ);
141
- // if(adjRefs.length > 1){
142
- // (<any>newTempl)[wasItemReffed] = true;
143
- // adjRefs[0].setAttribute('itemref', '<autogen>');
144
- // }
145
- const fragment = document.createDocumentFragment();
146
- let first = true;
147
- for(const adjRef of adjRefs){
148
- const clone = adjRef.cloneNode(true) as HTMLElement;
149
- if(refType === '#' && clone instanceof Element){
150
- if(first && adjRefs.length > 1){
151
- clone.setAttribute('itemref', '<autogen>');
152
- (<any>newTempl)[wasItemReffed] = true;
153
- first = false;
154
- }
155
- clone.removeAttribute('id');
156
- }
157
-
158
- fragment.appendChild(clone);
159
- }
160
- if(templ instanceof Element){
161
- const {doCleanup} = await import('./doCleanup.js');
162
- doCleanup(templ as HTMLElement, fragment);
163
-
164
- }else{
165
- //TODO: cleanup
166
- }
167
- newTempl.content.appendChild(fragment);
168
- templ = newTempl;
169
-
170
- }
171
- this.#templLookUp.set(refName, templ as HTMLTemplateElement);
58
+ if (!this.#buildAttrCoordinateMapFn) {
59
+ const { buildAttrCoordinateMap } = await import('./attrCoordinates.js');
60
+ this.#buildAttrCoordinateMapFn = buildAttrCoordinateMap;
172
61
  }
173
- return templ as HTMLTemplateElement;
174
62
  }
175
63
 
176
- disconnect(within: Node){
177
- const nodeToMonitor = this.#isComplex ? (within instanceof ShadowRoot ? within : within.getRootNode()) : within;
178
- const currentCount = refCount.get(nodeToMonitor);
179
- if(currentCount !== undefined){
180
- if(currentCount <= 1){
181
- const observer = mutationObserverLookup.get(nodeToMonitor);
182
- if(observer === undefined){
183
- console.warn(refCountErr);
184
- }else{
185
- observer.disconnect();
186
- mutationObserverLookup.delete(nodeToMonitor);
187
- refCount.delete(nodeToMonitor);
188
- }
189
- }else{
190
- refCount.set(nodeToMonitor, currentCount + 1);
191
- }
192
- }else{
193
- if(mutationObserverLookup.has(nodeToMonitor)){
194
- console.warn(refCountErr);
195
- }
196
- }
197
- this.dispatchEvent(new Event('disconnectedCallback'));
198
-
64
+ get disconnectedSignal(): AbortSignal {
65
+ return this.#abortController.signal;
199
66
  }
200
67
 
201
- async observe(within: Node, options?: MountObserverOptions){
202
- this.#options = options;
203
- const init = this.#mountInit;
204
- const {whereMediaMatches} = init;
205
- if(whereMediaMatches === undefined){
206
- await this.#observe2(within);
207
- return;
208
- }
209
- const mql = window.matchMedia(whereMediaMatches);
210
- if(mql.matches){
211
- await this.#observe2(within);
68
+ async observe(rootNode: Node): Promise<void> {
69
+ if (this.#rootNode) {
70
+ throw new Error('Already observing');
212
71
  }
213
- mql.addEventListener('change', async (e) => {
214
- if(e.matches){
215
- if(this.objNde === undefined){
216
- await this.#observe2(within);
217
- }else{
218
- await this.#mountAll();
219
- }
220
- }else{
221
- if(this.objNde !== undefined){
222
- await this.#dismountAll();
223
- }
224
- }
225
- });
226
- }
227
72
 
228
- async #observe2(within: Node){
229
- await this.#selector();
230
- this.objNde = new WeakRef(within);
231
- const nodeToMonitor = this.#isComplex ? (within instanceof ShadowRoot ? within : within.getRootNode()) : within;
232
- if(!mutationObserverLookup.has(nodeToMonitor)){
233
- mutationObserverLookup.set(nodeToMonitor, new RootMutObs(nodeToMonitor, this.#mountInit));
234
- refCount.set(nodeToMonitor, 1);
235
- }else{
236
- const currentCount = refCount.get(nodeToMonitor);
237
- if(currentCount === undefined){
238
- console.warn(refCountErr);
239
- }else{
240
- refCount.set(nodeToMonitor, currentCount + 1);
241
- }
73
+ this.#rootNode = new WeakRef(rootNode);
74
+
75
+ // Wait for whereAttr utilities to load if needed
76
+ if (this.#init.whereAttr && !this.#matchesWhereAttrFn) {
77
+ await this.#preloadWhereAttrUtilities();
242
78
  }
243
- const rootMutObs = mutationObserverLookup.get(within)!;
244
- const fullListOfAttrs = this.#fullListOfEnhancementAttrs;
245
- (rootMutObs as any as AddMutationEventListener).addEventListener('mutation-event', async (e: MutationEvent) => {
246
- //TODO: disconnected
247
- if(this.#isComplex){
248
- this.#inspectWithin(within, false);
249
- return;
250
- }
251
- const {mutationRecords} = e;
252
- const elsToInspect: Array<Element> = [];
253
- //const elsToDisconnect: Array<Element> = [];
254
- const doDisconnect = this.#mountInit.do?.disconnect;
255
- let attrChangeInfosMap: Map<Element, Array<AttrChangeInfo>> | undefined;
256
- for(const mutationRecord of mutationRecords){
257
- const {addedNodes, type, removedNodes} = mutationRecord;
258
- const addedElements = Array.from(addedNodes).filter(x => x instanceof Element) as Array<Element>;
259
- addedElements.forEach(x => elsToInspect.push(x));
260
- if(type === 'attributes'){
261
- const {target, attributeName, oldValue} = mutationRecord;
262
- if(target instanceof Element && attributeName !== null /*&& this.#mounted.has(target)*/){
263
-
264
- if(fullListOfAttrs !== undefined){
265
- const idx = fullListOfAttrs.indexOf(attributeName);
266
- if(idx !== -1){
267
- if(attrChangeInfosMap === undefined) attrChangeInfosMap = new Map();
268
- let attrChangeInfos = attrChangeInfosMap.get(target);
269
- if(attrChangeInfos === undefined){
270
- attrChangeInfos = [];
271
- attrChangeInfosMap.set(target, attrChangeInfos);
272
- }
273
- const newValue = target.getAttribute(attributeName);
274
- const parts = this.#attrParts![idx];
275
- const attrChangeInfo: AttrChangeInfo = {
276
- isSOfTAttr: false,
277
- oldValue,
278
- name: attributeName,
279
- newValue,
280
- idx,
281
- parts
282
- }
283
- attrChangeInfos.push(attrChangeInfo)
284
- }
285
-
79
+
80
+ // Process existing elements
81
+ this.#processNode(rootNode);
286
82
 
83
+ // Set up mutation observer
84
+ this.#mutationObserver = new MutationObserver((mutations) => {
85
+ const attrChanges: AttrChange[] = [];
86
+
87
+ for (const mutation of mutations) {
88
+ if (mutation.type === 'childList') {
89
+ for (const node of mutation.addedNodes) {
90
+ if (node.nodeType === Node.ELEMENT_NODE) {
91
+ this.#processNode(node);
287
92
  }
288
-
289
93
  }
290
- elsToInspect.push(target as Element);
291
- }
292
-
293
- const deletedElements = Array.from(removedNodes).filter(x => x instanceof Element) as Array<Element>;
294
- const {DisconnectEvent} = await import('./Events.js');
295
- for(const deletedElement of deletedElements){
296
- this.#disconnected.add(deletedElement);
297
- if(doDisconnect !== undefined){
298
- doDisconnect(deletedElement, this, {});
94
+ mutation.removedNodes.forEach(node => {
95
+ if (node.nodeType === Node.ELEMENT_NODE) {
96
+ this.#handleRemoval(node as Element);
97
+ }
98
+ });
99
+ } else if (mutation.type === 'attributes' && mutation.target.nodeType === Node.ELEMENT_NODE) {
100
+ // Handle attribute changes for mounted elements
101
+ const element = mutation.target as Element;
102
+ if (this.#mountedElements.has(element) && this.#init.whereAttr) {
103
+ const changes = this.#checkAttrChanges(element);
104
+ attrChanges.push(...changes);
299
105
  }
300
- this.dispatchEvent(new DisconnectEvent(deletedElement));
301
- }
302
-
303
- }
304
- if(attrChangeInfosMap !== undefined){
305
- const {AttrChangeEvent} = await import('./Events.js');
306
- for(const [key, value] of attrChangeInfosMap){
307
- this.dispatchEvent(new AttrChangeEvent(key, value))
308
106
  }
309
107
  }
310
- this.#filterAndMount(elsToInspect, within, true, false);
311
- for(const el of elsToInspect){
312
- await this.#inspectWithin(el, false);
108
+
109
+ // Batch and dispatch attribute changes
110
+ if (attrChanges.length > 0) {
111
+ this.dispatchEvent(new AttrChangeEvent(attrChanges));
313
112
  }
314
- }, {signal: this.#abortController.signal});
113
+ });
114
+
115
+ const observerConfig: MutationObserverInit = {
116
+ childList: true,
117
+ subtree: true
118
+ };
315
119
 
316
- await this.#inspectWithin(within, true);
120
+ // Add attribute observation if whereAttr is configured
121
+ if (this.#init.whereAttr) {
122
+ observerConfig.attributes = true;
123
+ observerConfig.attributeOldValue = true;
124
+ }
125
+
126
+ this.#mutationObserver.observe(rootNode, observerConfig);
317
127
  }
318
128
 
319
- static synthesize(within: Document | ShadowRoot, customElement: {new(): HTMLElement}, mose: MOSE){
320
- //TODO: make external
321
- mose.type = 'mountobserver';
322
- const name = customElements.getName(customElement);
323
- if(name === null) throw 400;
324
- let instance = within.querySelector(name);
325
- if(instance === null){
326
- instance = new customElement();
327
- if(within === document){
328
- within.head.appendChild(instance);
329
- }else{
330
- within.appendChild(instance);
331
- }
129
+ disconnect(): void {
130
+ if (this.#mutationObserver) {
131
+ this.#mutationObserver.disconnect();
132
+ this.#mutationObserver = undefined;
332
133
  }
333
- instance.appendChild(mose);
134
+ this.#abortController.abort();
135
+ this.#rootNode = undefined;
334
136
  }
335
137
 
336
- #confirmInstanceOf(el: Element, whereInstanceOf: Array<{new(): Element}>){
337
- for(const test of whereInstanceOf){
338
- if(el instanceof test) return true;
138
+ async #loadImports(): Promise<void> {
139
+ if (this.#importsLoaded || !this.#init.import) {
140
+ return;
339
141
  }
340
- return false;
142
+
143
+ // Dynamically load the import utilities only when needed
144
+ const { loadImports } = await import('./loadImports.js');
145
+ this.#modules = await loadImports(this.#init.import);
146
+ this.#importsLoaded = true;
147
+
148
+ this.dispatchEvent(new LoadEvent(this.#modules));
341
149
  }
342
150
 
343
- async #mount(matching: Array<Element>, initializing: boolean){
344
- //first unmount non matching
345
- const alreadyMounted = await this.#filterAndDismount();
346
- const mount = this.#mountInit.do?.mount;
347
- const {import: imp} = this.#mountInit;
348
- const me = this.mountedElements;
349
- const options = this.#options;
350
- for(const match of matching){
351
- if(alreadyMounted.has(match)) continue;
352
- if(!me.weakSet.has(match)){
353
- me.setWeak.add(new WeakRef(match));
354
- me.weakSet.add(match);
355
- }
356
- if(imp !== undefined){
357
- switch(typeof imp){
358
- case 'string':
359
- this.module = await import(imp);
360
- break;
361
- case 'object':
362
- if(Array.isArray(imp)){
363
- throw 'NI: Firefox'
364
- }
365
- break;
366
- case 'function':
367
- this.module = await imp(match, this, {
368
- stage: 'Import',
369
- initializing
370
- });
371
- break;
372
- }
373
- }
374
- if(mount !== undefined) {
375
- mount(match, this, {
376
- stage: 'PostImport',
377
- initializing
378
- })
379
- }
380
- if(options?.leaveBreadcrumb){
381
- if((<any>match)[guid] === undefined){
382
- (<any>match)[guid] = new Set();
383
- }
384
- (<any>match)[guid].add(this);
385
- }
386
- const {MountEvent} = await import('./Events.js');
387
- this.dispatchEvent(new MountEvent(match, initializing));
388
- //should we automatically call readAttrs?
389
- //the thinking is it might make more sense to call that after mounting
151
+ #processNode(node: Node): void {
152
+ // If it's an element node, check if it matches
153
+ if (node.nodeType === Node.ELEMENT_NODE) {
154
+ const element = node as Element;
390
155
 
391
- this.#mountedList?.push(new WeakRef(match));
156
+ if (this.#matchesSelector(element)) {
157
+ this.#handleMatch(element);
158
+ }
392
159
  }
393
- }
394
160
 
395
- readAttrs(match: Element, branchIndexes?: Set<number>){
396
- //TODO: externalize
397
- const fullListOfAttrs = this.#fullListOfEnhancementAttrs;
398
- const attrChangeInfos: Array<AttrChangeInfo> = [];
399
- const oldValue = null;
400
- if(fullListOfAttrs !== undefined){
401
- const attrParts = this.#attrParts
161
+ // Process children
162
+ if (node.nodeType === Node.ELEMENT_NODE || node.nodeType === Node.DOCUMENT_NODE) {
163
+ const root = node as Element | Document;
402
164
 
403
- for(let idx = 0, ii = fullListOfAttrs.length; idx < ii; idx++){
404
- const parts = attrParts![idx];
405
- const {branchIdx} = parts;
406
- if(branchIndexes !== undefined){
407
- if(!branchIndexes.has(branchIdx)) continue;
408
- }
409
- const name = fullListOfAttrs[idx];
410
-
411
- const newValue = match.getAttribute(name);
412
-
413
- attrChangeInfos.push({
414
- idx,
415
- isSOfTAttr: false,
416
- newValue,
417
- oldValue,
418
- name,
419
- parts
165
+ // If whereAttr is specified, we need to check all elements
166
+ // since we can't use querySelectorAll for complex attribute matching
167
+ if (this.#init.whereAttr) {
168
+ // Get all elements matching the CSS selector first
169
+ root.querySelectorAll(this.#init.whereElementMatches).forEach(child => {
170
+ if (this.#matchesSelector(child)) {
171
+ this.#handleMatch(child);
172
+ }
420
173
  });
421
- }
422
-
423
- }
424
- const {observedAttrsWhenMounted} = this.#mountInit;
425
- if(observedAttrsWhenMounted !== undefined){
426
- for(const observedAttr of observedAttrsWhenMounted){
427
- const attrIsString = typeof observedAttr === 'string';
428
- const name = attrIsString ? observedAttr : observedAttr.name;
429
- let mapsTo: string | undefined;
430
- let newValue = match.getAttribute(name);
431
- if(!attrIsString){
432
- const {customParser, instanceOf, mapsTo: mt, valIfNull} = observedAttr;
433
- if(instanceOf || customParser) throw 'NI';
434
- if(newValue === null) newValue = valIfNull;
435
- mapsTo = mt;
436
- }
437
- attrChangeInfos.push({
438
- isSOfTAttr: true,
439
- newValue,
440
- oldValue,
441
- name,
442
- mapsTo
174
+ } else {
175
+ // Optimize: use querySelectorAll directly when no whereAttr
176
+ root.querySelectorAll(this.#init.whereElementMatches).forEach(child => {
177
+ this.#handleMatch(child);
443
178
  });
444
179
  }
445
180
  }
446
-
447
- return attrChangeInfos;
448
181
  }
449
182
 
450
- async #dismount(unmatching: Array<Element>){
451
- const onDismount = this.#mountInit.do?.dismount;
452
- const {DismountEvent} = await import('./Events.js');
453
- for(const unmatch of unmatching){
454
- if(onDismount !== undefined){
455
- onDismount(unmatch, this, {});
456
- }
457
- this.dispatchEvent(new DismountEvent(unmatch));
183
+ #matchesSelector(element: Element): boolean {
184
+ // Check whereElementMatches condition
185
+ const matchesElement = element.matches(this.#init.whereElementMatches);
186
+
187
+ // If whereAttr is not specified, only check whereElementMatches
188
+ if (!this.#init.whereAttr) {
189
+ return matchesElement;
458
190
  }
191
+
192
+ // Use cached function (should be loaded by now from constructor)
193
+ if (!this.#matchesWhereAttrFn) {
194
+ console.warn('whereAttr utilities not loaded yet');
195
+ return false;
196
+ }
197
+
198
+ // Both conditions must be true (AND logic)
199
+ return matchesElement && this.#matchesWhereAttrFn(element, this.#init.whereAttr);
459
200
  }
460
201
 
461
- async #dismountAll(){
462
- const mounted = this.#mountedList;
463
- if(mounted === undefined) return;
464
- this.#dismount(mounted.map(x => x.deref()).filter(x => x !== undefined) as Array<Element>);
465
- }
466
-
467
- async #mountAll(){
468
- //TODO: copilot created, check if needed
469
- const {whereSatisfies, whereInstanceOf} = this.#mountInit;
470
- const match = await this.#selector();
471
- const els = Array.from(document.querySelectorAll(match));
472
- this.#filterAndMount(els, document.body, false, true);
473
- }
474
-
475
- async #filterAndDismount(): Promise<Set<Element>>{
476
- const returnSet = new Set<Element>();
477
- if(this.#mountedList !== undefined){
478
- const previouslyMounted = this.#mountedList.map(x => x.deref());
479
- const {whereSatisfies, whereInstanceOf} = this.#mountInit;
480
- const match = await this.#selector();
481
- const elsToUnMount = previouslyMounted.filter(x => {
482
- if(x === undefined) return false;
483
- if(!x.matches(match)) return true;
484
- //TODO: add check for outside
485
- if(whereSatisfies !== undefined){
486
- if(!whereSatisfies(x, this, {stage: 'Inspecting', initializing: false})) return true;
487
- }
488
- returnSet.add(x);
489
- return false;
490
- }) as Array<Element>;
491
- this.#dismount(elsToUnMount);
202
+ async #handleMatch(element: Element): Promise<void> {
203
+ if (this.#processedElements.has(element)) {
204
+ return;
492
205
  }
493
- this.#mountedList = Array.from(returnSet).map(x => new WeakRef(x));
494
- return returnSet;
495
- }
496
206
 
497
- #outsideCheck(oElement: Element, matchCandidate: Element, outside: string){
498
- const elementsToExclude = Array.from(oElement.querySelectorAll(outside));
499
- for(const elementToExclude of elementsToExclude){
500
- if(elementToExclude === matchCandidate || elementToExclude.contains(matchCandidate)) return false;
207
+ // Load imports if not already loaded
208
+ if (!this.#importsLoaded && this.#init.import) {
209
+ await this.#loadImports();
501
210
  }
502
- return true;
503
- }
504
211
 
505
- async #filterAndMount(els: Array<Element>, target: Node, checkMatch: boolean, initializing: boolean){
506
- const {whereSatisfies, whereInstanceOf, assigner, outside} = this.#mountInit;
507
- const match = await this.#selector();
508
- const elsToMount = els.filter(x => {
509
- if(checkMatch){
510
- if(!x.matches(match)) return false;
212
+ this.#processedElements.add(element);
213
+ this.#mountedElements.add(element);
511
214
 
512
- //TODO: check for outside
513
- }
514
- if(outside !== undefined){
515
- if(!this.#outsideCheck(this.objNde!.deref() as Element, x, outside)) return false;
516
- }
517
- if(whereSatisfies !== undefined){
518
- if(!whereSatisfies(x, this, {stage: 'Inspecting', initializing})) return false;
519
- }
520
- if(whereInstanceOf !== undefined){
521
- if(!this.#confirmInstanceOf(x, whereInstanceOf)) return false;
522
- }
523
- return true;
524
- });
525
- for(const elToMount of elsToMount){
526
- if(elToMount.matches(inclTemplQry)){
527
- if(elToMount instanceof HTMLTemplateElement && elToMount.getAttribute('rel') === 'preload'){
528
- (await import('./preloadContent.js')).preloadContent(elToMount/*, this.#mountInit.withTargetShadowRoot*/);
529
- }else{
530
- await this.#compose(elToMount as HTMLTemplateElement, 0);
531
- }
532
-
533
- }
534
-
215
+ const rootNode = this.#rootNode?.deref();
216
+ if (!rootNode) {
217
+ // Root node was garbage collected
218
+ return;
535
219
  }
536
- await bindishIt(els, target, {assigner});
537
- if(elsToMount.length === 0) return;
538
- this.#mount(elsToMount, initializing);
539
- }
540
220
 
541
- async #inspectWithin(within: Node, initializing: boolean){
542
- //the line below had an await for bindish, consistent with the rest of the code, but it was
543
- //getting into a catch-22 scenario frequently, blocking the code for resuming.
544
- //This was observed with per-each package, demo/ScopeScript.html, clicking refresh a few times
545
- //one will see the inconsistent behavior if await is added below.
546
- const idGenerators = Array.from((within as DocumentFragment).querySelectorAll('[-id]'))
547
- if(idGenerators[0]){
548
- const {genIds} = await import('./refid/genIds.js');
549
- for(const el of idGenerators){
550
- genIds(el);
551
- el.removeAttribute('-id');
221
+ const context: MountContext = {
222
+ modules: this.#modules,
223
+ observer: this,
224
+ observeInfo: {
225
+ rootNode
552
226
  }
553
- }
554
- bindish(within as DocumentFragment, within, {assigner: this.#mountInit.assigner});
555
- await this.composeFragment(within as DocumentFragment, 0);
556
- const match = await this.#selector();
557
- const els = Array.from((within as Element).querySelectorAll(match));
558
- this.#filterAndMount(els, within, false, initializing);
227
+ };
559
228
 
560
- }
229
+ // Apply assignGingerly if specified
230
+ if (this.#init.assignGingerly) {
231
+ const { assignGingerly } = await import('assign-gingerly/index.js');
232
+ assignGingerly(element, this.#init.assignGingerly);
233
+ }
561
234
 
562
- }
235
+ // Call do callback
236
+ if (this.#init.do) {
237
+ if (typeof this.#init.do === 'function') {
238
+ this.#init.do(element, context);
239
+ } else if (this.#init.do.mount) {
240
+ this.#init.do.mount(element, context);
241
+ }
242
+ }
563
243
 
564
- //ToDO: make external
565
- export function waitForIdleNodes(nodes: Array<Node>, idleTimeout?: number): Promise<void>{
566
- const mountInit: MountInit = {
567
- idleTimeout
568
- };
569
- return new Promise((resolve) => {
570
- const mutObservers: Array<RootMutObs> = [];
571
- for(const node of nodes){
572
- const mutObs = mutationObserverLookup.get(node);
573
- if(mutObs !== undefined){
574
- mutObservers.push(mutObs);
575
- }else{
576
- const currentCount = refCount.get(node) || 0;
577
- const newMutObs = new RootMutObs(node, mountInit);
578
- mutationObserverLookup.set(node, newMutObs);
579
- refCount.set(node, currentCount + 1);
580
- mutObservers.push(newMutObs);
244
+ // Dispatch mount event
245
+ this.dispatchEvent(new MountEvent(element, this.#modules));
246
+
247
+ // Check for initial attribute changes if whereAttr is configured
248
+ if (this.#init.whereAttr) {
249
+ const changes = this.#checkAttrChanges(element);
250
+ if (changes.length > 0) {
251
+ this.dispatchEvent(new AttrChangeEvent(changes));
581
252
  }
582
253
  }
583
- if(areAllIdle(mutObservers)){
584
- resolve();
254
+ }
255
+
256
+ #checkAttrChanges(element: Element): AttrChange[] {
257
+ if (!this.#init.whereAttr || !this.#buildAttrCoordinateMapFn) {
258
+ return [];
259
+ }
260
+
261
+ const isCustomElement = element.tagName.toLowerCase().includes('-');
262
+ const attrCoordMap = this.#buildAttrCoordinateMapFn(this.#init.whereAttr, isCustomElement);
263
+
264
+ // Get or create the attribute state for this element
265
+ let attrState = this.#elementAttrStates.get(element);
266
+ if (!attrState) {
267
+ attrState = new Map<string, string | null>();
268
+ this.#elementAttrStates.set(element, attrState);
585
269
  }
586
- for(const obs of mutObservers){
587
- obs.addEventListener('is-idle', () => {
588
- if(areAllIdle(mutObservers)){
589
- resolve();
270
+
271
+ const changes: AttrChange[] = [];
272
+ const currentAttrs = new Set<string>();
273
+
274
+ // Check all possible attributes from the coordinate map
275
+ for (const attrName of Object.keys(attrCoordMap)) {
276
+ const coordinate = attrCoordMap[attrName];
277
+ const currentValue = element.getAttribute(attrName);
278
+ const previousValue = attrState.get(attrName);
279
+
280
+ if (currentValue !== null) {
281
+ currentAttrs.add(attrName);
282
+ }
283
+
284
+ // Include if: currently has value OR previously had value but now removed
285
+ if (currentValue !== null || (previousValue !== undefined && currentValue === null)) {
286
+ // Check if value changed
287
+ if (currentValue !== previousValue) {
288
+ const attrNode = currentValue !== null ? element.getAttributeNode(attrName) : null;
289
+ const mapEntry = this.#init.map?.[coordinate] || null;
290
+
291
+ changes.push({
292
+ value: currentValue,
293
+ attrNode,
294
+ mapEntry,
295
+ attrName,
296
+ coordinate,
297
+ element
298
+ });
299
+
300
+ // Update state
301
+ if (currentValue !== null) {
302
+ attrState.set(attrName, currentValue);
303
+ } else {
304
+ attrState.delete(attrName);
305
+ }
590
306
  }
591
- });
307
+ }
592
308
  }
593
- });
594
- }
595
-
596
- //make external
597
- function areAllIdle(mutObs: Array<RootMutObs>){
598
- for(const mo of mutObs){
599
- if(!mo.isIdle) return false;
309
+
310
+ return changes;
600
311
  }
601
- return true;
602
- }
603
312
 
313
+ #handleRemoval(element: Element): void {
314
+ if (!this.#mountedElements.has(element)) {
315
+ return;
316
+ }
604
317
 
605
- const refCountErr = 'mount-observer ref count mismatch';
606
- export const inclTemplQry = 'template[src^="#"]:not([hidden]),template[src^="!"]:not([hidden])';
607
-
608
- export interface MountObserver extends IMountObserver{}
318
+ this.#mountedElements.delete(element);
609
319
 
320
+ const rootNode = this.#rootNode?.deref();
321
+ if (!rootNode) {
322
+ // Root node was garbage collected
323
+ return;
324
+ }
610
325
 
326
+ const context: MountContext = {
327
+ modules: this.#modules,
328
+ observer: this,
329
+ observeInfo: {
330
+ rootNode
331
+ }
332
+ };
611
333
 
334
+ // Call dismount callback
335
+ if (this.#init.do && typeof this.#init.do !== 'function' && this.#init.do.dismount) {
336
+ this.#init.do.dismount(element, context);
337
+ }
612
338
 
339
+ // Dispatch dismount event
340
+ this.dispatchEvent(new DismountEvent(element));
613
341
 
614
- //const hasRootInDefault = ['data', 'enh', 'data-enh']
342
+ // Check if element is being moved within the same root
343
+ // If it's truly disconnected, dispatch disconnect event
344
+ setTimeout(() => {
345
+ if (!rootNode.contains(element)) {
346
+ if (this.#init.do && typeof this.#init.do !== 'function' && this.#init.do.disconnect) {
347
+ this.#init.do.disconnect(element, context);
348
+ }
615
349
 
350
+ this.dispatchEvent(new DisconnectEvent(element));
351
+ }
352
+ }, 0);
353
+ }
354
+ }