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 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
- #rootMutObs;
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
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. 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:
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-attr',
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-attr-wow-this-is-deep="hello"></section>
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-attr', 'second-attr', ''],
353
+ hasBranchIn: ['first-aspect', 'second-aspect', ''],
352
354
  hasLeafIn: {
353
- 'first-attr': ['wow-this-is-deep', 'have-you-considered-using-json-for-this'],
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-attr',
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-attr', 'second-attr', '']],
408
+ hasBranchIn: [':', ['first-aspect', 'second-aspect', '']],
407
409
  hasLeafIn: {
408
- 'first-attr': ['--', ['wow-this-is-deep', 'have-you-considered-using-json-for-this']],
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mount-observer",
3
- "version": "0.0.8",
3
+ "version": "0.0.10",
4
4
  "description": "Observe and act on css matches.",
5
5
  "main": "MountObserver.js",
6
6
  "module": "MountObserver.js",
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 attrChangeEventHander = (e: IAttrChangeEvent) => void;
128
- export interface AddAttrChangeEventistener{
129
- addEventListener(eventName: attrChangeEventName, handler: attrChangeEventHander, options?: AddEventListenerOptions): void;
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