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 +76 -77
- package/MountObserver.ts +89 -89
- package/README.md +344 -24
- package/attrChanges.js +70 -0
- package/attrChanges.ts +90 -0
- package/emitEvents.js +103 -0
- package/emitEvents.ts +126 -0
- package/index.ts +4 -0
- package/mediaQuery.js +14 -11
- package/mediaQuery.ts +16 -13
- package/package.json +13 -1
- package/types.d.ts +17 -0
- package/whereOutside.js +19 -0
- package/whereOutside.ts +25 -0
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 =
|
|
9
|
-
|
|
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.#
|
|
101
|
-
const changes = this.#
|
|
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.#
|
|
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.#
|
|
208
|
-
|
|
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.#
|
|
251
|
+
if (this.#assignGingerlySource) {
|
|
223
252
|
const { assignGingerly } = await import('assign-gingerly/index.js');
|
|
224
|
-
assignGingerly(element, this.#
|
|
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.#
|
|
239
|
-
const changes = this.#
|
|
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
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
if (
|
|
254
|
-
|
|
255
|
-
this.#
|
|
256
|
-
}
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
const
|
|
264
|
-
if (
|
|
265
|
-
|
|
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
|
-
|
|
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
|
|
29
|
-
|
|
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.#
|
|
144
|
-
const changes = this.#
|
|
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.#
|
|
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.#
|
|
276
|
-
|
|
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.#
|
|
332
|
+
if (this.#assignGingerlySource) {
|
|
294
333
|
const { assignGingerly } = await import('assign-gingerly/index.js');
|
|
295
|
-
assignGingerly(element, this.#
|
|
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.#
|
|
312
|
-
const changes = this.#
|
|
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
|
-
|
|
320
|
-
|
|
321
|
-
|
|
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
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
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
|
-
|
|
335
|
-
const
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
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
|
-
|
|
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) {
|