ks-fwork 1.0.6 → 2.0.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/package.json +1 -4
- package/src/app.js +11 -7
- package/src/h.js +16 -0
- package/src/mount-dom.js +31 -10
- package/src/nodes-equal.js +11 -0
- package/src/patch-dom.js +178 -0
- package/src/utils/arrays.js +198 -0
- package/src/utils/objects.js +12 -0
- package/src/utils/strings.js +7 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ks-fwork",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "2.0.0",
|
|
4
4
|
"description": "Demo Frontend Framework",
|
|
5
5
|
"license": "ISC",
|
|
6
6
|
"author": "",
|
|
@@ -12,8 +12,5 @@
|
|
|
12
12
|
],
|
|
13
13
|
"scripts": {
|
|
14
14
|
"test": "echo \"Error: no test specified\" && exit 1"
|
|
15
|
-
},
|
|
16
|
-
"dependencies": {
|
|
17
|
-
"ks-fwork": "^1.0.1"
|
|
18
15
|
}
|
|
19
16
|
}
|
package/src/app.js
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import { destroyDOM } from "./destroy-dom.js";
|
|
2
2
|
import { Dispatcher } from "./dispatcher.js";
|
|
3
|
-
import { mountDOM} from "./mount-dom.js";
|
|
3
|
+
import { mountDOM } from "./mount-dom.js";
|
|
4
|
+
import { patchDOM } from "./patch-dom.js";
|
|
4
5
|
|
|
5
6
|
export function createApp({state, view, reducers = {} }) {
|
|
6
7
|
let parentEl = null;
|
|
7
8
|
let rootNode = null;
|
|
9
|
+
let isMounted = false;
|
|
8
10
|
|
|
9
11
|
const dispatcher = new Dispatcher();
|
|
10
12
|
const unsubscribers = [dispatcher.afterEveryCommand(renderApp)];
|
|
@@ -25,19 +27,19 @@ export function createApp({state, view, reducers = {} }) {
|
|
|
25
27
|
}
|
|
26
28
|
|
|
27
29
|
function renderApp() {
|
|
28
|
-
|
|
29
|
-
destroyDOM(rootNode);
|
|
30
|
-
}
|
|
30
|
+
const newRootNode = view(state, emit);
|
|
31
31
|
|
|
32
|
-
rootNode =
|
|
33
|
-
mountDOM(rootNode, parentEl);
|
|
32
|
+
rootNode = patchDOM(rootNode, newRootNode, parentEl);
|
|
34
33
|
}
|
|
35
34
|
|
|
36
35
|
return {
|
|
37
36
|
// Attach the app to a real DOM element and do the first render.
|
|
38
37
|
mount(_parentEl) {
|
|
38
|
+
if (isMounted) throw new Error('The application is already mounted');
|
|
39
|
+
|
|
39
40
|
parentEl = _parentEl;
|
|
40
|
-
|
|
41
|
+
rootNode = view(state, emit);
|
|
42
|
+
mountDOM(rootNode, parentEl);
|
|
41
43
|
},
|
|
42
44
|
|
|
43
45
|
// Detach the app: remove its DOM and cancel every subscription.
|
|
@@ -47,6 +49,8 @@ export function createApp({state, view, reducers = {} }) {
|
|
|
47
49
|
}
|
|
48
50
|
rootNode = null;
|
|
49
51
|
unsubscribers.forEach((unsubscribe) => unsubscribe());
|
|
52
|
+
|
|
53
|
+
isMounted = false;
|
|
50
54
|
},
|
|
51
55
|
}
|
|
52
56
|
}
|
package/src/h.js
CHANGED
|
@@ -51,4 +51,20 @@ export function lipsum(numberOfParagraphs) {
|
|
|
51
51
|
h('p', {}, [text])
|
|
52
52
|
)
|
|
53
53
|
);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function extractChildren(vNode) {
|
|
57
|
+
if (vNode.children == null) return [];
|
|
58
|
+
|
|
59
|
+
const children = [];
|
|
60
|
+
|
|
61
|
+
for (const child of vNode.children) {
|
|
62
|
+
if (child.type === DOM_TYPES.FRAGMENT) {
|
|
63
|
+
children.push(...extractChildren(child))
|
|
64
|
+
} else {
|
|
65
|
+
children.push(child);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
return children;
|
|
54
70
|
}
|
package/src/mount-dom.js
CHANGED
|
@@ -3,20 +3,20 @@ import { setAttributes } from './attributes.js'
|
|
|
3
3
|
import { addEventListeners } from "./events.js";
|
|
4
4
|
|
|
5
5
|
// Recursively mount a virtual node (and its subtree) into a real parent DOM element.
|
|
6
|
-
export function mountDOM(vNode, parentEl) {
|
|
6
|
+
export function mountDOM(vNode, parentEl, index) {
|
|
7
7
|
switch (vNode.type) {
|
|
8
8
|
case DOM_TYPES.TEXT: {
|
|
9
|
-
createTextNode(vNode, parentEl);
|
|
9
|
+
createTextNode(vNode, parentEl, index);
|
|
10
10
|
break;
|
|
11
11
|
}
|
|
12
12
|
|
|
13
13
|
case DOM_TYPES.ELEMENT: {
|
|
14
|
-
createElementNode(vNode, parentEl);
|
|
14
|
+
createElementNode(vNode, parentEl, index);
|
|
15
15
|
break;
|
|
16
16
|
}
|
|
17
17
|
|
|
18
18
|
case DOM_TYPES.FRAGMENT: {
|
|
19
|
-
createFragmentNode(vNode, parentEl);
|
|
19
|
+
createFragmentNode(vNode, parentEl, index);
|
|
20
20
|
break;
|
|
21
21
|
}
|
|
22
22
|
|
|
@@ -27,18 +27,18 @@ export function mountDOM(vNode, parentEl) {
|
|
|
27
27
|
}
|
|
28
28
|
|
|
29
29
|
// Create a real text node, save a reference to it on the vNode, and append it to the parent.
|
|
30
|
-
function createTextNode(vNode, parentEl) {
|
|
30
|
+
function createTextNode(vNode, parentEl, index) {
|
|
31
31
|
const { value } = vNode;
|
|
32
32
|
|
|
33
33
|
const textNode = document.createTextNode(value);
|
|
34
34
|
vNode.el = textNode;
|
|
35
35
|
|
|
36
|
-
|
|
36
|
+
insert(textNode, parentEl, index);
|
|
37
37
|
}
|
|
38
38
|
|
|
39
39
|
// Create a real element, apply its props, recursively mount its children into it,
|
|
40
40
|
// then append the finished element to the parent.
|
|
41
|
-
function createElementNode(vNode, parentEl) {
|
|
41
|
+
function createElementNode(vNode, parentEl, index) {
|
|
42
42
|
const { tag, props, children } = vNode;
|
|
43
43
|
|
|
44
44
|
const el = document.createElement(tag);
|
|
@@ -46,15 +46,17 @@ function createElementNode(vNode, parentEl) {
|
|
|
46
46
|
vNode.el = el;
|
|
47
47
|
|
|
48
48
|
children.forEach((child) => mountDOM(child, el));
|
|
49
|
-
|
|
49
|
+
insert(el, parentEl, index);
|
|
50
50
|
}
|
|
51
51
|
|
|
52
52
|
// Fragment has no DOM element. Mount its children directly into the parent.
|
|
53
|
-
function createFragmentNode(vNode, parentEl) {
|
|
53
|
+
function createFragmentNode(vNode, parentEl, index) {
|
|
54
54
|
const { children } = vNode;
|
|
55
55
|
vNode.el = parentEl;
|
|
56
56
|
|
|
57
|
-
children.forEach((child) =>
|
|
57
|
+
children.forEach((child, i) => {
|
|
58
|
+
mountDOM(child, parentEl, index ? index + i : null)
|
|
59
|
+
});
|
|
58
60
|
}
|
|
59
61
|
|
|
60
62
|
// Split props into event listeners ('on') and attributes and add them to DOM element.
|
|
@@ -63,4 +65,23 @@ function addProps(el, props, vNode) {
|
|
|
63
65
|
|
|
64
66
|
vNode.listeners = addEventListeners(events, el);
|
|
65
67
|
setAttributes(el, attrs);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function insert(el, parentEl, index) {
|
|
71
|
+
if (index == null) {
|
|
72
|
+
parentEl.append(el);
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
if (index < 0) {
|
|
77
|
+
throw new Error(`Index must be a positive number, got ${index}`);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const children = parentEl.childNodes;
|
|
81
|
+
|
|
82
|
+
if (index >= children.length) {
|
|
83
|
+
parentEl.append(el);
|
|
84
|
+
} else {
|
|
85
|
+
parentEl.insertBefore(el, children[index]);
|
|
86
|
+
}
|
|
66
87
|
}
|
package/src/patch-dom.js
ADDED
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
import { areNodesEqual } from "./nodes-equal.js";
|
|
2
|
+
import { destroyDOM } from "./destroy-dom.js";
|
|
3
|
+
import { mountDOM } from "./mount-dom.js";
|
|
4
|
+
import { DOM_TYPES, extractChildren } from "./h.js";
|
|
5
|
+
import { objectsDiff } from "./utils/objects.js";
|
|
6
|
+
import { removeAttribute, removeStyle, setAttribute, setStyle } from "./attributes.js";
|
|
7
|
+
import { ARRAY_DIFF_OP, arraysDiff, arraysDiffSequence } from "./utils/arrays.js";
|
|
8
|
+
import { isNotBlankOrEmptyString } from "./utils/strings.js";
|
|
9
|
+
import { addEventListener } from "./events.js";
|
|
10
|
+
|
|
11
|
+
export function patchDOM(oldVNode, newVNode, parentEl) {
|
|
12
|
+
if (!areNodesEqual(oldVNode, newVNode)) {
|
|
13
|
+
const index = findIndexInParent(parentEl, oldVNode.el);
|
|
14
|
+
destroyDOM();
|
|
15
|
+
mountDOM(newVNode, parentEl, index);
|
|
16
|
+
|
|
17
|
+
return newVNode;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
newVNode.el = oldVNode.el;
|
|
21
|
+
|
|
22
|
+
switch (newVNode.type) {
|
|
23
|
+
case DOM_TYPES.TEXT: {
|
|
24
|
+
patchText(oldVNode, newVNode);
|
|
25
|
+
return newVNode;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
case DOM_TYPES.ELEMENT: {
|
|
29
|
+
patchElement(oldVNode, newVNode);
|
|
30
|
+
break;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
patchChildren(oldVNode, newVNode);
|
|
35
|
+
|
|
36
|
+
return newVNode;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function findIndexInParent(parentEl, el) {
|
|
40
|
+
const index = Array.from(parentEl.childNodes).indexOf(el);
|
|
41
|
+
if (index < 0) return null;
|
|
42
|
+
|
|
43
|
+
return index;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function patchText(oldVNode, newVNode) {
|
|
47
|
+
const el = oldVNode.el;
|
|
48
|
+
const { value: oldText } = oldVNode;
|
|
49
|
+
const { value: newText } = newVNode;
|
|
50
|
+
|
|
51
|
+
if (oldText !== newText) {
|
|
52
|
+
el.nodeValue = newText;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function patchElement(oldVNode, newVNode) {
|
|
57
|
+
const el = oldVNode.el;
|
|
58
|
+
|
|
59
|
+
const {
|
|
60
|
+
class: oldClass,
|
|
61
|
+
style: oldStyle,
|
|
62
|
+
on: oldEvents,
|
|
63
|
+
...oldAttrs
|
|
64
|
+
} = oldVNode.props
|
|
65
|
+
|
|
66
|
+
const {
|
|
67
|
+
class: newClass,
|
|
68
|
+
style: newStyle,
|
|
69
|
+
on: newEvents,
|
|
70
|
+
...newAttrs
|
|
71
|
+
} = newVNode.props
|
|
72
|
+
|
|
73
|
+
const { listeners: oldListeners } = oldVNode;
|
|
74
|
+
|
|
75
|
+
patchAttrs(el, oldAttrs, newAttrs);
|
|
76
|
+
patchClasses(el, oldClass, newClass);
|
|
77
|
+
patchStyles(el, oldStyle, newStyle);
|
|
78
|
+
newVNode.listeners = patchEvents(el, oldListeners, oldEvents, newEvents);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function patchAttrs(el, oldAttrs, newAttrs) {
|
|
82
|
+
const { added, removed, updated } = objectsDiff(oldAttrs, newAttrs);
|
|
83
|
+
|
|
84
|
+
for (const attr of removed) {
|
|
85
|
+
removeAttribute(el, attr);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Concatenate added and updated attributes and set them to element.
|
|
89
|
+
for (const attr of added.concat(updated)) {
|
|
90
|
+
setAttribute(el, attr, newAttrs[attr]);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function patchClasses(el, oldClass, newClass) {
|
|
95
|
+
const oldClasses = toClassList(oldClass);
|
|
96
|
+
const newClasses = toClassList(newClass);
|
|
97
|
+
|
|
98
|
+
const { added, removed } = arraysDiff(oldClasses, newClasses);
|
|
99
|
+
|
|
100
|
+
if (removed.length > 0) {
|
|
101
|
+
el.classList.remove(...removed);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
if (added.length > 0) {
|
|
105
|
+
el.classList.add(...added);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function toClassList(classes = '') {
|
|
110
|
+
return Array.isArray(classes)
|
|
111
|
+
? classes.filter(isNotBlankOrEmptyString)
|
|
112
|
+
: classes.split(/(\s+)/).filter(isNotBlankOrEmptyString);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function patchStyles(el, oldStyle = {}, newStyle = {}) {
|
|
116
|
+
const { added, removed, updated} = objectsDiff(oldStyle, newStyle);
|
|
117
|
+
|
|
118
|
+
for (const style of removed) {
|
|
119
|
+
removeStyle(el, style);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
for (const style of added.concat(updated)) {
|
|
123
|
+
setStyle(el, style, newStyle[style]);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function patchEvents(el, oldListeners = {}, oldEvents = {}, newEvents = {}) {
|
|
128
|
+
const { removed, added, updated} = objectsDiff(oldEvents, newEvents);
|
|
129
|
+
|
|
130
|
+
for (const eventName of removed.concat(updated)) {
|
|
131
|
+
el.removeEventListener(eventName, oldListeners[eventName]);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const addedListeners = {};
|
|
135
|
+
|
|
136
|
+
for (const eventName of added.concat(updated)) {
|
|
137
|
+
addedListeners[eventName] = addEventListener(eventName, newEvents[eventName], el);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
return addedListeners;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function patchChildren(oldVNode, newVNode) {
|
|
144
|
+
const oldChildren = extractChildren(oldVNode);
|
|
145
|
+
const newChildren = extractChildren(newVNode);
|
|
146
|
+
const parentEl = oldVNode.el;
|
|
147
|
+
|
|
148
|
+
const diffSeq = arraysDiffSequence(oldChildren, newChildren, areNodesEqual);
|
|
149
|
+
|
|
150
|
+
for (const operation of diffSeq) {
|
|
151
|
+
const { from, index, item } = operation;
|
|
152
|
+
|
|
153
|
+
switch (operation.op) {
|
|
154
|
+
case ARRAY_DIFF_OP.ADD: {
|
|
155
|
+
mountDOM(item, parentEl, index)
|
|
156
|
+
break;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
case ARRAY_DIFF_OP.REMOVE: {
|
|
160
|
+
destroyDOM(item);
|
|
161
|
+
break;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
case ARRAY_DIFF_OP.MOVE: {
|
|
165
|
+
const el = oldChildren[from].el;
|
|
166
|
+
const elAtTargetIndex = parentEl.childNodes[index];
|
|
167
|
+
|
|
168
|
+
parentEl.insertBefore(el, elAtTargetIndex);
|
|
169
|
+
patchDOM(oldChildren[from], newChildren[index], parentEl)
|
|
170
|
+
break;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
case ARRAY_DIFF_OP.NOOP: {
|
|
174
|
+
patchDOM(oldChildren[from], newChildren[index], parentEl)
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
}
|
package/src/utils/arrays.js
CHANGED
|
@@ -1,4 +1,202 @@
|
|
|
1
1
|
// Filter out null values from the array.
|
|
2
2
|
export function withoutNulls(arr) {
|
|
3
3
|
return arr.filter((item) => item != null);
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
export function arraysDiff(oldArray, newArray) {
|
|
7
|
+
return {
|
|
8
|
+
added: newArray.filter(
|
|
9
|
+
(newItem) => !oldArray.includes(newItem)
|
|
10
|
+
),
|
|
11
|
+
removed: oldArray.filter(
|
|
12
|
+
(oldItem) => !newArray.includes(oldItem)
|
|
13
|
+
),
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export const ARRAY_DIFF_OP = {
|
|
18
|
+
ADD: 'add',
|
|
19
|
+
REMOVE: 'remove',
|
|
20
|
+
MOVE: 'move',
|
|
21
|
+
NOOP: 'noop'
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
class ArrayWithOriginalIndices {
|
|
25
|
+
#oldArray = [];
|
|
26
|
+
#originalIndices = [];
|
|
27
|
+
#equalsFn;
|
|
28
|
+
|
|
29
|
+
constructor(array, equalsFn) {
|
|
30
|
+
this.#oldArray = [...array];
|
|
31
|
+
this.#originalIndices = array.map((value, index) => index);
|
|
32
|
+
this.#equalsFn = equalsFn;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
get length() {
|
|
36
|
+
return this.#oldArray.length;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
originalIndexAt(index) {
|
|
40
|
+
return this.#originalIndices[index];
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// REMOVE
|
|
44
|
+
isRemoval(index, newArray) {
|
|
45
|
+
// Bail out if index is past the end of the old array.
|
|
46
|
+
if (index >= this.length) { return false; }
|
|
47
|
+
|
|
48
|
+
const item = this.#oldArray[index];
|
|
49
|
+
const indexInNewArray = newArray.findIndex((newArrayItem) => this.#equalsFn(item, newArrayItem))
|
|
50
|
+
|
|
51
|
+
return indexInNewArray === -1;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
removeItem(index) {
|
|
55
|
+
const operation = {
|
|
56
|
+
op: ARRAY_DIFF_OP.REMOVE,
|
|
57
|
+
index,
|
|
58
|
+
item: this.#oldArray[index],
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
this.#oldArray.splice(index, 1);
|
|
62
|
+
this.#originalIndices.splice(index, 1);
|
|
63
|
+
|
|
64
|
+
return operation;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// NOOP
|
|
68
|
+
isNoop(index, newArray) {
|
|
69
|
+
// Bail out if index is past the end of the old array.
|
|
70
|
+
if (index >= this.length) {return false; }
|
|
71
|
+
|
|
72
|
+
const item = this.#oldArray[index];
|
|
73
|
+
const newArrayItem = newArray[index];
|
|
74
|
+
|
|
75
|
+
return this.#equalsFn(item, newArrayItem);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
noopItem(index) {
|
|
79
|
+
return {
|
|
80
|
+
op: ARRAY_DIFF_OP.NOOP,
|
|
81
|
+
originalIndex: this.originalIndexAt(index),
|
|
82
|
+
index,
|
|
83
|
+
item: this.#oldArray[index],
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// ADD
|
|
88
|
+
isAddition(item, fromIndex) {
|
|
89
|
+
return this.findFromIndex(item, fromIndex) === -1;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
findFromIndex(item, fromIndex) {
|
|
93
|
+
for (let i = fromIndex; i < this.length; i++) {
|
|
94
|
+
if (this.#equalsFn(item, this.#oldArray[i])) {
|
|
95
|
+
return i;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return -1;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
addItem(item, index) {
|
|
103
|
+
const operation = {
|
|
104
|
+
op: ARRAY_DIFF_OP.ADD,
|
|
105
|
+
index,
|
|
106
|
+
item
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
this.#oldArray.splice(index, 0, item);
|
|
110
|
+
this.#originalIndices.splice(index, 0, -1);
|
|
111
|
+
|
|
112
|
+
return operation;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// MOVE
|
|
116
|
+
moveItem(item, toIndex) {
|
|
117
|
+
const fromIndex = this.findFromIndex(item, toIndex);
|
|
118
|
+
|
|
119
|
+
const operation = {
|
|
120
|
+
op: ARRAY_DIFF_OP.MOVE,
|
|
121
|
+
originalIndex: this.originalIndexAt(fromIndex),
|
|
122
|
+
from: fromIndex,
|
|
123
|
+
index: toIndex,
|
|
124
|
+
item: this.#oldArray[fromIndex],
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const [itemToMove] = this.#oldArray.splice(fromIndex, 1);
|
|
128
|
+
this.#oldArray.splice(toIndex, 0, itemToMove);
|
|
129
|
+
|
|
130
|
+
const [originalIndex] = this.#originalIndices.splice(fromIndex, 1);
|
|
131
|
+
this.#originalIndices.splice(toIndex, 0, originalIndex);
|
|
132
|
+
|
|
133
|
+
return operation;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// REMOVE ITEMS AFTER
|
|
137
|
+
removeItemsAfter(index) {
|
|
138
|
+
const operations = [];
|
|
139
|
+
|
|
140
|
+
while (this.length > index) {
|
|
141
|
+
operations.push(this.removeItem(index))
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
return operations;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
export function arraysDiffSequence(oldArray, newArray, equalsFn = (a, b) => a === b) {
|
|
149
|
+
const sequence = [];
|
|
150
|
+
|
|
151
|
+
// Store old array with original indices preserved.
|
|
152
|
+
const array = new ArrayWithOriginalIndices(oldArray, equalsFn);
|
|
153
|
+
|
|
154
|
+
for (let index = 0; index < newArray.length; index++) {
|
|
155
|
+
if (array.isRemoval(index, newArray)) {
|
|
156
|
+
sequence.push(array.removeItem(index));
|
|
157
|
+
index--
|
|
158
|
+
continue;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
if (array.isNoop(index, newArray)) {
|
|
162
|
+
sequence.push(array.noopItem(index));
|
|
163
|
+
continue;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
const item = newArray[index];
|
|
167
|
+
|
|
168
|
+
if (array.isAddition(item, index)) {
|
|
169
|
+
sequence.push(array.addItem(item, index));
|
|
170
|
+
continue;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
sequence.push(array.moveItem(item, index));
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
sequence.push(...array.removeItemsAfter(newArray.length));
|
|
177
|
+
|
|
178
|
+
return sequence;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
export function applyArraysDiffSequence(oldArray, diffSequence) {
|
|
182
|
+
return diffSequence.reduce((array, { op, item, index, from }) => {
|
|
183
|
+
switch (op) {
|
|
184
|
+
// Insert an item at the specified index.
|
|
185
|
+
case 'add':
|
|
186
|
+
array.splice(index, 0, item)
|
|
187
|
+
break
|
|
188
|
+
|
|
189
|
+
// Remove an element at the specified index.
|
|
190
|
+
case 'remove':
|
|
191
|
+
array.splice(index, 1)
|
|
192
|
+
break
|
|
193
|
+
|
|
194
|
+
// Remove element at index 'from' and move it to specified index.
|
|
195
|
+
case 'move':
|
|
196
|
+
array.splice(index, 0, array.splice(from, 1)[0])
|
|
197
|
+
break
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
return array
|
|
201
|
+
}, oldArray)
|
|
4
202
|
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export function objectsDiff(oldObj, newObj) {
|
|
2
|
+
const oldKeys = Object.keys(oldObj);
|
|
3
|
+
const newKeys = Object.keys(newObj);
|
|
4
|
+
|
|
5
|
+
return {
|
|
6
|
+
added: newKeys.filter((newKey) => !(newKey in oldObj)),
|
|
7
|
+
removed: oldKeys.filter((oldKey) => !(oldKey in newObj)),
|
|
8
|
+
updated: newKeys.filter(
|
|
9
|
+
(key) => key in oldObj && oldObj[key] !== newObj[key]
|
|
10
|
+
),
|
|
11
|
+
}
|
|
12
|
+
}
|