mount-observer 0.0.31 → 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.
- package/MountObserver.ts +426 -0
- package/RootMutObs.ts +40 -0
- package/Synthesizer.ts +121 -0
- package/compose.js +26 -14
- package/compose.ts +118 -0
- package/getWhereAttrSelector.ts +92 -0
- package/package.json +4 -3
- package/playwright.config.ts +29 -0
- package/ts-refs/LICENSE +21 -0
- package/ts-refs/README.md +18 -0
- package/ts-refs/be-enhanced/types.d.ts +31 -0
- package/ts-refs/be-value-added/types.d.ts +34 -0
- package/ts-refs/trans-render/types.d.ts +503 -0
- /package/{types.d.ts → ts-refs/mount-observer/types.d.ts} +0 -0
package/MountObserver.ts
ADDED
|
@@ -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
|
+
}
|
package/compose.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { inclTemplQry } from './MountObserver.js';
|
|
2
|
-
export const
|
|
2
|
+
export const childRefsKey = Symbol.for('Wr0WPVh84k+O93miuENdMA');
|
|
3
|
+
export const cloneKey = Symbol.for('LD97VKZYc02CQv23DT/6fQ');
|
|
3
4
|
export async function compose(self, el, level) {
|
|
4
5
|
const src = el.getAttribute('src');
|
|
5
6
|
el.removeAttribute('src');
|
|
@@ -69,25 +70,36 @@ export async function compose(self, el, level) {
|
|
|
69
70
|
el.dispatchEvent(new LoadEvent(clone));
|
|
70
71
|
}
|
|
71
72
|
if (level === 0) {
|
|
72
|
-
const
|
|
73
|
+
const refs = [];
|
|
73
74
|
for (const child of clone.children) {
|
|
74
|
-
|
|
75
|
+
refs.push(new WeakRef(child));
|
|
75
76
|
}
|
|
76
|
-
el[
|
|
77
|
+
el[childRefsKey] = refs;
|
|
77
78
|
}
|
|
78
|
-
if
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
parent.shadowRoot?.append(clone);
|
|
79
|
+
//if template has itemscope attribute, assume want to do some data binding before instantiating into
|
|
80
|
+
//DOM fragment.
|
|
81
|
+
let cloneStashed = false;
|
|
82
|
+
if (el.hasAttribute('itemscope')) {
|
|
83
|
+
el[cloneKey] = clone;
|
|
84
|
+
cloneStashed = true;
|
|
85
85
|
}
|
|
86
86
|
else {
|
|
87
|
-
|
|
87
|
+
if (shadowRootModeOnLoad !== null) {
|
|
88
|
+
const parent = el.parentElement;
|
|
89
|
+
if (parent === null)
|
|
90
|
+
throw 404;
|
|
91
|
+
if (parent.shadowRoot === null)
|
|
92
|
+
parent.attachShadow({ mode: shadowRootModeOnLoad });
|
|
93
|
+
parent.shadowRoot?.append(clone);
|
|
94
|
+
}
|
|
95
|
+
else {
|
|
96
|
+
el.after(clone);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
if (!cloneStashed) {
|
|
100
|
+
if (level !== 0 || (slots.length === 0 && el.attributes.length === 0))
|
|
101
|
+
el.remove();
|
|
88
102
|
}
|
|
89
|
-
if (level !== 0 || (slots.length === 0 && !el.hasAttribute('itemscope')))
|
|
90
|
-
el.remove();
|
|
91
103
|
}
|
|
92
104
|
export class LoadEvent extends Event {
|
|
93
105
|
clone;
|