-
-
Notifications
You must be signed in to change notification settings - Fork 139
/
Copy pathdiff.js
360 lines (335 loc) · 12.5 KB
/
diff.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
/* virtual-dom diffing algorithm, applies patches as detected */
window = typeof window === 'undefined' ? {} : window;
window['diff'] = function diff(currentObj, newObj, parent, doc) {
if (!currentObj && !newObj) return;
else if (!currentObj && newObj) window['createNode'](newObj, parent, doc);
else if (currentObj && !newObj) window['destroyNode'](currentObj, parent);
else {
if (currentObj.type === 'vtext') {
if (newObj.type === 'vnode') window['replaceTextWithElement'](currentObj, newObj, parent, doc);
else window['diffTextNodes'](currentObj, newObj);
} else {
if (newObj.type === 'vnode') window['diffVNodes'](currentObj, newObj, parent, doc);
else window['replaceElementWithText'](currentObj, newObj, parent, doc);
}
}
};
window['destroyNode'] = function destroyNode(obj, parent) {
window['callBeforeDestroyedRecursive'](obj);
parent.removeChild(obj['domRef']);
window['callDestroyedRecursive'](obj);
};
window['callDestroyedRecursive'] = function callDestroyedRecursive(obj) {
window['callDestroyed'](obj);
for (var i in obj.children)
window['callDestroyedRecursive'](obj.children[i]);
};
window['callDestroyed'] = function callDestroyed(obj) {
if (obj['onDestroyed']) obj['onDestroyed']();
};
window['callBeforeDestroyed'] = function callBeforeDestroyed(obj) {
if (obj['onBeforeDestroyed']) obj['onBeforeDestroyed']();
};
window['callBeforeDestroyedRecursive'] = function callBeforeDestroyedRecursive(obj) {
window['callBeforeDestroyed'](obj);
for (var i in obj.children)
window['callBeforeDestroyedRecursive'](obj.children[i]);
};
window['diffTextNodes'] = function diffTextNodes(c, n) {
if (c['text'] !== n['text']) c['domRef'].textContent = n['text'];
n['domRef'] = c['domRef'];
};
window['replaceElementWithText'] = function replaceElementWithText(c, n, parent, doc) {
n['domRef'] = doc.createTextNode(n['text']);
window['callBeforeDestroyedRecursive'](c);
parent.replaceChild(n['domRef'], c['domRef']);
window['callDestroyedRecursive'](c);
};
window['replaceTextWithElement'] = function replaceTextWithElement(c, n, parent, doc) {
window['createElement'](n, doc);
parent.replaceChild(n['domRef'], c['domRef']);
window['callCreated'](n);
};
window['callCreated'] = function callCreated(obj) {
if (obj['onCreated']) obj['onCreated']();
};
window['populate'] = function populate(c, n, doc) {
if (!c) c = {
props: null,
css: null,
children: []
}
window['diffProps'](c['props'], n['props'], n['domRef'], n['ns'] === 'svg');
window['diffCss'](c['css'], n['css'], n['domRef']);
window['diffChildren'](c['children'], n['children'], n['domRef'], doc);
};
window['diffVNodes'] = function diffVNodes(c, n, parent, doc) {
if (c['tag'] === n['tag'] && n['key'] === c['key']) {
n['domRef'] = c['domRef'];
window['populate'](c, n, doc);
} else {
window['createElement'](n, doc);
window['callBeforeDestroyedRecursive'](c);
parent.replaceChild(n['domRef'], c['domRef']);
window['callDestroyedRecursive'](c);
window['callCreated'](n);
}
};
window['diffProps'] = function diffProps(cProps, nProps, node, isSvg) {
var newProp;
/* Is current prop in new prop list? */
for (var c in cProps) {
newProp = nProps[c];
/* If current property no longer exists, remove it */
if (newProp === undefined) {
/* current key is not in node, remove it from DOM, if SVG, remove attribute */
if (isSvg || !(c in node))
node.removeAttribute(c, cProps[c]);
else
node[c] = '';
} else {
/* Already on DOM from previous diff, continue */
if (newProp === cProps[c] && c !== 'checked' && c !== 'value') continue;
if (isSvg) {
if (c === 'href')
node.setAttributeNS('http://www.w3.org/1999/xlink', 'href', newProp);
else
node.setAttribute(c, newProp);
} else if (c in node && !(c === 'list' || c === 'form')) {
node[c] = newProp;
} else {
node.setAttribute(c, newProp);
}
}
}
/* add remaining */
for (var n in nProps) {
if (cProps && cProps[n]) continue;
newProp = nProps[n];
/* Only add new properties, skip (continue) if they already exist in current property map */
if (isSvg) {
if (n === 'href')
node.setAttributeNS('http://www.w3.org/1999/xlink', 'href', newProp);
else
node.setAttribute(n, newProp);
} else if (n in node && !(n === 'list' || n === 'form')) {
node[n] = nProps[n];
} else {
node.setAttribute(n, newProp);
}
}
};
window['diffCss'] = function diffCss(cCss, nCss, node) {
var result;
/* is current attribute in new attribute list? */
for (var c in cCss) {
result = nCss[c];
if (!result) {
/* current key is not in node */
node.style[c] = null;
} else if (result !== cCss[c]) {
node.style[c] = result;
}
}
/* add remaining */
for (var n in nCss) {
if (cCss && cCss[n]) continue;
node.style[n] = nCss[n];
}
};
window['hasKeys'] = function hasKeys(ns, cs) {
return ns.length > 0 && cs.length > 0 && ns[0]['key'] != null && cs[0]['key'] != null;
};
window['diffChildren'] = function diffChildren(cs, ns, parent, doc) {
var longest = ns.length > cs.length ? ns.length : cs.length;
if (window['hasKeys'](ns, cs)) {
window['syncChildren'](cs, ns, parent, doc);
} else {
for (var i = 0; i < longest; i++)
window['diff'](cs[i], ns[i], parent, doc);
}
};
window['createElement'] = function createElement(obj, doc) {
if (obj['ns'] === 'svg') {
obj['domRef'] = doc.createElementNS('http://www.w3.org/2000/svg', obj['tag']);
} else if (obj['ns'] === 'mathml') {
obj['domRef'] = doc.createElementNS('http://www.w3.org/1998/Math/MathML', obj['tag']);
} else {
obj['domRef'] = doc.createElement(obj['tag']);
}
window['populate'](null, obj, doc);
};
window['createNode'] = function createNode(obj, parent, doc) {
if (obj.type === 'vnode') window['createElement'](obj, doc);
else obj['domRef'] = doc.createTextNode(obj['text']);
parent.appendChild(obj['domRef']);
window['callCreated'](obj);
};
/* Child reconciliation algorithm, inspired by kivi and Bobril */
window['syncChildren'] = function syncChildren(os, ns, parent, doc) {
var oldFirstIndex = 0,
newFirstIndex = 0,
oldLastIndex = os.length - 1,
newLastIndex = ns.length - 1,
nFirst, nLast, oLast, oFirst, tmp, found, node;
for (;;) {
/* check base case, first > last for both new and old
[ ] -- old children empty (fully-swapped)
[ ] -- new children empty (fully-swapped)
*/
if (newFirstIndex > newLastIndex && oldFirstIndex > oldLastIndex) {
break;
}
/* Initialize */
nFirst = ns[newFirstIndex];
nLast = ns[newLastIndex];
oFirst = os[oldFirstIndex];
oLast = os[oldLastIndex];
/* No more old nodes, create and insert all remaining nodes
-> [ ] <- old children
-> [ a b c ] <- new children
*/
if (oldFirstIndex > oldLastIndex) {
window['diff'](null, nFirst, parent, doc);
/* insertBefore's semantics will append a node if the second argument provided is `null` or `undefined`.
Otherwise, it will insert node['domRef'] before oLast['domRef']. */
parent.insertBefore(nFirst['domRef'], oFirst ? oFirst['domRef'] : null);
os.splice(newFirstIndex, 0, nFirst);
newFirstIndex++;
}
/* No more new nodes, delete all remaining nodes in old list
-> [ a b c ] <- old children
-> [ ] <- new children
*/
else if (newFirstIndex > newLastIndex) {
tmp = oldLastIndex;
while (oldLastIndex >= oldFirstIndex) {
parent.removeChild(os[oldLastIndex--]['domRef']);
}
os.splice(oldFirstIndex, tmp - oldFirstIndex + 1);
break;
}
/* happy path, everything aligns, we continue
-> oldFirstIndex -> [ a b c ] <- oldLastIndex
-> newFirstIndex -> [ a b c ] <- newLastIndex
check if nFirst and oFirst align, if so, check nLast and oLast
*/
else if (oFirst['key'] === nFirst['key']) {
window['diff'](os[oldFirstIndex++], ns[newFirstIndex++], parent, doc);
} else if (oLast['key'] === nLast['key']) {
window['diff'](os[oldLastIndex--], ns[newLastIndex--], parent, doc);
}
/* flip-flop case, nodes have been swapped, in some way or another
both could have been swapped.
-> [ a b c ] <- old children
-> [ c b a ] <- new children
*/
else if (oFirst['key'] === nLast['key'] && nFirst['key'] === oLast['key']) {
window['swapDomRefs'](node, oLast['domRef'], oFirst['domRef'], parent);
window['swap'](os, oldFirstIndex, oldLastIndex);
window['diff'](os[oldFirstIndex++], ns[newFirstIndex++], parent, doc);
window['diff'](os[oldLastIndex--], ns[newLastIndex--], parent, doc);
}
/* Or just one could be swapped (d's align here)
This is top left and bottom right match case.
We move d to end of list, mutate old vdom to reflect the change
We then continue without affecting indexes, hoping to land in a better case
-> [ d a b ] <- old children
-> [ a b d ] <- new children
becomes
-> [ a b d ] <- old children
-> [ a b d ] <- new children
and now we happy path
*/
else if (oFirst['key'] === nLast['key']) {
/* insertAfter */
parent.insertBefore(oFirst['domRef'], oLast['domRef'].nextSibling);
/* swap positions in old vdom */
os.splice(oldLastIndex,0,os.splice(oldFirstIndex,1)[0]);
window['diff'](os[oldLastIndex--], ns[newLastIndex--], parent, doc);
}
/* This is top right and bottom lefts match case.
We move d to end of list, mutate old vdom to reflect the change
-> [ b a d ] <- old children
-> [ d b a ] <- new children
becomes
-> [ d b a ] <- old children
-> [ d b a ] <- new children
and now we happy path
*/
else if (oLast['key'] === nFirst['key']) {
/* insertAfter */
parent.insertBefore(oLast['domRef'], oFirst['domRef']);
/* swap positions in old vdom */
os.splice(oldFirstIndex,0, os.splice(oldLastIndex,1)[0]);
window['diff'](os[oldFirstIndex++], nFirst, parent, doc);
newFirstIndex++;
}
/* The 'you're screwed' case, nothing aligns, pull the ripcord, do something more fancy
This can happen when the list is sorted, for example.
-> [ a e c ] <- old children
-> [ b e d ] <- new children
*/
else {
/* final case, perform linear search to check if new key exists in old map, decide what to do from there */
found = false;
tmp = oldFirstIndex;
while (tmp <= oldLastIndex) {
if (os[tmp]['key'] === nFirst['key']) {
found = true;
node = os[tmp];
break;
}
tmp++;
}
/* If new key was found in old map this means it was moved, hypothetically as below
-> [ a e b c ] <- old children
-> [ b e a j ] <- new children
^
In the above case 'b' has been moved, so we need to insert 'b' before 'a' in both vDOM and DOM
We also increase oldFirstIndex and newFirstIndex.
This results in new list below w/ updated index position
-> [ b a e c ] <- old children
-> [ b e a j ] <- new children
^
*/
if (found) {
/* Move item to correct position */
os.splice(oldFirstIndex,0, os.splice(tmp,1)[0]);
/* optionally perform `diff` here */
window['diff'](os[oldFirstIndex++], nFirst, parent, doc);
/* Swap DOM references */
parent.insertBefore(node['domRef'], os[oldFirstIndex]['domRef']);
/* increment counters */
newFirstIndex++;
}
/* If new key was *not* found in the old map this means it must now be created, example below
-> [ a e d c ] <- old children
-> [ b e a j ] <- new children
^
In the above case 'b' does not exist in the old map, so we create a new element and DOM reference.
We then insertBefore in both vDOM and DOM.
-> [ b a e d c ] <- old children
-> [ b e a j ] <- new children
^
*/
else {
window['createElement'](nFirst, doc);
parent.insertBefore(nFirst['domRef'], oFirst['domRef']);
os.splice(oldFirstIndex++, 0, nFirst);
newFirstIndex++;
oldLastIndex++;
}
}
}
};
window['swapDomRefs'] = function swapDomRefs(tmp,a,b,p) {
tmp = a.nextSibling;
p.insertBefore(a,b);
p.insertBefore(b,tmp);
};
window['swap']= function swap(os,l,r) {
var k = os[l];
os[l] = os[r];
os[r] = k;
};