mount-observer 0.1.11 → 0.1.12
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 +1 -0
- package/DefineCustomElementHandler.ts +1 -0
- package/EnhanceMountedElementHandler.js +3 -2
- package/EnhanceMountedElementHandler.ts +3 -2
- package/EvtRt.js +8 -3
- package/EvtRt.ts +12 -3
- package/MountObserver.js +119 -19
- package/MountObserver.ts +200 -56
- package/README.md +153 -74
- package/connectionMonitor.js +116 -0
- package/connectionMonitor.ts +164 -0
- package/elementIntersection.js +67 -0
- package/elementIntersection.ts +96 -0
- package/observedRootHas.js +87 -0
- package/package.json +1 -1
- package/rootSizeObserver.js +124 -0
- package/rootSizeObserver.ts +157 -0
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { EvtRt } from './EvtRt.js';
|
|
2
2
|
export class DefineCustomElementHandler extends EvtRt {
|
|
3
3
|
mount(mountedElement, MountConfig, context) {
|
|
4
|
+
this.abort();
|
|
4
5
|
// Check if modules are specified
|
|
5
6
|
if (!context.modules || context.modules.length === 0) {
|
|
6
7
|
throw new Error('Must specify an ES Module');
|
|
@@ -3,6 +3,7 @@ import { MountConfig, MountContext } from './types/mount-observer/types.js';
|
|
|
3
3
|
|
|
4
4
|
export class DefineCustomElementHandler extends EvtRt {
|
|
5
5
|
mount(mountedElement: Element, MountConfig: MountConfig, context: MountContext): void {
|
|
6
|
+
this.abort();
|
|
6
7
|
// Check if modules are specified
|
|
7
8
|
if (!context.modules || context.modules.length === 0) {
|
|
8
9
|
throw new Error('Must specify an ES Module');
|
|
@@ -8,6 +8,7 @@ import 'assign-gingerly/object-extension.js';
|
|
|
8
8
|
*/
|
|
9
9
|
export class EnhanceMountedElementHandler extends EvtRt {
|
|
10
10
|
async mount(mountedElement, MountConfig, context) {
|
|
11
|
+
this.abort();
|
|
11
12
|
// Check if modules are specified
|
|
12
13
|
if (!context.modules || context.modules.length === 0) {
|
|
13
14
|
throw new Error('Must specify an ES Module with import property');
|
|
@@ -23,13 +24,13 @@ export class EnhanceMountedElementHandler extends EvtRt {
|
|
|
23
24
|
throw new Error('Registry item "spawn" property must be a constructor function');
|
|
24
25
|
}
|
|
25
26
|
// Spawn the enhancement
|
|
26
|
-
this
|
|
27
|
+
this.#spawnEnhancement(mountedElement, registryItem, context);
|
|
27
28
|
}
|
|
28
29
|
/**
|
|
29
30
|
* Spawn the enhancement using element.enh.get().
|
|
30
31
|
* Polyfills customElementRegistry if needed for browsers without scoped registry support.
|
|
31
32
|
*/
|
|
32
|
-
|
|
33
|
+
#spawnEnhancement(element, registryItem, context) {
|
|
33
34
|
// Polyfill element.customElementRegistry if it doesn't exist (for browsers without scoped registries)
|
|
34
35
|
if (!element.customElementRegistry) {
|
|
35
36
|
Object.defineProperty(element, 'customElementRegistry', {
|
|
@@ -11,6 +11,7 @@ import 'assign-gingerly/object-extension.js';
|
|
|
11
11
|
*/
|
|
12
12
|
export class EnhanceMountedElementHandler extends EvtRt {
|
|
13
13
|
async mount(mountedElement: Element, MountConfig: MountConfig, context: MountContext){
|
|
14
|
+
this.abort();
|
|
14
15
|
// Check if modules are specified
|
|
15
16
|
if (!context.modules || context.modules.length === 0) {
|
|
16
17
|
throw new Error('Must specify an ES Module with import property');
|
|
@@ -31,14 +32,14 @@ export class EnhanceMountedElementHandler extends EvtRt {
|
|
|
31
32
|
}
|
|
32
33
|
|
|
33
34
|
// Spawn the enhancement
|
|
34
|
-
this
|
|
35
|
+
this.#spawnEnhancement(mountedElement, registryItem, context);
|
|
35
36
|
}
|
|
36
37
|
|
|
37
38
|
/**
|
|
38
39
|
* Spawn the enhancement using element.enh.get().
|
|
39
40
|
* Polyfills customElementRegistry if needed for browsers without scoped registry support.
|
|
40
41
|
*/
|
|
41
|
-
|
|
42
|
+
#spawnEnhancement(element: Element, registryItem: any, context: MountContext): void {
|
|
42
43
|
// Polyfill element.customElementRegistry if it doesn't exist (for browsers without scoped registries)
|
|
43
44
|
if (!(element as any).customElementRegistry) {
|
|
44
45
|
Object.defineProperty(element, 'customElementRegistry', {
|
package/EvtRt.js
CHANGED
|
@@ -1,13 +1,18 @@
|
|
|
1
1
|
import { DismountEvent, MountEvent, DisconnectEvent, dismountEventName, disconnectEventName, mountEventName } from './Events.js';
|
|
2
2
|
export class EvtRt {
|
|
3
|
+
#ac;
|
|
3
4
|
constructor(mountedElement, ctx) {
|
|
4
5
|
const { observer, MountConfig } = ctx;
|
|
6
|
+
this.#ac = new AbortController();
|
|
5
7
|
const et = observer.getNotifier(mountedElement);
|
|
6
|
-
et.addEventListener(mountEventName, this);
|
|
7
|
-
et.addEventListener(disconnectEventName, this);
|
|
8
|
-
et.addEventListener(dismountEventName, this);
|
|
8
|
+
et.addEventListener(mountEventName, this, { signal: this.#ac.signal });
|
|
9
|
+
et.addEventListener(disconnectEventName, this, { signal: this.#ac.signal });
|
|
10
|
+
et.addEventListener(dismountEventName, this, { signal: this.#ac.signal });
|
|
9
11
|
this.mount(mountedElement, MountConfig, ctx);
|
|
10
12
|
}
|
|
13
|
+
abort() {
|
|
14
|
+
this.#ac.abort();
|
|
15
|
+
}
|
|
11
16
|
mount(mountedElement, MountConfig, context) {
|
|
12
17
|
console.log({ mountedElement, MountConfig, context });
|
|
13
18
|
}
|
package/EvtRt.ts
CHANGED
|
@@ -5,16 +5,25 @@ import {
|
|
|
5
5
|
dismountEventName, disconnectEventName, mountEventName
|
|
6
6
|
} from './Events.js';
|
|
7
7
|
export class EvtRt implements EventListenerObject{
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
#ac: AbortController;
|
|
11
|
+
|
|
8
12
|
constructor(mountedElement: Element, ctx: MountContext ){
|
|
9
13
|
const {observer, MountConfig} = ctx;
|
|
14
|
+
this.#ac = new AbortController();
|
|
10
15
|
const et = observer.getNotifier(mountedElement);
|
|
11
|
-
et.addEventListener(mountEventName, this);
|
|
12
|
-
et.addEventListener(disconnectEventName, this);
|
|
13
|
-
et.addEventListener(dismountEventName, this);
|
|
16
|
+
et.addEventListener(mountEventName, this, {signal: this.#ac.signal});
|
|
17
|
+
et.addEventListener(disconnectEventName, this, {signal: this.#ac.signal});
|
|
18
|
+
et.addEventListener(dismountEventName, this, {signal: this.#ac.signal});
|
|
14
19
|
this.mount(mountedElement, MountConfig, ctx);
|
|
15
20
|
|
|
16
21
|
}
|
|
17
22
|
|
|
23
|
+
abort(){
|
|
24
|
+
this.#ac.abort();
|
|
25
|
+
}
|
|
26
|
+
|
|
18
27
|
mount(mountedElement: Element, MountConfig: MountConfig, context: MountContext){
|
|
19
28
|
console.log({mountedElement, MountConfig, context});
|
|
20
29
|
}
|
package/MountObserver.js
CHANGED
|
@@ -25,7 +25,13 @@ export class MountObserver extends EventTarget {
|
|
|
25
25
|
#rootNode;
|
|
26
26
|
#importsLoaded = false;
|
|
27
27
|
#mediaQueryCleanup;
|
|
28
|
+
#rootSizeCleanup;
|
|
29
|
+
#intersectionCleanup;
|
|
30
|
+
#connectionCleanup;
|
|
31
|
+
#intersectionObserver;
|
|
28
32
|
#mediaMatches = true;
|
|
33
|
+
#rootSizeMatches = true;
|
|
34
|
+
#connectionMatches = true;
|
|
29
35
|
#asgMtSource;
|
|
30
36
|
#asgDisMtSource;
|
|
31
37
|
#stageMtSource;
|
|
@@ -33,12 +39,39 @@ export class MountObserver extends EventTarget {
|
|
|
33
39
|
#assignTentatively;
|
|
34
40
|
#elementNotifiers = new WeakMap();
|
|
35
41
|
#notifierMountedElements = new WeakSet();
|
|
42
|
+
#mergeHandlerDefaults(config) {
|
|
43
|
+
const doValue = config.do;
|
|
44
|
+
// Only process if do is a string (single handler reference)
|
|
45
|
+
if (typeof doValue !== 'string') {
|
|
46
|
+
return config;
|
|
47
|
+
}
|
|
48
|
+
// Look up the handler class
|
|
49
|
+
const HandlerClass = MountObserver.#handlerRegistry.get(doValue);
|
|
50
|
+
if (!HandlerClass) {
|
|
51
|
+
// Validation will catch this later
|
|
52
|
+
return config;
|
|
53
|
+
}
|
|
54
|
+
// Extract static properties from the handler class
|
|
55
|
+
const handlerDefaults = {};
|
|
56
|
+
const proto = HandlerClass;
|
|
57
|
+
// Get all static properties
|
|
58
|
+
for (const key of Object.getOwnPropertyNames(proto)) {
|
|
59
|
+
if (key !== 'prototype' && key !== 'length' && key !== 'name') {
|
|
60
|
+
handlerDefaults[key] = proto[key];
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
// Merge: handler defaults first, then inline config (inline trumps)
|
|
64
|
+
// Using object spread - inline config overwrites handler defaults
|
|
65
|
+
return { ...handlerDefaults, ...config };
|
|
66
|
+
}
|
|
36
67
|
constructor(config, options = {}) {
|
|
37
68
|
super();
|
|
38
|
-
|
|
69
|
+
// Merge handler defaults if do is a string reference
|
|
70
|
+
const mergedConfig = this.#mergeHandlerDefaults(config);
|
|
71
|
+
this.#init = mergedConfig;
|
|
39
72
|
this.#options = options;
|
|
40
73
|
this.#abortController = new AbortController();
|
|
41
|
-
const { assignOnMount, assignOnDismount, stageOnMount, do: doValue, reference, loadingEagerness, import: imp } =
|
|
74
|
+
const { assignOnMount, assignOnDismount, stageOnMount, do: doValue, reference, loadingEagerness, import: imp } = mergedConfig;
|
|
42
75
|
// Make a copy of assignOnMount config using structuredClone
|
|
43
76
|
if (assignOnMount !== undefined) {
|
|
44
77
|
this.#asgMtSource = structuredClone(assignOnMount);
|
|
@@ -112,6 +145,33 @@ export class MountObserver extends EventTarget {
|
|
|
112
145
|
this.#mediaMatches = result.mediaMatches;
|
|
113
146
|
this.#mediaQueryCleanup = result.cleanup;
|
|
114
147
|
}
|
|
148
|
+
async #setupRootSizeObserver() {
|
|
149
|
+
if (!this.#rootNode) {
|
|
150
|
+
throw new Error('Cannot setup root size observer before observe() is called');
|
|
151
|
+
}
|
|
152
|
+
const { setupRootSizeObserver } = await import('./rootSizeObserver.js');
|
|
153
|
+
const result = setupRootSizeObserver(this.#init, this.#rootNode, this.#mountedElements, this.#modules, this, (node) => this.#processNode(node));
|
|
154
|
+
this.#rootSizeMatches = result.conditionMatches;
|
|
155
|
+
this.#rootSizeCleanup = result.cleanup;
|
|
156
|
+
}
|
|
157
|
+
async #setupElementIntersection() {
|
|
158
|
+
if (!this.#rootNode) {
|
|
159
|
+
throw new Error('Cannot setup element intersection before observe() is called');
|
|
160
|
+
}
|
|
161
|
+
const { setupElementIntersection } = await import('./elementIntersection.js');
|
|
162
|
+
const result = setupElementIntersection(this.#init, this.#rootNode, this.#mountedElements, this.#modules, this, (element) => this.#matchesSelector(element), (element) => this.#handleMatch(element));
|
|
163
|
+
this.#intersectionObserver = result.intersectionObserver;
|
|
164
|
+
this.#intersectionCleanup = result.cleanup;
|
|
165
|
+
}
|
|
166
|
+
async #setupConnectionMonitor() {
|
|
167
|
+
if (!this.#rootNode) {
|
|
168
|
+
throw new Error('Cannot setup connection monitor before observe() is called');
|
|
169
|
+
}
|
|
170
|
+
const { setupConnectionMonitor } = await import('./connectionMonitor.js');
|
|
171
|
+
const result = setupConnectionMonitor(this.#init, this.#rootNode, this.#mountedElements, this.#modules, this, (node) => this.#processNode(node));
|
|
172
|
+
this.#connectionMatches = result.conditionMatches;
|
|
173
|
+
this.#connectionCleanup = result.cleanup;
|
|
174
|
+
}
|
|
115
175
|
get disconnectedSignal() {
|
|
116
176
|
return this.#abortController.signal;
|
|
117
177
|
}
|
|
@@ -142,18 +202,30 @@ export class MountObserver extends EventTarget {
|
|
|
142
202
|
if (this.#init.withMediaMatching) {
|
|
143
203
|
await this.#setupMediaQuery();
|
|
144
204
|
}
|
|
205
|
+
// Set up root size observer if specified (needs rootNode to be set first)
|
|
206
|
+
if (this.#init.whereObservedRootSizeMatches) {
|
|
207
|
+
await this.#setupRootSizeObserver();
|
|
208
|
+
}
|
|
209
|
+
// Set up element intersection observer if specified (needs rootNode to be set first)
|
|
210
|
+
if (this.#init.whereElementIntersectsWith) {
|
|
211
|
+
await this.#setupElementIntersection();
|
|
212
|
+
}
|
|
213
|
+
// Set up connection monitor if specified (needs rootNode to be set first)
|
|
214
|
+
if (this.#init.whereConnectionHas) {
|
|
215
|
+
await this.#setupConnectionMonitor();
|
|
216
|
+
}
|
|
145
217
|
// Wait for eager imports to complete if they were started in constructor
|
|
146
218
|
if (this.#init.loadingEagerness === 'eager' && this.#init.import && !this.#importsLoaded) {
|
|
147
219
|
await this.#loadImports();
|
|
148
220
|
}
|
|
149
|
-
// Process existing elements only if
|
|
150
|
-
if (this.#mediaMatches) {
|
|
221
|
+
// Process existing elements only if all conditions match
|
|
222
|
+
if (this.#mediaMatches && this.#rootSizeMatches && this.#connectionMatches) {
|
|
151
223
|
this.#processNode(rootNode);
|
|
152
224
|
}
|
|
153
225
|
// Create mutation callback
|
|
154
226
|
this.#mutationCallback = (mutations) => {
|
|
155
|
-
// Skip processing if
|
|
156
|
-
if (!this.#mediaMatches) {
|
|
227
|
+
// Skip processing if any condition doesn't match
|
|
228
|
+
if (!this.#mediaMatches || !this.#rootSizeMatches || !this.#connectionMatches) {
|
|
157
229
|
return;
|
|
158
230
|
}
|
|
159
231
|
for (const mutation of mutations) {
|
|
@@ -190,6 +262,21 @@ export class MountObserver extends EventTarget {
|
|
|
190
262
|
this.#mediaQueryCleanup();
|
|
191
263
|
this.#mediaQueryCleanup = undefined;
|
|
192
264
|
}
|
|
265
|
+
// Remove root size observer
|
|
266
|
+
if (this.#rootSizeCleanup) {
|
|
267
|
+
this.#rootSizeCleanup();
|
|
268
|
+
this.#rootSizeCleanup = undefined;
|
|
269
|
+
}
|
|
270
|
+
// Remove intersection observer
|
|
271
|
+
if (this.#intersectionCleanup) {
|
|
272
|
+
this.#intersectionCleanup();
|
|
273
|
+
this.#intersectionCleanup = undefined;
|
|
274
|
+
}
|
|
275
|
+
// Remove connection monitor
|
|
276
|
+
if (this.#connectionCleanup) {
|
|
277
|
+
this.#connectionCleanup();
|
|
278
|
+
this.#connectionCleanup = undefined;
|
|
279
|
+
}
|
|
193
280
|
this.#abortController.abort();
|
|
194
281
|
this.#rootNode = undefined;
|
|
195
282
|
}
|
|
@@ -201,18 +288,18 @@ export class MountObserver extends EventTarget {
|
|
|
201
288
|
const { loadImports } = await import('./loadImports.js');
|
|
202
289
|
this.#modules = await loadImports(this.#init.import);
|
|
203
290
|
this.#importsLoaded = true;
|
|
204
|
-
// Validate referenced
|
|
291
|
+
// Validate referenced whereInstanceOf if reference is specified
|
|
205
292
|
if (this.#init.reference !== undefined) {
|
|
206
293
|
const references = arr(this.#init.reference);
|
|
207
294
|
for (const index of references) {
|
|
208
295
|
const module = this.#modules[index];
|
|
209
|
-
if (module && module.
|
|
296
|
+
if (module && module.whereInstanceOf !== undefined) {
|
|
210
297
|
// Validate that it's a Constructor or array of Constructors
|
|
211
|
-
const
|
|
212
|
-
const constructors = arr(
|
|
298
|
+
const whereInstanceOf = module.whereInstanceOf;
|
|
299
|
+
const constructors = arr(whereInstanceOf);
|
|
213
300
|
for (const constructor of constructors) {
|
|
214
301
|
if (typeof constructor !== 'function') {
|
|
215
|
-
throw new Error(`Referenced module at index ${index} exports invalid
|
|
302
|
+
throw new Error(`Referenced module at index ${index} exports invalid whereInstanceOf: must be a Constructor or array of Constructors`);
|
|
216
303
|
}
|
|
217
304
|
}
|
|
218
305
|
}
|
|
@@ -224,7 +311,12 @@ export class MountObserver extends EventTarget {
|
|
|
224
311
|
// If it's an element node, check if it matches
|
|
225
312
|
if (node.nodeType === Node.ELEMENT_NODE) {
|
|
226
313
|
const element = node;
|
|
227
|
-
|
|
314
|
+
// If intersection observer is active, start observing the element
|
|
315
|
+
// The intersection callback will handle mounting when it intersects
|
|
316
|
+
if (this.#intersectionObserver) {
|
|
317
|
+
this.#intersectionObserver.observe(element);
|
|
318
|
+
}
|
|
319
|
+
else if (this.#matchesSelector(element)) {
|
|
228
320
|
this.#handleMatch(element);
|
|
229
321
|
}
|
|
230
322
|
}
|
|
@@ -233,7 +325,11 @@ export class MountObserver extends EventTarget {
|
|
|
233
325
|
const root = node;
|
|
234
326
|
// Get all elements matching the CSS selector first
|
|
235
327
|
root.querySelectorAll(this.#init.matching).forEach(child => {
|
|
236
|
-
|
|
328
|
+
// If intersection observer is active, start observing the element
|
|
329
|
+
if (this.#intersectionObserver) {
|
|
330
|
+
this.#intersectionObserver.observe(child);
|
|
331
|
+
}
|
|
332
|
+
else if (this.#matchesSelector(child)) {
|
|
237
333
|
this.#handleMatch(child);
|
|
238
334
|
}
|
|
239
335
|
});
|
|
@@ -256,22 +352,26 @@ export class MountObserver extends EventTarget {
|
|
|
256
352
|
return false;
|
|
257
353
|
}
|
|
258
354
|
}
|
|
259
|
-
// Check
|
|
260
|
-
if (this.#init.
|
|
261
|
-
|
|
355
|
+
// Check whereObservedRootSizeMatches condition if specified
|
|
356
|
+
if (this.#init.whereObservedRootSizeMatches && !this.#rootSizeMatches) {
|
|
357
|
+
return false;
|
|
358
|
+
}
|
|
359
|
+
// Check whereInstanceOf condition if specified
|
|
360
|
+
if (this.#init.whereInstanceOf) {
|
|
361
|
+
const constructors = arr(this.#init.whereInstanceOf);
|
|
262
362
|
// Element must be an instance of at least one constructor (OR logic for array)
|
|
263
363
|
const matchesInstanceOf = constructors.some(constructor => element instanceof constructor);
|
|
264
364
|
if (!matchesInstanceOf) {
|
|
265
365
|
return false;
|
|
266
366
|
}
|
|
267
367
|
}
|
|
268
|
-
// Check referenced
|
|
368
|
+
// Check referenced whereInstanceOf if imports are loaded and reference is specified
|
|
269
369
|
if (this.#importsLoaded && this.#init.reference !== undefined) {
|
|
270
370
|
const references = arr(this.#init.reference);
|
|
271
371
|
for (const index of references) {
|
|
272
372
|
const module = this.#modules[index];
|
|
273
|
-
if (module && module.
|
|
274
|
-
const constructors = arr(module.
|
|
373
|
+
if (module && module.whereInstanceOf !== undefined) {
|
|
374
|
+
const constructors = arr(module.whereInstanceOf);
|
|
275
375
|
// Element must be an instance of at least one constructor (OR logic within this module)
|
|
276
376
|
const matchesInstanceOf = constructors.some((constructor) => element instanceof constructor);
|
|
277
377
|
if (!matchesInstanceOf) {
|