mount-observer 0.1.1 → 0.1.2

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.
package/MountObserver.js CHANGED
@@ -1,12 +1,17 @@
1
- import { MountEvent, DismountEvent, DisconnectEvent, LoadEvent, AttrChangeEvent } from './Events.js';
1
+ import { MountEvent, DismountEvent, DisconnectEvent, LoadEvent, AttrChangeEvent, } from './Events.js';
2
2
  import { registerSharedObserver, unregisterSharedObserver } from './SharedMutationObserver.js';
3
+ import { whereOutside } from './whereOutside.js';
3
4
  export class MountObserver extends EventTarget {
4
5
  #init;
5
6
  #options;
6
7
  #abortController;
7
8
  #modules = [];
8
- #mountedElements = new WeakSet();
9
- #processedElements = new WeakSet();
9
+ #mountedElements = {
10
+ weakSet: new WeakSet(),
11
+ setWeak: new Set()
12
+ };
13
+ #processedDoForElement = new WeakSet();
14
+ #processedEventsForElement = new WeakMap();
10
15
  #mutationCallback;
11
16
  #rootNode;
12
17
  #importsLoaded = false;
@@ -14,13 +19,19 @@ export class MountObserver extends EventTarget {
14
19
  #elementOnceAttrs = new WeakMap();
15
20
  #matchesWhereAttrFn = null;
16
21
  #buildAttrCoordinateMapFn = null;
22
+ #checkAttrChangesFn = null;
17
23
  #mediaQueryCleanup;
18
24
  #mediaMatches = true;
25
+ #assignGingerlySource;
19
26
  constructor(init, options = {}) {
20
27
  super();
21
28
  this.#init = init;
22
29
  this.#options = options;
23
30
  this.#abortController = new AbortController();
31
+ // Make a copy of assignGingerly config using structuredClone
32
+ if (init.assignGingerly !== undefined) {
33
+ this.#assignGingerlySource = structuredClone(init.assignGingerly);
34
+ }
24
35
  if (options.disconnectedSignal) {
25
36
  options.disconnectedSignal.addEventListener('abort', () => {
26
37
  this.disconnect();
@@ -44,6 +55,13 @@ export class MountObserver extends EventTarget {
44
55
  const { buildAttrCoordinateMap } = await import('./attrCoordinates.js');
45
56
  this.#buildAttrCoordinateMapFn = buildAttrCoordinateMap;
46
57
  }
58
+ if (!this.#checkAttrChangesFn) {
59
+ const { checkAttrChanges } = await import('./attrChanges.js');
60
+ // Create a bound function that passes the required parameters
61
+ this.#checkAttrChangesFn = (element) => {
62
+ return checkAttrChanges(element, this.#init, this.#buildAttrCoordinateMapFn, this.#elementAttrStates, this.#elementOnceAttrs);
63
+ };
64
+ }
47
65
  }
48
66
  async #setupMediaQuery() {
49
67
  if (!this.#rootNode) {
@@ -97,8 +115,8 @@ export class MountObserver extends EventTarget {
97
115
  else if (mutation.type === 'attributes' && mutation.target.nodeType === Node.ELEMENT_NODE) {
98
116
  // Handle attribute changes for mounted elements
99
117
  const element = mutation.target;
100
- if (this.#mountedElements.has(element) && this.#init.whereAttr) {
101
- const changes = this.#checkAttrChanges(element);
118
+ if (this.#mountedElements.weakSet.has(element) && this.#checkAttrChangesFn) {
119
+ const changes = this.#checkAttrChangesFn(element);
102
120
  attrChanges.push(...changes);
103
121
  }
104
122
  }
@@ -171,6 +189,13 @@ export class MountObserver extends EventTarget {
171
189
  if (!matchesElement) {
172
190
  return false;
173
191
  }
192
+ // Check whereOutside condition if specified (donut hole scoping)
193
+ if (this.#init.whereOutside) {
194
+ const rootNode = this.#rootNode?.deref();
195
+ if (!rootNode || !whereOutside(rootNode, element, this.#init.whereOutside)) {
196
+ return false;
197
+ }
198
+ }
174
199
  // Check whereAttr condition if specified
175
200
  if (this.#init.whereAttr) {
176
201
  // Use cached function (should be loaded by now from constructor)
@@ -197,15 +222,19 @@ export class MountObserver extends EventTarget {
197
222
  return true;
198
223
  }
199
224
  async #handleMatch(element) {
200
- if (this.#processedElements.has(element)) {
225
+ if (this.#processedDoForElement.has(element)) {
201
226
  return;
202
227
  }
203
228
  // Load imports if not already loaded
204
229
  if (!this.#importsLoaded && this.#init.import) {
205
230
  await this.#loadImports();
206
231
  }
207
- this.#processedElements.add(element);
208
- this.#mountedElements.add(element);
232
+ this.#processedDoForElement.add(element);
233
+ // Add to both WeakSet and Set<WeakRef> for efficient operations
234
+ if (!this.#mountedElements.weakSet.has(element)) {
235
+ this.#mountedElements.weakSet.add(element);
236
+ this.#mountedElements.setWeak.add(new WeakRef(element));
237
+ }
209
238
  const rootNode = this.#rootNode?.deref();
210
239
  if (!rootNode) {
211
240
  // Root node was garbage collected
@@ -219,9 +248,9 @@ export class MountObserver extends EventTarget {
219
248
  }
220
249
  };
221
250
  // Apply assignGingerly if specified
222
- if (this.#init.assignGingerly) {
251
+ if (this.#assignGingerlySource) {
223
252
  const { assignGingerly } = await import('assign-gingerly/index.js');
224
- assignGingerly(element, this.#init.assignGingerly);
253
+ assignGingerly(element, this.#assignGingerlySource);
225
254
  }
226
255
  // Call do callback
227
256
  if (this.#init.do) {
@@ -234,85 +263,55 @@ export class MountObserver extends EventTarget {
234
263
  }
235
264
  // Dispatch mount event
236
265
  this.dispatchEvent(new MountEvent(element, this.#modules, this.#init));
266
+ // Emit events from mounted element if configured
267
+ if (this.#init.mountedElemEmits) {
268
+ const { emitMountedElementEvents } = await import('./emitEvents.js');
269
+ await emitMountedElementEvents(element, this.#init, this.#processedEventsForElement);
270
+ }
237
271
  // Check for initial attribute changes if whereAttr is configured
238
- if (this.#init.whereAttr) {
239
- const changes = this.#checkAttrChanges(element);
272
+ if (this.#checkAttrChangesFn) {
273
+ const changes = this.#checkAttrChangesFn(element);
240
274
  if (changes.length > 0) {
241
275
  this.dispatchEvent(new AttrChangeEvent(changes, this.#init));
242
276
  }
243
277
  }
244
278
  }
245
- #checkAttrChanges(element) {
246
- if (!this.#init.whereAttr || !this.#buildAttrCoordinateMapFn) {
247
- return [];
248
- }
249
- const isCustomElement = element.tagName.toLowerCase().includes('-');
250
- const attrCoordMap = this.#buildAttrCoordinateMapFn(this.#init.whereAttr, isCustomElement);
251
- // Get or create the attribute state for this element
252
- let attrState = this.#elementAttrStates.get(element);
253
- if (!attrState) {
254
- attrState = new Map();
255
- this.#elementAttrStates.set(element, attrState);
256
- }
257
- const changes = [];
258
- const currentAttrs = new Set();
259
- // Check all possible attributes from the coordinate map
260
- for (const attrName of Object.keys(attrCoordMap)) {
261
- const coordinate = attrCoordMap[attrName];
262
- const currentValue = element.getAttribute(attrName);
263
- const previousValue = attrState.get(attrName);
264
- if (currentValue !== null) {
265
- currentAttrs.add(attrName);
266
- }
267
- // Check if this attribute has "once: true" in its map entry
268
- const mapEntry = this.#init.map?.[coordinate] || null;
269
- const isOnce = mapEntry?.once === true;
270
- // If "once" is true, check if we've already seen this attribute
271
- if (isOnce) {
272
- let onceAttrs = this.#elementOnceAttrs.get(element);
273
- if (!onceAttrs) {
274
- onceAttrs = new Set();
275
- this.#elementOnceAttrs.set(element, onceAttrs);
276
- }
277
- // If we've already seen this attribute, skip it
278
- if (onceAttrs.has(attrName)) {
279
- continue;
280
- }
281
- // Mark this attribute as seen if it currently has a value
282
- if (currentValue !== null) {
283
- onceAttrs.add(attrName);
284
- }
285
- }
286
- // Include if: currently has value OR previously had value but now removed
287
- if (currentValue !== null || (previousValue !== undefined && currentValue === null)) {
288
- // Check if value changed
289
- if (currentValue !== previousValue) {
290
- const attrNode = currentValue !== null ? element.getAttributeNode(attrName) : null;
291
- changes.push({
292
- value: currentValue,
293
- attrNode,
294
- mapEntry,
295
- attrName,
296
- coordinate,
297
- element
298
- });
299
- // Update state
300
- if (currentValue !== null) {
301
- attrState.set(attrName, currentValue);
302
- }
303
- else {
304
- attrState.delete(attrName);
305
- }
306
- }
279
+ async assignGingerly(config) {
280
+ // Handle undefined case
281
+ if (config === undefined) {
282
+ this.#assignGingerlySource = undefined;
283
+ return;
284
+ }
285
+ const { assignGingerly } = await import('assign-gingerly/index.js');
286
+ // Update the source config for future mounted elements
287
+ if (this.#assignGingerlySource === undefined) {
288
+ // No existing config, just clone the passed in object
289
+ this.#assignGingerlySource = structuredClone(config);
290
+ }
291
+ else {
292
+ // Merge into existing config using assignGingerly
293
+ assignGingerly(this.#assignGingerlySource, config);
294
+ }
295
+ // Apply to already mounted elements using setWeak for iteration
296
+ for (const ref of this.#mountedElements.setWeak) {
297
+ const element = ref.deref();
298
+ if (element) {
299
+ assignGingerly(element, config);
307
300
  }
308
301
  }
309
- return changes;
310
302
  }
311
303
  #handleRemoval(element) {
312
- if (!this.#mountedElements.has(element)) {
304
+ if (!this.#mountedElements.weakSet.has(element)) {
313
305
  return;
314
306
  }
315
- this.#mountedElements.delete(element);
307
+ // Remove from both structures
308
+ this.#mountedElements.weakSet.delete(element);
309
+ for (const ref of this.#mountedElements.setWeak) {
310
+ if (ref.deref() === element) {
311
+ this.#mountedElements.setWeak.delete(ref);
312
+ break;
313
+ }
314
+ }
316
315
  const rootNode = this.#rootNode?.deref();
317
316
  if (!rootNode) {
318
317
  // Root node was garbage collected
package/MountObserver.ts CHANGED
@@ -3,7 +3,10 @@ import {
3
3
  MountObserverOptions,
4
4
  IMountObserver,
5
5
  MountContext,
6
- AttrChange
6
+ AttrChange,
7
+ WeakDual,
8
+ EventConfig,
9
+ EventConstructor
7
10
  } from './types.js';
8
11
  import {
9
12
  MountEvent,
@@ -11,22 +14,25 @@ import {
11
14
  DisconnectEvent,
12
15
  LoadEvent,
13
16
  AttrChangeEvent,
14
- MediaMatchEvent,
15
- MediaUnmatchEvent
16
17
  } from './Events.js';
17
18
  import {
18
19
  registerSharedObserver,
19
20
  unregisterSharedObserver,
20
21
  type MutationCallback
21
22
  } from './SharedMutationObserver.js';
23
+ import { whereOutside } from './whereOutside.js';
22
24
 
23
25
  export class MountObserver extends EventTarget implements IMountObserver {
24
26
  #init: MountInit;
25
27
  #options: MountObserverOptions;
26
28
  #abortController: AbortController;
27
29
  #modules: any[] = [];
28
- #mountedElements = new WeakSet<Element>();
29
- #processedElements = new WeakSet<Element>();
30
+ #mountedElements: WeakDual<Element> = {
31
+ weakSet: new WeakSet(),
32
+ setWeak: new Set()
33
+ };
34
+ #processedDoForElement = new WeakSet<Element>();
35
+ #processedEventsForElement = new WeakMap<Element, Set<string>>();
30
36
  #mutationCallback: MutationCallback | undefined;
31
37
  #rootNode: WeakRef<Node> | undefined;
32
38
  #importsLoaded = false;
@@ -34,8 +40,10 @@ export class MountObserver extends EventTarget implements IMountObserver {
34
40
  #elementOnceAttrs = new WeakMap<Element, Set<string>>();
35
41
  #matchesWhereAttrFn: ((element: Element, whereAttr: any) => boolean) | null = null;
36
42
  #buildAttrCoordinateMapFn: ((whereAttr: any, isCustomElement: boolean) => any) | null = null;
43
+ #checkAttrChangesFn: ((element: Element) => AttrChange[]) | null = null;
37
44
  #mediaQueryCleanup?: () => void;
38
45
  #mediaMatches: boolean = true;
46
+ #assignGingerlySource: Record<string, any> | undefined;
39
47
 
40
48
  constructor(init: MountInit, options: MountObserverOptions = {}) {
41
49
  super();
@@ -43,6 +51,11 @@ export class MountObserver extends EventTarget implements IMountObserver {
43
51
  this.#options = options;
44
52
  this.#abortController = new AbortController();
45
53
 
54
+ // Make a copy of assignGingerly config using structuredClone
55
+ if (init.assignGingerly !== undefined) {
56
+ this.#assignGingerlySource = structuredClone(init.assignGingerly);
57
+ }
58
+
46
59
  if (options.disconnectedSignal) {
47
60
  options.disconnectedSignal.addEventListener('abort', () => {
48
61
  this.disconnect();
@@ -69,6 +82,19 @@ export class MountObserver extends EventTarget implements IMountObserver {
69
82
  const { buildAttrCoordinateMap } = await import('./attrCoordinates.js');
70
83
  this.#buildAttrCoordinateMapFn = buildAttrCoordinateMap;
71
84
  }
85
+ if (!this.#checkAttrChangesFn) {
86
+ const { checkAttrChanges } = await import('./attrChanges.js');
87
+ // Create a bound function that passes the required parameters
88
+ this.#checkAttrChangesFn = (element: Element) => {
89
+ return checkAttrChanges(
90
+ element,
91
+ this.#init,
92
+ this.#buildAttrCoordinateMapFn!,
93
+ this.#elementAttrStates,
94
+ this.#elementOnceAttrs
95
+ );
96
+ };
97
+ }
72
98
  }
73
99
 
74
100
  async #setupMediaQuery(): Promise<void> {
@@ -140,8 +166,8 @@ export class MountObserver extends EventTarget implements IMountObserver {
140
166
  } else if (mutation.type === 'attributes' && mutation.target.nodeType === Node.ELEMENT_NODE) {
141
167
  // Handle attribute changes for mounted elements
142
168
  const element = mutation.target as Element;
143
- if (this.#mountedElements.has(element) && this.#init.whereAttr) {
144
- const changes = this.#checkAttrChanges(element);
169
+ if (this.#mountedElements.weakSet.has(element) && this.#checkAttrChangesFn) {
170
+ const changes = this.#checkAttrChangesFn(element);
145
171
  attrChanges.push(...changes);
146
172
  }
147
173
  }
@@ -231,6 +257,14 @@ export class MountObserver extends EventTarget implements IMountObserver {
231
257
  return false;
232
258
  }
233
259
 
260
+ // Check whereOutside condition if specified (donut hole scoping)
261
+ if (this.#init.whereOutside) {
262
+ const rootNode = this.#rootNode?.deref();
263
+ if (!rootNode || !whereOutside(rootNode, element, this.#init.whereOutside)) {
264
+ return false;
265
+ }
266
+ }
267
+
234
268
  // Check whereAttr condition if specified
235
269
  if (this.#init.whereAttr) {
236
270
  // Use cached function (should be loaded by now from constructor)
@@ -263,7 +297,7 @@ export class MountObserver extends EventTarget implements IMountObserver {
263
297
  }
264
298
 
265
299
  async #handleMatch(element: Element): Promise<void> {
266
- if (this.#processedElements.has(element)) {
300
+ if (this.#processedDoForElement.has(element)) {
267
301
  return;
268
302
  }
269
303
 
@@ -272,8 +306,13 @@ export class MountObserver extends EventTarget implements IMountObserver {
272
306
  await this.#loadImports();
273
307
  }
274
308
 
275
- this.#processedElements.add(element);
276
- this.#mountedElements.add(element);
309
+ this.#processedDoForElement.add(element);
310
+
311
+ // Add to both WeakSet and Set<WeakRef> for efficient operations
312
+ if (!this.#mountedElements.weakSet.has(element)) {
313
+ this.#mountedElements.weakSet.add(element);
314
+ this.#mountedElements.setWeak.add(new WeakRef(element));
315
+ }
277
316
 
278
317
  const rootNode = this.#rootNode?.deref();
279
318
  if (!rootNode) {
@@ -290,9 +329,9 @@ export class MountObserver extends EventTarget implements IMountObserver {
290
329
  };
291
330
 
292
331
  // Apply assignGingerly if specified
293
- if (this.#init.assignGingerly) {
332
+ if (this.#assignGingerlySource) {
294
333
  const { assignGingerly } = await import('assign-gingerly/index.js');
295
- assignGingerly(element, this.#init.assignGingerly);
334
+ assignGingerly(element, this.#assignGingerlySource);
296
335
  }
297
336
 
298
337
  // Call do callback
@@ -307,100 +346,61 @@ export class MountObserver extends EventTarget implements IMountObserver {
307
346
  // Dispatch mount event
308
347
  this.dispatchEvent(new MountEvent(element, this.#modules, this.#init));
309
348
 
349
+ // Emit events from mounted element if configured
350
+ if (this.#init.mountedElemEmits) {
351
+ const { emitMountedElementEvents } = await import('./emitEvents.js');
352
+ await emitMountedElementEvents(element, this.#init, this.#processedEventsForElement);
353
+ }
354
+
310
355
  // Check for initial attribute changes if whereAttr is configured
311
- if (this.#init.whereAttr) {
312
- const changes = this.#checkAttrChanges(element);
356
+ if (this.#checkAttrChangesFn) {
357
+ const changes = this.#checkAttrChangesFn(element);
313
358
  if (changes.length > 0) {
314
359
  this.dispatchEvent(new AttrChangeEvent(changes, this.#init));
315
360
  }
316
361
  }
317
362
  }
318
363
 
319
- #checkAttrChanges(element: Element): AttrChange[] {
320
- if (!this.#init.whereAttr || !this.#buildAttrCoordinateMapFn) {
321
- return [];
364
+ async assignGingerly(config: Record<string, any> | undefined): Promise<void> {
365
+ // Handle undefined case
366
+ if (config === undefined) {
367
+ this.#assignGingerlySource = undefined;
368
+ return;
322
369
  }
323
-
324
- const isCustomElement = element.tagName.toLowerCase().includes('-');
325
- const attrCoordMap = this.#buildAttrCoordinateMapFn(this.#init.whereAttr, isCustomElement);
326
-
327
- // Get or create the attribute state for this element
328
- let attrState = this.#elementAttrStates.get(element);
329
- if (!attrState) {
330
- attrState = new Map<string, string | null>();
331
- this.#elementAttrStates.set(element, attrState);
370
+
371
+ const { assignGingerly } = await import('assign-gingerly/index.js');
372
+
373
+ // Update the source config for future mounted elements
374
+ if (this.#assignGingerlySource === undefined) {
375
+ // No existing config, just clone the passed in object
376
+ this.#assignGingerlySource = structuredClone(config);
377
+ } else {
378
+ // Merge into existing config using assignGingerly
379
+ assignGingerly(this.#assignGingerlySource, config);
332
380
  }
333
-
334
- const changes: AttrChange[] = [];
335
- const currentAttrs = new Set<string>();
336
-
337
- // Check all possible attributes from the coordinate map
338
- for (const attrName of Object.keys(attrCoordMap)) {
339
- const coordinate = attrCoordMap[attrName];
340
- const currentValue = element.getAttribute(attrName);
341
- const previousValue = attrState.get(attrName);
342
-
343
- if (currentValue !== null) {
344
- currentAttrs.add(attrName);
345
- }
346
-
347
- // Check if this attribute has "once: true" in its map entry
348
- const mapEntry = this.#init.map?.[coordinate] || null;
349
- const isOnce = mapEntry?.once === true;
350
-
351
- // If "once" is true, check if we've already seen this attribute
352
- if (isOnce) {
353
- let onceAttrs = this.#elementOnceAttrs.get(element);
354
- if (!onceAttrs) {
355
- onceAttrs = new Set<string>();
356
- this.#elementOnceAttrs.set(element, onceAttrs);
357
- }
358
-
359
- // If we've already seen this attribute, skip it
360
- if (onceAttrs.has(attrName)) {
361
- continue;
362
- }
363
-
364
- // Mark this attribute as seen if it currently has a value
365
- if (currentValue !== null) {
366
- onceAttrs.add(attrName);
367
- }
368
- }
369
-
370
- // Include if: currently has value OR previously had value but now removed
371
- if (currentValue !== null || (previousValue !== undefined && currentValue === null)) {
372
- // Check if value changed
373
- if (currentValue !== previousValue) {
374
- const attrNode = currentValue !== null ? element.getAttributeNode(attrName) : null;
375
-
376
- changes.push({
377
- value: currentValue,
378
- attrNode,
379
- mapEntry,
380
- attrName,
381
- coordinate,
382
- element
383
- });
384
-
385
- // Update state
386
- if (currentValue !== null) {
387
- attrState.set(attrName, currentValue);
388
- } else {
389
- attrState.delete(attrName);
390
- }
391
- }
381
+
382
+ // Apply to already mounted elements using setWeak for iteration
383
+ for (const ref of this.#mountedElements.setWeak) {
384
+ const element = ref.deref();
385
+ if (element) {
386
+ assignGingerly(element, config);
392
387
  }
393
388
  }
394
-
395
- return changes;
396
389
  }
397
390
 
398
391
  #handleRemoval(element: Element): void {
399
- if (!this.#mountedElements.has(element)) {
392
+ if (!this.#mountedElements.weakSet.has(element)) {
400
393
  return;
401
394
  }
402
395
 
403
- this.#mountedElements.delete(element);
396
+ // Remove from both structures
397
+ this.#mountedElements.weakSet.delete(element);
398
+ for (const ref of this.#mountedElements.setWeak) {
399
+ if (ref.deref() === element) {
400
+ this.#mountedElements.setWeak.delete(ref);
401
+ break;
402
+ }
403
+ }
404
404
 
405
405
  const rootNode = this.#rootNode?.deref();
406
406
  if (!rootNode) {