@quenk/wml 2.13.10 → 2.14.0

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.
Files changed (60) hide show
  1. package/lib/cli.d.ts +8 -8
  2. package/lib/cli.js +1 -3
  3. package/lib/cli.js.map +1 -1
  4. package/lib/cli.ts +48 -60
  5. package/lib/compile/codegen.d.ts +4 -9
  6. package/lib/compile/codegen.js +164 -369
  7. package/lib/compile/codegen.js.map +1 -1
  8. package/lib/compile/codegen.ts +644 -952
  9. package/lib/compile/index.d.ts +2 -2
  10. package/lib/compile/index.js +4 -4
  11. package/lib/compile/index.js.map +1 -1
  12. package/lib/compile/index.ts +14 -17
  13. package/lib/compile/transform.d.ts +8 -6
  14. package/lib/compile/transform.js +46 -18
  15. package/lib/compile/transform.js.map +1 -1
  16. package/lib/compile/transform.ts +139 -116
  17. package/lib/{dom.d.ts → dom/index.d.ts} +8 -2
  18. package/lib/{dom.js → dom/index.js} +84 -66
  19. package/lib/dom/index.js.map +1 -0
  20. package/lib/dom/index.ts +425 -0
  21. package/lib/dom/monitor.d.ts +33 -0
  22. package/lib/dom/monitor.js +60 -0
  23. package/lib/dom/monitor.js.map +1 -0
  24. package/lib/dom/monitor.ts +75 -0
  25. package/lib/index.d.ts +10 -95
  26. package/lib/index.js +10 -10
  27. package/lib/index.js.map +1 -1
  28. package/lib/index.ts +57 -182
  29. package/lib/main.js +17 -17
  30. package/lib/main.js.map +1 -1
  31. package/lib/main.ts +38 -44
  32. package/lib/parse/ast.d.ts +12 -6
  33. package/lib/parse/ast.js +68 -58
  34. package/lib/parse/ast.js.map +1 -1
  35. package/lib/parse/ast.ts +400 -482
  36. package/lib/parse/generated.d.ts +3 -5
  37. package/lib/parse/generated.js +9504 -9264
  38. package/lib/parse/index.d.ts +2 -3
  39. package/lib/parse/index.js.map +1 -1
  40. package/lib/parse/index.ts +7 -9
  41. package/lib/parse/test.js +194 -192
  42. package/lib/parse/test.js.map +1 -1
  43. package/lib/parse/test.ts +294 -404
  44. package/lib/parse/wml.y +4 -0
  45. package/lib/tsconfig.json +19 -20
  46. package/lib/util.d.ts +10 -0
  47. package/lib/util.js +21 -0
  48. package/lib/util.js.map +1 -0
  49. package/lib/util.ts +39 -0
  50. package/lib/view/frame.d.ts +103 -0
  51. package/lib/view/frame.js +206 -0
  52. package/lib/view/frame.js.map +1 -0
  53. package/lib/view/frame.ts +249 -0
  54. package/lib/view/index.d.ts +58 -0
  55. package/lib/view/index.js +48 -0
  56. package/lib/view/index.js.map +1 -0
  57. package/lib/view/index.ts +97 -0
  58. package/package.json +4 -3
  59. package/lib/dom.js.map +0 -1
  60. package/lib/dom.ts +0 -475
