mount-observer 0.0.8 → 0.0.9

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,92 @@ 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.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.before(clone);
115
+ el.remove();
116
+ }
117
+ #templLookUp = new Map();
118
+ #findByID(id, fragment) {
119
+ if (this.#templLookUp.has(id))
120
+ return this.#templLookUp.get(id);
121
+ let templ = fragment.getElementById(id);
122
+ if (templ === null) {
123
+ let rootToSearchOutwardFrom = ((fragment.isConnected ? fragment.getRootNode() : this.#mountInit.withTargetShadowRoot) || document);
124
+ templ = rootToSearchOutwardFrom.getElementById(id);
125
+ while (templ === null && rootToSearchOutwardFrom !== document) {
126
+ rootToSearchOutwardFrom = (rootToSearchOutwardFrom.host || rootToSearchOutwardFrom).getRootNode();
127
+ templ = rootToSearchOutwardFrom.getElementById(id);
128
+ }
129
+ }
130
+ if (templ !== null)
131
+ this.#templLookUp.set(id, templ);
132
+ return templ;
133
+ }
47
134
  unobserve(within) {
48
135
  const nodeToMonitor = this.#isComplex ? (within instanceof ShadowRoot ? within : within.getRootNode()) : within;
49
136
  const currentCount = refCount.get(nodeToMonitor);
@@ -70,6 +157,7 @@ export class MountObserver extends EventTarget {
70
157
  }
71
158
  }
72
159
  async observe(within) {
160
+ this.#observe = new WeakRef(within);
73
161
  const nodeToMonitor = this.#isComplex ? (within instanceof ShadowRoot ? within : within.getRootNode()) : within;
74
162
  if (!mutationObserverLookup.has(nodeToMonitor)) {
75
163
  mutationObserverLookup.set(nodeToMonitor, new RootMutObs(nodeToMonitor));
@@ -271,14 +359,21 @@ export class MountObserver extends EventTarget {
271
359
  }
272
360
  return true;
273
361
  });
362
+ for (const elToMount of elsToMount) {
363
+ if (elToMount.matches(biQry)) {
364
+ await this.#birtalizeMatch(elToMount, 0);
365
+ }
366
+ }
274
367
  this.#mount(elsToMount, initializing);
275
368
  }
276
369
  async #inspectWithin(within, initializing) {
370
+ await this.#birtualizeFragment(within, 0);
277
371
  const els = Array.from(within.querySelectorAll(await this.#selector()));
278
372
  this.#filterAndMount(els, false, initializing);
279
373
  }
280
374
  }
281
375
  const refCountErr = 'mount-observer ref count mismatch';
376
+ const biQry = 'b-i[href^="#"]:not([disabled])';
282
377
  // https://github.com/webcomponents-cg/community-protocols/issues/12#issuecomment-872415080
283
378
  /**
284
379
  * The `mutation-event` event represents something that happened.
@@ -320,4 +415,12 @@ export class AttrChangeEvent extends Event {
320
415
  this.attrChangeInfo = attrChangeInfo;
321
416
  }
322
417
  }
418
+ export class LoadEvent extends Event {
419
+ clone;
420
+ static eventName = 'load';
421
+ constructor(clone) {
422
+ super(LoadEvent.eventName);
423
+ this.clone = clone;
424
+ }
425
+ }
323
426
  //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-17
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,47 @@ 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 special tag that takes the form:
447
+
448
+ ```html
449
+ <div>Some prior stuff</div>
450
+ <b-i href=#my-snippet-of-html>
451
+ <div slot=slot1>hello</div>
452
+ <div slot=slot2>goodbye<div>
453
+ </b-i>
454
+ <div>Some additional stuff</div>
455
+ ```
456
+
457
+ "b-i" stands for "birtual inclusion" where birtual is a made up word that is a portmanteau of birth and virtual.
458
+
459
+ When it encounters such a thing, it searches "upwardly" through the chain of ShadowRoots for a template with id=my-kinda-sorta-custom-element (in this case), and caches them as it finds them.
460
+
461
+ Let's say the template looks as follows:
462
+
463
+ ```html
464
+ <template id=my-snippet-of-html>
465
+ This is an example of a snippet of HTML that appears repeatedly.
466
+ <slot name=slot1></slot>
467
+ <slot name=slot2></slot>
468
+ </template>
469
+ ```
470
+
471
+ What we would end up with is:
472
+
473
+
474
+ ```html
475
+ <div>Some prior stuff</div>
476
+ This is an example of a snippet of HTML that appears repeatedly.
477
+ <div>hello</div>
478
+ <div>goodbye</div>
479
+ <div>Some additional stuff</div>
480
+ ```
481
+
482
+ Some significant differences with genuine slot support as used with (ShadowDOM'd) custom elements
483
+
484
+ 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.
485
+ 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.9",
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