mount-observer 0.1.1 → 0.1.3

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.ts CHANGED
@@ -3,30 +3,48 @@ import {
3
3
  MountObserverOptions,
4
4
  IMountObserver,
5
5
  MountContext,
6
- AttrChange
6
+ AttrChange,
7
+ WeakDual,
8
+ EventConfig,
9
+ EventConstructor,
10
+ Constructor
7
11
  } from './types.js';
12
+ import { arr } from './arr.js';
8
13
  import {
9
14
  MountEvent,
10
15
  DismountEvent,
11
16
  DisconnectEvent,
12
17
  LoadEvent,
13
18
  AttrChangeEvent,
14
- MediaMatchEvent,
15
- MediaUnmatchEvent
16
19
  } from './Events.js';
17
20
  import {
18
21
  registerSharedObserver,
19
22
  unregisterSharedObserver,
20
23
  type MutationCallback
21
24
  } from './SharedMutationObserver.js';
25
+ import { whereOutside } from './whereOutside.js';
22
26
 
23
27
  export class MountObserver extends EventTarget implements IMountObserver {
28
+ // Static registry for registered handlers
29
+ static #handlerRegistry = new Map<string, Constructor>();
30
+
31
+ static define(name: string, handler: Constructor): void {
32
+ if (this.#handlerRegistry.has(name)) {
33
+ throw new Error(`${name} already in use`);
34
+ }
35
+ this.#handlerRegistry.set(name, handler);
36
+ }
37
+
24
38
  #init: MountInit;
25
39
  #options: MountObserverOptions;
26
40
  #abortController: AbortController;
27
41
  #modules: any[] = [];
28
- #mountedElements = new WeakSet<Element>();
29
- #processedElements = new WeakSet<Element>();
42
+ #mountedElements: WeakDual<Element> = {
43
+ weakSet: new WeakSet(),
44
+ setWeak: new Set()
45
+ };
46
+ #processedDoForElement = new WeakSet<Element>();
47
+ #processedEventsForElement = new WeakMap<Element, Set<string>>();
30
48
  #mutationCallback: MutationCallback | undefined;
31
49
  #rootNode: WeakRef<Node> | undefined;
32
50
  #importsLoaded = false;