@@ -0,0 +1,425 @@
1
+ /**
2
+ * This module provides functions used in templates to generate supported DOM
3
+ * nodes.
4
+ *
5
+ * The idea here is to provide an abstraction over DOM construction so
6
+ * we can detect whether we are in a browser or elsewhere and adjust to
7
+ * suite.
8
+ */
9
+
10
+ import { Record, mapTo, forEach } from "@quenk/noni/lib/data/record";
11
+ import { Type, isFunction, isObject } from "@quenk/noni/lib/data/type";
12
+
13
+ const DOCTYPE = "<!DOCTYPE html>";
14
+
15
+ const ATTR_ESC_MAP: { [key: string]: string } = {
16
+ "&": "\\u0026",
17
+
18
+ ">": "\\u003e",
19
+
20
+ "<": "\\u003c",
21
+
22
+ '"': "\\u0022",
23
+
24
+ "=": "\\u003d",
25
+
26
+ "\u005c": "\\u005c",
27
+
28
+ "\u2028": "\\u2028",
29
+
30
+ "\u2029": "\\u2029",
31
+ };
32
+
33
+ const attrsEscRegex = new RegExp(`[${mapTo(ATTR_ESC_MAP, (_, k) => k)}]`, "g");
34
+
35
+ const HTML_ENT_MAP: { [key: string]: string } = {
36
+ '"': "&quot;",
37
+
38
+ "&": "&amp;",
39
+
40
+ "'": "&apos;",
41
+
42
+ "<": "&lt;",
43
+
44
+ ">": "&gt;",
45
+ };
46
+
47
+ const htmlEscRegex = new RegExp(
48
+ `[${mapTo(HTML_ENT_MAP, (_, k) => k).join("")}]`,
49
+ "g",
50
+ );
51
+
52
+ const voidElements = [
53
+ "area",
54
+ "base",
55
+ "br",
56
+ "col",
57
+ "command",
58
+ "embed",
59
+ "hr",
60
+ "img",
61
+ "input",
62
+ "keygen",
63
+ "link",
64
+ "meta",
65
+ "param",
66
+ "source",
67
+ "track",
68
+ "wbr",
69
+ ];
70
+
71
+ /**
72
+ * WMLDOMAttrs is a record of attributes bound for a WMLDOMElement.
73
+ */
74
+ export interface WMLDOMAttrs extends Record<string | number | Function> {}
75
+
76
+ /**
77
+ * WMLNodeList implementation.
78
+ * @private
79
+ */
80
+ export class WMLNodeList implements NodeListOf<WMLDOMNode> {
81
+ entries(): ArrayIterator<[number, WMLDOMNode]> {
82
+ throw new Error("Method not implemented.");
83
+ }
84
+ keys(): ArrayIterator<number> {
85
+ throw new Error("Method not implemented.");
86
+ }
87
+ values(): ArrayIterator<WMLDOMNode> {
88
+ throw new Error("Method not implemented.");
89
+ }
90
+ [Symbol.iterator](): ArrayIterator<WMLDOMNode> {
91
+ throw new Error("Method not implemented.");
92
+ }
93
+ [Symbol.dispose]() {
94
+ throw new Error("Method not implemented.");
95
+ }
96
+
97
+ [index: number]: WMLDOMNode;
98
+
99
+ length = 0;
100
+
101
+ item() {
102
+ return <WMLDOMNode>(<Type>null);
103
+ }
104
+
105
+ forEach() {}
106
+ }
107
+
108
+ /**
109
+ * WMLDOMNode implements the properties and methods of the DOM Node interface to
110
+ * allow a fake DOM to be built in server side code.
111
+ *
112
+ * Most of the methods and properties cannot be relied on and should not be
113
+ * used.
114
+ */
115
+ export class WMLDOMNode implements Node {
116
+ constructor(
117
+ public nodeName: string,
118
+ public nodeType: number,
119
+ ) {}
120
+
121
+ readonly ATTRIBUTE_NODE = 2;
122
+
123
+ readonly CDATA_SECTION_NODE = 4;
124
+
125
+ readonly COMMENT_NODE = 8;
126
+
127
+ readonly DOCUMENT_FRAGMENT_NODE = 11;
128
+
129
+ readonly DOCUMENT_NODE = 9;
130
+
131
+ readonly DOCUMENT_POSITION_CONTAINED_BY = 16;
132
+
133
+ readonly DOCUMENT_POSITION_CONTAINS = 8;
134
+
135
+ readonly DOCUMENT_POSITION_DISCONNECTED = 1;
136
+
137
+ readonly DOCUMENT_POSITION_FOLLOWING = 4;
138
+
139
+ readonly DOCUMENT_POSITION_IMPLEMENTATION_SPECIFIC = 32;
140
+
141
+ readonly DOCUMENT_POSITION_PRECEDING = 2;
142
+
143
+ readonly DOCUMENT_TYPE_NODE = 10;
144
+
145
+ readonly ELEMENT_NODE = 1;
146
+
147
+ readonly ENTITY_NODE = 6;
148
+
149
+ readonly ENTITY_REFERENCE_NODE = 5;
150
+
151
+ readonly NOTATION_NODE = 12;
152
+
153
+ readonly PROCESSING_INSTRUCTION_NODE = 7;
154
+
155
+ readonly TEXT_NODE = 3;
156
+
157
+ baseURI = "";
158
+
159
+ childNodes = new WMLNodeList();
160
+
161
+ firstChild = null;
162
+
163
+ isConnected = false;
164
+
165
+ lastChild = null;
166
+
167
+ namespaceURI = null;
168
+
169
+ nextSibling = null;
170
+
171
+ nodeValue = null;
172
+
173
+ ownerDocument = null;
174
+
175
+ parentElement = null;
176
+
177
+ parentNode = null;
178
+
179
+ previousSibling = null;
180
+
181
+ get textContent() {
182
+ return "";
183
+ }
184
+
185
+ [Symbol.dispose]() {}
186
+
187
+ addEventListener() {}
188
+
189
+ dispatchEvent() {
190
+ return false;
191
+ }
192
+
193
+ removeEventListener() {}
194
+
195
+ appendChild<T extends Node>(newChild: T): T {
196
+ return newChild;
197
+ }
198
+
199
+ cloneNode() {
200
+ return this;
201
+ }
202
+
203
+ compareDocumentPosition() {
204
+ return 0;
205
+ }
206
+
207
+ contains() {
208
+ return false;
209
+ }
210
+
211
+ getRootNode(): Node {
212
+ return this;
213
+ }
214
+
215
+ hasChildNodes() {
216
+ return false;
217
+ }
218
+
219
+ insertBefore<T extends Node>(newChild: T): T {
220
+ return newChild;
221
+ }
222
+
223
+ isDefaultNamespace() {
224
+ return false;
225
+ }
226
+
227
+ isEqualNode() {
228
+ return false;
229
+ }
230
+
231
+ isSameNode() {
232
+ return false;
233
+ }
234
+
235
+ lookupNamespaceURI() {
236
+ return null;
237
+ }
238
+
239
+ lookupPrefix() {
240
+ return null;
241
+ }
242
+
243
+ normalize() {}
244
+
245
+ remove() {}
246
+
247
+ before() {}
248
+
249
+ after() {}
250
+
251
+ replaceWith() {}
252
+
253
+ removeChild<T extends Node>(oldChild: T): T {
254
+ return oldChild;
255
+ }
256
+
257
+ replaceChild<T extends Node>(_: Node, oldChild: T): T {
258
+ return oldChild;
259
+ }
260
+ }
261
+
262
+ /**
263
+ * WMLDOMText is used to represent Text nodes on the server side.
264
+ * @private
265
+ */
266
+ export class WMLDOMText extends WMLDOMNode {
267
+ constructor(
268
+ public value: string,
269
+ public escape = true,
270
+ ) {
271
+ super("#text", -1);
272
+ }
273
+
274
+ get textContent() {
275
+ return this.escape ? escapeHTML(this.value) : this.value;
276
+ }
277
+ }
278
+
279
+ /**
280
+ * WMLDOMElement is used to represent Element nodes on the server side.
281
+ * @private
282
+ */
283
+ export class WMLDOMElement extends WMLDOMNode {
284
+ constructor(
285
+ public tag: string,
286
+ public attrs: WMLDOMAttrs,
287
+ public children: Node[] = [],
288
+ ) {
289
+ super(tag, -1);
290
+ }
291
+
292
+ get innerHTML(): string {
293
+ return this.children
294
+ .map((c) =>
295
+ c.nodeName === "#text"
296
+ ? (<Text>c).textContent
297
+ : (<HTMLElement>c).outerHTML,
298
+ )
299
+ .join("");
300
+ }
301
+
302
+ get outerHTML() {
303
+ let { tag } = this;
304
+ let content = this.innerHTML;
305
+
306
+ let attrs = mapTo(this.attrs, (value, name) => {
307
+ if (isObject(value) && value instanceof WMLDOMText)
308
+ return `${name}="${value.textContent}"`;
309
+ else if (isFunction(value) || isObject(value) || value == null) return "";
310
+ else return `${name}="${escapeAttrValue(String(value))}"`;
311
+ }).join(" ");
312
+
313
+ attrs = attrs.trim() != "" ? ` ${attrs}` : "";
314
+
315
+ let open = `<${tag}${attrs}>`;
316
+
317
+ if (tag === "html") open = `${DOCTYPE}${open}`;
318
+
319
+ return voidElements.indexOf(tag) > -1 ? open : `${open}${content}</${tag}>`;
320
+ }
321
+
322
+ setAttribute(key: string, value: Type) {
323
+ this.attrs[key] = value;
324
+ }
325
+
326
+ appendChild<T extends Node>(newChild: T): T {
327
+ this.children.push(newChild);
328
+ return newChild;
329
+ }
330
+ }
331
+
332
+ /**
333
+ * isBrowser is set to true if we detect a window and document global variable.
334
+ */
335
+ export const isBrowser =
336
+ typeof window !== "undefined" && typeof window.document !== "undefined";
337
+
338
+ /**
339
+ * escapeAttrValue for safe browser display.
340
+ */
341
+ export const escapeAttrValue = (value: string) =>
342
+ value.replace(attrsEscRegex, (hit) => ATTR_ESC_MAP[hit]);
343
+
344
+ /**
345
+ * escapeHTML for safe browser display.
346
+ */
347
+ export const escapeHTML = (value: string) =>
348
+ value.replace(htmlEscRegex, (hit) => HTML_ENT_MAP[hit]);
349
+
350
+ /**
351
+ * createTextNode wrapper.
352
+ */
353
+ export const createTextNode = (txt: Type): Node => {
354
+ let str = String(txt == null ? "" : txt);
355
+ return isBrowser ? document.createTextNode(str) : new WMLDOMText(str);
356
+ };
357
+
358
+ /**
359
+ * createUnsafeNode allows raw strings to be output without escaping.
360
+ *
361
+ * THIS MUST ONLY BE USED IF YOU ARE 100% SURE THE STRING IS SAFE TO OUTPUT!
362
+ */
363
+ export const createUnsafeNode = (txt: Type): Node => {
364
+ let str = String(txt == null ? "" : txt);
365
+ return isBrowser ? createBrowserUnsafeNode(str) : new WMLDOMText(str, false);
366
+ };
367
+
368
+ const createBrowserUnsafeNode = (html: string) => {
369
+ let tmpl = document.createElement("template");
370
+ tmpl.innerHTML = html;
371
+ return tmpl.content;
372
+ };
373
+
374
+ export { createTextNode as text, createUnsafeNode as unsafe };
375
+
376
+ const namespaces = new Map([["svg", "http://www.w3.org/2000/svg"]]);
377
+
378
+ /**
379
+ * createElement wrapper.
380
+ */
381
+ export const createElement = (
382
+ tag: string,
383
+ attrs: WMLDOMAttrs = {},
384
+ children: Node[] = [],
385
+ ns = "",
386
+ ): Element => {
387
+ if (!isBrowser) {
388
+ // XXX: The whole Element interface is not implemented but on server
389
+ // side we are only interested in setAttribute.
390
+ return <Element>(<Type>new WMLDOMElement(tag, attrs, children));
391
+ } else {
392
+ let e = ns
393
+ ? document.createElementNS(namespaces.get(ns) ?? "", tag)
394
+ : document.createElement(tag);
395
+
396
+ forEach(attrs, (value: Type, key) => {
397
+ if (typeof value === "function") {
398
+ (<Type>e)[key] = value;
399
+ } else if (typeof value === "string") {
400
+ // prevent setting things like disabled=""
401
+ if (value !== "") e.setAttribute(key, value);
402
+ } else if (typeof value === "boolean") {
403
+ e.setAttribute(key, "");
404
+ } else if (!isBrowser && value instanceof WMLDOMText) {
405
+ e.setAttribute(key, <Type>value);
406
+ }
407
+ });
408
+
409
+ children.forEach((c) => {
410
+ switch (typeof c) {
411
+ case "string":
412
+ case "number":
413
+ case "boolean":
414
+ e.appendChild(<Node>document.createTextNode("" + c));
415
+ case "object":
416
+ e.appendChild(<Node>c);
417
+ break;
418
+ default:
419
+ throw new TypeError(`Can not adopt child ${c} of type ${typeof c}`);
420
+ }
421
+ });
422
+
423
+ return e;
424
+ }
425
+ };
@@ -0,0 +1,33 @@
1
+ import { Content } from "..";
2
+ export type Handler = () => void;
3
+ /**
4
+ * DOMEventCallbacks is an object interested in receiving the lifecycle
5
+ * events of a DOM tree.
6
+ */
7
+ export interface DOMEventCallbacks {
8
+ onDOMConnected?: () => void;
9
+ onDOMDisconnected?: () => void;
10
+ }
11
+ /**
12
+ * DOMMonitor is used to allow WML components to execute code when added or
13
+ * removed from DOM.
14
+ *
15
+ * This constructor is not used directly, instead components must use the
16
+ * getInstance() static method.
17
+ */
18
+ export declare class DOMMonitor {
19
+ connectHandlers: WeakMap<Content, Handler>;
20
+ disconnectHandlers: WeakMap<Content, Handler>;
21
+ constructor(connectHandlers?: WeakMap<Content, Handler>, disconnectHandlers?: WeakMap<Content, Handler>);
22
+ _observer: MutationObserver;
23
+ static getInstance(): DOMMonitor;
24
+ _checkAndDispatch(handlers: WeakMap<Content, Handler>, dom: NodeList): void;
25
+ init(): void;
26
+ /**
27
+ * monitor a DOM tree for lifecylce changes.
28
+ *
29
+ * If the specified handler does not declare any of the required methods
30
+ * no monitoring will be configured for the DOM node.
31
+ */
32
+ monitor(dom: Content, handler: DOMEventCallbacks): void;
33
+ }
@@ -0,0 +1,60 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.DOMMonitor = void 0;
4
+ let monitor;
5
+ /**
6
+ * DOMMonitor is used to allow WML components to execute code when added or
7
+ * removed from DOM.
8
+ *
9
+ * This constructor is not used directly, instead components must use the
10
+ * getInstance() static method.
11
+ */
12
+ class DOMMonitor {
13
+ constructor(connectHandlers = new WeakMap(), disconnectHandlers = new WeakMap()) {
14
+ this.connectHandlers = connectHandlers;
15
+ this.disconnectHandlers = disconnectHandlers;
16
+ this._observer = new MutationObserver((list) => {
17
+ for (let mutation of list) {
18
+ (this._checkAndDispatch(this.connectHandlers, mutation.addedNodes),
19
+ this._checkAndDispatch(this.disconnectHandlers, mutation.removedNodes));
20
+ }
21
+ });
22
+ }
23
+ static getInstance() {
24
+ if (!monitor) {
25
+ monitor = new DOMMonitor();
26
+ monitor.init();
27
+ }
28
+ return monitor;
29
+ }
30
+ _checkAndDispatch(handlers, dom) {
31
+ let stack = [...dom];
32
+ while (stack.length) {
33
+ let next = stack.pop();
34
+ if (!next.children)
35
+ continue;
36
+ stack.push(...next.children);
37
+ handlers.get(next)?.();
38
+ }
39
+ }
40
+ init() {
41
+ this._observer.observe(document.body, {
42
+ childList: true,
43
+ subtree: true,
44
+ });
45
+ }
46
+ /**
47
+ * monitor a DOM tree for lifecylce changes.
48
+ *
49
+ * If the specified handler does not declare any of the required methods
50
+ * no monitoring will be configured for the DOM node.
51
+ */
52
+ monitor(dom, handler) {
53
+ if (handler.onDOMConnected)
54
+ this.connectHandlers.set(dom, () => handler.onDOMConnected?.());
55
+ if (handler.onDOMDisconnected)
56
+ this.disconnectHandlers.set(dom, () => handler.onDOMDisconnected?.());
57
+ }
58
+ }
59
+ exports.DOMMonitor = DOMMonitor;
60
+ //# sourceMappingURL=monitor.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"monitor.js","sourceRoot":"","sources":["monitor.ts"],"names":[],"mappings":";;;AAcA,IAAI,OAAmB,CAAC;AAExB;;;;;;GAMG;AACH,MAAa,UAAU;IACrB,YACS,kBAAkB,IAAI,OAAO,EAAoB,EACjD,qBAAqB,IAAI,OAAO,EAAoB;QADpD,oBAAe,GAAf,eAAe,CAAkC;QACjD,uBAAkB,GAAlB,kBAAkB,CAAkC;QAG7D,cAAS,GAAqB,IAAI,gBAAgB,CAAC,CAAC,IAAI,EAAE,EAAE;YAC1D,KAAK,IAAI,QAAQ,IAAI,IAAI,EAAE,CAAC;gBAC1B,CAAC,IAAI,CAAC,iBAAiB,CAAC,IAAI,CAAC,eAAe,EAAE,QAAQ,CAAC,UAAU,CAAC;oBAChE,IAAI,CAAC,iBAAiB,CAAC,IAAI,CAAC,kBAAkB,EAAE,QAAQ,CAAC,YAAY,CAAC,CAAC,CAAC;YAC5E,CAAC;QACH,CAAC,CAAC,CAAC;IAPA,CAAC;IASJ,MAAM,CAAC,WAAW;QAChB,IAAI,CAAC,OAAO,EAAE,CAAC;YACb,OAAO,GAAG,IAAI,UAAU,EAAE,CAAC;YAC3B,OAAO,CAAC,IAAI,EAAE,CAAC;QACjB,CAAC;QACD,OAAO,OAAO,CAAC;IACjB,CAAC;IAED,iBAAiB,CAAC,QAAmC,EAAE,GAAa;QAClE,IAAI,KAAK,GAAG,CAAC,GAAG,GAAG,CAAC,CAAC;QACrB,OAAO,KAAK,CAAC,MAAM,EAAE,CAAC;YACpB,IAAI,IAAI,GAAY,KAAK,CAAC,GAAG,EAAE,CAAC;YAChC,IAAI,CAAC,IAAI,CAAC,QAAQ;gBAAE,SAAS;YAC7B,KAAK,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC,QAAQ,CAAC,CAAC;YAC7B,QAAQ,CAAC,GAAG,CAAC,IAAI,CAAC,EAAE,EAAE,CAAC;QACzB,CAAC;IACH,CAAC;IAED,IAAI;QACF,IAAI,CAAC,SAAS,CAAC,OAAO,CAAC,QAAQ,CAAC,IAAI,EAAE;YACpC,SAAS,EAAE,IAAI;YACf,OAAO,EAAE,IAAI;SACd,CAAC,CAAC;IACL,CAAC;IAED;;;;;OAKG;IACH,OAAO,CAAC,GAAY,EAAE,OAA0B;QAC9C,IAAI,OAAO,CAAC,cAAc;YACxB,IAAI,CAAC,eAAe,CAAC,GAAG,CAAC,GAAG,EAAE,GAAG,EAAE,CAAC,OAAO,CAAC,cAAc,EAAE,EAAE,CAAC,CAAC;QAElE,IAAI,OAAO,CAAC,iBAAiB;YAC3B,IAAI,CAAC,kBAAkB,CAAC,GAAG,CAAC,GAAG,EAAE,GAAG,EAAE,CAAC,OAAO,CAAC,iBAAiB,EAAE,EAAE,CAAC,CAAC;IAC1E,CAAC;CACF;AAnDD,gCAmDC"}
@@ -0,0 +1,75 @@
1
+ import { Content } from "..";
2
+
3
+ export type Handler = () => void;
4
+
5
+ /**
6
+ * DOMEventCallbacks is an object interested in receiving the lifecycle
7
+ * events of a DOM tree.
8
+ */
9
+ export interface DOMEventCallbacks {
10
+ onDOMConnected?: () => void;
11
+
12
+ onDOMDisconnected?: () => void;
13
+ }
14
+
15
+ let monitor: DOMMonitor;
16
+
17
+ /**
18
+ * DOMMonitor is used to allow WML components to execute code when added or
19
+ * removed from DOM.
20
+ *
21
+ * This constructor is not used directly, instead components must use the
22
+ * getInstance() static method.
23
+ */
24
+ export class DOMMonitor {
25
+ constructor(
26
+ public connectHandlers = new WeakMap<Content, Handler>(),
27
+ public disconnectHandlers = new WeakMap<Content, Handler>(),
28
+ ) {}
29
+
30
+ _observer: MutationObserver = new MutationObserver((list) => {
31
+ for (let mutation of list) {
32
+ (this._checkAndDispatch(this.connectHandlers, mutation.addedNodes),
33
+ this._checkAndDispatch(this.disconnectHandlers, mutation.removedNodes));
34
+ }
35
+ });
36
+
37
+ static getInstance() {
38
+ if (!monitor) {
39
+ monitor = new DOMMonitor();
40
+ monitor.init();
41
+ }
42
+ return monitor;
43
+ }
44
+
45
+ _checkAndDispatch(handlers: WeakMap<Content, Handler>, dom: NodeList) {
46
+ let stack = [...dom];
47
+ while (stack.length) {
48
+ let next = <Element>stack.pop();
49
+ if (!next.children) continue;
50
+ stack.push(...next.children);
51
+ handlers.get(next)?.();
52
+ }
53
+ }
54
+
55
+ init() {
56
+ this._observer.observe(document.body, {
57
+ childList: true,
58
+ subtree: true,
59
+ });
60
+ }
61
+
62
+ /**
63
+ * monitor a DOM tree for lifecylce changes.
64
+ *
65
+ * If the specified handler does not declare any of the required methods
66
+ * no monitoring will be configured for the DOM node.
67
+ */
68
+ monitor(dom: Content, handler: DOMEventCallbacks) {
69
+ if (handler.onDOMConnected)
70
+ this.connectHandlers.set(dom, () => handler.onDOMConnected?.());
71
+
72
+ if (handler.onDOMDisconnected)
73
+ this.disconnectHandlers.set(dom, () => handler.onDOMDisconnected?.());
74
+ }
75
+ }