mount-observer 0.0.20 → 0.0.22
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 +24 -6
- package/README.md +174 -34
- package/Synthesizer.js +78 -0
- package/getWhereAttrSelector.js +66 -64
- package/package.json +3 -2
- package/types.d.ts +34 -14
package/MountObserver.js
CHANGED
|
@@ -5,7 +5,7 @@ export class MountObserver extends EventTarget {
|
|
|
5
5
|
#mountInit;
|
|
6
6
|
//#rootMutObs: RootMutObs | undefined;
|
|
7
7
|
#abortController;
|
|
8
|
-
|
|
8
|
+
mountedElements;
|
|
9
9
|
#mountedList;
|
|
10
10
|
#disconnected;
|
|
11
11
|
//#unmounted: WeakSet<Element>;
|
|
@@ -15,7 +15,7 @@ export class MountObserver extends EventTarget {
|
|
|
15
15
|
super();
|
|
16
16
|
const { on, whereElementIntersectsWith, whereMediaMatches } = init;
|
|
17
17
|
let isComplex = false;
|
|
18
|
-
//TODO:
|
|
18
|
+
//TODO: study this problem further. Starting to think this is basically not polyfillable
|
|
19
19
|
if (on !== undefined) {
|
|
20
20
|
const reducedMatch = on.replaceAll(':not(', '');
|
|
21
21
|
isComplex = reducedMatch.includes(' ') || (reducedMatch.includes(':') && reducedMatch.includes('('));
|
|
@@ -25,7 +25,7 @@ export class MountObserver extends EventTarget {
|
|
|
25
25
|
throw 'NI'; //not implemented
|
|
26
26
|
this.#mountInit = init;
|
|
27
27
|
this.#abortController = new AbortController();
|
|
28
|
-
this
|
|
28
|
+
this.mountedElements = new WeakSet();
|
|
29
29
|
this.#disconnected = new WeakSet();
|
|
30
30
|
//this.#unmounted = new WeakSet();
|
|
31
31
|
}
|
|
@@ -41,7 +41,7 @@ export class MountObserver extends EventTarget {
|
|
|
41
41
|
if (whereAttr === undefined)
|
|
42
42
|
return withoutAttrs;
|
|
43
43
|
const { getWhereAttrSelector } = await import('./getWhereAttrSelector.js');
|
|
44
|
-
const info = getWhereAttrSelector(whereAttr, withoutAttrs);
|
|
44
|
+
const info = await getWhereAttrSelector(whereAttr, withoutAttrs);
|
|
45
45
|
const { fullListOfAttrs, calculatedSelector, partitionedAttrs } = info;
|
|
46
46
|
this.#fullListOfAttrs = fullListOfAttrs;
|
|
47
47
|
this.#attrParts = partitionedAttrs;
|
|
@@ -152,6 +152,7 @@ export class MountObserver extends EventTarget {
|
|
|
152
152
|
const parts = this.#attrParts[idx];
|
|
153
153
|
const attrChangeInfo = {
|
|
154
154
|
oldValue,
|
|
155
|
+
name: attributeName,
|
|
155
156
|
newValue,
|
|
156
157
|
idx,
|
|
157
158
|
parts
|
|
@@ -180,6 +181,22 @@ export class MountObserver extends EventTarget {
|
|
|
180
181
|
}, { signal: this.#abortController.signal });
|
|
181
182
|
await this.#inspectWithin(within, true);
|
|
182
183
|
}
|
|
184
|
+
synthesize(within, customElement, mose) {
|
|
185
|
+
const name = customElements.getName(customElement);
|
|
186
|
+
if (name === null)
|
|
187
|
+
throw 400;
|
|
188
|
+
let instance = within.querySelector(name);
|
|
189
|
+
if (instance === null) {
|
|
190
|
+
instance = new customElement();
|
|
191
|
+
if (within === document) {
|
|
192
|
+
within.head.appendChild(instance);
|
|
193
|
+
}
|
|
194
|
+
else {
|
|
195
|
+
within.appendChild(instance);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
instance.appendChild(mose);
|
|
199
|
+
}
|
|
183
200
|
#confirmInstanceOf(el, whereInstanceOf) {
|
|
184
201
|
for (const test of whereInstanceOf) {
|
|
185
202
|
if (el instanceof test)
|
|
@@ -195,7 +212,7 @@ export class MountObserver extends EventTarget {
|
|
|
195
212
|
for (const match of matching) {
|
|
196
213
|
if (alreadyMounted.has(match))
|
|
197
214
|
continue;
|
|
198
|
-
this
|
|
215
|
+
this.mountedElements.add(match);
|
|
199
216
|
if (imp !== undefined) {
|
|
200
217
|
switch (typeof imp) {
|
|
201
218
|
case 'string':
|
|
@@ -245,6 +262,7 @@ export class MountObserver extends EventTarget {
|
|
|
245
262
|
idx,
|
|
246
263
|
newValue,
|
|
247
264
|
oldValue,
|
|
265
|
+
name,
|
|
248
266
|
parts
|
|
249
267
|
});
|
|
250
268
|
}
|
|
@@ -351,7 +369,7 @@ export class DisconnectEvent extends Event {
|
|
|
351
369
|
export class AttrChangeEvent extends Event {
|
|
352
370
|
mountedElement;
|
|
353
371
|
attrChangeInfos;
|
|
354
|
-
static eventName = '
|
|
372
|
+
static eventName = 'attrChange';
|
|
355
373
|
constructor(mountedElement, attrChangeInfos) {
|
|
356
374
|
super(AttrChangeEvent.eventName);
|
|
357
375
|
this.mountedElement = mountedElement;
|
package/README.md
CHANGED
|
@@ -11,7 +11,7 @@ Author: Bruce B. Anderson (with valuable feedback from @doeixd )
|
|
|
11
11
|
|
|
12
12
|
Issues / pr's / polyfill: [mount-observer](https://github.com/bahrus/mount-observer)
|
|
13
13
|
|
|
14
|
-
Last Update: 2024-5-
|
|
14
|
+
Last Update: 2024-5-22
|
|
15
15
|
|
|
16
16
|
## Benefits of this API
|
|
17
17
|
|
|
@@ -30,8 +30,6 @@ There is quite a bit of functionality this proposal would open up, that is excee
|
|
|
30
30
|
|
|
31
31
|
2. For simple css matches, like "my-element", or "[name='hello']" it is enough to use a mutation observer, and only observe the elements within the specified DOM region (more on that below). But as CSS has evolved, it is quite easy to think of numerous css selectors that would require us to expand our mutation observer to need to scan the entire Shadow DOM realm, or the entire DOM tree outside any Shadow DOM, for any and all mutations (including attribute changes), and re-evaluate every single element within the specified DOM region for new matches or old matches that no longer match. Things like child selectors, :has, and so on. All this is done, miraculously, by the browser in a performant way. Reproducing this in userland using JavaScript alone, matching the same performance seems impossible.
|
|
32
32
|
|
|
33
|
-
|
|
34
|
-
|
|
35
33
|
3. Knowing when an element, previously being monitored for, passes totally "out-of-scope", so that no more hard references to the element remain. This would allow for cleanup of no longer needed weak references without requiring polling.
|
|
36
34
|
|
|
37
35
|
### Most significant use cases.
|
|
@@ -113,7 +111,57 @@ Previously, this proposal called for allowing arrow functions as well, thinking
|
|
|
113
111
|
|
|
114
112
|
This proposal would also include support for JSON and HTML module imports.
|
|
115
113
|
|
|
114
|
+
## MountObserver script element
|
|
115
|
+
|
|
116
|
+
Following an approach similar to the [speculation api](https://developer.chrome.com/blog/speculation-rules-improvements), we can add a script element anywhere in the DOM:
|
|
117
|
+
|
|
118
|
+
```html
|
|
119
|
+
<script type="mountobserver" id=myMountObserver onmount="
|
|
120
|
+
const {matchingElement} = event;
|
|
121
|
+
const {localName} = matchingElement;
|
|
122
|
+
if(!customElements.get(localName)) {
|
|
123
|
+
customElements.define(localName, modules[1].MyElement);
|
|
124
|
+
}
|
|
125
|
+
observer.disconnect();
|
|
126
|
+
">
|
|
127
|
+
{
|
|
128
|
+
"on":"my-element",
|
|
129
|
+
"import": [
|
|
130
|
+
["./my-element-small.css", {type: "css"}],
|
|
131
|
+
"./my-element.js",
|
|
132
|
+
]
|
|
133
|
+
}
|
|
134
|
+
</script>
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
The objects modules, observer, mountedElements (array of weak refs) would be available as properties of the script element:
|
|
138
|
+
|
|
139
|
+
```JavaScript
|
|
140
|
+
const {modules, observer, mountedElements, mountInit} = myMountObserver;
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
The "scope" of the observer would be the ShadowRoot containing the script element (or the document outside Shadow if placed outside any shadow DOM, like in the head element).
|
|
144
|
+
|
|
145
|
+
No arrays of settings would be supported within a single tag (as this causes issues as far as supporting a single onmount, ondismount, etc event attributes).
|
|
146
|
+
|
|
147
|
+
## Shadow Root inheritance
|
|
148
|
+
|
|
149
|
+
Inside a shadow root, we can plop a script element, also with type mountobserver, optionally giving it the same id as above:
|
|
150
|
+
|
|
151
|
+
```html
|
|
152
|
+
#shadowRoot
|
|
153
|
+
<script id=myMountObserver type=mountobserver>
|
|
154
|
+
{
|
|
155
|
+
"on":"your-element"
|
|
156
|
+
}
|
|
157
|
+
</script>
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
If no id is found in the parent ShadowRoot (or in the parent window if the shadow root is at the top level), then this becomes a new set of rules to observe.
|
|
116
161
|
|
|
162
|
+
But if a matching id is found, then the values from the parent script element get merged in with the one in the child, with the child settings, including the event handling attributes.
|
|
163
|
+
|
|
164
|
+
We will come back to some [additional features](#mountobserver-script-element-minutiae) of using these script elements later, but wanted to cover the highlights of this proposal before getting bogged down in some tedious logistics.
|
|
117
165
|
|
|
118
166
|
## Binding from a distance
|
|
119
167
|
|
|
@@ -275,46 +323,75 @@ The alternative to providing this feature, which I'm leaning towards, is to just
|
|
|
275
323
|
|
|
276
324
|
## A tribute to attributes
|
|
277
325
|
|
|
326
|
+
Attributes of DOM elements are tricky. They've been around since the get-go, and they've survived multiple generations of the Web where different philosophies have prevailed, so prepare yourself for some subtle discussion in what follows.
|
|
327
|
+
|
|
278
328
|
Extra support is provided for monitoring attributes. There are two primary reasons for needing to provide special support for attributes with this API:
|
|
279
329
|
|
|
280
330
|
Being that for both custom elements, as well as (hopefully) [custom enhancements](https://github.com/WICG/webcomponents/issues/1000) we need to carefully work with sets of "owned" [observed](https://github.com/WICG/webcomponents/issues/1045) attributes, and in some cases we may need to manage combinations of prefixes and suffixes for better name-spacing management, creating the most effective css query becomes challenging.
|
|
281
331
|
|
|
282
332
|
We want to be alerted by the discovery of elements adorned by these attributes, but then continue to be alerted to changes of their values, and we can't enumerate which values we are interested in, so we must subscribe to all values as they change.
|
|
283
333
|
|
|
284
|
-
|
|
285
|
-
### Scenario 1 -- Custom Element integration with ObserveObservedAttributes API [WIP]
|
|
334
|
+
## Attributes of attributes
|
|
286
335
|
|
|
287
|
-
|
|
336
|
+
I think it is useful to divide [attributes](https://jakearchibald.com/2024/attributes-vs-properties/) that we would want to observe into two categories:
|
|
288
337
|
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
338
|
+
1. Invariably named, prefix-less, "top-level" attributes that serve as the "source of the truth" for key features of the DOM element itself. We will refer to these attributes as "Source of Truth" attributes.
|
|
339
|
+
|
|
340
|
+
Examples are many built-in global attributes, like lang, or contenteditable, or more specialized examples such as "content" for the meta tag. I think in the vast majority of cases, setting the property values corresponding to these attributes results in directly reflecting those property values to the attributes. There are exceptions, especially for non-string attributes like the checked property of the input element / type=checkbox. And there are usually no events we can subscribe to in order to know when the property changes. Hijacking the property setter in order to observe changes may not always work or feel very resilient. So monitoring the attribute value is often the most effective way of observing when the property/attribute state for these elements change. And some attributes (like the microdata attributes such as itemprop) don't even have properties that they pair with!
|
|
341
|
+
|
|
342
|
+
|
|
343
|
+
2. In contrast, there are scenarios where we want to support somewhat fluid, renamable attributes within different Shadow DOM scopes, which add behavior/enhancement capabilities on top of built-in or third party custom elements. We'll refer to these attributes as "Enhancement Attributes."
|
|
344
|
+
|
|
345
|
+
We want our api to be able to distinguish between these two, and to be able to combine both types in one mount observer instance.
|
|
346
|
+
|
|
347
|
+
> [!NOTE]
|
|
348
|
+
> The most important reason for pointing out this distinction is this: "Source of Truth" attributes will only be *observed*, and will **not** trigger mount/unmount states unless they are part of the "on" selector string. And unlike all the other "where" conditions this proposal supports, the where clauses for the "Enhancement Attributes" are "one-way" -- they trigger a "mount" event / callback, followed by the ability to observe the stream of changes (including removal of those attributes), but they never trigger a "dismount".
|
|
349
|
+
|
|
350
|
+
### Counterpoint
|
|
351
|
+
|
|
352
|
+
Does it make sense to even support "Source of Truth" attributes in a "MountObserver" api, if they have no impact on mounted state?
|
|
353
|
+
|
|
354
|
+
We think it does, because some Enhancement Attributes will need to work in conjunction with Source of Truth attributes, in order to provide the observer a coherent picture of the full state of the element.
|
|
355
|
+
|
|
356
|
+
This realization (hopefully correct) struck me while trying to implement a [userland implementation](https://github.com/bahrus/be-intl) of [this proposal](https://github.com/whatwg/html/issues/9294).
|
|
357
|
+
|
|
358
|
+
|
|
359
|
+
### Source of Truth Attributes
|
|
360
|
+
|
|
361
|
+
Let's focus on the first scenario. It doesn't make sense to use the word "where" for these, because we don't want these attributes to affect our mount/dismount state
|
|
362
|
+
|
|
363
|
+
```JavaScript
|
|
364
|
+
import {MountObserver} from 'mount-observer/MountObserver.js';
|
|
365
|
+
const mo = new MountObserver({
|
|
366
|
+
on: '*',
|
|
367
|
+
observedAttrsWhenMounted: ['lang', 'contenteditable']
|
|
368
|
+
});
|
|
369
|
+
|
|
370
|
+
mo.addEventListener('attrChange', e => {
|
|
371
|
+
console.log(e);
|
|
372
|
+
// {
|
|
373
|
+
// matchingElement,
|
|
374
|
+
// attrChangeInfo:[{
|
|
375
|
+
// idx: 0,
|
|
376
|
+
// name: 'lang'
|
|
377
|
+
// oldValue: null,
|
|
378
|
+
// newValue: 'en-GB',
|
|
379
|
+
// }]
|
|
380
|
+
// }
|
|
381
|
+
});
|
|
309
382
|
```
|
|
310
|
-
-->
|
|
311
383
|
|
|
384
|
+
### Help with parsing?
|
|
385
|
+
|
|
386
|
+
This proposal is likely to evolve going forward, attempting to synthesize [separate ideas](https://github.com/WICG/webcomponents/issues/1045) for declaratively specifying how to interpret the attributes, parsing them so that they may be merged into properties of a class instance.
|
|
387
|
+
|
|
388
|
+
But for now, such support is not part of this proposal (though we can see a glimpse of what that support might look like below).
|
|
312
389
|
|
|
313
390
|
### Custom Enhancements in userland
|
|
314
391
|
|
|
315
|
-
[This proposal could take quite a while to see the light of day, if ever](https://github.com/WICG/webcomponents/issues/1000).
|
|
392
|
+
[This proposal, support for (progressive) enhancement of built-in or third-party custom elements, could take quite a while to see the light of day, if ever](https://github.com/WICG/webcomponents/issues/1000).
|
|
316
393
|
|
|
317
|
-
In the meantime, we want to provide the most help for providing for custom enhancements in userland, and for any other kind of (progressive) enhancement based on attributes going forward.
|
|
394
|
+
In the meantime, we want to provide the most help for providing for custom enhancements in userland, and for any other kind of (progressive) enhancement based on (server-rendered) attributes going forward.
|
|
318
395
|
|
|
319
396
|
Suppose we have a (progressive) enhancement that we want to apply based on the presence of 1 or more attributes.
|
|
320
397
|
|
|
@@ -352,7 +429,7 @@ We want to also support:
|
|
|
352
429
|
|
|
353
430
|
Based on the current unspoken rules, no one will raise an eyebrow with these attributes, because the platform has indicated it will generally avoid dashes in attributes (with an exception or two that will only happen in a blue moon, like aria-*).
|
|
354
431
|
|
|
355
|
-
But now when we consider applying this enhancement to custom elements, we have a new risk. What's to prevent the custom element from having an attribute named my-enhancement?
|
|
432
|
+
But now when we consider applying this enhancement to third party custom elements, we have a new risk. What's to prevent the custom element from having an attribute named my-enhancement?
|
|
356
433
|
|
|
357
434
|
So let's say we want to insist that on custom elements, we must have the data- prefix?
|
|
358
435
|
|
|
@@ -360,7 +437,9 @@ And we want to support an alternative, more semantic sounding prefix to data, sa
|
|
|
360
437
|
|
|
361
438
|
Here's what the api **doesn't** provide (as originally proposed):
|
|
362
439
|
|
|
363
|
-
|
|
440
|
+
#### The carpal syndrome syntax
|
|
441
|
+
|
|
442
|
+
Using the same expression structure as above, we would end up with this avalanche of settings:
|
|
364
443
|
|
|
365
444
|
```JavaScript
|
|
366
445
|
import {MountObserver} from '../MountObserver.js';
|
|
@@ -394,7 +473,9 @@ const mo = new MountObserver({
|
|
|
394
473
|
});
|
|
395
474
|
```
|
|
396
475
|
|
|
397
|
-
|
|
476
|
+
#### The DRY Way
|
|
477
|
+
|
|
478
|
+
This seems like a much better approach, and is supported by this proposal:
|
|
398
479
|
|
|
399
480
|
```JavaScript
|
|
400
481
|
import {MountObserver} from '../MountObserver.js';
|
|
@@ -430,7 +511,7 @@ MountObserver provides a breakdown of the matching attribute when encountered:
|
|
|
430
511
|
}
|
|
431
512
|
}
|
|
432
513
|
});
|
|
433
|
-
mo.addEventListener('
|
|
514
|
+
mo.addEventListener('attrChange', e => {
|
|
434
515
|
console.log(e);
|
|
435
516
|
// {
|
|
436
517
|
// matchingElement,
|
|
@@ -501,6 +582,8 @@ Tentative rules:
|
|
|
501
582
|
|
|
502
583
|
The thinking here is that longer roots indicate higher "specificity", so it is safer to use that one.
|
|
503
584
|
|
|
585
|
+
|
|
586
|
+
|
|
504
587
|
## Preemptive downloading
|
|
505
588
|
|
|
506
589
|
There are two significant steps to imports, each of which imposes a cost:
|
|
@@ -517,7 +600,7 @@ So for this we add option:
|
|
|
517
600
|
```JavaScript
|
|
518
601
|
const observer = new MountObserver({
|
|
519
602
|
on: 'my-element',
|
|
520
|
-
|
|
603
|
+
loadingEagerness: 'eager',
|
|
521
604
|
import: './my-element.js',
|
|
522
605
|
do:{
|
|
523
606
|
mount: (matchingElement, {modules}) => customElements.define(modules[0].MyElement)
|
|
@@ -527,6 +610,9 @@ const observer = new MountObserver({
|
|
|
527
610
|
|
|
528
611
|
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.
|
|
529
612
|
|
|
613
|
+
> [!NOTE]
|
|
614
|
+
> As a result of the google IO 2024 talks, I became aware that there is some similarity between this proposal and the [speculation rules api](https://developer.chrome.com/blog/speculation-rules-improvements). This motivated the change to the property from "loading" to loadingEagerness above.
|
|
615
|
+
|
|
530
616
|
## Intra document html imports
|
|
531
617
|
|
|
532
618
|
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.
|
|
@@ -629,3 +715,57 @@ This proposal (and polyfill) also supports the option to utilize ShadowDOM / slo
|
|
|
629
715
|
|
|
630
716
|
The discussion there leads to an open question whether a processing instruction would be better. I think the compose tag would make much more sense, vs a processing instruction, as it could then support slotted children (behaving similar to the Beatles' example above). Or maybe another tag should be introduced that is the equivalent of the slot, to avoid confusion. or some equivalent. But I strongly suspect that could significantly reduce the payload size of some documents, if we can reuse blocks of HTML, inserting sections of customized content for each instance.
|
|
631
717
|
|
|
718
|
+
## MountObserver script element minutiae
|
|
719
|
+
|
|
720
|
+
Often, we will want to define a large number of "mount observers" programmatically, and we need it to be done in a generic way. This is a problem space that [be-hive](https://github.com/bahrus/be-hive) is grappling with. In particular, we want to publish enhancements that take advantage of this inheritable infrastructure of declarative configuration, but we don't want to burden the developer with having to manually list all these configurations, we want it to happen automatically.
|
|
721
|
+
|
|
722
|
+
To support this, we propose these highlights:
|
|
723
|
+
|
|
724
|
+
1. Adding a "synthesize" method to the MountObserver api, only if observing a shadowRoot (or the top level document). This would provide a kind of passage way from the imperative api to the declarative one.
|
|
725
|
+
2. Synthesize method appends a script element of type MountObserver, that dispatches event from the synthesizing custom element it gets appended to, so subscribers don't need to add a general mutation observer in order to know when parent shadow roots had a MountObserver script tag inserted.
|
|
726
|
+
|
|
727
|
+
|
|
728
|
+
So developers can develop a custom element, used to group families of MountObservers together.
|
|
729
|
+
|
|
730
|
+
If one inspects the DOM, one would see grouped (already "parsed") MountObservers, like so:
|
|
731
|
+
|
|
732
|
+
```html
|
|
733
|
+
<be-hive>
|
|
734
|
+
<script type=mountobserver id=be-searching></script>
|
|
735
|
+
<script type=mountobserver id=be-counted></script>
|
|
736
|
+
</be-hive>
|
|
737
|
+
```
|
|
738
|
+
|
|
739
|
+
But the developer would not need to set these up automatically.
|
|
740
|
+
|
|
741
|
+
Instead, the framework developer would define a custom element that inherits from base class that this proposal/polyfill provides.
|
|
742
|
+
|
|
743
|
+
Let's say the framework developer creates an extending Web Component with constructor: BeHive.
|
|
744
|
+
|
|
745
|
+
Then rather than invoking:
|
|
746
|
+
|
|
747
|
+
```JavaScript
|
|
748
|
+
mountObserver.observe(rootNode);
|
|
749
|
+
```
|
|
750
|
+
|
|
751
|
+
we would invoke:
|
|
752
|
+
|
|
753
|
+
```JavaScript
|
|
754
|
+
mountObserver.synthesize(rootNode, BeHive, mountObserverScriptElement)
|
|
755
|
+
```
|
|
756
|
+
|
|
757
|
+
The MountObserver api would:
|
|
758
|
+
|
|
759
|
+
1. Use [customElements.getName](https://developer.mozilla.org/en-US/docs/Web/API/CustomElementRegistry/getName) to get the name of the custom element (say it is 'be-hive').
|
|
760
|
+
2. Search for a be-hive tag inside the root node (with special logic for the "head" element). If not found, create it.
|
|
761
|
+
3. Place the script element inside.
|
|
762
|
+
|
|
763
|
+
|
|
764
|
+
Then in our shadowroot, rather than adding a script type=mountobserver for every single mount observer we want to inherit, we could reference the group via simply:
|
|
765
|
+
|
|
766
|
+
```html
|
|
767
|
+
<be-hive></be-hive>
|
|
768
|
+
```
|
|
769
|
+
|
|
770
|
+
|
|
771
|
+
|
package/Synthesizer.js
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { MountObserver } from './MountObserver.js';
|
|
2
|
+
export class Synthesizer extends HTMLElement {
|
|
3
|
+
#mutationObserver;
|
|
4
|
+
mountObserverElements = [];
|
|
5
|
+
mutationCallback(mutationList) {
|
|
6
|
+
for (const mutation of mutationList) {
|
|
7
|
+
const { addedNodes } = mutation;
|
|
8
|
+
for (const node of addedNodes) {
|
|
9
|
+
if (!(node instanceof HTMLScriptElement))
|
|
10
|
+
continue;
|
|
11
|
+
const mose = node;
|
|
12
|
+
this.mountObserverElements.push(mose);
|
|
13
|
+
this.#import(mose);
|
|
14
|
+
const e = new SyntheticEvent(mose);
|
|
15
|
+
this.dispatchEvent(e);
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
connectedCallback() {
|
|
20
|
+
this.hidden = true;
|
|
21
|
+
const init = {
|
|
22
|
+
childList: true
|
|
23
|
+
};
|
|
24
|
+
this.#mutationObserver = new MutationObserver(this.mutationCallback);
|
|
25
|
+
this.#mutationObserver.observe(this.getRootNode());
|
|
26
|
+
this.#inherit();
|
|
27
|
+
}
|
|
28
|
+
#import(mose) {
|
|
29
|
+
const { init, do: d, id } = mose;
|
|
30
|
+
const se = document.createElement('script');
|
|
31
|
+
se.init = init;
|
|
32
|
+
se.id = id;
|
|
33
|
+
se.do = d;
|
|
34
|
+
const mi = {
|
|
35
|
+
do: d,
|
|
36
|
+
...init
|
|
37
|
+
};
|
|
38
|
+
const mo = new MountObserver(mi);
|
|
39
|
+
se.observer = mo;
|
|
40
|
+
this.appendChild(se);
|
|
41
|
+
}
|
|
42
|
+
#inherit() {
|
|
43
|
+
const rn = this.getRootNode();
|
|
44
|
+
const host = rn.host;
|
|
45
|
+
if (!host)
|
|
46
|
+
return;
|
|
47
|
+
const parentShadowRealm = host.getRootNode();
|
|
48
|
+
const { localName } = this;
|
|
49
|
+
const parentScopeSynthesizer = parentShadowRealm.querySelector(localName);
|
|
50
|
+
const { mountObserverElements } = parentScopeSynthesizer;
|
|
51
|
+
for (const moe of mountObserverElements) {
|
|
52
|
+
this.#import(moe);
|
|
53
|
+
}
|
|
54
|
+
if (parentScopeSynthesizer !== null) {
|
|
55
|
+
parentScopeSynthesizer.addEventListener(SyntheticEvent.eventName, e => {
|
|
56
|
+
this.#import(e.mountObserverElement);
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
disconnectedCallback() {
|
|
61
|
+
if (this.#mutationObserver !== undefined) {
|
|
62
|
+
this.#mutationObserver.disconnect();
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
// https://github.com/webcomponents-cg/community-protocols/issues/12#issuecomment-872415080
|
|
67
|
+
/**
|
|
68
|
+
* The `mutation-event` event represents something that happened.
|
|
69
|
+
* We can document it here.
|
|
70
|
+
*/
|
|
71
|
+
export class SyntheticEvent extends Event {
|
|
72
|
+
mountObserverElement;
|
|
73
|
+
static eventName = 'synthesize';
|
|
74
|
+
constructor(mountObserverElement) {
|
|
75
|
+
super(SyntheticEvent.eventName);
|
|
76
|
+
this.mountObserverElement = mountObserverElement;
|
|
77
|
+
}
|
|
78
|
+
}
|
package/getWhereAttrSelector.js
CHANGED
|
@@ -1,72 +1,74 @@
|
|
|
1
|
-
export function getWhereAttrSelector(whereAttr, withoutAttrs) {
|
|
2
|
-
const { hasBase, hasBranchIn, hasRootIn } = whereAttr;
|
|
3
|
-
const
|
|
4
|
-
start: '',
|
|
5
|
-
context: 'Both'
|
|
6
|
-
}];
|
|
7
|
-
const fullListOfAttrs = [];
|
|
1
|
+
export async function getWhereAttrSelector(whereAttr, withoutAttrs) {
|
|
2
|
+
const { hasBase, hasBranchIn, hasRootIn, isIn } = whereAttr;
|
|
3
|
+
const fullListOfAttrs = isIn !== undefined ? [...isIn] : [];
|
|
8
4
|
const partitionedAttrs = [];
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
let
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
5
|
+
if (hasBase !== undefined) {
|
|
6
|
+
const hasRootInGuaranteed = hasRootIn || [{
|
|
7
|
+
start: '',
|
|
8
|
+
context: 'Both'
|
|
9
|
+
}];
|
|
10
|
+
let prefixLessMatches = [];
|
|
11
|
+
const hasBaseIsString = typeof hasBase === 'string';
|
|
12
|
+
const baseSelector = hasBaseIsString ? hasBase : hasBase[1];
|
|
13
|
+
const rootToBaseDelimiter = hasBaseIsString ? '-' : hasBase[0];
|
|
14
|
+
if (hasBranchIn !== undefined) {
|
|
15
|
+
let baseToBranchDelimiter = '-';
|
|
16
|
+
let branches;
|
|
17
|
+
if (hasBranchIn.length === 2 && Array.isArray(hasBranchIn[1])) {
|
|
18
|
+
baseToBranchDelimiter = hasBranchIn[0];
|
|
19
|
+
branches = hasBranchIn[1];
|
|
20
|
+
}
|
|
21
|
+
else {
|
|
22
|
+
branches = hasBranchIn;
|
|
23
|
+
}
|
|
24
|
+
prefixLessMatches = branches.map(x => ({
|
|
25
|
+
rootToBaseDelimiter,
|
|
26
|
+
base: baseSelector,
|
|
27
|
+
baseToBranchDelimiter: x ? baseToBranchDelimiter : '',
|
|
28
|
+
branch: x
|
|
29
|
+
}));
|
|
19
30
|
}
|
|
20
31
|
else {
|
|
21
|
-
|
|
32
|
+
prefixLessMatches.push({
|
|
33
|
+
rootToBaseDelimiter,
|
|
34
|
+
base: baseSelector,
|
|
35
|
+
});
|
|
22
36
|
}
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
}
|
|
58
|
-
else {
|
|
59
|
-
const name = `${startAndRootToBaseDelimiter}${base}`;
|
|
60
|
-
fullListOfAttrs.push(name);
|
|
61
|
-
partitionedAttrs.push({
|
|
62
|
-
root: start,
|
|
63
|
-
name,
|
|
64
|
-
base,
|
|
65
|
-
rootCnfg,
|
|
66
|
-
branchIdx
|
|
67
|
-
});
|
|
37
|
+
for (const rootCnfg of hasRootInGuaranteed) {
|
|
38
|
+
const { start } = rootCnfg;
|
|
39
|
+
for (const match of prefixLessMatches) {
|
|
40
|
+
const { base, baseToBranchDelimiter, branch, rootToBaseDelimiter } = match;
|
|
41
|
+
let branchIdx = 0;
|
|
42
|
+
for (const prefixLessMatch of prefixLessMatches) {
|
|
43
|
+
const { base, baseToBranchDelimiter, branch } = prefixLessMatch;
|
|
44
|
+
const startAndRootToBaseDelimiter = start ? `${start}${rootToBaseDelimiter}` : '';
|
|
45
|
+
//TODO: could probably reduce the size of the code below
|
|
46
|
+
if (branch) {
|
|
47
|
+
//will always have branch?
|
|
48
|
+
const name = `${startAndRootToBaseDelimiter}${base}${baseToBranchDelimiter}${branch}`;
|
|
49
|
+
fullListOfAttrs.push(name);
|
|
50
|
+
partitionedAttrs.push({
|
|
51
|
+
root: start,
|
|
52
|
+
name,
|
|
53
|
+
base,
|
|
54
|
+
branch,
|
|
55
|
+
branchIdx,
|
|
56
|
+
rootCnfg
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
else {
|
|
60
|
+
const name = `${startAndRootToBaseDelimiter}${base}`;
|
|
61
|
+
fullListOfAttrs.push(name);
|
|
62
|
+
partitionedAttrs.push({
|
|
63
|
+
root: start,
|
|
64
|
+
name,
|
|
65
|
+
base,
|
|
66
|
+
rootCnfg,
|
|
67
|
+
branchIdx
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
branchIdx++;
|
|
68
71
|
}
|
|
69
|
-
branchIdx++;
|
|
70
72
|
}
|
|
71
73
|
}
|
|
72
74
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "mount-observer",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.22",
|
|
4
4
|
"description": "Observe and act on css matches.",
|
|
5
5
|
"main": "MountObserver.js",
|
|
6
6
|
"module": "MountObserver.js",
|
|
@@ -10,7 +10,8 @@
|
|
|
10
10
|
},
|
|
11
11
|
"exports": {
|
|
12
12
|
".": "./MountObserver.js",
|
|
13
|
-
"./MountObserver.js": "./MountObserver.js"
|
|
13
|
+
"./MountObserver.js": "./MountObserver.js",
|
|
14
|
+
"./Synthesizer.js": "./Synthesizer.js"
|
|
14
15
|
},
|
|
15
16
|
"files": [
|
|
16
17
|
"*.js",
|
package/types.d.ts
CHANGED
|
@@ -1,20 +1,19 @@
|
|
|
1
|
-
|
|
1
|
+
//import { MountObserver } from "./MountObserver";
|
|
2
|
+
|
|
3
|
+
export interface JSONSerializableMountInit{
|
|
2
4
|
readonly on?: CSSMatch,
|
|
3
|
-
//readonly attribMatches?: Array<AttribMatch>,
|
|
4
|
-
readonly withTargetShadowRoot?: ShadowRoot,
|
|
5
5
|
readonly whereAttr?: WhereAttr,
|
|
6
6
|
readonly whereElementIntersectsWith?: IntersectionObserverInit,
|
|
7
7
|
readonly whereMediaMatches?: MediaQuery,
|
|
8
|
+
readonly import?: ImportString | [ImportString, ImportAssertions] | PipelineProcessor,
|
|
9
|
+
|
|
10
|
+
}
|
|
11
|
+
export interface MountInit extends JSONSerializableMountInit{
|
|
12
|
+
|
|
13
|
+
readonly withTargetShadowRoot?: ShadowRoot,
|
|
8
14
|
readonly whereInstanceOf?: Array<{new(): Element}>,
|
|
9
15
|
readonly whereSatisfies?: PipelineProcessor<boolean>,
|
|
10
|
-
readonly
|
|
11
|
-
readonly do?: {
|
|
12
|
-
readonly mount?: PipelineProcessor,
|
|
13
|
-
readonly dismount?: PipelineProcessor,
|
|
14
|
-
readonly disconnect?: PipelineProcessor,
|
|
15
|
-
readonly reconfirm?: PipelineProcessor,
|
|
16
|
-
readonly exit?: PipelineProcessor,
|
|
17
|
-
}
|
|
16
|
+
readonly do?: MountObserverCallbacks
|
|
18
17
|
// /**
|
|
19
18
|
// * Purpose -- there are scenarios where we may only want to affect changes that occur after the initial
|
|
20
19
|
// * server rendering, so we only want to mount elements that appear
|
|
@@ -22,6 +21,14 @@ export interface MountInit{
|
|
|
22
21
|
// readonly ignoreInitialMatches?: boolean,
|
|
23
22
|
}
|
|
24
23
|
|
|
24
|
+
export interface MountObserverCallbacks{
|
|
25
|
+
readonly mount?: PipelineProcessor,
|
|
26
|
+
readonly dismount?: PipelineProcessor,
|
|
27
|
+
readonly disconnect?: PipelineProcessor,
|
|
28
|
+
readonly reconfirm?: PipelineProcessor,
|
|
29
|
+
readonly exit?: PipelineProcessor,
|
|
30
|
+
}
|
|
31
|
+
|
|
25
32
|
export interface RootCnfg{
|
|
26
33
|
start: string,
|
|
27
34
|
context: 'BuiltIn' | 'CustomElement' | 'Both'
|
|
@@ -30,7 +37,8 @@ export interface RootCnfg{
|
|
|
30
37
|
//export type RootAttrOptions = Array<string | RootCnfg>;
|
|
31
38
|
export type delimiter = string;
|
|
32
39
|
export interface WhereAttr{
|
|
33
|
-
|
|
40
|
+
isIn?: Array<string>,
|
|
41
|
+
hasBase?: string | [delimiter, string],
|
|
34
42
|
hasBranchIn?: Array<string> | [delimiter, Array<string>],
|
|
35
43
|
hasRootIn?: Array<RootCnfg>,
|
|
36
44
|
/**
|
|
@@ -58,6 +66,7 @@ export interface IMountObserver {
|
|
|
58
66
|
observe(within: Node): void;
|
|
59
67
|
disconnect(within: Node): void;
|
|
60
68
|
module?: any;
|
|
69
|
+
mountedElements: WeakSet<Element>;
|
|
61
70
|
}
|
|
62
71
|
|
|
63
72
|
export interface MountContext{
|
|
@@ -96,8 +105,8 @@ interface AttrChangeInfo{
|
|
|
96
105
|
oldValue: string | null,
|
|
97
106
|
newValue: string | null,
|
|
98
107
|
idx: number,
|
|
108
|
+
name: string,
|
|
99
109
|
parts: AttrParts,
|
|
100
|
-
//parsedNewValue?: any,
|
|
101
110
|
}
|
|
102
111
|
|
|
103
112
|
//#region mount event
|
|
@@ -139,7 +148,7 @@ export interface AddDisconnectEventListener {
|
|
|
139
148
|
//endregion
|
|
140
149
|
|
|
141
150
|
//#region attribute change event
|
|
142
|
-
export type attrChangeEventName = '
|
|
151
|
+
export type attrChangeEventName = 'attrChange';
|
|
143
152
|
export interface IAttrChangeEvent extends IMountEvent {
|
|
144
153
|
attrChangeInfos: Array<AttrChangeInfo>,
|
|
145
154
|
}
|
|
@@ -160,3 +169,14 @@ export interface AddLoadEventListener{
|
|
|
160
169
|
}
|
|
161
170
|
//#endregion
|
|
162
171
|
|
|
172
|
+
//#region MountObserver Script Element
|
|
173
|
+
export interface MountObserverScriptElement extends HTMLScriptElement{
|
|
174
|
+
init: JSONSerializableMountInit;
|
|
175
|
+
//mountedElements: Array<WeakRef<Element>>;
|
|
176
|
+
observer: IMountObserver;
|
|
177
|
+
do: MountObserverCallbacks;
|
|
178
|
+
}
|
|
179
|
+
//#endregion
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
|