mount-observer 0.1.0 → 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/Events.js +41 -7
- package/Events.ts +31 -13
- package/MountObserver.js +154 -99
- package/MountObserver.ts +190 -105
- package/README.md +344 -24
- package/SharedMutationObserver.js +70 -0
- package/SharedMutationObserver.ts +96 -0
- package/attrChanges.js +70 -0
- package/attrChanges.ts +90 -0
- package/emitEvents.js +103 -0
- package/emitEvents.ts +126 -0
- package/index.ts +9 -2
- package/mediaQuery.js +89 -0
- package/mediaQuery.ts +116 -0
- package/package.json +19 -4
- package/types.d.ts +35 -0
- package/whereOutside.js +19 -0
- package/whereOutside.ts +25 -0
- package/constants.js +0 -6
- package/constants.ts +0 -7
package/MountObserver.ts
CHANGED
|
@@ -3,29 +3,47 @@ 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,
|
|
10
13
|
DismountEvent,
|
|
11
14
|
DisconnectEvent,
|
|
12
15
|
LoadEvent,
|
|
13
|
-
AttrChangeEvent
|
|
16
|
+
AttrChangeEvent,
|
|
14
17
|
} from './Events.js';
|
|
18
|
+
import {
|
|
19
|
+
registerSharedObserver,
|
|
20
|
+
unregisterSharedObserver,
|
|
21
|
+
type MutationCallback
|
|
22
|
+
} from './SharedMutationObserver.js';
|
|
23
|
+
import { whereOutside } from './whereOutside.js';
|
|
15
24
|
|
|
16
25
|
export class MountObserver extends EventTarget implements IMountObserver {
|
|
17
26
|
#init: MountInit;
|
|
18
27
|
#options: MountObserverOptions;
|
|
19
28
|
#abortController: AbortController;
|
|
20
29
|
#modules: any[] = [];
|
|
21
|
-
#mountedElements
|
|
22
|
-
|
|
23
|
-
|
|
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>>();
|
|
36
|
+
#mutationCallback: MutationCallback | undefined;
|
|
24
37
|
#rootNode: WeakRef<Node> | undefined;
|
|
25
38
|
#importsLoaded = false;
|
|
26
39
|
#elementAttrStates = new WeakMap<Element, Map<string, string | null>>();
|
|
40
|
+
#elementOnceAttrs = new WeakMap<Element, Set<string>>();
|
|
27
41
|
#matchesWhereAttrFn: ((element: Element, whereAttr: any) => boolean) | null = null;
|
|
28
42
|
#buildAttrCoordinateMapFn: ((whereAttr: any, isCustomElement: boolean) => any) | null = null;
|
|
43
|
+
#checkAttrChangesFn: ((element: Element) => AttrChange[]) | null = null;
|
|
44
|
+
#mediaQueryCleanup?: () => void;
|
|
45
|
+
#mediaMatches: boolean = true;
|
|
46
|
+
#assignGingerlySource: Record<string, any> | undefined;
|
|
29
47
|
|
|
30
48
|
constructor(init: MountInit, options: MountObserverOptions = {}) {
|
|
31
49
|
super();
|
|
@@ -33,6 +51,11 @@ export class MountObserver extends EventTarget implements IMountObserver {
|
|
|
33
51
|
this.#options = options;
|
|
34
52
|
this.#abortController = new AbortController();
|
|
35
53
|
|
|
54
|
+
// Make a copy of assignGingerly config using structuredClone
|
|
55
|
+
if (init.assignGingerly !== undefined) {
|
|
56
|
+
this.#assignGingerlySource = structuredClone(init.assignGingerly);
|
|
57
|
+
}
|
|
58
|
+
|
|
36
59
|
if (options.disconnectedSignal) {
|
|
37
60
|
options.disconnectedSignal.addEventListener('abort', () => {
|
|
38
61
|
this.disconnect();
|
|
@@ -59,6 +82,38 @@ export class MountObserver extends EventTarget implements IMountObserver {
|
|
|
59
82
|
const { buildAttrCoordinateMap } = await import('./attrCoordinates.js');
|
|
60
83
|
this.#buildAttrCoordinateMapFn = buildAttrCoordinateMap;
|
|
61
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
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
async #setupMediaQuery(): Promise<void> {
|
|
101
|
+
if (!this.#rootNode) {
|
|
102
|
+
throw new Error('Cannot setup media query before observe() is called');
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const { setupMediaQuery } = await import('./mediaQuery.js');
|
|
106
|
+
const result = setupMediaQuery(
|
|
107
|
+
this.#init,
|
|
108
|
+
this.#rootNode,
|
|
109
|
+
this.#mountedElements,
|
|
110
|
+
this.#modules,
|
|
111
|
+
this,
|
|
112
|
+
(node) => this.#processNode(node)
|
|
113
|
+
);
|
|
114
|
+
|
|
115
|
+
this.#mediaMatches = result.mediaMatches;
|
|
116
|
+
this.#mediaQueryCleanup = result.cleanup;
|
|
62
117
|
}
|
|
63
118
|
|
|
64
119
|
get disconnectedSignal(): AbortSignal {
|
|
@@ -72,16 +127,28 @@ export class MountObserver extends EventTarget implements IMountObserver {
|
|
|
72
127
|
|
|
73
128
|
this.#rootNode = new WeakRef(rootNode);
|
|
74
129
|
|
|
130
|
+
// Set up media query if specified (needs rootNode to be set first)
|
|
131
|
+
if (this.#init.whereMediaMatches) {
|
|
132
|
+
await this.#setupMediaQuery();
|
|
133
|
+
}
|
|
134
|
+
|
|
75
135
|
// Wait for whereAttr utilities to load if needed
|
|
76
136
|
if (this.#init.whereAttr && !this.#matchesWhereAttrFn) {
|
|
77
137
|
await this.#preloadWhereAttrUtilities();
|
|
78
138
|
}
|
|
79
139
|
|
|
80
|
-
// Process existing elements
|
|
81
|
-
this.#
|
|
140
|
+
// Process existing elements only if media matches
|
|
141
|
+
if (this.#mediaMatches) {
|
|
142
|
+
this.#processNode(rootNode);
|
|
143
|
+
}
|
|
82
144
|
|
|
83
|
-
//
|
|
84
|
-
this.#
|
|
145
|
+
// Create mutation callback
|
|
146
|
+
this.#mutationCallback = (mutations) => {
|
|
147
|
+
// Skip processing if media doesn't match
|
|
148
|
+
if (!this.#mediaMatches) {
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
|
|
85
152
|
const attrChanges: AttrChange[] = [];
|
|
86
153
|
|
|
87
154
|
for (const mutation of mutations) {
|
|
@@ -99,8 +166,8 @@ export class MountObserver extends EventTarget implements IMountObserver {
|
|
|
99
166
|
} else if (mutation.type === 'attributes' && mutation.target.nodeType === Node.ELEMENT_NODE) {
|
|
100
167
|
// Handle attribute changes for mounted elements
|
|
101
168
|
const element = mutation.target as Element;
|
|
102
|
-
if (this.#mountedElements.has(element) && this.#
|
|
103
|
-
const changes = this.#
|
|
169
|
+
if (this.#mountedElements.weakSet.has(element) && this.#checkAttrChangesFn) {
|
|
170
|
+
const changes = this.#checkAttrChangesFn(element);
|
|
104
171
|
attrChanges.push(...changes);
|
|
105
172
|
}
|
|
106
173
|
}
|
|
@@ -108,9 +175,9 @@ export class MountObserver extends EventTarget implements IMountObserver {
|
|
|
108
175
|
|
|
109
176
|
// Batch and dispatch attribute changes
|
|
110
177
|
if (attrChanges.length > 0) {
|
|
111
|
-
this.dispatchEvent(new AttrChangeEvent(attrChanges));
|
|
178
|
+
this.dispatchEvent(new AttrChangeEvent(attrChanges, this.#init));
|
|
112
179
|
}
|
|
113
|
-
}
|
|
180
|
+
};
|
|
114
181
|
|
|
115
182
|
const observerConfig: MutationObserverInit = {
|
|
116
183
|
childList: true,
|
|
@@ -123,14 +190,25 @@ export class MountObserver extends EventTarget implements IMountObserver {
|
|
|
123
190
|
observerConfig.attributeOldValue = true;
|
|
124
191
|
}
|
|
125
192
|
|
|
126
|
-
|
|
193
|
+
// Register with shared mutation observer
|
|
194
|
+
registerSharedObserver(rootNode, this.#mutationCallback, observerConfig);
|
|
127
195
|
}
|
|
128
196
|
|
|
129
197
|
disconnect(): void {
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
198
|
+
const rootNode = this.#rootNode?.deref();
|
|
199
|
+
|
|
200
|
+
// Unregister from shared mutation observer
|
|
201
|
+
if (rootNode && this.#mutationCallback) {
|
|
202
|
+
unregisterSharedObserver(rootNode, this.#mutationCallback);
|
|
203
|
+
this.#mutationCallback = undefined;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// Remove media query listener
|
|
207
|
+
if (this.#mediaQueryCleanup) {
|
|
208
|
+
this.#mediaQueryCleanup();
|
|
209
|
+
this.#mediaQueryCleanup = undefined;
|
|
133
210
|
}
|
|
211
|
+
|
|
134
212
|
this.#abortController.abort();
|
|
135
213
|
this.#rootNode = undefined;
|
|
136
214
|
}
|
|
@@ -145,7 +223,7 @@ export class MountObserver extends EventTarget implements IMountObserver {
|
|
|
145
223
|
this.#modules = await loadImports(this.#init.import);
|
|
146
224
|
this.#importsLoaded = true;
|
|
147
225
|
|
|
148
|
-
this.dispatchEvent(new LoadEvent(this.#modules));
|
|
226
|
+
this.dispatchEvent(new LoadEvent(this.#modules, this.#init));
|
|
149
227
|
}
|
|
150
228
|
|
|
151
229
|
#processNode(node: Node): void {
|
|
@@ -162,45 +240,64 @@ export class MountObserver extends EventTarget implements IMountObserver {
|
|
|
162
240
|
if (node.nodeType === Node.ELEMENT_NODE || node.nodeType === Node.DOCUMENT_NODE) {
|
|
163
241
|
const root = node as Element | Document;
|
|
164
242
|
|
|
165
|
-
//
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
// Get all elements matching the CSS selector first
|
|
169
|
-
root.querySelectorAll(this.#init.whereElementMatches).forEach(child => {
|
|
170
|
-
if (this.#matchesSelector(child)) {
|
|
171
|
-
this.#handleMatch(child);
|
|
172
|
-
}
|
|
173
|
-
});
|
|
174
|
-
} else {
|
|
175
|
-
// Optimize: use querySelectorAll directly when no whereAttr
|
|
176
|
-
root.querySelectorAll(this.#init.whereElementMatches).forEach(child => {
|
|
243
|
+
// Get all elements matching the CSS selector first
|
|
244
|
+
root.querySelectorAll(this.#init.whereElementMatches).forEach(child => {
|
|
245
|
+
if (this.#matchesSelector(child)) {
|
|
177
246
|
this.#handleMatch(child);
|
|
178
|
-
}
|
|
179
|
-
}
|
|
247
|
+
}
|
|
248
|
+
});
|
|
180
249
|
}
|
|
181
250
|
}
|
|
182
251
|
|
|
183
252
|
#matchesSelector(element: Element): boolean {
|
|
253
|
+
//TODO: reduce redundncy with this.#init?
|
|
184
254
|
// Check whereElementMatches condition
|
|
185
255
|
const matchesElement = element.matches(this.#init.whereElementMatches);
|
|
256
|
+
if (!matchesElement) {
|
|
257
|
+
return false;
|
|
258
|
+
}
|
|
186
259
|
|
|
187
|
-
//
|
|
188
|
-
if (
|
|
189
|
-
|
|
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
|
+
}
|
|
190
266
|
}
|
|
191
267
|
|
|
192
|
-
//
|
|
193
|
-
if (
|
|
194
|
-
|
|
195
|
-
|
|
268
|
+
// Check whereAttr condition if specified
|
|
269
|
+
if (this.#init.whereAttr) {
|
|
270
|
+
// Use cached function (should be loaded by now from constructor)
|
|
271
|
+
if (!this.#matchesWhereAttrFn) {
|
|
272
|
+
console.warn('whereAttr utilities not loaded yet');
|
|
273
|
+
return false;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
if (!this.#matchesWhereAttrFn(element, this.#init.whereAttr)) {
|
|
277
|
+
return false;
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
// Check whereInstanceOf condition if specified
|
|
282
|
+
if (this.#init.whereInstanceOf) {
|
|
283
|
+
const constructors = Array.isArray(this.#init.whereInstanceOf)
|
|
284
|
+
? this.#init.whereInstanceOf
|
|
285
|
+
: [this.#init.whereInstanceOf];
|
|
286
|
+
|
|
287
|
+
// Element must be an instance of at least one constructor (OR logic for array)
|
|
288
|
+
const matchesInstanceOf = constructors.some(constructor => element instanceof constructor);
|
|
289
|
+
|
|
290
|
+
if (!matchesInstanceOf) {
|
|
291
|
+
return false;
|
|
292
|
+
}
|
|
196
293
|
}
|
|
197
294
|
|
|
198
|
-
//
|
|
199
|
-
return
|
|
295
|
+
// All conditions passed
|
|
296
|
+
return true;
|
|
200
297
|
}
|
|
201
298
|
|
|
202
299
|
async #handleMatch(element: Element): Promise<void> {
|
|
203
|
-
if (this.#
|
|
300
|
+
if (this.#processedDoForElement.has(element)) {
|
|
204
301
|
return;
|
|
205
302
|
}
|
|
206
303
|
|
|
@@ -209,8 +306,13 @@ export class MountObserver extends EventTarget implements IMountObserver {
|
|
|
209
306
|
await this.#loadImports();
|
|
210
307
|
}
|
|
211
308
|
|
|
212
|
-
this.#
|
|
213
|
-
|
|
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
|
+
}
|
|
214
316
|
|
|
215
317
|
const rootNode = this.#rootNode?.deref();
|
|
216
318
|
if (!rootNode) {
|
|
@@ -227,9 +329,9 @@ export class MountObserver extends EventTarget implements IMountObserver {
|
|
|
227
329
|
};
|
|
228
330
|
|
|
229
331
|
// Apply assignGingerly if specified
|
|
230
|
-
if (this.#
|
|
332
|
+
if (this.#assignGingerlySource) {
|
|
231
333
|
const { assignGingerly } = await import('assign-gingerly/index.js');
|
|
232
|
-
assignGingerly(element, this.#
|
|
334
|
+
assignGingerly(element, this.#assignGingerlySource);
|
|
233
335
|
}
|
|
234
336
|
|
|
235
337
|
// Call do callback
|
|
@@ -242,80 +344,63 @@ export class MountObserver extends EventTarget implements IMountObserver {
|
|
|
242
344
|
}
|
|
243
345
|
|
|
244
346
|
// Dispatch mount event
|
|
245
|
-
this.dispatchEvent(new MountEvent(element, this.#modules));
|
|
347
|
+
this.dispatchEvent(new MountEvent(element, this.#modules, this.#init));
|
|
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
|
+
}
|
|
246
354
|
|
|
247
355
|
// Check for initial attribute changes if whereAttr is configured
|
|
248
|
-
if (this.#
|
|
249
|
-
const changes = this.#
|
|
356
|
+
if (this.#checkAttrChangesFn) {
|
|
357
|
+
const changes = this.#checkAttrChangesFn(element);
|
|
250
358
|
if (changes.length > 0) {
|
|
251
|
-
this.dispatchEvent(new AttrChangeEvent(changes));
|
|
359
|
+
this.dispatchEvent(new AttrChangeEvent(changes, this.#init));
|
|
252
360
|
}
|
|
253
361
|
}
|
|
254
362
|
}
|
|
255
363
|
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
364
|
+
async assignGingerly(config: Record<string, any> | undefined): Promise<void> {
|
|
365
|
+
// Handle undefined case
|
|
366
|
+
if (config === undefined) {
|
|
367
|
+
this.#assignGingerlySource = undefined;
|
|
368
|
+
return;
|
|
259
369
|
}
|
|
260
|
-
|
|
261
|
-
const
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
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);
|
|
269
380
|
}
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
const
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
const coordinate = attrCoordMap[attrName];
|
|
277
|
-
const currentValue = element.getAttribute(attrName);
|
|
278
|
-
const previousValue = attrState.get(attrName);
|
|
279
|
-
|
|
280
|
-
if (currentValue !== null) {
|
|
281
|
-
currentAttrs.add(attrName);
|
|
282
|
-
}
|
|
283
|
-
|
|
284
|
-
// Include if: currently has value OR previously had value but now removed
|
|
285
|
-
if (currentValue !== null || (previousValue !== undefined && currentValue === null)) {
|
|
286
|
-
// Check if value changed
|
|
287
|
-
if (currentValue !== previousValue) {
|
|
288
|
-
const attrNode = currentValue !== null ? element.getAttributeNode(attrName) : null;
|
|
289
|
-
const mapEntry = this.#init.map?.[coordinate] || null;
|
|
290
|
-
|
|
291
|
-
changes.push({
|
|
292
|
-
value: currentValue,
|
|
293
|
-
attrNode,
|
|
294
|
-
mapEntry,
|
|
295
|
-
attrName,
|
|
296
|
-
coordinate,
|
|
297
|
-
element
|
|
298
|
-
});
|
|
299
|
-
|
|
300
|
-
// Update state
|
|
301
|
-
if (currentValue !== null) {
|
|
302
|
-
attrState.set(attrName, currentValue);
|
|
303
|
-
} else {
|
|
304
|
-
attrState.delete(attrName);
|
|
305
|
-
}
|
|
306
|
-
}
|
|
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);
|
|
307
387
|
}
|
|
308
388
|
}
|
|
309
|
-
|
|
310
|
-
return changes;
|
|
311
389
|
}
|
|
312
390
|
|
|
313
391
|
#handleRemoval(element: Element): void {
|
|
314
|
-
if (!this.#mountedElements.has(element)) {
|
|
392
|
+
if (!this.#mountedElements.weakSet.has(element)) {
|
|
315
393
|
return;
|
|
316
394
|
}
|
|
317
395
|
|
|
318
|
-
|
|
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
|
+
}
|
|
319
404
|
|
|
320
405
|
const rootNode = this.#rootNode?.deref();
|
|
321
406
|
if (!rootNode) {
|
|
@@ -337,7 +422,7 @@ export class MountObserver extends EventTarget implements IMountObserver {
|
|
|
337
422
|
}
|
|
338
423
|
|
|
339
424
|
// Dispatch dismount event
|
|
340
|
-
this.dispatchEvent(new DismountEvent(element));
|
|
425
|
+
this.dispatchEvent(new DismountEvent(element, 'where-element-matches-failed', this.#init));
|
|
341
426
|
|
|
342
427
|
// Check if element is being moved within the same root
|
|
343
428
|
// If it's truly disconnected, dispatch disconnect event
|
|
@@ -347,7 +432,7 @@ export class MountObserver extends EventTarget implements IMountObserver {
|
|
|
347
432
|
this.#init.do.disconnect(element, context);
|
|
348
433
|
}
|
|
349
434
|
|
|
350
|
-
this.dispatchEvent(new DisconnectEvent(element));
|
|
435
|
+
this.dispatchEvent(new DisconnectEvent(element, this.#init));
|
|
351
436
|
}
|
|
352
437
|
}, 0);
|
|
353
438
|
}
|