@@ -34,8 +52,13 @@ export class MountObserver extends EventTarget implements IMountObserver {
34
52
  #elementOnceAttrs = new WeakMap<Element, Set<string>>();
35
53
  #matchesWhereAttrFn: ((element: Element, whereAttr: any) => boolean) | null = null;
36
54
  #buildAttrCoordinateMapFn: ((whereAttr: any, isCustomElement: boolean) => any) | null = null;
55
+ #checkAttrChangesFn: ((element: Element) => AttrChange[]) | null = null;
37
56
  #mediaQueryCleanup?: () => void;
38
57
  #mediaMatches: boolean = true;
58
+ #asgMtSource: Record<string, any> | undefined;
59
+ #asgDisMtSource: Record<string, any> | undefined;
60
+ #elementNotifiers = new WeakMap<Element, EventTarget>();
61
+ #notifierMountedElements = new WeakSet<Element>();
39
62
 
40
63
  constructor(init: MountInit, options: MountObserverOptions = {}) {
41
64
  super();
@@ -43,23 +66,89 @@ export class MountObserver extends EventTarget implements IMountObserver {
43
66
  this.#options = options;
44
67
  this.#abortController = new AbortController();
45
68
 
69
+ const {
70
+ assignOnMount, assignOnDismount, do: doValue, reference, whereAttr, loadingEagerness,
71
+ import: imp
72
+ } = init;
73
+ // Make a copy of assignOnMount config using structuredClone
74
+ if (assignOnMount !== undefined) {
75
+ this.#asgMtSource = structuredClone(assignOnMount);
76
+ }
77
+ if (assignOnDismount !== undefined) {
78
+ this.#asgDisMtSource = structuredClone(assignOnDismount);
79
+ }
80
+
46
81
  if (options.disconnectedSignal) {
47
82
  options.disconnectedSignal.addEventListener('abort', () => {
48
83
  this.disconnect();
49
84
  });
50
85
  }
51
86
 
87
+ // Validate do property if it contains string references
88
+ if (doValue !== undefined) {
89
+ this.#validateDoHandlers();
90
+ }
91
+
92
+ // Validate reference property if present
93
+ if (reference !== undefined) {
94
+ this.#validateReference();
95
+ }
96
+
52
97
  // Preload whereAttr utilities if needed
53
- if (init.whereAttr) {
98
+ if (whereAttr) {
54
99
  this.#preloadWhereAttrUtilities();
55
100
  }
56
101
 
57
102
  // Start loading imports if eager
58
- if (init.loadingEagerness === 'eager' && init.import) {
103
+ if (loadingEagerness === 'eager' && imp) {
59
104
  this.#loadImports();
60
105
  }
61
106
  }
62
107
 
108
+ #validateDoHandlers(): void {
109
+ const doValue = this.#init.do;
110
+ if (doValue === undefined) return;
111
+
112
+ const handlers = Array.isArray(doValue) ? doValue : [doValue];
113
+
114
+ for (const handler of handlers) {
115
+ if (typeof handler === 'string') {
116
+ if (!MountObserver.#handlerRegistry.has(handler)) {
117
+ throw new Error(`No handler defined for ${handler}`);
118
+ }
119
+ }
120
+ }
121
+ }
122
+
123
+ #validateReference(): void {
124
+ if (!this.#init.import) {
125
+ throw new Error('reference property requires import to be defined');
126
+ }
127
+
128
+ // Normalize import to array
129
+ const imports = Array.isArray(this.#init.import)
130
+ ? this.#init.import
131
+ : [this.#init.import];
132
+
133
+ // Normalize reference to array
134
+ const references = arr(this.#init.reference);
135
+
136
+ // Validate each reference index
137
+ for (const index of references) {
138
+ // Check if index is within bounds
139
+ if (index < 0 || index >= imports.length) {
140
+ throw new Error(`reference index ${index} is out of bounds (import array length: ${imports.length})`);
141
+ }
142
+
143
+ const importItem = imports[index];
144
+
145
+ // Check if it's a JS module (not a 2D array with type option)
146
+ if (Array.isArray(importItem)) {
147
+ throw new Error(`reference index ${index} points to a non-JS module import (array with type option)`);
148
+ }
149
+ }
150
+ }
151
+
63
152
  async #preloadWhereAttrUtilities(): Promise<void> {
64
153
  if (!this.#matchesWhereAttrFn) {
65
154
  const { matchesWhereAttr } = await import('./whereAttr.js');
@@ -69,6 +158,19 @@ export class MountObserver extends EventTarget implements IMountObserver {
69
158
  const { buildAttrCoordinateMap } = await import('./attrCoordinates.js');
70
159
  this.#buildAttrCoordinateMapFn = buildAttrCoordinateMap;
71
160
  }
161
+ if (!this.#checkAttrChangesFn) {
162
+ const { checkAttrChanges } = await import('./attrChanges.js');
163
+ // Create a bound function that passes the required parameters
164
+ this.#checkAttrChangesFn = (element: Element) => {
165
+ return checkAttrChanges(
166
+ element,
167
+ this.#init,
168
+ this.#buildAttrCoordinateMapFn!,
169
+ this.#elementAttrStates,
170
+ this.#elementOnceAttrs
171
+ );
172
+ };
173
+ }
72
174
  }
73
175
 
74
176
  async #setupMediaQuery(): Promise<void> {
@@ -94,10 +196,26 @@ export class MountObserver extends EventTarget implements IMountObserver {
94
196
  return this.#abortController.signal;
95
197
  }
96
198
 
199
+ getNotifier(element: Element): EventTarget {
200
+ // Return cached notifier if it exists
201
+ let notifier = this.#elementNotifiers.get(element);
202
+ if (notifier) {
203
+ return notifier;
204
+ }
205
+
206
+ // Create new EventTarget for this element
207
+ notifier = new EventTarget();
208
+ this.#elementNotifiers.set(element, notifier);
209
+ return notifier;
210
+ }
211
+
97
212
  async observe(rootNode: Node): Promise<void> {
98
213
  if (this.#rootNode) {
99
214
  throw new Error('Already observing');
100
215
  }
216
+ if(this.#asgMtSource || this.#asgDisMtSource){
217
+ await import('assign-gingerly/object-extension.js');
218
+ }
101
219
 
102
220
  this.#rootNode = new WeakRef(rootNode);
103
221
 
@@ -111,6 +229,11 @@ export class MountObserver extends EventTarget implements IMountObserver {
111
229
  await this.#preloadWhereAttrUtilities();
112
230
  }
113
231
 
232
+ // Wait for eager imports to complete if they were started in constructor
233
+ if (this.#init.loadingEagerness === 'eager' && this.#init.import && !this.#importsLoaded) {
234
+ await this.#loadImports();
235
+ }
236
+
114
237
  // Process existing elements only if media matches
115
238
  if (this.#mediaMatches) {
116
239
  this.#processNode(rootNode);
@@ -140,8 +263,8 @@ export class MountObserver extends EventTarget implements IMountObserver {
140
263
  } else if (mutation.type === 'attributes' && mutation.target.nodeType === Node.ELEMENT_NODE) {
141
264
  // Handle attribute changes for mounted elements
142
265
  const element = mutation.target as Element;
143
- if (this.#mountedElements.has(element) && this.#init.whereAttr) {
144
- const changes = this.#checkAttrChanges(element);
266
+ if (this.#mountedElements.weakSet.has(element) && this.#checkAttrChangesFn) {
267
+ const changes = this.#checkAttrChangesFn(element);
145
268
  attrChanges.push(...changes);
146
269
  }
147
270
  }
@@ -150,6 +273,22 @@ export class MountObserver extends EventTarget implements IMountObserver {
150
273
  // Batch and dispatch attribute changes
151
274
  if (attrChanges.length > 0) {
152
275
  this.dispatchEvent(new AttrChangeEvent(attrChanges, this.#init));
276
+
277
+ // Dispatch filtered attrchange events to element-specific notifiers
278
+ const changesByElement = new Map<Element, AttrChange[]>();
279
+ for (const change of attrChanges) {
280
+ if (!changesByElement.has(change.element)) {
281
+ changesByElement.set(change.element, []);
282
+ }
283
+ changesByElement.get(change.element)!.push(change);
284
+ }
285
+
286
+ for (const [element, changes] of changesByElement) {
287
+ const notifier = this.#elementNotifiers.get(element);
288
+ if (notifier) {
289
+ notifier.dispatchEvent(new AttrChangeEvent(changes, this.#init));
290
+ }
291
+ }
153
292
  }
154
293
  };
155
294
 
@@ -197,6 +336,26 @@ export class MountObserver extends EventTarget implements IMountObserver {
197
336
  this.#modules = await loadImports(this.#init.import);
198
337
  this.#importsLoaded = true;
199
338
 
339
+ // Validate referenced whereInstanceOf if reference is specified
340
+ if (this.#init.reference !== undefined) {
341
+ const references = arr(this.#init.reference);
342
+
343
+ for (const index of references) {
344
+ const module = this.#modules[index];
345
+ if (module && module.whereInstanceOf !== undefined) {
346
+ // Validate that it's a Constructor or array of Constructors
347
+ const whereInstanceOf = module.whereInstanceOf;
348
+ const constructors = arr(whereInstanceOf);
349
+
350
+ for (const constructor of constructors) {
351
+ if (typeof constructor !== 'function') {
352
+ throw new Error(`Referenced module at index ${index} exports invalid whereInstanceOf: must be a Constructor or array of Constructors`);
353
+ }
354
+ }
355
+ }
356
+ }
357
+ }
358
+
200
359
  this.dispatchEvent(new LoadEvent(this.#modules, this.#init));
201
360
  }
202
361
 
@@ -231,6 +390,14 @@ export class MountObserver extends EventTarget implements IMountObserver {
231
390
  return false;
232
391
  }
233
392
 
393
+ // Check whereOutside condition if specified (donut hole scoping)
394
+ if (this.#init.whereOutside) {
395
+ const rootNode = this.#rootNode?.deref();
396
+ if (!rootNode || !whereOutside(rootNode, element, this.#init.whereOutside)) {
397
+ return false;
398
+ }
399
+ }
400
+
234
401
  // Check whereAttr condition if specified
235
402
  if (this.#init.whereAttr) {
236
403
  // Use cached function (should be loaded by now from constructor)
@@ -246,9 +413,7 @@ export class MountObserver extends EventTarget implements IMountObserver {
246
413
 
247
414
  // Check whereInstanceOf condition if specified
248
415
  if (this.#init.whereInstanceOf) {
249
- const constructors = Array.isArray(this.#init.whereInstanceOf)
250
- ? this.#init.whereInstanceOf
251
- : [this.#init.whereInstanceOf];
416
+ const constructors = arr(this.#init.whereInstanceOf);
252
417
 
253
418
  // Element must be an instance of at least one constructor (OR logic for array)
254
419
  const matchesInstanceOf = constructors.some(constructor => element instanceof constructor);
@@ -258,12 +423,31 @@ export class MountObserver extends EventTarget implements IMountObserver {
258
423
  }
259
424
  }
260
425
 
426
+ // Check referenced whereInstanceOf if imports are loaded and reference is specified
427
+ if (this.#importsLoaded && this.#init.reference !== undefined) {
428
+ const references = arr(this.#init.reference);
429
+
430
+ for (const index of references) {
431
+ const module = this.#modules[index];
432
+ if (module && module.whereInstanceOf !== undefined) {
433
+ const constructors = arr(module.whereInstanceOf);
434
+
435
+ // Element must be an instance of at least one constructor (OR logic within this module)
436
+ const matchesInstanceOf = constructors.some((constructor: Constructor) => element instanceof constructor);
437
+
438
+ if (!matchesInstanceOf) {
439
+ return false;
440
+ }
441
+ }
442
+ }
443
+ }
444
+
261
445
  // All conditions passed
262
446
  return true;
263
447
  }
264
448
 
265
449
  async #handleMatch(element: Element): Promise<void> {
266
- if (this.#processedElements.has(element)) {
450
+ if (this.#processedDoForElement.has(element)) {
267
451
  return;
268
452
  }
269
453
 
@@ -272,8 +456,13 @@ export class MountObserver extends EventTarget implements IMountObserver {
272
456
  await this.#loadImports();
273
457
  }
274
458
 
275
- this.#processedElements.add(element);
276
- this.#mountedElements.add(element);
459
+ this.#processedDoForElement.add(element);
460
+
461
+ // Add to both WeakSet and Set<WeakRef> for efficient operations
462
+ if (!this.#mountedElements.weakSet.has(element)) {
463
+ this.#mountedElements.weakSet.add(element);
464
+ this.#mountedElements.setWeak.add(new WeakRef(element));
465
+ }
277
466
 
278
467
  const rootNode = this.#rootNode?.deref();
279
468
  if (!rootNode) {
@@ -284,123 +473,138 @@ export class MountObserver extends EventTarget implements IMountObserver {
284
473
  const context: MountContext = {
285
474
  modules: this.#modules,
286
475
  observer: this,
287
- observeInfo: {
288
- rootNode
289
- }
476
+ rootNode,
477
+ mountInit: this.#init,
290
478
  };
291
479
 
292
480
  // Apply assignGingerly if specified
293
- if (this.#init.assignGingerly) {
294
- const { assignGingerly } = await import('assign-gingerly/index.js');
295
- assignGingerly(element, this.#init.assignGingerly);
481
+ if (this.#asgMtSource) {
482
+ element.assignGingerly(this.#asgMtSource);
483
+ }
484
+
485
+ // Check if notifier exists BEFORE calling do callback
486
+ const notifierExistedBeforeDo = this.#elementNotifiers.has(element);
487
+
488
+ // Call do callback(s) - can be string, function, or array
489
+ if (this.#init.do !== undefined) {
490
+ const doHandlers = Array.isArray(this.#init.do) ? this.#init.do : [this.#init.do];
491
+
492
+ for (const handler of doHandlers) {
493
+ if (typeof handler === 'string') {
494
+ // Registered handler - instantiate it
495
+ const HandlerClass = MountObserver.#handlerRegistry.get(handler);
496
+ if (HandlerClass) {
497
+ new HandlerClass(element, context);
498
+ }
499
+ } else if (typeof handler === 'function') {
500
+ // Inline function
501
+ handler(element, context);
502
+ }
503
+ }
296
504
  }
297
505
 
298
- // Call do callback
299
- if (this.#init.do) {
300
- if (typeof this.#init.do === 'function') {
301
- this.#init.do(element, context);
302
- } else if (this.#init.do.mount) {
303
- this.#init.do.mount(element, context);
506
+ // Call referenced do functions from imported modules
507
+ if (this.#init.reference !== undefined) {
508
+ const references = arr(this.#init.reference);
509
+
510
+ for (const index of references) {
511
+ const module = this.#modules[index];
512
+ if (module && typeof module.do === 'function') {
513
+ module.do(element, context);
514
+ }
304
515
  }
305
516
  }
306
517
 
307
518
  // Dispatch mount event
308
- this.dispatchEvent(new MountEvent(element, this.#modules, this.#init));
519
+ const mountEvent = new MountEvent(element, this.#modules, this.#init, context);
520
+ this.dispatchEvent(mountEvent);
521
+
522
+ // Dispatch to element-specific notifier only if:
523
+ // 1. Notifier existed before do callback (wasn't just created), AND
524
+ // 2. Element hasn't already received a mount event on its notifier
525
+ if (notifierExistedBeforeDo && !this.#notifierMountedElements.has(element)) {
526
+ const notifier = this.#elementNotifiers.get(element);
527
+ if (notifier) {
528
+ this.#notifierMountedElements.add(element);
529
+ notifier.dispatchEvent(mountEvent);
530
+ }
531
+ }
532
+
533
+ // Emit events from mounted element if configured
534
+ if (this.#init.mountedElemEmits) {
535
+ const { emitMountedElementEvents } = await import('./emitEvents.js');
536
+ await emitMountedElementEvents(element, this.#init, this.#processedEventsForElement);
537
+ }
309
538
 
310
539
  // Check for initial attribute changes if whereAttr is configured
311
- if (this.#init.whereAttr) {
312
- const changes = this.#checkAttrChanges(element);
540
+ if (this.#checkAttrChangesFn) {
541
+ const changes = this.#checkAttrChangesFn(element);
313
542
  if (changes.length > 0) {
314
543
  this.dispatchEvent(new AttrChangeEvent(changes, this.#init));
544
+
545
+ // Also dispatch to element-specific notifier
546
+ const notifier = this.#elementNotifiers.get(element);
547
+ if (notifier) {
548
+ notifier.dispatchEvent(new AttrChangeEvent(changes, this.#init));
549
+ }
315
550
  }
316
551
  }
317
552
  }
318
553
 
319
- #checkAttrChanges(element: Element): AttrChange[] {
320
- if (!this.#init.whereAttr || !this.#buildAttrCoordinateMapFn) {
321
- return [];
554
+ async assignGingerly(config: Record<string, any> | undefined): Promise<void> {
555
+ // Handle undefined case
556
+ if (config === undefined) {
557
+ this.#asgMtSource = undefined;
558
+ return;
322
559
  }
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);
560
+
561
+ await import('assign-gingerly/object-extension.js');
562
+
563
+ // Update the source config for future mounted elements
564
+ if (this.#asgMtSource === undefined) {
565
+ // No existing config, just clone the passed in object
566
+ this.#asgMtSource = structuredClone(config);
567
+ } else {
568
+ // Merge into existing config using assignGingerly
569
+ this.#asgMtSource.assignGingerly(config);
570
+ //assignGingerly(this.#asgMtSource, config);
332
571
  }
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
- }
572
+
573
+ // Apply to already mounted elements using setWeak for iteration
574
+ for (const ref of this.#mountedElements.setWeak) {
575
+ const element = ref.deref();
576
+ if (element) {
577
+ element.assignGingerly(config);
578
+ //assignGingerly(element, config);
392
579
  }
393
580
  }
394
-
395
- return changes;
396
581
  }
397
582
 
398
- #handleRemoval(element: Element): void {
399
- if (!this.#mountedElements.has(element)) {
583
+ async #handleRemoval(element: Element): Promise<void> {
584
+ if (!this.#mountedElements.weakSet.has(element)) {
400
585
  return;
401
586
  }
402
587
 
403
- this.#mountedElements.delete(element);
588
+
589
+
590
+ // Apply assignGingerly if specified for dismount
591
+ if (this.#asgDisMtSource) {
592
+ element.assignGingerly(this.#asgDisMtSource);
593
+ }
594
+ // Remove from both structures
595
+ this.#mountedElements.weakSet.delete(element);
596
+ for (const ref of this.#mountedElements.setWeak) {
597
+ if (ref.deref() === element) {
598
+ this.#mountedElements.setWeak.delete(ref);
599
+ break;
600
+ }
601
+ }
602
+
603
+ // Remove from processed set so element can be re-mounted
604
+ this.#processedDoForElement.delete(element);
605
+
606
+ // Remove from notifier mounted tracking so mount event can fire again
607
+ this.#notifierMountedElements.delete(element);
404
608
 
405
609
  const rootNode = this.#rootNode?.deref();
406
610
  if (!rootNode) {
@@ -411,28 +615,34 @@ export class MountObserver extends EventTarget implements IMountObserver {
411
615
  const context: MountContext = {
412
616
  modules: this.#modules,
413
617
  observer: this,
414
- observeInfo: {
415
- rootNode
416
- }
618
+ rootNode,
619
+ mountInit: this.#init,
417
620
  };
418
621
 
419
- // Call dismount callback
420
- if (this.#init.do && typeof this.#init.do !== 'function' && this.#init.do.dismount) {
421
- this.#init.do.dismount(element, context);
422
- }
423
622
 
424
623
  // Dispatch dismount event
425
- this.dispatchEvent(new DismountEvent(element, 'where-element-matches-failed', this.#init));
624
+ const dismountEvent = new DismountEvent(element, 'where-element-matches-failed', this.#init);
625
+ this.dispatchEvent(dismountEvent);
626
+
627
+ // Dispatch to element-specific notifier
628
+ const notifier = this.#elementNotifiers.get(element);
629
+ if (notifier) {
630
+ notifier.dispatchEvent(dismountEvent);
631
+ }
426
632
 
427
633
  // Check if element is being moved within the same root
428
634
  // If it's truly disconnected, dispatch disconnect event
429
635
  setTimeout(() => {
430
636
  if (!rootNode.contains(element)) {
431
- if (this.#init.do && typeof this.#init.do !== 'function' && this.#init.do.disconnect) {
432
- this.#init.do.disconnect(element, context);
433
- }
434
637
 
435
- this.dispatchEvent(new DisconnectEvent(element, this.#init));
638
+ const disconnectEvent = new DisconnectEvent(element, this.#init);
639
+ this.dispatchEvent(disconnectEvent);
640
+
641
+ // Dispatch to element-specific notifier
642
+ const notifier = this.#elementNotifiers.get(element);
643
+ if (notifier) {
644
+ notifier.dispatchEvent(disconnectEvent);
645
+ }
436
646
  }
437
647
  }, 0);
438
648
  }