web-toolkit-x 0.0.1 → 0.71.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.
- package/AUTHORS +3 -0
- package/LICENSE +1 -1
- package/NOTICE +7 -0
- package/README.md +169 -6
- package/dist/release/global.d.ts +10 -0
- package/dist/release/index.d.ts +650 -0
- package/dist/release/index.d.ts.map +1 -0
- package/dist/release/index.js +2837 -0
- package/dist/release/index.js.map +1 -0
- package/dist/release/toolkit-0.71.0.js +2840 -0
- package/dist/release/toolkit-0.71.0.js.map +1 -0
- package/package.json +75 -18
- package/src/core/bragi.ts +122 -0
- package/src/core/browser.ts +555 -0
- package/src/core/child-nodes.ts +77 -0
- package/src/core/comment.ts +24 -0
- package/src/core/component.ts +190 -0
- package/src/core/created-roots.ts +38 -0
- package/src/core/custom-element.ts +191 -0
- package/src/core/description.ts +241 -0
- package/src/core/diff.ts +590 -0
- package/src/core/dispatcher.ts +221 -0
- package/src/core/dom.ts +124 -0
- package/src/core/draft.ts +609 -0
- package/src/core/lifecycle.ts +301 -0
- package/src/core/nodes.ts +13 -0
- package/src/core/patch.ts +412 -0
- package/src/core/plugins.ts +231 -0
- package/src/core/reconciler.ts +185 -0
- package/src/core/renderer.ts +75 -0
- package/src/core/runtime.ts +36 -0
- package/src/core/sandbox.ts +128 -0
- package/src/core/service.ts +39 -0
- package/src/core/template.ts +472 -0
- package/src/core/text.ts +24 -0
- package/src/core/toolkit.ts +220 -0
- package/src/core/utils.ts +148 -0
- package/src/core/virtual-dom.ts +118 -0
- package/src/core/virtual-element.ts +72 -0
- package/src/core/virtual-node.ts +87 -0
- package/src/core/web-component.ts +285 -0
- package/src/global.d.ts +10 -0
- package/src/index.ts +62 -0
- package/src/plugins/logger.ts +38 -0
- package/src/release.ts +3 -0
- package/index.js +0 -1
package/src/core/diff.ts
ADDED
|
@@ -0,0 +1,590 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
ComponentDescription,
|
|
3
|
+
ElementDescription,
|
|
4
|
+
Listener,
|
|
5
|
+
NodeDescription,
|
|
6
|
+
} from './description.js'
|
|
7
|
+
import CreatedRoots from './created-roots.js'
|
|
8
|
+
import Lifecycle, { throwErrors } from './lifecycle.js'
|
|
9
|
+
import type {
|
|
10
|
+
Component,
|
|
11
|
+
ParentVirtualNode,
|
|
12
|
+
VirtualElement,
|
|
13
|
+
VirtualNode,
|
|
14
|
+
WebComponent,
|
|
15
|
+
} from './nodes.js'
|
|
16
|
+
import Patch from './patch.js'
|
|
17
|
+
import Reconciler from './reconciler.js'
|
|
18
|
+
import type { State } from './dispatcher.js'
|
|
19
|
+
import Template from './template.js'
|
|
20
|
+
import { runtime } from './runtime.js'
|
|
21
|
+
import VirtualDOM from './virtual-dom.js'
|
|
22
|
+
|
|
23
|
+
/*
|
|
24
|
+
* Returns the keys added to, removed from and changed in the next object,
|
|
25
|
+
* in the order of the objects.
|
|
26
|
+
*/
|
|
27
|
+
const compareKeys = <T>(
|
|
28
|
+
current: Record<string, T>,
|
|
29
|
+
next: Record<string, T>,
|
|
30
|
+
isEqual: (value: T, nextValue: T) => boolean = (value, nextValue) =>
|
|
31
|
+
value === nextValue,
|
|
32
|
+
) => {
|
|
33
|
+
const keys = Object.keys(current)
|
|
34
|
+
const nextKeys = Object.keys(next)
|
|
35
|
+
return {
|
|
36
|
+
added: nextKeys.filter(key => !keys.includes(key)),
|
|
37
|
+
removed: keys.filter(key => !nextKeys.includes(key)),
|
|
38
|
+
changed: keys.filter(
|
|
39
|
+
key => nextKeys.includes(key) && !isEqual(current[key]!, next[key]!),
|
|
40
|
+
),
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
class Diff {
|
|
45
|
+
declare root: WebComponent
|
|
46
|
+
declare patches: Patch[]
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Creates a new instance bound to a root component
|
|
50
|
+
* with an empty list of patches.
|
|
51
|
+
*/
|
|
52
|
+
constructor(
|
|
53
|
+
root: WebComponent,
|
|
54
|
+
currentState: State | undefined,
|
|
55
|
+
nextState: State,
|
|
56
|
+
) {
|
|
57
|
+
this.root = root
|
|
58
|
+
this.patches = []
|
|
59
|
+
// nothing is applied when the calculation fails, e.g. when rendering
|
|
60
|
+
CreatedRoots.destroyingRootsOnError(() =>
|
|
61
|
+
this.calculate(currentState, nextState),
|
|
62
|
+
)
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Adds the patch to the underlying list.
|
|
67
|
+
*/
|
|
68
|
+
addPatch(patch: Patch) {
|
|
69
|
+
return this.patches.push(patch)
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Applies all the patches onto the bound root node.
|
|
74
|
+
*/
|
|
75
|
+
apply(): Patch[] {
|
|
76
|
+
if (this.patches.length) {
|
|
77
|
+
// the update is completed when hooks or patches throw, as stopping
|
|
78
|
+
// would leave the DOM, the virtual nodes and the hooks called out of
|
|
79
|
+
// sync, and the errors are thrown once it is applied
|
|
80
|
+
const errors: unknown[] = []
|
|
81
|
+
Lifecycle.collectingErrors(errors, () =>
|
|
82
|
+
Lifecycle.beforeUpdate(this.patches),
|
|
83
|
+
)
|
|
84
|
+
// a nested root failing to render its props keeps its description,
|
|
85
|
+
// to render them on the next update
|
|
86
|
+
const failedRoots = new Set<VirtualNode>()
|
|
87
|
+
const applied: Patch[] = []
|
|
88
|
+
for (const patch of this.patches) {
|
|
89
|
+
if (
|
|
90
|
+
patch.type === Patch.Type.UPDATE_NODE &&
|
|
91
|
+
failedRoots.has(patch.node)
|
|
92
|
+
) {
|
|
93
|
+
continue
|
|
94
|
+
}
|
|
95
|
+
try {
|
|
96
|
+
Patch.apply(patch)
|
|
97
|
+
} catch (error) {
|
|
98
|
+
errors.push(error)
|
|
99
|
+
if (patch.type === Patch.Type.UPDATE_ROOT) {
|
|
100
|
+
failedRoots.add(patch.root)
|
|
101
|
+
}
|
|
102
|
+
continue
|
|
103
|
+
}
|
|
104
|
+
applied.push(patch)
|
|
105
|
+
}
|
|
106
|
+
Lifecycle.collectingErrors(errors, () => Lifecycle.afterUpdate(applied))
|
|
107
|
+
throwErrors(errors, 'Errors applying the update')
|
|
108
|
+
}
|
|
109
|
+
return this.patches
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Calculates and returns all patches needed for transformation
|
|
114
|
+
* of the rendered DOM fragment from one state to another.
|
|
115
|
+
*/
|
|
116
|
+
calculate(currentState: State | undefined, nextState: State) {
|
|
117
|
+
if (!currentState) {
|
|
118
|
+
this.addPatch(Patch.initRootComponent(this.root))
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
if (Diff.deepEqual(currentState, nextState)) {
|
|
122
|
+
return
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const template: unknown[] = [this.root.constructor, nextState]
|
|
126
|
+
if (this.root.description.children) {
|
|
127
|
+
template.push(
|
|
128
|
+
...this.root.description.children.map(child => child.asTemplate),
|
|
129
|
+
)
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const description = Template.describe(template) as ComponentDescription
|
|
133
|
+
|
|
134
|
+
// the root is rendered initially even when the description is equal
|
|
135
|
+
if (
|
|
136
|
+
!this.root.isInitialized ||
|
|
137
|
+
!Diff.deepEqual(this.root.description, description)
|
|
138
|
+
) {
|
|
139
|
+
this.componentPatches(this.root, description)
|
|
140
|
+
}
|
|
141
|
+
if (this.root.description.attrs || description.attrs) {
|
|
142
|
+
this.attributePatches(
|
|
143
|
+
this.root.description.attrs,
|
|
144
|
+
description.attrs,
|
|
145
|
+
this.root,
|
|
146
|
+
true,
|
|
147
|
+
)
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Renders the descendants with normalized props and children passed
|
|
153
|
+
* from the parent component.
|
|
154
|
+
*
|
|
155
|
+
* Calculates the patches needed for transformation of a component
|
|
156
|
+
* to match the given description.
|
|
157
|
+
*/
|
|
158
|
+
componentPatches(component: Component, description: ComponentDescription) {
|
|
159
|
+
const nodeDescription = component.renderDescription(
|
|
160
|
+
description.props,
|
|
161
|
+
description.childrenAsTemplates,
|
|
162
|
+
)
|
|
163
|
+
this.componentContentPatches(nodeDescription, component)
|
|
164
|
+
|
|
165
|
+
this.addPatch(Patch.updateNode(component, description))
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
componentContentPatches(
|
|
169
|
+
description: NodeDescription | null,
|
|
170
|
+
parent: Component,
|
|
171
|
+
) {
|
|
172
|
+
const content = parent.content
|
|
173
|
+
|
|
174
|
+
if (!content && !description) {
|
|
175
|
+
return
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// content and description are either both present or both missing
|
|
179
|
+
if (!content || !description) {
|
|
180
|
+
throw new Error('Invalid component state!')
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// update
|
|
184
|
+
if (content.description.isCompatible(description)) {
|
|
185
|
+
this.childPatches(content, description)
|
|
186
|
+
return
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// replace
|
|
190
|
+
const node = VirtualDOM.createFromDescription(
|
|
191
|
+
description,
|
|
192
|
+
parent,
|
|
193
|
+
this.root,
|
|
194
|
+
)
|
|
195
|
+
this.addPatch(Patch.setContent(node!, parent))
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* Calculates patches for transformation of specified child node
|
|
200
|
+
* to match given description.
|
|
201
|
+
*/
|
|
202
|
+
childPatches(child: VirtualNode, description: NodeDescription): void {
|
|
203
|
+
// the only comparison of the descriptions, as they are compared deeply
|
|
204
|
+
if (Diff.deepEqual(child.description, description)) {
|
|
205
|
+
return
|
|
206
|
+
}
|
|
207
|
+
if (child.isComponent()) {
|
|
208
|
+
if (child.isRoot()) {
|
|
209
|
+
// the child nodes of a Web Component are rendered in the light DOM
|
|
210
|
+
this.childrenPatches(child.childNodes, description.children, child)
|
|
211
|
+
// the root renders its own content when the patches are applied,
|
|
212
|
+
// before the node is updated, as it compares its description
|
|
213
|
+
this.addPatch(
|
|
214
|
+
Patch.updateRoot(child, description as ComponentDescription),
|
|
215
|
+
)
|
|
216
|
+
this.addPatch(Patch.updateNode(child, description))
|
|
217
|
+
return
|
|
218
|
+
}
|
|
219
|
+
return this.componentPatches(child, description as ComponentDescription)
|
|
220
|
+
}
|
|
221
|
+
if (child.isElement()) {
|
|
222
|
+
return this.elementPatches(child, description as ElementDescription)
|
|
223
|
+
}
|
|
224
|
+
throw new Error(`Unsupported node type: ${child.nodeType}`)
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* Calculates patches for transformation of an element to match given
|
|
229
|
+
* description.
|
|
230
|
+
*/
|
|
231
|
+
elementPatches(element: VirtualElement, description: ElementDescription) {
|
|
232
|
+
this.classNamePatches(element.description.class, description.class, element)
|
|
233
|
+
this.stylePatches(element.description.style, description.style, element)
|
|
234
|
+
this.attributePatches(element.description.attrs, description.attrs, element)
|
|
235
|
+
this.listenerPatches(
|
|
236
|
+
element.description.listeners,
|
|
237
|
+
description.listeners,
|
|
238
|
+
element,
|
|
239
|
+
)
|
|
240
|
+
this.datasetPatches(
|
|
241
|
+
element.description.dataset,
|
|
242
|
+
description.dataset,
|
|
243
|
+
element,
|
|
244
|
+
)
|
|
245
|
+
this.propertiesPatches(
|
|
246
|
+
element.description.properties,
|
|
247
|
+
description.properties,
|
|
248
|
+
element,
|
|
249
|
+
)
|
|
250
|
+
|
|
251
|
+
if (element.description.custom || description.custom) {
|
|
252
|
+
this.attributePatches(
|
|
253
|
+
element.description.custom && element.description.custom.attrs,
|
|
254
|
+
description.custom && description.custom.attrs,
|
|
255
|
+
element,
|
|
256
|
+
true,
|
|
257
|
+
)
|
|
258
|
+
this.listenerPatches(
|
|
259
|
+
element.description.custom && element.description.custom.listeners,
|
|
260
|
+
description.custom && description.custom.listeners,
|
|
261
|
+
element,
|
|
262
|
+
true,
|
|
263
|
+
)
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
if (element.children || description.children) {
|
|
267
|
+
this.childrenPatches(element.children, description.children, element)
|
|
268
|
+
}
|
|
269
|
+
this.addPatch(Patch.updateNode(element, description))
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
classNamePatches(current = '', next = '', target: VirtualElement) {
|
|
273
|
+
if (current !== next) {
|
|
274
|
+
this.addPatch(Patch.setClassName(next, target))
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
stylePatches(
|
|
279
|
+
current: Record<string, string> = {},
|
|
280
|
+
next: Record<string, string> = {},
|
|
281
|
+
target: VirtualElement,
|
|
282
|
+
) {
|
|
283
|
+
const { added, removed, changed } = compareKeys(current, next)
|
|
284
|
+
|
|
285
|
+
for (const prop of added) {
|
|
286
|
+
this.addPatch(Patch.setStyleProperty(prop, next[prop]!, target))
|
|
287
|
+
}
|
|
288
|
+
for (const prop of removed) {
|
|
289
|
+
this.addPatch(Patch.removeStyleProperty(prop, target))
|
|
290
|
+
}
|
|
291
|
+
for (const prop of changed) {
|
|
292
|
+
this.addPatch(Patch.setStyleProperty(prop, next[prop]!, target))
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
attributePatches(
|
|
297
|
+
current: Record<string, string> = {},
|
|
298
|
+
next: Record<string, string> = {},
|
|
299
|
+
target: VirtualElement | WebComponent,
|
|
300
|
+
isCustom = false,
|
|
301
|
+
) {
|
|
302
|
+
const { added, removed, changed } = compareKeys(current, next)
|
|
303
|
+
|
|
304
|
+
for (const attr of added) {
|
|
305
|
+
this.addPatch(Patch.setAttribute(attr, next[attr]!, target, isCustom))
|
|
306
|
+
}
|
|
307
|
+
for (const attr of removed) {
|
|
308
|
+
this.addPatch(Patch.removeAttribute(attr, target, isCustom))
|
|
309
|
+
}
|
|
310
|
+
for (const attr of changed) {
|
|
311
|
+
this.addPatch(Patch.setAttribute(attr, next[attr]!, target, isCustom))
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
listenerPatches(
|
|
316
|
+
current: Record<string, Listener> = {},
|
|
317
|
+
next: Record<string, Listener> = {},
|
|
318
|
+
target: VirtualElement,
|
|
319
|
+
isCustom = false,
|
|
320
|
+
) {
|
|
321
|
+
// listeners bound by the sandbox are equal when bound to the same method
|
|
322
|
+
const { added, removed, changed } = compareKeys(
|
|
323
|
+
current,
|
|
324
|
+
next,
|
|
325
|
+
(listener, nextListener) =>
|
|
326
|
+
listener === nextListener ||
|
|
327
|
+
(listener.source !== undefined &&
|
|
328
|
+
listener.source === nextListener.source),
|
|
329
|
+
)
|
|
330
|
+
|
|
331
|
+
for (const event of added) {
|
|
332
|
+
this.addPatch(Patch.addListener(event, next[event]!, target, isCustom))
|
|
333
|
+
}
|
|
334
|
+
for (const event of removed) {
|
|
335
|
+
this.addPatch(
|
|
336
|
+
Patch.removeListener(event, current[event]!, target, isCustom),
|
|
337
|
+
)
|
|
338
|
+
}
|
|
339
|
+
for (const event of changed) {
|
|
340
|
+
this.addPatch(
|
|
341
|
+
Patch.replaceListener(
|
|
342
|
+
event,
|
|
343
|
+
current[event]!,
|
|
344
|
+
next[event]!,
|
|
345
|
+
target,
|
|
346
|
+
isCustom,
|
|
347
|
+
),
|
|
348
|
+
)
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
datasetPatches(
|
|
353
|
+
current: Record<string, string> = {},
|
|
354
|
+
next: Record<string, string> = {},
|
|
355
|
+
target: VirtualElement,
|
|
356
|
+
) {
|
|
357
|
+
const { added, removed, changed } = compareKeys(current, next)
|
|
358
|
+
|
|
359
|
+
for (const attr of added) {
|
|
360
|
+
this.addPatch(Patch.setDataAttribute(attr, next[attr]!, target))
|
|
361
|
+
}
|
|
362
|
+
for (const attr of removed) {
|
|
363
|
+
this.addPatch(Patch.removeDataAttribute(attr, target))
|
|
364
|
+
}
|
|
365
|
+
for (const attr of changed) {
|
|
366
|
+
this.addPatch(Patch.setDataAttribute(attr, next[attr]!, target))
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
propertiesPatches(
|
|
371
|
+
current: Record<string, unknown> = {},
|
|
372
|
+
next: Record<string, unknown> = {},
|
|
373
|
+
target: VirtualElement,
|
|
374
|
+
) {
|
|
375
|
+
const { added, removed, changed } = compareKeys(
|
|
376
|
+
current,
|
|
377
|
+
next,
|
|
378
|
+
(value, nextValue) => Diff.deepEqual(value, nextValue),
|
|
379
|
+
)
|
|
380
|
+
|
|
381
|
+
for (const key of added) {
|
|
382
|
+
this.addPatch(Patch.setProperty(key, next[key], target))
|
|
383
|
+
}
|
|
384
|
+
for (const key of removed) {
|
|
385
|
+
this.addPatch(Patch.deleteProperty(key, target))
|
|
386
|
+
}
|
|
387
|
+
for (const key of changed) {
|
|
388
|
+
this.addPatch(Patch.setProperty(key, next[key], target))
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
childrenPatches(
|
|
393
|
+
sourceNodes: VirtualNode[] = [],
|
|
394
|
+
targetDescriptions: NodeDescription[] = [],
|
|
395
|
+
parent: ParentVirtualNode,
|
|
396
|
+
) {
|
|
397
|
+
const Move = Reconciler.Move
|
|
398
|
+
|
|
399
|
+
const created = new Set<VirtualNode>()
|
|
400
|
+
const createdNodesMap = new Map<string, VirtualNode>()
|
|
401
|
+
|
|
402
|
+
const createNode = (description: NodeDescription, key: string) => {
|
|
403
|
+
const node = VirtualDOM.createFromDescription(
|
|
404
|
+
description,
|
|
405
|
+
parent,
|
|
406
|
+
this.root,
|
|
407
|
+
)
|
|
408
|
+
created.add(node!)
|
|
409
|
+
createdNodesMap.set(key, node!)
|
|
410
|
+
return node!
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
const from = sourceNodes.map(
|
|
414
|
+
(node, index) => node.key || Diff.createKey(index),
|
|
415
|
+
)
|
|
416
|
+
const to = targetDescriptions.map(
|
|
417
|
+
(description, index) => description.key || Diff.createKey(index),
|
|
418
|
+
)
|
|
419
|
+
|
|
420
|
+
if (runtime().isDebug()) {
|
|
421
|
+
const assertUniqueKeys = (keys: string[]) => {
|
|
422
|
+
if (keys.length) {
|
|
423
|
+
const uniqueKeys = [...new Set(keys)]
|
|
424
|
+
if (uniqueKeys.length !== keys.length) {
|
|
425
|
+
throw new Error(`Non-unique keys detected in: ${keys.join(', ')}`)
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
assertUniqueKeys(from)
|
|
430
|
+
assertUniqueKeys(to)
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
// with the same keys in the same order, there are no nodes to move
|
|
434
|
+
if (from.length === to.length && from.every((key, i) => key === to[i])) {
|
|
435
|
+
for (let i = 0; i < sourceNodes.length; i++) {
|
|
436
|
+
this.elementChildPatches(
|
|
437
|
+
sourceNodes[i]!,
|
|
438
|
+
targetDescriptions[i]!,
|
|
439
|
+
parent,
|
|
440
|
+
)
|
|
441
|
+
}
|
|
442
|
+
return
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
// the first of the items with the same key, which only debug rejects
|
|
446
|
+
const mapByKey = <T>(keys: string[], items: T[]) => {
|
|
447
|
+
const map = new Map<string, T>()
|
|
448
|
+
keys.forEach((key, index) => {
|
|
449
|
+
if (!map.has(key)) {
|
|
450
|
+
map.set(key, items[index]!)
|
|
451
|
+
}
|
|
452
|
+
})
|
|
453
|
+
return map
|
|
454
|
+
}
|
|
455
|
+
const sourceNodesMap = mapByKey(from, sourceNodes)
|
|
456
|
+
const targetDescriptionsMap = mapByKey(to, targetDescriptions)
|
|
457
|
+
|
|
458
|
+
const getNode = (key: string, isMove: boolean): VirtualNode => {
|
|
459
|
+
const sourceNode = sourceNodesMap.get(key)
|
|
460
|
+
if (sourceNode) {
|
|
461
|
+
return sourceNode
|
|
462
|
+
}
|
|
463
|
+
if (isMove) {
|
|
464
|
+
return createdNodesMap.get(key)!
|
|
465
|
+
}
|
|
466
|
+
return createNode(targetDescriptionsMap.get(key)!, key)
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
const moves = Reconciler.calculateMoves(from, to)
|
|
470
|
+
|
|
471
|
+
const children = [...sourceNodes]
|
|
472
|
+
for (const move of moves) {
|
|
473
|
+
const node = getNode(move.item, move.name === Move.Name.MOVE)
|
|
474
|
+
switch (move.name) {
|
|
475
|
+
case Move.Name.REMOVE:
|
|
476
|
+
this.addPatch(Patch.removeChild(node, move.at!, parent))
|
|
477
|
+
Move.remove(node, move.at!).make(children)
|
|
478
|
+
continue
|
|
479
|
+
case Move.Name.INSERT:
|
|
480
|
+
this.addPatch(Patch.insertChild(node, move.at!, parent))
|
|
481
|
+
Move.insert(node, move.at!).make(children)
|
|
482
|
+
continue
|
|
483
|
+
case Move.Name.MOVE:
|
|
484
|
+
this.addPatch(Patch.moveChild(node, move.from!, move.to!, parent))
|
|
485
|
+
Move.move(node, move.from!, move.to!).make(children)
|
|
486
|
+
continue
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
for (let i = 0; i < children.length; i++) {
|
|
490
|
+
const child = children[i]!
|
|
491
|
+
if (!created.has(child)) {
|
|
492
|
+
const targetDescription = targetDescriptions[i]!
|
|
493
|
+
this.elementChildPatches(child, targetDescription, parent)
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
elementChildPatches(
|
|
499
|
+
child: VirtualNode,
|
|
500
|
+
description: NodeDescription,
|
|
501
|
+
parent: ParentVirtualNode,
|
|
502
|
+
) {
|
|
503
|
+
if (child.description.isCompatible(description)) {
|
|
504
|
+
this.childPatches(child, description)
|
|
505
|
+
} else {
|
|
506
|
+
const node = VirtualDOM.createFromDescription(
|
|
507
|
+
description,
|
|
508
|
+
parent,
|
|
509
|
+
this.root,
|
|
510
|
+
)
|
|
511
|
+
this.addPatch(Patch.replaceChild(child, node!, parent))
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
/**
|
|
516
|
+
* Returns a normalized type of given item.
|
|
517
|
+
*/
|
|
518
|
+
static getType(item: unknown): string {
|
|
519
|
+
const type = typeof item
|
|
520
|
+
if (type !== 'object') {
|
|
521
|
+
return type
|
|
522
|
+
}
|
|
523
|
+
if (item === null) {
|
|
524
|
+
return 'null'
|
|
525
|
+
}
|
|
526
|
+
if (Array.isArray(item)) {
|
|
527
|
+
return 'array'
|
|
528
|
+
}
|
|
529
|
+
return 'object'
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
static createKey(index: number): string {
|
|
533
|
+
return String(index).padStart(8, '0')
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
static deepEqual(current: unknown, next: unknown): boolean {
|
|
537
|
+
if (Object.is(current, next)) {
|
|
538
|
+
return true
|
|
539
|
+
}
|
|
540
|
+
const type = this.getType(current)
|
|
541
|
+
const nextType = this.getType(next)
|
|
542
|
+
if (type !== nextType) {
|
|
543
|
+
return false
|
|
544
|
+
}
|
|
545
|
+
if (type === 'array') {
|
|
546
|
+
const currentArray = current as unknown[]
|
|
547
|
+
const nextArray = next as unknown[]
|
|
548
|
+
if (currentArray.length !== nextArray.length) {
|
|
549
|
+
return false
|
|
550
|
+
}
|
|
551
|
+
for (let i = 0; i < currentArray.length; i++) {
|
|
552
|
+
const equal = this.deepEqual(currentArray[i], nextArray[i])
|
|
553
|
+
if (!equal) {
|
|
554
|
+
return false
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
return true
|
|
558
|
+
} else if (type === 'object') {
|
|
559
|
+
const currentObject = current as Record<string, unknown>
|
|
560
|
+
const nextObject = next as Record<string, unknown>
|
|
561
|
+
if (currentObject.constructor !== nextObject.constructor) {
|
|
562
|
+
return false
|
|
563
|
+
}
|
|
564
|
+
// these keep their values in internal slots, not in own keys
|
|
565
|
+
if (current instanceof Date) {
|
|
566
|
+
return Object.is(current.getTime(), (next as Date).getTime())
|
|
567
|
+
}
|
|
568
|
+
if (current instanceof Map || current instanceof Set) {
|
|
569
|
+
return this.deepEqual([...current], [...(next as typeof current)])
|
|
570
|
+
}
|
|
571
|
+
const keys = Object.keys(currentObject)
|
|
572
|
+
if (keys.length !== Object.keys(nextObject).length) {
|
|
573
|
+
return false
|
|
574
|
+
}
|
|
575
|
+
// with as many keys, the same keys when each one is in both objects
|
|
576
|
+
for (const key of keys) {
|
|
577
|
+
if (
|
|
578
|
+
!Object.prototype.propertyIsEnumerable.call(nextObject, key) ||
|
|
579
|
+
!this.deepEqual(currentObject[key], nextObject[key])
|
|
580
|
+
) {
|
|
581
|
+
return false
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
return true
|
|
585
|
+
}
|
|
586
|
+
return false
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
export default Diff
|