mount-observer 0.0.13 → 0.0.15
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 +8 -90
- package/README.md +52 -6
- package/birtualizeMatch.js +92 -0
- package/package.json +1 -1
package/MountObserver.js
CHANGED
|
@@ -10,7 +10,7 @@ export class MountObserver extends EventTarget {
|
|
|
10
10
|
#disconnected;
|
|
11
11
|
//#unmounted: WeakSet<Element>;
|
|
12
12
|
#isComplex;
|
|
13
|
-
|
|
13
|
+
objNde;
|
|
14
14
|
constructor(init) {
|
|
15
15
|
super();
|
|
16
16
|
const { on, whereElementIntersectsWith, whereMediaMatches } = init;
|
|
@@ -45,86 +45,18 @@ export class MountObserver extends EventTarget {
|
|
|
45
45
|
this.#calculatedSelector = calculatedSelector;
|
|
46
46
|
return this.#calculatedSelector;
|
|
47
47
|
}
|
|
48
|
-
async
|
|
48
|
+
async birtualizeFragment(fragment, level) {
|
|
49
49
|
const bis = fragment.querySelectorAll(inclTemplQry);
|
|
50
50
|
for (const bi of bis) {
|
|
51
51
|
await this.#birtalizeMatch(bi, level);
|
|
52
52
|
}
|
|
53
53
|
}
|
|
54
54
|
async #birtalizeMatch(el, level) {
|
|
55
|
-
const
|
|
56
|
-
el
|
|
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 slotQry = `slot[name="${name}"]`;
|
|
69
|
-
const targets = Array.from(clone.querySelectorAll(slotQry));
|
|
70
|
-
const innerTempls = clone.querySelectorAll(inclTemplQry);
|
|
71
|
-
for (const innerTempl of innerTempls) {
|
|
72
|
-
const innerSlots = innerTempl.content.querySelectorAll(slotQry);
|
|
73
|
-
for (const innerSlot of innerSlots) {
|
|
74
|
-
targets.push(innerSlot);
|
|
75
|
-
}
|
|
76
|
-
}
|
|
77
|
-
for (const target of targets) {
|
|
78
|
-
const slotClone = slot.cloneNode(true);
|
|
79
|
-
target.after(slotClone);
|
|
80
|
-
target.remove();
|
|
81
|
-
}
|
|
82
|
-
}
|
|
83
|
-
await this.#birtualizeFragment(clone, level + 1);
|
|
84
|
-
if (level === 0) {
|
|
85
|
-
const slotMap = el.getAttribute('slotmap');
|
|
86
|
-
let map = slotMap === null ? undefined : JSON.parse(slotMap);
|
|
87
|
-
const slots = clone.querySelectorAll('[slot]');
|
|
88
|
-
for (const slot of slots) {
|
|
89
|
-
if (map !== undefined) {
|
|
90
|
-
const slotName = slot.slot;
|
|
91
|
-
for (const key in map) {
|
|
92
|
-
if (slot.matches(key)) {
|
|
93
|
-
const targetAttSymbols = map[key];
|
|
94
|
-
for (const sym of targetAttSymbols) {
|
|
95
|
-
switch (sym) {
|
|
96
|
-
case '|':
|
|
97
|
-
slot.setAttribute('itemprop', slotName);
|
|
98
|
-
break;
|
|
99
|
-
case '$':
|
|
100
|
-
slot.setAttribute('itemscope', '');
|
|
101
|
-
slot.setAttribute('itemprop', slotName);
|
|
102
|
-
break;
|
|
103
|
-
case '@':
|
|
104
|
-
slot.setAttribute('name', slotName);
|
|
105
|
-
break;
|
|
106
|
-
case '.':
|
|
107
|
-
slot.classList.add(slotName);
|
|
108
|
-
break;
|
|
109
|
-
case '%':
|
|
110
|
-
slot.part.add(slotName);
|
|
111
|
-
break;
|
|
112
|
-
}
|
|
113
|
-
}
|
|
114
|
-
}
|
|
115
|
-
}
|
|
116
|
-
}
|
|
117
|
-
slot.removeAttribute('slot');
|
|
118
|
-
}
|
|
119
|
-
el.dispatchEvent(new LoadEvent(clone));
|
|
120
|
-
//console.log('dispatched')
|
|
121
|
-
}
|
|
122
|
-
el.after(clone);
|
|
123
|
-
if (level !== 0 || slots.length === 0)
|
|
124
|
-
el.remove();
|
|
55
|
+
const { birtualizeMatch } = await import('./birtualizeMatch.js');
|
|
56
|
+
await birtualizeMatch(this, el, level);
|
|
125
57
|
}
|
|
126
58
|
#templLookUp = new Map();
|
|
127
|
-
|
|
59
|
+
findByID(id, fragment) {
|
|
128
60
|
if (this.#templLookUp.has(id))
|
|
129
61
|
return this.#templLookUp.get(id);
|
|
130
62
|
let templ = fragment.getElementById(id);
|
|
@@ -166,7 +98,7 @@ export class MountObserver extends EventTarget {
|
|
|
166
98
|
}
|
|
167
99
|
}
|
|
168
100
|
async observe(within) {
|
|
169
|
-
this
|
|
101
|
+
this.objNde = new WeakRef(within);
|
|
170
102
|
const nodeToMonitor = this.#isComplex ? (within instanceof ShadowRoot ? within : within.getRootNode()) : within;
|
|
171
103
|
if (!mutationObserverLookup.has(nodeToMonitor)) {
|
|
172
104
|
mutationObserverLookup.set(nodeToMonitor, new RootMutObs(nodeToMonitor));
|
|
@@ -227,9 +159,6 @@ export class MountObserver extends EventTarget {
|
|
|
227
159
|
}
|
|
228
160
|
const deletedElements = Array.from(removedNodes).filter(x => x instanceof Element);
|
|
229
161
|
for (const deletedElement of deletedElements) {
|
|
230
|
-
// if(!this.#mounted.has(deletedElement)) continue;
|
|
231
|
-
// this.#mounted.delete(deletedElement);
|
|
232
|
-
// this.#mountedList = this.#mountedList?.filter(x => x.deref() !== deletedElement);
|
|
233
162
|
this.#disconnected.add(deletedElement);
|
|
234
163
|
if (doDisconnect !== undefined) {
|
|
235
164
|
doDisconnect(deletedElement, this, {});
|
|
@@ -239,9 +168,7 @@ export class MountObserver extends EventTarget {
|
|
|
239
168
|
}
|
|
240
169
|
this.#filterAndMount(elsToInspect, true, false);
|
|
241
170
|
}, { signal: this.#abortController.signal });
|
|
242
|
-
//if(ignoreInitialMatches !== true){
|
|
243
171
|
await this.#inspectWithin(within, true);
|
|
244
|
-
//}
|
|
245
172
|
}
|
|
246
173
|
#confirmInstanceOf(el, whereInstanceOf) {
|
|
247
174
|
for (const test of whereInstanceOf) {
|
|
@@ -268,7 +195,6 @@ export class MountObserver extends EventTarget {
|
|
|
268
195
|
case 'object':
|
|
269
196
|
if (Array.isArray(imp)) {
|
|
270
197
|
throw 'NI: Firefox';
|
|
271
|
-
//this.module = await import(imp[0], imp[1]);
|
|
272
198
|
}
|
|
273
199
|
break;
|
|
274
200
|
case 'function':
|
|
@@ -376,13 +302,13 @@ export class MountObserver extends EventTarget {
|
|
|
376
302
|
this.#mount(elsToMount, initializing);
|
|
377
303
|
}
|
|
378
304
|
async #inspectWithin(within, initializing) {
|
|
379
|
-
await this
|
|
305
|
+
await this.birtualizeFragment(within, 0);
|
|
380
306
|
const els = Array.from(within.querySelectorAll(await this.#selector()));
|
|
381
307
|
this.#filterAndMount(els, false, initializing);
|
|
382
308
|
}
|
|
383
309
|
}
|
|
384
310
|
const refCountErr = 'mount-observer ref count mismatch';
|
|
385
|
-
const inclTemplQry = 'template[href^="#"]:not([hidden])';
|
|
311
|
+
export const inclTemplQry = 'template[href^="#"]:not([hidden])';
|
|
386
312
|
// https://github.com/webcomponents-cg/community-protocols/issues/12#issuecomment-872415080
|
|
387
313
|
/**
|
|
388
314
|
* The `mutation-event` event represents something that happened.
|
|
@@ -424,12 +350,4 @@ export class AttrChangeEvent extends Event {
|
|
|
424
350
|
this.attrChangeInfo = attrChangeInfo;
|
|
425
351
|
}
|
|
426
352
|
}
|
|
427
|
-
export class LoadEvent extends Event {
|
|
428
|
-
clone;
|
|
429
|
-
static eventName = 'load';
|
|
430
|
-
constructor(clone) {
|
|
431
|
-
super(LoadEvent.eventName);
|
|
432
|
-
this.clone = clone;
|
|
433
|
-
}
|
|
434
|
-
}
|
|
435
353
|
//const hasRootInDefault = ['data', 'enh', 'data-enh']
|
package/README.md
CHANGED
|
@@ -11,13 +11,13 @@ 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-20
|
|
15
15
|
|
|
16
16
|
## Benefits of this API
|
|
17
17
|
|
|
18
18
|
What follows is a far more ambitious alternative to the [lazy custom element proposal](https://github.com/w3c/webcomponents/issues/782). The goals of the MountObserver api are more encompassing, and less focused on registering custom elements. In fact, this proposal addresses numerous use cases in one api. It is basically mapping common filtering conditions in the DOM, to mounting a "campaign" of some sort, like importing a resource, and/or progressively enhancing an element, and/or "binding from a distance".
|
|
19
19
|
|
|
20
|
-
["Binding from a distance"](https://github.com/WICG/webcomponents/issues/1035#issuecomment-1806393525) refers to empowering the developer to essentially manage their own "stylesheets" -- but rather than for purposes of styling, using these rules to attach behaviors, set property values, etc, to the HTML as it streams in. Libraries that take this approach include [Corset](https://corset.dev/) and [trans-render](https://github.com/bahrus/trans-render). The concept has been promoted by a [number](https://bkardell.com/blog/CSSLike.html) [of](https://www.w3.org/TR/NOTE-AS) prominent voices in the community.
|
|
20
|
+
["Binding from a distance"](https://github.com/WICG/webcomponents/issues/1035#issuecomment-1806393525) refers to empowering the developer to essentially manage their own "stylesheets" -- but rather than for purposes of styling, using these rules to attach behaviors, set property values, etc, to the HTML as it streams in. Libraries that take this approach include [Corset](https://corset.dev/) and [trans-render](https://github.com/bahrus/trans-render). The concept has been promoted by a [number](https://bkardell.com/blog/CSSLike.html) [of](https://www.w3.org/TR/NOTE-AS) [prominent](https://www.xanthir.com/blog/b4K_0) voices in the community.
|
|
21
21
|
|
|
22
22
|
The underlying theme is this api is meant to make it easy for the developer to do the right thing, by encouraging lazy loading and smaller footprints. It rolls up most all the other observer api's into one.
|
|
23
23
|
|
|
@@ -439,11 +439,13 @@ const observer = new MountObserver({
|
|
|
439
439
|
|
|
440
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.
|
|
441
441
|
|
|
442
|
-
##
|
|
442
|
+
## Intra document html imports
|
|
443
443
|
|
|
444
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
445
|
|
|
446
|
-
|
|
446
|
+
Also, this proposal is partly focused on better management of importing resources "from a distance", in particular via imports carried out via http. Is it such a stretch to look closely at scenarios where that distance happens to be shorter, i.e. found somewhere [in the document tree structure](https://github.com/tc39/proposal-module-expressions)?
|
|
447
|
+
|
|
448
|
+
The mount-observer is always on the lookout for template tags with an href attribute starting with #:
|
|
447
449
|
|
|
448
450
|
```html
|
|
449
451
|
<template href=#id-of-source-template></template>
|
|
@@ -485,5 +487,49 @@ This is an example of a snippet of HTML that appears repeatedly.
|
|
|
485
487
|
|
|
486
488
|
Some significant differences with genuine slot support as used with (ShadowDOM'd) custom elements
|
|
487
489
|
|
|
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.
|
|
490
|
+
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. It takes the approach of morphing from slots to a JS host object model that binds to where all the slots were "from a distance".
|
|
491
|
+
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" (birth-only, virtual) 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.
|
|
492
|
+
|
|
493
|
+
## Intra document html imports with Shadow DOM support
|
|
494
|
+
|
|
495
|
+
This proposal (and polyfill) also supports the option to utilize ShadowDOM / slot updates:
|
|
496
|
+
|
|
497
|
+
```html
|
|
498
|
+
<template id=chorus>
|
|
499
|
+
<template href=#beautiful>
|
|
500
|
+
<span slot=subjectIs>
|
|
501
|
+
<slot name=subjectIs1></slot>
|
|
502
|
+
</span>
|
|
503
|
+
</template>
|
|
504
|
+
|
|
505
|
+
<div>No matter what they say</div>
|
|
506
|
+
<div prop-pronoun>Words
|
|
507
|
+
<slot name=verb1></slot> bring
|
|
508
|
+
<slot name=pronoun1></slot> down</div>
|
|
509
|
+
<div>Oh no</div>
|
|
510
|
+
<template href=#beautiful>
|
|
511
|
+
<span slot=subjectIs>
|
|
512
|
+
<slot name=subjectIs2></slot>
|
|
513
|
+
</span>
|
|
514
|
+
</template>
|
|
515
|
+
<div>In every single way</div>
|
|
516
|
+
<div>Yes words
|
|
517
|
+
<slot name=verb2></slot> bring
|
|
518
|
+
<slot name=pronoun2></slot> down
|
|
519
|
+
</div>
|
|
520
|
+
<div>Oh no</div>
|
|
521
|
+
|
|
522
|
+
<template href=#down></template>
|
|
523
|
+
</template>
|
|
524
|
+
|
|
525
|
+
<div class=chorus>
|
|
526
|
+
<template href=#chorus shadowRootModeOnLoad=open></template>
|
|
527
|
+
<span slot=verb1>can't</span>
|
|
528
|
+
<span slot=verb2>can't</span>
|
|
529
|
+
<span slot=pronoun1>me</span>
|
|
530
|
+
<span slot=pronoun2>me</span>
|
|
531
|
+
<span slot=subjectIs1>I am</span>
|
|
532
|
+
<span slot=subjectIs2>I am</span>
|
|
533
|
+
</div>
|
|
534
|
+
```
|
|
535
|
+
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { inclTemplQry } from './MountObserver.js';
|
|
2
|
+
export async function birtualizeMatch(self, el, level) {
|
|
3
|
+
const href = el.getAttribute('href');
|
|
4
|
+
el.removeAttribute('href');
|
|
5
|
+
const templID = href.substring(1);
|
|
6
|
+
const fragment = self.objNde?.deref();
|
|
7
|
+
if (fragment === undefined)
|
|
8
|
+
return;
|
|
9
|
+
const templ = self.findByID(templID, fragment);
|
|
10
|
+
if (!(templ instanceof HTMLTemplateElement))
|
|
11
|
+
throw 404;
|
|
12
|
+
const clone = templ.content.cloneNode(true);
|
|
13
|
+
const slots = el.content.querySelectorAll(`[slot]`);
|
|
14
|
+
for (const slot of slots) {
|
|
15
|
+
const name = slot.getAttribute('slot');
|
|
16
|
+
const slotQry = `slot[name="${name}"]`;
|
|
17
|
+
const targets = Array.from(clone.querySelectorAll(slotQry));
|
|
18
|
+
const innerTempls = clone.querySelectorAll(inclTemplQry);
|
|
19
|
+
for (const innerTempl of innerTempls) {
|
|
20
|
+
const innerSlots = innerTempl.content.querySelectorAll(slotQry);
|
|
21
|
+
for (const innerSlot of innerSlots) {
|
|
22
|
+
targets.push(innerSlot);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
for (const target of targets) {
|
|
26
|
+
const slotClone = slot.cloneNode(true);
|
|
27
|
+
target.after(slotClone);
|
|
28
|
+
target.remove();
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
await self.birtualizeFragment(clone, level + 1);
|
|
32
|
+
const shadowRootModeOnLoad = el.getAttribute('shadowRootModeOnLoad');
|
|
33
|
+
if (shadowRootModeOnLoad === null && level === 0) {
|
|
34
|
+
const slotMap = el.getAttribute('slotmap');
|
|
35
|
+
let map = slotMap === null ? undefined : JSON.parse(slotMap);
|
|
36
|
+
const slots = clone.querySelectorAll('[slot]');
|
|
37
|
+
for (const slot of slots) {
|
|
38
|
+
if (map !== undefined) {
|
|
39
|
+
const slotName = slot.slot;
|
|
40
|
+
for (const key in map) {
|
|
41
|
+
if (slot.matches(key)) {
|
|
42
|
+
const targetAttSymbols = map[key];
|
|
43
|
+
for (const sym of targetAttSymbols) {
|
|
44
|
+
switch (sym) {
|
|
45
|
+
case '|':
|
|
46
|
+
slot.setAttribute('itemprop', slotName);
|
|
47
|
+
break;
|
|
48
|
+
case '$':
|
|
49
|
+
slot.setAttribute('itemscope', '');
|
|
50
|
+
slot.setAttribute('itemprop', slotName);
|
|
51
|
+
break;
|
|
52
|
+
case '@':
|
|
53
|
+
slot.setAttribute('name', slotName);
|
|
54
|
+
break;
|
|
55
|
+
case '.':
|
|
56
|
+
slot.classList.add(slotName);
|
|
57
|
+
break;
|
|
58
|
+
case '%':
|
|
59
|
+
slot.part.add(slotName);
|
|
60
|
+
break;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
slot.removeAttribute('slot');
|
|
67
|
+
}
|
|
68
|
+
el.dispatchEvent(new LoadEvent(clone));
|
|
69
|
+
//console.log('dispatched')
|
|
70
|
+
}
|
|
71
|
+
if (shadowRootModeOnLoad !== null) {
|
|
72
|
+
const parent = el.parentElement;
|
|
73
|
+
if (parent === null)
|
|
74
|
+
throw 404;
|
|
75
|
+
if (parent.shadowRoot === null)
|
|
76
|
+
parent.attachShadow({ mode: shadowRootModeOnLoad });
|
|
77
|
+
parent.shadowRoot?.append(clone);
|
|
78
|
+
}
|
|
79
|
+
else {
|
|
80
|
+
el.after(clone);
|
|
81
|
+
}
|
|
82
|
+
if (level !== 0 || slots.length === 0)
|
|
83
|
+
el.remove();
|
|
84
|
+
}
|
|
85
|
+
export class LoadEvent extends Event {
|
|
86
|
+
clone;
|
|
87
|
+
static eventName = 'load';
|
|
88
|
+
constructor(clone) {
|
|
89
|
+
super(LoadEvent.eventName);
|
|
90
|
+
this.clone = clone;
|
|
91
|
+
}
|
|
92
|
+
}
|