mount-observer 0.0.32 → 0.0.33

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.
@@ -0,0 +1,426 @@
1
+ import {MountInit, IMountObserver, AddMutationEventListener,
2
+ MutationEvent, dismountEventName, mountEventName, IMountEvent, IDismountEvent,
3
+ disconnectedEventName, IDisconnectEvent, IAttrChangeEvent, attrChangeEventName, AttrChangeInfo, loadEventName, ILoadEvent,
4
+ AttrParts,
5
+ MOSE
6
+ } from './ts-refs/mount-observer/types';
7
+ import {RootMutObs} from './RootMutObs.js';
8
+ export {MOSE} from './ts-refs/mount-observer/types';
9
+
10
+ const mutationObserverLookup = new WeakMap<Node, RootMutObs>();
11
+ const refCount = new WeakMap<Node, number>();
12
+ export class MountObserver extends EventTarget implements IMountObserver{
13
+
14
+ #mountInit: MountInit;
15
+ //#rootMutObs: RootMutObs | undefined;
16
+ #abortController: AbortController;
17
+ mountedElements: WeakSet<Element>;
18
+ #mountedList: Array<WeakRef<Element>> | undefined;
19
+ #disconnected: WeakSet<Element>;
20
+ //#unmounted: WeakSet<Element>;
21
+ #isComplex: boolean;
22
+ objNde: WeakRef<Node> | undefined;
23
+
24
+ constructor(init: MountInit){
25
+ super();
26
+ const {on, whereElementIntersectsWith, whereMediaMatches} = init;
27
+ let isComplex = false;
28
+ //TODO: study this problem further. Starting to think this is basically not polyfillable
29
+ if(on !== undefined){
30
+ const reducedMatch = on.replaceAll(':not(', '');
31
+ isComplex = reducedMatch.includes(' ') || (reducedMatch.includes(':') && reducedMatch.includes('('));
32
+ }
33
+ this.#isComplex = isComplex;
34
+ if(whereElementIntersectsWith || whereMediaMatches) throw 'NI'; //not implemented
35
+ this.#mountInit = init;
36
+ this.#abortController = new AbortController();
37
+ this.mountedElements = new WeakSet();
38
+ this.#disconnected = new WeakSet();
39
+ //this.#unmounted = new WeakSet();
40
+ }
41
+
42
+ #calculatedSelector: string | undefined;
43
+ #attrParts: Array<AttrParts> | undefined;
44
+
45
+ #fullListOfEnhancementAttrs: Array<string> | undefined;
46
+ //get #attrVals
47
+ async #selector() : Promise<string>{
48
+ if(this.#calculatedSelector !== undefined) return this.#calculatedSelector;
49
+ const {on, whereAttr} = this.#mountInit;
50
+ const withoutAttrs = on || '*';
51
+ if(whereAttr === undefined) return withoutAttrs;
52
+ const {getWhereAttrSelector} = await import('./getWhereAttrSelector.js');
53
+ const info = await getWhereAttrSelector(whereAttr, withoutAttrs);
54
+ const {fullListOfAttrs, calculatedSelector, partitionedAttrs} = info;
55
+ this.#fullListOfEnhancementAttrs = fullListOfAttrs;
56
+ this.#attrParts = partitionedAttrs;
57
+ this.#calculatedSelector = calculatedSelector
58
+ return this.#calculatedSelector;
59
+ }
60
+
61
+ async composeFragment(fragment: DocumentFragment, level: number){
62
+ const bis = fragment.querySelectorAll(inclTemplQry) as NodeListOf<HTMLTemplateElement>;
63
+ for(const bi of bis){
64
+ await this.#compose(bi, level);
65
+ }
66
+ }
67
+
68
+ async #compose(el: HTMLTemplateElement, level: number){
69
+ const {compose} = await import('./compose.js');
70
+ await compose(this, el, level);
71
+ }
72
+ #templLookUp: Map<string, HTMLElement> = new Map();
73
+ findByID(id: string, fragment: DocumentFragment): HTMLElement | null{
74
+ if(this.#templLookUp.has(id)) return this.#templLookUp.get(id)!;
75
+ let templ = fragment.getElementById(id);
76
+ if(templ === null){
77
+ let rootToSearchOutwardFrom = ((fragment.isConnected ? fragment.getRootNode() : this.#mountInit.withTargetShadowRoot) || document) as any;
78
+ templ = rootToSearchOutwardFrom.getElementById(id);
79
+ while(templ === null && rootToSearchOutwardFrom !== (document as any as DocumentFragment) ){
80
+ rootToSearchOutwardFrom = (rootToSearchOutwardFrom.host || rootToSearchOutwardFrom).getRootNode() as DocumentFragment;
81
+ templ = rootToSearchOutwardFrom.getElementById(id);
82
+ }
83
+ }
84
+ if(templ !== null) this.#templLookUp.set(id, templ);
85
+ return templ;
86
+ }
87
+
88
+ disconnect(within: Node){
89
+ const nodeToMonitor = this.#isComplex ? (within instanceof ShadowRoot ? within : within.getRootNode()) : within;
90
+ const currentCount = refCount.get(nodeToMonitor);
91
+ if(currentCount !== undefined){
92
+ if(currentCount <= 1){
93
+ const observer = mutationObserverLookup.get(nodeToMonitor);
94
+ if(observer === undefined){
95
+ console.warn(refCountErr);
96
+ }else{
97
+ observer.disconnect();
98
+ mutationObserverLookup.delete(nodeToMonitor);
99
+ refCount.delete(nodeToMonitor);
100
+ }
101
+ }else{
102
+ refCount.set(nodeToMonitor, currentCount + 1);
103
+ }
104
+ }else{
105
+ if(mutationObserverLookup.has(nodeToMonitor)){
106
+ console.warn(refCountErr);
107
+ }
108
+ }
109
+ this.dispatchEvent(new Event('disconnectedCallback'));
110
+
111
+ }
112
+
113
+ async observe(within: Node){
114
+ await this.#selector();
115
+ this.objNde = new WeakRef(within);
116
+ const nodeToMonitor = this.#isComplex ? (within instanceof ShadowRoot ? within : within.getRootNode()) : within;
117
+ if(!mutationObserverLookup.has(nodeToMonitor)){
118
+ mutationObserverLookup.set(nodeToMonitor, new RootMutObs(nodeToMonitor));
119
+ refCount.set(nodeToMonitor, 1);
120
+ }else{
121
+ const currentCount = refCount.get(nodeToMonitor);
122
+ if(currentCount === undefined){
123
+ console.warn(refCountErr);
124
+ }else{
125
+ refCount.set(nodeToMonitor, currentCount + 1);
126
+ }
127
+ }
128
+ const rootMutObs = mutationObserverLookup.get(within)!;
129
+ const fullListOfAttrs = this.#fullListOfEnhancementAttrs;
130
+ (rootMutObs as any as AddMutationEventListener).addEventListener('mutation-event', async (e: MutationEvent) => {
131
+ //TODO: disconnected
132
+ if(this.#isComplex){
133
+ this.#inspectWithin(within, false);
134
+ return;
135
+ }
136
+ const {mutationRecords} = e;
137
+ const elsToInspect: Array<Element> = [];
138
+ //const elsToDisconnect: Array<Element> = [];
139
+ const doDisconnect = this.#mountInit.do?.disconnect;
140
+ let attrChangeInfosMap: Map<Element, Array<AttrChangeInfo>> | undefined;
141
+ for(const mutationRecord of mutationRecords){
142
+ const {addedNodes, type, removedNodes} = mutationRecord;
143
+ const addedElements = Array.from(addedNodes).filter(x => x instanceof Element) as Array<Element>;
144
+ addedElements.forEach(x => elsToInspect.push(x));
145
+ if(type === 'attributes'){
146
+ const {target, attributeName, oldValue} = mutationRecord;
147
+ if(target instanceof Element && attributeName !== null /*&& this.#mounted.has(target)*/){
148
+
149
+ if(fullListOfAttrs !== undefined){
150
+ const idx = fullListOfAttrs.indexOf(attributeName);
151
+ if(idx !== -1){
152
+ if(attrChangeInfosMap === undefined) attrChangeInfosMap = new Map();
153
+ let attrChangeInfos = attrChangeInfosMap.get(target);
154
+ if(attrChangeInfos === undefined){
155
+ attrChangeInfos = [];
156
+ attrChangeInfosMap.set(target, attrChangeInfos);
157
+ }
158
+ const newValue = target.getAttribute(attributeName);
159
+ const parts = this.#attrParts![idx];
160
+ const attrChangeInfo: AttrChangeInfo = {
161
+ isSOfTAttr: false,
162
+ oldValue,
163
+ name: attributeName,
164
+ newValue,
165
+ idx,
166
+ parts
167
+ }
168
+ attrChangeInfos.push(attrChangeInfo)
169
+ }
170
+
171
+
172
+ }
173
+
174
+ }
175
+ elsToInspect.push(target as Element);
176
+ }
177
+
178
+ const deletedElements = Array.from(removedNodes).filter(x => x instanceof Element) as Array<Element>;
179
+ for(const deletedElement of deletedElements){
180
+ this.#disconnected.add(deletedElement);
181
+ if(doDisconnect !== undefined){
182
+ doDisconnect(deletedElement, this, {});
183
+ }
184
+ this.dispatchEvent(new DisconnectEvent(deletedElement));
185
+ }
186
+
187
+ }
188
+ if(attrChangeInfosMap !== undefined){
189
+ for(const [key, value] of attrChangeInfosMap){
190
+ this.dispatchEvent(new AttrChangeEvent(key, value))
191
+ }
192
+ }
193
+ this.#filterAndMount(elsToInspect, true, false);
194
+ }, {signal: this.#abortController.signal});
195
+
196
+ await this.#inspectWithin(within, true);
197
+
198
+ }
199
+
200
+ static synthesize(within: Document | ShadowRoot, customElement: {new(): HTMLElement}, mose: MOSE){
201
+ mose.type = 'mountobserver';
202
+ const name = customElements.getName(customElement);
203
+ if(name === null) throw 400;
204
+ let instance = within.querySelector(name);
205
+ if(instance === null){
206
+ instance = new customElement();
207
+ if(within === document){
208
+ within.head.appendChild(instance);
209
+ }else{
210
+ within.appendChild(instance);
211
+ }
212
+ }
213
+ instance.appendChild(mose);
214
+ }
215
+
216
+ #confirmInstanceOf(el: Element, whereInstanceOf: Array<{new(): Element}>){
217
+ for(const test of whereInstanceOf){
218
+ if(el instanceof test) return true;
219
+ }
220
+ return false;
221
+ }
222
+
223
+ async #mount(matching: Array<Element>, initializing: boolean){
224
+ //first unmount non matching
225
+ const alreadyMounted = await this.#filterAndDismount();
226
+ const mount = this.#mountInit.do?.mount;
227
+ const {import: imp} = this.#mountInit;
228
+
229
+ for(const match of matching){
230
+ if(alreadyMounted.has(match)) continue;
231
+ this.mountedElements.add(match);
232
+ if(imp !== undefined){
233
+ switch(typeof imp){
234
+ case 'string':
235
+ this.module = await import(imp);
236
+ break;
237
+ case 'object':
238
+ if(Array.isArray(imp)){
239
+ throw 'NI: Firefox'
240
+ }
241
+ break;
242
+ case 'function':
243
+ this.module = await imp(match, this, {
244
+ stage: 'Import',
245
+ initializing
246
+ });
247
+ break;
248
+ }
249
+ }
250
+ if(mount !== undefined) {
251
+ mount(match, this, {
252
+ stage: 'PostImport',
253
+ initializing
254
+ })
255
+ }
256
+ this.dispatchEvent(new MountEvent(match, initializing));
257
+ //should we automatically call readAttrs?
258
+ //the thinking is it might make more sense to call that after mounting
259
+
260
+ this.#mountedList?.push(new WeakRef(match));
261
+ }
262
+ }
263
+
264
+ readAttrs(match: Element, branchIndexes?: Set<number>){
265
+ const fullListOfAttrs = this.#fullListOfEnhancementAttrs;
266
+ const attrChangeInfos: Array<AttrChangeInfo> = [];
267
+ const oldValue = null;
268
+ if(fullListOfAttrs !== undefined){
269
+ const attrParts = this.#attrParts
270
+
271
+ for(let idx = 0, ii = fullListOfAttrs.length; idx < ii; idx++){
272
+ const parts = attrParts![idx];
273
+ const {branchIdx} = parts;
274
+ if(branchIndexes !== undefined){
275
+ if(!branchIndexes.has(branchIdx)) continue;
276
+ }
277
+ const name = fullListOfAttrs[idx];
278
+
279
+ const newValue = match.getAttribute(name);
280
+
281
+ attrChangeInfos.push({
282
+ idx,
283
+ isSOfTAttr: false,
284
+ newValue,
285
+ oldValue,
286
+ name,
287
+ parts
288
+ });
289
+ }
290
+
291
+ }
292
+ const {observedAttrsWhenMounted} = this.#mountInit;
293
+ if(observedAttrsWhenMounted !== undefined){
294
+ for(const observedAttr of observedAttrsWhenMounted){
295
+ const attrIsString = typeof observedAttr === 'string';
296
+ const name = attrIsString ? observedAttr : observedAttr.name;
297
+ let mapsTo: string | undefined;
298
+ let newValue = match.getAttribute(name);
299
+ if(!attrIsString){
300
+ const {customParser, instanceOf, mapsTo: mt, valIfNull} = observedAttr;
301
+ if(instanceOf || customParser) throw 'NI';
302
+ if(newValue === null) newValue = valIfNull;
303
+ mapsTo = mt;
304
+ }
305
+ attrChangeInfos.push({
306
+ isSOfTAttr: true,
307
+ newValue,
308
+ oldValue,
309
+ name,
310
+ mapsTo
311
+ });
312
+ }
313
+ }
314
+
315
+ return attrChangeInfos;
316
+ }
317
+
318
+ async #dismount(unmatching: Array<Element>){
319
+ const onDismount = this.#mountInit.do?.dismount
320
+ for(const unmatch of unmatching){
321
+ if(onDismount !== undefined){
322
+ onDismount(unmatch, this, {});
323
+ }
324
+ this.dispatchEvent(new DismountEvent(unmatch));
325
+ }
326
+ }
327
+
328
+ async #filterAndDismount(): Promise<Set<Element>>{
329
+ const returnSet = new Set<Element>();
330
+ if(this.#mountedList !== undefined){
331
+ const previouslyMounted = this.#mountedList.map(x => x.deref());
332
+ const {whereSatisfies, whereInstanceOf} = this.#mountInit;
333
+ const match = await this.#selector();
334
+ const elsToUnMount = previouslyMounted.filter(x => {
335
+ if(x === undefined) return false;
336
+ if(!x.matches(match)) return true;
337
+ if(whereSatisfies !== undefined){
338
+ if(!whereSatisfies(x, this, {stage: 'Inspecting', initializing: false})) return true;
339
+ }
340
+ returnSet.add(x);
341
+ return false;
342
+ }) as Array<Element>;
343
+ this.#dismount(elsToUnMount);
344
+ }
345
+ this.#mountedList = Array.from(returnSet).map(x => new WeakRef(x));
346
+ return returnSet;
347
+ }
348
+
349
+ async #filterAndMount(els: Array<Element>, checkMatch: boolean, initializing: boolean){
350
+ const {whereSatisfies, whereInstanceOf} = this.#mountInit;
351
+ const match = await this.#selector();
352
+ const elsToMount = els.filter(x => {
353
+ if(checkMatch){
354
+ if(!x.matches(match)) return false;
355
+ }
356
+ if(whereSatisfies !== undefined){
357
+ if(!whereSatisfies(x, this, {stage: 'Inspecting', initializing})) return false;
358
+ }
359
+ if(whereInstanceOf !== undefined){
360
+ if(!this.#confirmInstanceOf(x, whereInstanceOf)) return false;
361
+ }
362
+ return true;
363
+ });
364
+ for(const elToMount of elsToMount){
365
+ if(elToMount.matches(inclTemplQry)){
366
+ await this.#compose(elToMount as HTMLTemplateElement, 0)
367
+ }
368
+ }
369
+ this.#mount(elsToMount, initializing);
370
+ }
371
+
372
+ async #inspectWithin(within: Node, initializing: boolean){
373
+ await this.composeFragment(within as DocumentFragment, 0);
374
+ const els = Array.from((within as Element).querySelectorAll(await this.#selector()));
375
+ this.#filterAndMount(els, false, initializing);
376
+ }
377
+
378
+
379
+
380
+ }
381
+
382
+ const refCountErr = 'mount-observer ref count mismatch';
383
+ export const inclTemplQry = 'template[src^="#"]:not([hidden])';
384
+ export interface MountObserver extends IMountObserver{}
385
+
386
+ // https://github.com/webcomponents-cg/community-protocols/issues/12#issuecomment-872415080
387
+ /**
388
+ * The `mutation-event` event represents something that happened.
389
+ * We can document it here.
390
+ */
391
+ export class MountEvent extends Event implements IMountEvent {
392
+ static eventName: mountEventName = 'mount';
393
+
394
+ constructor(public mountedElement: Element, public initializing: boolean) {
395
+ super(MountEvent.eventName);
396
+ }
397
+ }
398
+
399
+ export class DismountEvent extends Event implements IDismountEvent{
400
+ static eventName: dismountEventName = 'dismount';
401
+
402
+ constructor(public dismountedElement: Element){
403
+ super(DismountEvent.eventName);
404
+ }
405
+ }
406
+
407
+ export class DisconnectEvent extends Event implements IDisconnectEvent{
408
+ static eventName: disconnectedEventName = 'disconnect';
409
+
410
+ constructor(public disconnectedElement: Element){
411
+ super(DisconnectEvent.eventName);
412
+ }
413
+ }
414
+
415
+ export class AttrChangeEvent extends Event implements IAttrChangeEvent{
416
+ static eventName: attrChangeEventName = 'attrChange';
417
+ constructor(public mountedElement: Element, public attrChangeInfos: Array<AttrChangeInfo>){
418
+ super(AttrChangeEvent.eventName);
419
+ }
420
+ }
421
+
422
+
423
+
424
+
425
+ //const hasRootInDefault = ['data', 'enh', 'data-enh']
426
+
package/RootMutObs.ts ADDED
@@ -0,0 +1,40 @@
1
+ import {mutationEventName, AddMutationEventListener} from './ts-refs/mount-observer/types';
2
+
3
+ export class RootMutObs extends EventTarget{
4
+ constructor(rootNode: Node ){
5
+ super();
6
+ this.#mutationObserver = new MutationObserver(mutationRecords => {
7
+ this.dispatchEvent(new MutationEvent(mutationRecords))
8
+ })
9
+ this.#mutationObserver.observe(rootNode, {
10
+ subtree: true,
11
+ childList: true,
12
+ attributes: true,
13
+ attributeOldValue: true,
14
+ });
15
+ }
16
+ #mutationObserver: MutationObserver;
17
+ disconnect(){
18
+ this.#mutationObserver.disconnect();
19
+ }
20
+ }
21
+
22
+
23
+
24
+ // https://github.com/webcomponents-cg/community-protocols/issues/12#issuecomment-872415080
25
+
26
+ /**
27
+ * The `mutation-event` event represents something that happened.
28
+ * We can document it here.
29
+ */
30
+ export class MutationEvent extends Event implements MutationEvent {
31
+ static eventName: mutationEventName = 'mutation-event';
32
+
33
+ constructor(public mutationRecords: Array<MutationRecord>) {
34
+ // Since these are hard-coded, dispatchers can't get them wrong
35
+ super(MutationEvent.eventName);
36
+ }
37
+ }
38
+
39
+
40
+
package/Synthesizer.ts ADDED
@@ -0,0 +1,121 @@
1
+ import {MountInit, MOSE} from './ts-refs/mount-observer/types';
2
+ import {MountObserver} from './MountObserver.js';
3
+
4
+ export abstract class Synthesizer extends HTMLElement{
5
+ #mutationObserver: MutationObserver | undefined;
6
+
7
+ mountObserverElements: Array<MOSE> = [];
8
+
9
+ mutationCallback(mutationList: Array<MutationRecord>){
10
+ for (const mutation of mutationList) {
11
+ const {addedNodes} = mutation;
12
+ for(const node of addedNodes){
13
+ if(!(node instanceof HTMLScriptElement) || node.type !== 'mountobserver') continue;
14
+ const mose = node as MOSE;
15
+ this.mountObserverElements.push(mose);
16
+ this.activate(mose);
17
+ const e = new SynthesizeEvent(mose);
18
+ this.dispatchEvent(e);
19
+ }
20
+
21
+ }
22
+ }
23
+
24
+ connectedCallback(){
25
+ this.hidden = true;
26
+ const init: MutationObserverInit = {
27
+ childList: true
28
+ };
29
+ this.querySelectorAll('script[type="mountobserver"]').forEach(s => {
30
+ const mose = s as MOSE;
31
+ this.mountObserverElements.push(mose);
32
+ this.activate(mose);
33
+ })
34
+ this.#mutationObserver = new MutationObserver(this.mutationCallback.bind(this));
35
+ this.#mutationObserver.observe(this, init);
36
+ this.inherit();
37
+ }
38
+
39
+ checkIfAllowed(mose: MOSE){
40
+ if(this.hasAttribute('passthrough')) return false;
41
+ const {id} = mose;
42
+ if(this.hasAttribute('include')){
43
+ const split = this.getAttribute('include')!.split(' ');
44
+ if(!split.includes(id)) return false;
45
+ }
46
+ if(this.hasAttribute('exclude')){
47
+ const split = this.getAttribute('exclude')!.split(' ');
48
+ if(split.includes(id)) return false;
49
+ }
50
+ return true;
51
+ }
52
+
53
+ activate(mose: MOSE){
54
+ if(!this.checkIfAllowed(mose)) return;
55
+ const {init, do: d} = mose;
56
+ const mi: MountInit = {
57
+ do: d,
58
+ ...init
59
+ };
60
+ const mo = new MountObserver(mi);
61
+ mose.observer = mo;
62
+ mo.observe(this.getRootNode());
63
+ }
64
+
65
+ import(mose: MOSE){
66
+ const {init, do: d, id, synConfig} = mose;
67
+ const se = document.createElement('script') as MOSE;
68
+ se.type='mountobserver';
69
+ se.init = {...init};
70
+ se.id = id;
71
+ se.do = {...d};
72
+ se.synConfig = {...synConfig};
73
+ this.appendChild(se);
74
+ }
75
+
76
+ inherit(){
77
+ const rn = this.getRootNode();
78
+ const host = (<any>rn).host;
79
+ if(!host) return;
80
+ const parentShadowRealm = host.getRootNode();
81
+ const {localName} = this;
82
+ let parentScopeSynthesizer = parentShadowRealm.querySelector(localName) as Synthesizer;
83
+ if(parentScopeSynthesizer === null) {
84
+ parentScopeSynthesizer = document.createElement(localName) as Synthesizer;
85
+ if(parentShadowRealm === document) {
86
+ document.head.appendChild(parentScopeSynthesizer);
87
+ }else{
88
+ parentShadowRealm.appendChild(parentScopeSynthesizer);
89
+ }
90
+ };
91
+ const {mountObserverElements} = parentScopeSynthesizer;
92
+ for(const moe of mountObserverElements){
93
+ this.import(moe);
94
+ }
95
+ parentScopeSynthesizer.addEventListener(SynthesizeEvent.eventName, e => {
96
+ this.import((e as SynthesizeEvent).mountObserverElement)
97
+ })
98
+
99
+ }
100
+ disconnectedCallback(){
101
+ if(this.#mutationObserver !== undefined){
102
+ this.#mutationObserver.disconnect();
103
+ }
104
+ for(const mose of this.mountObserverElements){
105
+ mose.observer.disconnect(this.getRootNode());
106
+ }
107
+ }
108
+ }
109
+
110
+ // https://github.com/webcomponents-cg/community-protocols/issues/12#issuecomment-872415080
111
+ /**
112
+ * The `mutation-event` event represents something that happened.
113
+ * We can document it here.
114
+ */
115
+ export class SynthesizeEvent extends Event{
116
+ static eventName = 'synthesize';
117
+
118
+ constructor(public mountObserverElement: MOSE) {
119
+ super(SynthesizeEvent.eventName);
120
+ }
121
+ }