@mml-io/observable-dom 0.0.42
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/build/index.d.ts +1 -0
- package/build/index.js +601 -0
- package/build/index.js.map +7 -0
- package/package.json +23 -0
- package/src/JSDOMRunner.ts +299 -0
- package/src/ObservableDom.ts +447 -0
- package/src/index.ts +2 -0
- package/src/utils.ts +13 -0
|
@@ -0,0 +1,447 @@
|
|
|
1
|
+
import {
|
|
2
|
+
LogMessage,
|
|
3
|
+
ObservableDomInterface,
|
|
4
|
+
ObservableDomMessage,
|
|
5
|
+
ObservableDOMParameters,
|
|
6
|
+
RemoteEvent,
|
|
7
|
+
StaticVirtualDomElement,
|
|
8
|
+
StaticVirtualDomMutationIdsRecord,
|
|
9
|
+
} from "@mml-io/observable-dom-common";
|
|
10
|
+
|
|
11
|
+
import { virtualDomElementToStatic } from "./utils";
|
|
12
|
+
|
|
13
|
+
export type DOMRunnerMessage = {
|
|
14
|
+
loaded?: boolean;
|
|
15
|
+
mutationList?: Array<MutationRecord>;
|
|
16
|
+
logMessage?: LogMessage;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
export type DOMRunnerInterface = {
|
|
20
|
+
getDocument(): Document;
|
|
21
|
+
getWindow(): Window & {
|
|
22
|
+
CustomEvent: typeof CustomEvent;
|
|
23
|
+
Text: typeof Text;
|
|
24
|
+
HTMLScriptElement: typeof HTMLScriptElement;
|
|
25
|
+
Comment: typeof Comment;
|
|
26
|
+
}; // TODO - Define this without using JSDOM types
|
|
27
|
+
addIPCWebsocket(webSocket: WebSocket): void;
|
|
28
|
+
dispatchRemoteEventFromConnectionId(
|
|
29
|
+
connectionId: number,
|
|
30
|
+
realElement: Element,
|
|
31
|
+
remoteEvent: RemoteEvent,
|
|
32
|
+
): void;
|
|
33
|
+
dispose(): void;
|
|
34
|
+
getDocumentTime(): number;
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
export type DOMRunnerFactory = (
|
|
38
|
+
htmlPath: string,
|
|
39
|
+
htmlContents: string,
|
|
40
|
+
params: object,
|
|
41
|
+
callback: (domRunnerMessage: DOMRunnerMessage) => void,
|
|
42
|
+
) => DOMRunnerInterface;
|
|
43
|
+
|
|
44
|
+
export type LiveVirtualDomElement = Omit<StaticVirtualDomElement, "childNodes"> & {
|
|
45
|
+
realElement: Element | Text;
|
|
46
|
+
childNodes: Array<LiveVirtualDomElement>;
|
|
47
|
+
parent: LiveVirtualDomElement | null;
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
export class ObservableDom implements ObservableDomInterface {
|
|
51
|
+
private nodeToNodeId = new Map<LiveVirtualDomElement, number>();
|
|
52
|
+
private nodeIdToNode = new Map<number, LiveVirtualDomElement>();
|
|
53
|
+
private realElementToVirtualElement = new Map<Element | Text, LiveVirtualDomElement>();
|
|
54
|
+
private ignoreTextNodes = true;
|
|
55
|
+
private callback: (message: ObservableDomMessage) => void;
|
|
56
|
+
private nextNodeId = 1;
|
|
57
|
+
private htmlPath: string;
|
|
58
|
+
private domRunner: DOMRunnerInterface;
|
|
59
|
+
|
|
60
|
+
private documentTimeIntervalTimer: NodeJS.Timer;
|
|
61
|
+
|
|
62
|
+
constructor(
|
|
63
|
+
observableDOMParameters: ObservableDOMParameters,
|
|
64
|
+
callback: (message: ObservableDomMessage) => void,
|
|
65
|
+
runnerFactory: DOMRunnerFactory,
|
|
66
|
+
) {
|
|
67
|
+
this.htmlPath = observableDOMParameters.htmlPath;
|
|
68
|
+
this.ignoreTextNodes = observableDOMParameters.ignoreTextNodes;
|
|
69
|
+
this.callback = callback;
|
|
70
|
+
|
|
71
|
+
this.documentTimeIntervalTimer = setInterval(() => {
|
|
72
|
+
this.callback({
|
|
73
|
+
documentTime: this.getDocumentTime(),
|
|
74
|
+
});
|
|
75
|
+
}, observableDOMParameters.pingIntervalMilliseconds || 5000);
|
|
76
|
+
|
|
77
|
+
this.domRunner = runnerFactory(
|
|
78
|
+
observableDOMParameters.htmlPath,
|
|
79
|
+
observableDOMParameters.htmlContents,
|
|
80
|
+
observableDOMParameters.params,
|
|
81
|
+
(domRunnerMessage: DOMRunnerMessage) => {
|
|
82
|
+
if (domRunnerMessage.loaded) {
|
|
83
|
+
this.createVirtualDomElementWithChildren(
|
|
84
|
+
this.domRunner.getDocument() as unknown as Element,
|
|
85
|
+
null,
|
|
86
|
+
);
|
|
87
|
+
|
|
88
|
+
const snapshot = virtualDomElementToStatic(
|
|
89
|
+
this.getVirtualDomElementForRealElementOrThrow(
|
|
90
|
+
this.domRunner.getDocument() as unknown as Element,
|
|
91
|
+
),
|
|
92
|
+
);
|
|
93
|
+
|
|
94
|
+
this.callback({
|
|
95
|
+
snapshot,
|
|
96
|
+
documentTime: this.getDocumentTime(),
|
|
97
|
+
});
|
|
98
|
+
} else if (domRunnerMessage.mutationList) {
|
|
99
|
+
this.processModificationList(domRunnerMessage.mutationList);
|
|
100
|
+
} else if (domRunnerMessage.logMessage) {
|
|
101
|
+
this.callback({
|
|
102
|
+
logMessage: domRunnerMessage.logMessage,
|
|
103
|
+
documentTime: this.getDocumentTime(),
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
},
|
|
107
|
+
);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
public addIPCWebsocket(webSocket: WebSocket) {
|
|
111
|
+
return this.domRunner.addIPCWebsocket(webSocket);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
public addConnectedUserId(connectionId: number): void {
|
|
115
|
+
this.domRunner.getWindow().dispatchEvent(
|
|
116
|
+
new (this.domRunner.getWindow().CustomEvent)("connected", {
|
|
117
|
+
detail: { connectionId },
|
|
118
|
+
}),
|
|
119
|
+
);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
public removeConnectedUserId(connectionId: number): void {
|
|
123
|
+
this.domRunner.getWindow().dispatchEvent(
|
|
124
|
+
new (this.domRunner.getWindow().CustomEvent)("disconnected", {
|
|
125
|
+
detail: { connectionId },
|
|
126
|
+
}),
|
|
127
|
+
);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
private processModificationList(mutationList: Array<MutationRecord>): void {
|
|
131
|
+
const documentEl = this.domRunner.getDocument() as unknown as Element;
|
|
132
|
+
const documentVirtualDomElement = this.realElementToVirtualElement.get(documentEl);
|
|
133
|
+
if (!documentVirtualDomElement) {
|
|
134
|
+
throw new Error(`document not created in processModificationList`);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
if (mutationList.length > 1) {
|
|
138
|
+
// TODO - walk back through the records to derive the intermediate states (e.g. if an attribute is later added to
|
|
139
|
+
// an element created in an earlier record then it should not have that attribute when the element is added.
|
|
140
|
+
// This is important as incorrect attribute sets can affect visibility and expected client performance.
|
|
141
|
+
console.error(
|
|
142
|
+
"More than one mutation record received. It is possible that intermediate states are incorrect.",
|
|
143
|
+
);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
for (const mutation of mutationList) {
|
|
147
|
+
if (this.isIgnoredElement(mutation.target as Element | Text)) {
|
|
148
|
+
continue;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
if (
|
|
152
|
+
mutation.type === "attributes" &&
|
|
153
|
+
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
154
|
+
this.isIgnoredAttribute(mutation.target as Element | Text, mutation.attributeName!)
|
|
155
|
+
) {
|
|
156
|
+
continue;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
this.addKnownNodesInMutation(mutation);
|
|
160
|
+
|
|
161
|
+
// Convert the "real" DOM MutationRecord into a "virtual" DOM MutationRecord that references the VirtualDOMElements
|
|
162
|
+
// This is done so that the same process for handling mutations can be used for both changes to a live DOM and also
|
|
163
|
+
// to diffs between DOM snapshots when reloading
|
|
164
|
+
const firstNonIgnoredPreviousSibling = mutation.previousSibling
|
|
165
|
+
? this.getFirstNonIgnoredPreviousSibling(mutation.previousSibling as Element | Text)
|
|
166
|
+
: null;
|
|
167
|
+
const targetElement = this.getVirtualDomElementForRealElementOrThrow(
|
|
168
|
+
mutation.target as Element | Text,
|
|
169
|
+
);
|
|
170
|
+
const addedNodes: Array<StaticVirtualDomElement> = [];
|
|
171
|
+
for (const node of mutation.addedNodes) {
|
|
172
|
+
if (this.isIgnoredElement(node as Element | Text)) {
|
|
173
|
+
continue;
|
|
174
|
+
}
|
|
175
|
+
const virtualDomElement = this.getVirtualDomElementForRealElementOrThrow(
|
|
176
|
+
node as Element | Text,
|
|
177
|
+
);
|
|
178
|
+
addedNodes.push(virtualDomElementToStatic(virtualDomElement));
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
const removedNodeIds: Array<number> = [];
|
|
182
|
+
for (const node of mutation.removedNodes) {
|
|
183
|
+
if (this.isIgnoredElement(node as Element | Text)) {
|
|
184
|
+
continue;
|
|
185
|
+
}
|
|
186
|
+
const virtualDomElement = this.getVirtualDomElementForRealElementOrThrow(
|
|
187
|
+
node as Element | Text,
|
|
188
|
+
);
|
|
189
|
+
removedNodeIds.push(virtualDomElement.nodeId);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
const mutationRecord: StaticVirtualDomMutationIdsRecord = {
|
|
193
|
+
type: mutation.type,
|
|
194
|
+
targetId: targetElement.nodeId,
|
|
195
|
+
addedNodes,
|
|
196
|
+
removedNodeIds,
|
|
197
|
+
previousSiblingId:
|
|
198
|
+
firstNonIgnoredPreviousSibling !== null
|
|
199
|
+
? this.getVirtualDomElementForRealElementOrThrow(firstNonIgnoredPreviousSibling).nodeId
|
|
200
|
+
: null,
|
|
201
|
+
attribute: mutation.attributeName
|
|
202
|
+
? {
|
|
203
|
+
attributeName: mutation.attributeName,
|
|
204
|
+
value: (mutation.target as Element).getAttribute(mutation.attributeName),
|
|
205
|
+
}
|
|
206
|
+
: null,
|
|
207
|
+
};
|
|
208
|
+
|
|
209
|
+
this.callback({
|
|
210
|
+
mutation: mutationRecord,
|
|
211
|
+
documentTime: this.getDocumentTime(),
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
this.removeKnownNodesInMutation(mutation);
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
private addKnownNodesInMutation(mutation: MutationRecord): void {
|
|
219
|
+
const targetNode = mutation.target as Element | Text;
|
|
220
|
+
const virtualDomElement = this.realElementToVirtualElement.get(targetNode);
|
|
221
|
+
if (!virtualDomElement) {
|
|
222
|
+
throw new Error(
|
|
223
|
+
"Unknown node in addKnownNodesInMutation:" + targetNode + "," + mutation.type,
|
|
224
|
+
);
|
|
225
|
+
}
|
|
226
|
+
if (mutation.type === "childList") {
|
|
227
|
+
let previousSibling = mutation.previousSibling;
|
|
228
|
+
let index = 0;
|
|
229
|
+
while (previousSibling && this.isIgnoredElement(previousSibling as Element | Text)) {
|
|
230
|
+
previousSibling = previousSibling.previousSibling;
|
|
231
|
+
}
|
|
232
|
+
if (previousSibling) {
|
|
233
|
+
const previousSiblingElement = this.realElementToVirtualElement.get(
|
|
234
|
+
previousSibling as Element | Text,
|
|
235
|
+
);
|
|
236
|
+
if (!previousSiblingElement) {
|
|
237
|
+
throw new Error("Unknown previous sibling");
|
|
238
|
+
}
|
|
239
|
+
index = virtualDomElement.childNodes.indexOf(previousSiblingElement);
|
|
240
|
+
if (index === -1) {
|
|
241
|
+
throw new Error("Previous sibling is not currently a child of the parent element");
|
|
242
|
+
}
|
|
243
|
+
index += 1;
|
|
244
|
+
}
|
|
245
|
+
mutation.addedNodes.forEach((node: Node) => {
|
|
246
|
+
const asElementOrText = node as Element | Text;
|
|
247
|
+
const childVirtualDomElement = this.createVirtualDomElementWithChildren(
|
|
248
|
+
asElementOrText,
|
|
249
|
+
virtualDomElement,
|
|
250
|
+
);
|
|
251
|
+
if (childVirtualDomElement) {
|
|
252
|
+
if (virtualDomElement.childNodes.indexOf(childVirtualDomElement) === -1) {
|
|
253
|
+
virtualDomElement.childNodes.splice(index, 0, childVirtualDomElement);
|
|
254
|
+
index++;
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
});
|
|
258
|
+
} else if (mutation.type === "attributes") {
|
|
259
|
+
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
260
|
+
const attributeName = mutation.attributeName!;
|
|
261
|
+
if (this.isIgnoredAttribute(targetNode, attributeName)) {
|
|
262
|
+
return;
|
|
263
|
+
}
|
|
264
|
+
const attributeValue = (targetNode as Element).getAttribute(attributeName);
|
|
265
|
+
if (attributeValue === null) {
|
|
266
|
+
delete virtualDomElement.attributes[attributeName];
|
|
267
|
+
} else {
|
|
268
|
+
virtualDomElement.attributes[attributeName] = attributeValue;
|
|
269
|
+
}
|
|
270
|
+
} else if (mutation.type === "characterData") {
|
|
271
|
+
virtualDomElement.textContent = targetNode.textContent ? targetNode.textContent : undefined;
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
private removeKnownNodesInMutation(mutation: MutationRecord): void {
|
|
276
|
+
const targetNode = mutation.target as Element | Text;
|
|
277
|
+
const virtualDomElement = this.realElementToVirtualElement.get(targetNode);
|
|
278
|
+
if (!virtualDomElement) {
|
|
279
|
+
throw new Error("Unknown node in mutation list:" + targetNode + ", " + mutation.type);
|
|
280
|
+
}
|
|
281
|
+
if (mutation.type === "childList") {
|
|
282
|
+
for (const node of mutation.removedNodes) {
|
|
283
|
+
const asElementOrText = node as Element | Text;
|
|
284
|
+
if (this.isIgnoredElement(asElementOrText)) {
|
|
285
|
+
continue;
|
|
286
|
+
}
|
|
287
|
+
const childDomElement = this.realElementToVirtualElement.get(asElementOrText);
|
|
288
|
+
if (!childDomElement) {
|
|
289
|
+
console.warn(this.htmlPath, "Unknown node in removeKnownNodesInMutation");
|
|
290
|
+
continue;
|
|
291
|
+
} else {
|
|
292
|
+
this.removeVirtualDomElement(childDomElement);
|
|
293
|
+
const index = virtualDomElement.childNodes.indexOf(childDomElement);
|
|
294
|
+
virtualDomElement.childNodes.splice(index, 1);
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
return;
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
private removeVirtualDomElement(virtualDomElement: LiveVirtualDomElement): void {
|
|
302
|
+
this.nodeIdToNode.delete(virtualDomElement.nodeId);
|
|
303
|
+
this.nodeToNodeId.delete(virtualDomElement);
|
|
304
|
+
this.realElementToVirtualElement.delete(virtualDomElement.realElement);
|
|
305
|
+
for (const child of virtualDomElement.childNodes) {
|
|
306
|
+
this.removeVirtualDomElement(child);
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
private createVirtualDomElementWithChildren(
|
|
311
|
+
node: Element | Text,
|
|
312
|
+
parent: LiveVirtualDomElement | null,
|
|
313
|
+
): LiveVirtualDomElement | null {
|
|
314
|
+
const virtualElement = this.createVirtualDomElement(node, parent);
|
|
315
|
+
if (!virtualElement) {
|
|
316
|
+
return null;
|
|
317
|
+
}
|
|
318
|
+
if ((node as Element).childNodes) {
|
|
319
|
+
for (let i = 0; i < (node as Element).childNodes.length; i++) {
|
|
320
|
+
const child = (node as Element).childNodes[i];
|
|
321
|
+
const childVirtualElement = this.createVirtualDomElementWithChildren(
|
|
322
|
+
child as Element | Text,
|
|
323
|
+
virtualElement,
|
|
324
|
+
);
|
|
325
|
+
if (childVirtualElement) {
|
|
326
|
+
virtualElement.childNodes.push(childVirtualElement);
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
return virtualElement;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
private createVirtualDomElement(
|
|
335
|
+
node: Element | Text,
|
|
336
|
+
parent: LiveVirtualDomElement | null,
|
|
337
|
+
): LiveVirtualDomElement | null {
|
|
338
|
+
if (this.isIgnoredElement(node)) {
|
|
339
|
+
return null;
|
|
340
|
+
}
|
|
341
|
+
const existingValue = this.realElementToVirtualElement.get(node);
|
|
342
|
+
if (existingValue !== undefined) {
|
|
343
|
+
throw new Error("Node already has a virtual element: " + node.nodeName);
|
|
344
|
+
}
|
|
345
|
+
if (!node) {
|
|
346
|
+
throw new Error("Cannot assign node id to null");
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
const attributes: { [key: string]: string } = {};
|
|
350
|
+
if ((node as any).attributes) {
|
|
351
|
+
const asHTMLElement = node as HTMLElement;
|
|
352
|
+
for (const key of asHTMLElement.getAttributeNames()) {
|
|
353
|
+
const value = asHTMLElement.getAttribute(key);
|
|
354
|
+
if (value === null) {
|
|
355
|
+
throw new Error("Null attribute value for key: " + key);
|
|
356
|
+
}
|
|
357
|
+
if (!this.isIgnoredAttribute(node, key)) {
|
|
358
|
+
attributes[key] = value;
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
const nodeId = this.nextNodeId++;
|
|
364
|
+
const virtualElement: LiveVirtualDomElement = {
|
|
365
|
+
nodeId,
|
|
366
|
+
tag: node.nodeName,
|
|
367
|
+
attributes,
|
|
368
|
+
childNodes: [],
|
|
369
|
+
realElement: node,
|
|
370
|
+
parent,
|
|
371
|
+
};
|
|
372
|
+
if (node instanceof this.domRunner.getWindow().Text && node.textContent) {
|
|
373
|
+
virtualElement.textContent = node.textContent;
|
|
374
|
+
}
|
|
375
|
+
this.nodeToNodeId.set(virtualElement, nodeId);
|
|
376
|
+
this.nodeIdToNode.set(nodeId, virtualElement);
|
|
377
|
+
this.realElementToVirtualElement.set(node, virtualElement);
|
|
378
|
+
return virtualElement;
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
private getFirstNonIgnoredPreviousSibling(node: Element | Text): Element | Text | null {
|
|
382
|
+
let currentNode = node;
|
|
383
|
+
if (!this.isIgnoredElement(currentNode)) {
|
|
384
|
+
return currentNode;
|
|
385
|
+
}
|
|
386
|
+
while (currentNode && currentNode.previousSibling) {
|
|
387
|
+
currentNode = currentNode.previousSibling as Element | Text;
|
|
388
|
+
if (!this.isIgnoredElement(currentNode)) {
|
|
389
|
+
return currentNode;
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
return null;
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
private getVirtualDomElementForRealElementOrThrow(
|
|
396
|
+
realElement: Element | Text,
|
|
397
|
+
): LiveVirtualDomElement {
|
|
398
|
+
const virtualElement = this.realElementToVirtualElement.get(realElement);
|
|
399
|
+
if (!virtualElement) {
|
|
400
|
+
throw new Error(`Virtual element not found for real element`);
|
|
401
|
+
}
|
|
402
|
+
return virtualElement;
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
private isIgnoredElement(node: Element | Text): boolean {
|
|
406
|
+
if (this.ignoreTextNodes && node instanceof this.domRunner.getWindow().Text) {
|
|
407
|
+
return true;
|
|
408
|
+
} else if (node instanceof this.domRunner.getWindow().HTMLScriptElement) {
|
|
409
|
+
return true;
|
|
410
|
+
} else if (node instanceof this.domRunner.getWindow().Comment) {
|
|
411
|
+
return true;
|
|
412
|
+
}
|
|
413
|
+
return false;
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
private isIgnoredAttribute(node: Element | Text, attributeName: string): boolean {
|
|
417
|
+
return attributeName.startsWith("on");
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
public dispatchRemoteEventFromConnectionId(connectionId: number, remoteEvent: RemoteEvent): void {
|
|
421
|
+
const domNode = this.nodeIdToNode.get(remoteEvent.nodeId);
|
|
422
|
+
if (!domNode) {
|
|
423
|
+
console.error("Unknown node ID in remote event: " + remoteEvent.nodeId);
|
|
424
|
+
return;
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
if (domNode instanceof this.domRunner.getWindow().Text) {
|
|
428
|
+
console.warn("Cannot dispatch remote event to text node");
|
|
429
|
+
return;
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
this.domRunner.dispatchRemoteEventFromConnectionId(
|
|
433
|
+
connectionId,
|
|
434
|
+
domNode.realElement as Element,
|
|
435
|
+
remoteEvent,
|
|
436
|
+
);
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
public dispose() {
|
|
440
|
+
clearInterval(this.documentTimeIntervalTimer);
|
|
441
|
+
this.domRunner.dispose();
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
private getDocumentTime() {
|
|
445
|
+
return this.domRunner.getDocumentTime();
|
|
446
|
+
}
|
|
447
|
+
}
|
package/src/index.ts
ADDED
package/src/utils.ts
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { StaticVirtualDomElement } from "@mml-io/observable-dom-common";
|
|
2
|
+
|
|
3
|
+
import { LiveVirtualDomElement } from "./ObservableDom";
|
|
4
|
+
|
|
5
|
+
export function virtualDomElementToStatic(el: LiveVirtualDomElement): StaticVirtualDomElement {
|
|
6
|
+
return {
|
|
7
|
+
nodeId: el.nodeId,
|
|
8
|
+
tag: el.tag,
|
|
9
|
+
attributes: el.attributes,
|
|
10
|
+
childNodes: el.childNodes.map((child) => virtualDomElementToStatic(child)),
|
|
11
|
+
textContent: el.textContent,
|
|
12
|
+
};
|
|
13
|
+
}
|