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 CHANGED
@@ -5,7 +5,7 @@ export class MountObserver extends EventTarget {
5
5
  #mountInit;
6
6
  //#rootMutObs: RootMutObs | undefined;
7
7
  #abortController;
8
- #mounted;
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: further this problem further. Starting to think this is basically not polyfillable
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.#mounted = new WeakSet();
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.#mounted.add(match);
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 = 'attr-change';
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-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
- Example:
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
- ```html
290
- <div id=div>
291
- <my-custom-element my-first-observed-attribute="hello"></my-custom-element>
292
- </div>
293
- <script type=module>
294
- import {MountObserver} from '../MountObserver.js';
295
- const mo = new MountObserver({
296
- on: '*',
297
- whereInstanceOf: [MyCustomElement]
298
- });
299
- mo.addEventListener('parsed-attrs-changed', e => {
300
- const {matchingElement, modifiedObjectFieldValues, preModifiedFieldValues} = e;
301
- console.log({matchingElement, modifiedObjectFieldValues, preModifiedFieldValues});
302
- });
303
- mo.observe(div);
304
- setTimeout(() => {
305
- const myCustomElement = document.querySelector('my-custom-element');
306
- myCustomElement.setAttribute('my-first-observed-attribute', 'good-bye');
307
- }, 1000);
308
- </script>
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
- ## Rejected option -- The carpal syndrome syntax
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
- ## Supported -- The DRY Way
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('observed-attr-change', e => {
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
- loading: 'eager',
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
+ }
@@ -1,72 +1,74 @@
1
- export function getWhereAttrSelector(whereAttr, withoutAttrs) {
2
- const { hasBase, hasBranchIn, hasRootIn } = whereAttr;
3
- const hasRootInGuaranteed = hasRootIn || [{
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
- let prefixLessMatches = [];
10
- const hasBaseIsString = typeof hasBase === 'string';
11
- const baseSelector = hasBaseIsString ? hasBase : hasBase[1];
12
- const rootToBaseDelimiter = hasBaseIsString ? '-' : hasBase[0];
13
- if (hasBranchIn !== undefined) {
14
- let baseToBranchDelimiter = '-';
15
- let branches;
16
- if (hasBranchIn.length === 2 && Array.isArray(hasBranchIn[1])) {
17
- baseToBranchDelimiter = hasBranchIn[0];
18
- branches = hasBranchIn[1];
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
- branches = hasBranchIn;
32
+ prefixLessMatches.push({
33
+ rootToBaseDelimiter,
34
+ base: baseSelector,
35
+ });
22
36
  }
23
- prefixLessMatches = branches.map(x => ({
24
- rootToBaseDelimiter,
25
- base: baseSelector,
26
- baseToBranchDelimiter: x ? baseToBranchDelimiter : '',
27
- branch: x
28
- }));
29
- }
30
- else {
31
- prefixLessMatches.push({
32
- rootToBaseDelimiter,
33
- base: baseSelector,
34
- });
35
- }
36
- for (const rootCnfg of hasRootInGuaranteed) {
37
- const { start } = rootCnfg;
38
- for (const match of prefixLessMatches) {
39
- const { base, baseToBranchDelimiter, branch, rootToBaseDelimiter } = match;
40
- let branchIdx = 0;
41
- for (const prefixLessMatch of prefixLessMatches) {
42
- const { base, baseToBranchDelimiter, branch } = prefixLessMatch;
43
- const startAndRootToBaseDelimiter = start ? `${start}${rootToBaseDelimiter}` : '';
44
- //TODO: could probably reduce the size of the code below
45
- if (branch) {
46
- //will always have branch?
47
- const name = `${startAndRootToBaseDelimiter}${base}${baseToBranchDelimiter}${branch}`;
48
- fullListOfAttrs.push(name);
49
- partitionedAttrs.push({
50
- root: start,
51
- name,
52
- base,
53
- branch,
54
- branchIdx,
55
- rootCnfg
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.20",
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
- export interface MountInit{
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 import?: ImportString | [ImportString, ImportAssertions] | PipelineProcessor,
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
- hasBase: string | [delimiter, string],
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 = 'attr-change';
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
+