mount-observer 0.0.8 → 0.0.10
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 +105 -1
- package/README.md +60 -10
- package/package.json +1 -1
- package/types.d.ts +15 -3
package/MountObserver.js
CHANGED
|
@@ -3,13 +3,14 @@ const mutationObserverLookup = new WeakMap();
|
|
|
3
3
|
const refCount = new WeakMap();
|
|
4
4
|
export class MountObserver extends EventTarget {
|
|
5
5
|
#mountInit;
|
|
6
|
-
|
|
6
|
+
//#rootMutObs: RootMutObs | undefined;
|
|
7
7
|
#abortController;
|
|
8
8
|
#mounted;
|
|
9
9
|
#mountedList;
|
|
10
10
|
#disconnected;
|
|
11
11
|
//#unmounted: WeakSet<Element>;
|
|
12
12
|
#isComplex;
|
|
13
|
+
#observe;
|
|
13
14
|
constructor(init) {
|
|
14
15
|
super();
|
|
15
16
|
const { on, whereElementIntersectsWith, whereMediaMatches } = init;
|
|
@@ -44,6 +45,93 @@ export class MountObserver extends EventTarget {
|
|
|
44
45
|
this.#calculatedSelector = calculatedSelector;
|
|
45
46
|
return this.#calculatedSelector;
|
|
46
47
|
}
|
|
48
|
+
async #birtualizeFragment(fragment, level) {
|
|
49
|
+
const bis = Array.from(fragment.querySelectorAll(biQry));
|
|
50
|
+
for (const bi of bis) {
|
|
51
|
+
await this.#birtalizeMatch(bi, level);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
async #birtalizeMatch(el, level) {
|
|
55
|
+
const href = el.getAttribute('href');
|
|
56
|
+
el.removeAttribute('href');
|
|
57
|
+
const templID = href.substring(1);
|
|
58
|
+
const fragment = this.#observe?.deref();
|
|
59
|
+
if (fragment === undefined)
|
|
60
|
+
return;
|
|
61
|
+
const templ = this.#findByID(templID, fragment);
|
|
62
|
+
if (!(templ instanceof HTMLTemplateElement))
|
|
63
|
+
throw 404;
|
|
64
|
+
const clone = templ.content.cloneNode(true);
|
|
65
|
+
const slots = el.content.querySelectorAll(`[slot]`);
|
|
66
|
+
for (const slot of slots) {
|
|
67
|
+
const name = slot.getAttribute('slot');
|
|
68
|
+
const targets = Array.from(clone.querySelectorAll(`slot[name="${name}"]`));
|
|
69
|
+
for (const target of targets) {
|
|
70
|
+
const slotClone = slot.cloneNode(true);
|
|
71
|
+
target.after(slotClone);
|
|
72
|
+
target.remove();
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
this.#birtualizeFragment(clone, level + 1);
|
|
76
|
+
if (level === 0) {
|
|
77
|
+
const slotMap = el.getAttribute('slotmap');
|
|
78
|
+
let map = slotMap === null ? undefined : JSON.parse(slotMap);
|
|
79
|
+
const slots = Array.from(clone.querySelectorAll('[slot]'));
|
|
80
|
+
for (const slot of slots) {
|
|
81
|
+
if (map !== undefined) {
|
|
82
|
+
const slotName = slot.slot;
|
|
83
|
+
for (const key in map) {
|
|
84
|
+
if (slot.matches(key)) {
|
|
85
|
+
const targetAttSymbols = map[key];
|
|
86
|
+
for (const sym of targetAttSymbols) {
|
|
87
|
+
switch (sym) {
|
|
88
|
+
case '|':
|
|
89
|
+
slot.setAttribute('itemprop', slotName);
|
|
90
|
+
break;
|
|
91
|
+
case '$':
|
|
92
|
+
slot.setAttribute('itemscope', '');
|
|
93
|
+
slot.setAttribute('itemprop', slotName);
|
|
94
|
+
break;
|
|
95
|
+
case '@':
|
|
96
|
+
slot.setAttribute('name', slotName);
|
|
97
|
+
break;
|
|
98
|
+
case '.':
|
|
99
|
+
slot.classList.add(slotName);
|
|
100
|
+
break;
|
|
101
|
+
case '%':
|
|
102
|
+
slot.part.add(slotName);
|
|
103
|
+
break;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
slot.removeAttribute('slot');
|
|
110
|
+
}
|
|
111
|
+
el.dispatchEvent(new LoadEvent(clone));
|
|
112
|
+
//console.log('dispatched')
|
|
113
|
+
}
|
|
114
|
+
el.after(clone);
|
|
115
|
+
if (level !== 0 || slots.length === 0)
|
|
116
|
+
el.remove();
|
|
117
|
+
}
|
|
118
|
+
#templLookUp = new Map();
|
|
119
|
+
#findByID(id, fragment) {
|
|
120
|
+
if (this.#templLookUp.has(id))
|
|
121
|
+
return this.#templLookUp.get(id);
|
|
122
|
+
let templ = fragment.getElementById(id);
|
|
123
|
+
if (templ === null) {
|
|
124
|
+
let rootToSearchOutwardFrom = ((fragment.isConnected ? fragment.getRootNode() : this.#mountInit.withTargetShadowRoot) || document);
|
|
125
|
+
templ = rootToSearchOutwardFrom.getElementById(id);
|
|
126
|
+
while (templ === null && rootToSearchOutwardFrom !== document) {
|
|
127
|
+
rootToSearchOutwardFrom = (rootToSearchOutwardFrom.host || rootToSearchOutwardFrom).getRootNode();
|
|
128
|
+
templ = rootToSearchOutwardFrom.getElementById(id);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
if (templ !== null)
|
|
132
|
+
this.#templLookUp.set(id, templ);
|
|
133
|
+
return templ;
|
|
134
|
+
}
|
|
47
135
|
unobserve(within) {
|
|
48
136
|
const nodeToMonitor = this.#isComplex ? (within instanceof ShadowRoot ? within : within.getRootNode()) : within;
|
|
49
137
|
const currentCount = refCount.get(nodeToMonitor);
|
|
@@ -70,6 +158,7 @@ export class MountObserver extends EventTarget {
|
|
|
70
158
|
}
|
|
71
159
|
}
|
|
72
160
|
async observe(within) {
|
|
161
|
+
this.#observe = new WeakRef(within);
|
|
73
162
|
const nodeToMonitor = this.#isComplex ? (within instanceof ShadowRoot ? within : within.getRootNode()) : within;
|
|
74
163
|
if (!mutationObserverLookup.has(nodeToMonitor)) {
|
|
75
164
|
mutationObserverLookup.set(nodeToMonitor, new RootMutObs(nodeToMonitor));
|
|
@@ -271,14 +360,21 @@ export class MountObserver extends EventTarget {
|
|
|
271
360
|
}
|
|
272
361
|
return true;
|
|
273
362
|
});
|
|
363
|
+
for (const elToMount of elsToMount) {
|
|
364
|
+
if (elToMount.matches(biQry)) {
|
|
365
|
+
await this.#birtalizeMatch(elToMount, 0);
|
|
366
|
+
}
|
|
367
|
+
}
|
|
274
368
|
this.#mount(elsToMount, initializing);
|
|
275
369
|
}
|
|
276
370
|
async #inspectWithin(within, initializing) {
|
|
371
|
+
await this.#birtualizeFragment(within, 0);
|
|
277
372
|
const els = Array.from(within.querySelectorAll(await this.#selector()));
|
|
278
373
|
this.#filterAndMount(els, false, initializing);
|
|
279
374
|
}
|
|
280
375
|
}
|
|
281
376
|
const refCountErr = 'mount-observer ref count mismatch';
|
|
377
|
+
const biQry = 'template[href^="#"]:not([hidden])';
|
|
282
378
|
// https://github.com/webcomponents-cg/community-protocols/issues/12#issuecomment-872415080
|
|
283
379
|
/**
|
|
284
380
|
* The `mutation-event` event represents something that happened.
|
|
@@ -320,4 +416,12 @@ export class AttrChangeEvent extends Event {
|
|
|
320
416
|
this.attrChangeInfo = attrChangeInfo;
|
|
321
417
|
}
|
|
322
418
|
}
|
|
419
|
+
export class LoadEvent extends Event {
|
|
420
|
+
clone;
|
|
421
|
+
static eventName = 'load';
|
|
422
|
+
constructor(clone) {
|
|
423
|
+
super(LoadEvent.eventName);
|
|
424
|
+
this.clone = clone;
|
|
425
|
+
}
|
|
426
|
+
}
|
|
323
427
|
//const hasRootInDefault = ['data', 'enh', 'data-enh']
|
package/README.md
CHANGED
|
@@ -11,7 +11,7 @@ Author: Bruce B. Anderson
|
|
|
11
11
|
|
|
12
12
|
Issues / pr's / polyfill: [mount-observer](https://github.com/bahrus/mount-observer)
|
|
13
13
|
|
|
14
|
-
Last Update: 2024-2-
|
|
14
|
+
Last Update: 2024-2-18
|
|
15
15
|
|
|
16
16
|
## Benefits of this API
|
|
17
17
|
|
|
@@ -153,7 +153,9 @@ const observer = new MountObserver({
|
|
|
153
153
|
})
|
|
154
154
|
```
|
|
155
155
|
|
|
156
|
-
Callbacks like we see above are useful for tight coupling, and probably are unmatched in terms of performance.
|
|
156
|
+
Callbacks like we see above are useful for tight coupling, and probably are unmatched in terms of performance. The expression that the "do" field points to could also be a (stateful) user defined class instance.
|
|
157
|
+
|
|
158
|
+
However, since these rules may be of interest to multiple parties, it is useful to also provide the ability for multiple parties to subscribe to these css rules. This can be done via:
|
|
157
159
|
|
|
158
160
|
## Subscribing
|
|
159
161
|
|
|
@@ -304,7 +306,7 @@ const mo = new MountObserver({
|
|
|
304
306
|
builtIn: true
|
|
305
307
|
},
|
|
306
308
|
{
|
|
307
|
-
name: 'my-enhancement-first-
|
|
309
|
+
name: 'my-enhancement-first-aspect',
|
|
308
310
|
builtIn: true
|
|
309
311
|
},
|
|
310
312
|
{
|
|
@@ -339,7 +341,7 @@ MountObserver provides a breakdown of the matching attribute when encountered:
|
|
|
339
341
|
|
|
340
342
|
```html
|
|
341
343
|
<div id=div>
|
|
342
|
-
<section class=hello my-enhancement-first-
|
|
344
|
+
<section class=hello my-enhancement-first-aspect-wow-this-is-deep="hello"></section>
|
|
343
345
|
</div>
|
|
344
346
|
<script type=module>
|
|
345
347
|
import {MountObserver} from '../MountObserver.js';
|
|
@@ -348,9 +350,9 @@ MountObserver provides a breakdown of the matching attribute when encountered:
|
|
|
348
350
|
whereAttr:{
|
|
349
351
|
hasRootIn: ['data', 'enh', 'data-enh'],
|
|
350
352
|
hasBase: 'my-enhancement',
|
|
351
|
-
hasBranchIn: ['first-
|
|
353
|
+
hasBranchIn: ['first-aspect', 'second-aspect', ''],
|
|
352
354
|
hasLeafIn: {
|
|
353
|
-
'first-
|
|
355
|
+
'first-aspect': ['wow-this-is-deep', 'have-you-considered-using-json-for-this'],
|
|
354
356
|
}
|
|
355
357
|
}
|
|
356
358
|
});
|
|
@@ -362,7 +364,7 @@ MountObserver provides a breakdown of the matching attribute when encountered:
|
|
|
362
364
|
// name: 'data-my-enhancement-first-aspect-wow-this-is-deep'
|
|
363
365
|
// root: 'data',
|
|
364
366
|
// base: 'my-enhancement',
|
|
365
|
-
// branch: 'first-
|
|
367
|
+
// branch: 'first-aspect',
|
|
366
368
|
// leaf: 'wow-this-is-deep',
|
|
367
369
|
// oldValue: null,
|
|
368
370
|
// newValue: 'good-bye'
|
|
@@ -395,7 +397,7 @@ Possibly some libraries may prefer to mix it up a bit:
|
|
|
395
397
|
</div>
|
|
396
398
|
```
|
|
397
399
|
|
|
398
|
-
To support, specify the delimiter thusly:
|
|
400
|
+
To support such syntax, specify the delimiter thusly:
|
|
399
401
|
|
|
400
402
|
```JavaScript
|
|
401
403
|
const mo = new MountObserver({
|
|
@@ -403,9 +405,9 @@ const mo = new MountObserver({
|
|
|
403
405
|
whereAttr:{
|
|
404
406
|
hasRootIn: ['data', 'enh', 'data-enh'],
|
|
405
407
|
hasBase: ['-', 'my-enhancement'],
|
|
406
|
-
hasBranchIn: [':', ['first-
|
|
408
|
+
hasBranchIn: [':', ['first-aspect', 'second-aspect', '']],
|
|
407
409
|
hasLeafIn: {
|
|
408
|
-
'first-
|
|
410
|
+
'first-aspect': ['--', ['wow-this-is-deep', 'have-you-considered-using-json-for-this']],
|
|
409
411
|
}
|
|
410
412
|
}
|
|
411
413
|
});
|
|
@@ -437,3 +439,51 @@ const observer = new MountObserver({
|
|
|
437
439
|
|
|
438
440
|
So what this does is only check for the presence of an element with tag name "my-element", and it starts downloading the resource, even before the element has "mounted" based on other criteria.
|
|
439
441
|
|
|
442
|
+
## Birtual Inclusions
|
|
443
|
+
|
|
444
|
+
This proposal "sneaks in" one more feature, that perhaps should stand separately as its own proposal. Because the MountObserver api allows us to attach behaviors on the fly based on css matching, and because the MountObserver would provide developers the "first point of contact" for such functionality, the efficiency argument seemingly "screams out" for this feature.
|
|
445
|
+
|
|
446
|
+
The mount-observer is always on the lookout for a template tags with an href attribute starting with #:
|
|
447
|
+
|
|
448
|
+
```html
|
|
449
|
+
<template href=#id-of-source-template></template>
|
|
450
|
+
```
|
|
451
|
+
|
|
452
|
+
For example:
|
|
453
|
+
|
|
454
|
+
```html
|
|
455
|
+
<div>Some prior stuff</div>
|
|
456
|
+
<template href=#id-of-source-template>
|
|
457
|
+
<div slot=slot1>hello</div>
|
|
458
|
+
<div slot=slot2>goodbye<div>
|
|
459
|
+
</template>
|
|
460
|
+
<div>Some additional stuff</div>
|
|
461
|
+
```
|
|
462
|
+
|
|
463
|
+
When it encounters such a thing, it searches "upwardly" through the chain of ShadowRoots for a template with id=id-of-source-template (in this case), and caches them as it finds them.
|
|
464
|
+
|
|
465
|
+
Let's say the source template looks as follows:
|
|
466
|
+
|
|
467
|
+
```html
|
|
468
|
+
<template id=id-of-source-template>
|
|
469
|
+
This is an example of a snippet of HTML that appears repeatedly.
|
|
470
|
+
<slot name=slot1></slot>
|
|
471
|
+
<slot name=slot2></slot>
|
|
472
|
+
</template>
|
|
473
|
+
```
|
|
474
|
+
|
|
475
|
+
What we would end up with is:
|
|
476
|
+
|
|
477
|
+
|
|
478
|
+
```html
|
|
479
|
+
<div>Some prior stuff</div>
|
|
480
|
+
This is an example of a snippet of HTML that appears repeatedly.
|
|
481
|
+
<div>hello</div>
|
|
482
|
+
<div>goodbye</div>
|
|
483
|
+
<div>Some additional stuff</div>
|
|
484
|
+
```
|
|
485
|
+
|
|
486
|
+
Some significant differences with genuine slot support as used with (ShadowDOM'd) custom elements
|
|
487
|
+
|
|
488
|
+
1. There is no mechanism for updating the slots. That is something under investigation with this userland [custom enhancement](https://github.com/bahrus/be-inclusive), that could possibly lead to a future implementation request tied to template instantiation.
|
|
489
|
+
2. ShadowDOM's slots act on a "many to one" basis. Multiple light children with identical slot identifiers all get merged into a single (first?) matching slot within the Shadow DOM. These birtual inclusions, instead, follow the opposite approach -- a single element with a slot identifier can get cloned into multiple slot targets as it weaves itself into the templates as they get merged together.
|
package/package.json
CHANGED
package/types.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
export interface MountInit{
|
|
2
2
|
readonly on?: CSSMatch,
|
|
3
3
|
//readonly attribMatches?: Array<AttribMatch>,
|
|
4
|
+
readonly withTargetShadowRoot?: ShadowRoot,
|
|
4
5
|
readonly whereAttr?: WhereAttr,
|
|
5
6
|
readonly whereElementIntersectsWith?: IntersectionObserverInit,
|
|
6
7
|
readonly whereMediaMatches?: MediaQuery,
|
|
@@ -124,9 +125,20 @@ export type attrChangeEventName = 'attr-change';
|
|
|
124
125
|
export interface IAttrChangeEvent extends IMountEvent {
|
|
125
126
|
attrChangeInfo: AttrChangeInfo,
|
|
126
127
|
}
|
|
127
|
-
export type
|
|
128
|
-
export interface
|
|
129
|
-
addEventListener(eventName: attrChangeEventName, handler:
|
|
128
|
+
export type attrChangeEventHandler = (e: IAttrChangeEvent) => void;
|
|
129
|
+
export interface AddAttrChangeEventListener{
|
|
130
|
+
addEventListener(eventName: attrChangeEventName, handler: attrChangeEventHandler, options?: AddEventListenerOptions): void;
|
|
131
|
+
}
|
|
132
|
+
//#endregion
|
|
133
|
+
|
|
134
|
+
//#region load event
|
|
135
|
+
export type loadEventName = 'load';
|
|
136
|
+
export interface ILoadEvent {
|
|
137
|
+
clone: DocumentFragment
|
|
138
|
+
}
|
|
139
|
+
export type loadEventHandler = (e: ILoadEvent) => void;
|
|
140
|
+
export interface AddLoadEventListener{
|
|
141
|
+
addEventListener(eventName: loadEventName, handler: loadEventHandler, options?: AddEventListenerOptions): void
|
|
130
142
|
}
|
|
131
143
|
//#endregion
|
|
132
144
|
|