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/DefineCustomElementHandler.js +64 -0
- package/DefineCustomElementHandler.ts +77 -0
- package/Events.js +11 -9
- package/Events.ts +9 -4
- package/EvtRt.js +34 -0
- package/EvtRt.ts +42 -0
- package/MountObserver.js +275 -106
- package/MountObserver.ts +328 -118
- package/README.md +857 -203
- package/SharedMutationObserver.js +9 -6
- package/SharedMutationObserver.ts +11 -8
- package/arr.js +13 -0
- package/arr.ts +13 -0
- package/attrChanges.js +70 -0
- package/attrChanges.ts +90 -0
- package/emitEvents.js +103 -0
- package/emitEvents.ts +126 -0
- package/index.js +13 -1
- package/index.ts +14 -1
- package/loadImports.js +2 -1
- package/loadImports.ts +2 -1
- package/mediaQuery.js +15 -17
- package/mediaQuery.ts +18 -20
- package/package.json +27 -3
- package/types.d.ts +38 -15
- package/whereOutside.js +19 -0
- package/whereOutside.ts +25 -0
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
|
|
29
|
-
|
|
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 (
|
|
98
|
+
if (whereAttr) {
|
|
54
99
|
this.#preloadWhereAttrUtilities();
|
|
55
100
|
}
|
|
56
101
|
|
|
57
102
|
// Start loading imports if eager
|
|
58
|
-
if (
|
|
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.#
|
|
144
|
-
const changes = this.#
|
|
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 =
|
|
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.#
|
|
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.#
|
|
276
|
-
|
|
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
|
-
|
|
288
|
-
|
|
289
|
-
}
|
|
476
|
+
rootNode,
|
|
477
|
+
mountInit: this.#init,
|
|
290
478
|
};
|
|
291
479
|
|
|
292
480
|
// Apply assignGingerly if specified
|
|
293
|
-
if (this.#
|
|
294
|
-
|
|
295
|
-
|
|
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
|
|
299
|
-
if (this.#init.
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
this.#
|
|
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
|
-
|
|
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.#
|
|
312
|
-
const changes = this.#
|
|
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
|
-
|
|
320
|
-
|
|
321
|
-
|
|
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
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
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
|
-
|
|
335
|
-
const
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
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
|
-
|
|
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
|
-
|
|
415
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
}
|