betal-fe 1.0.0 → 2.1.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/dist/betal-fe.js +360 -16
- package/package.json +1 -1
package/dist/betal-fe.js
CHANGED
|
@@ -15,9 +15,143 @@ function removeEventListeners(listeners = {}, el) {
|
|
|
15
15
|
});
|
|
16
16
|
}
|
|
17
17
|
|
|
18
|
+
const ARRAY_DIFF_OP = {
|
|
19
|
+
ADD: "add",
|
|
20
|
+
REMOVE: "remove",
|
|
21
|
+
MOVE: "move",
|
|
22
|
+
NOOP: "noop",
|
|
23
|
+
};
|
|
18
24
|
function withoutNulls(arr) {
|
|
19
25
|
return arr.filter((item) => item != null);
|
|
20
26
|
}
|
|
27
|
+
function arraysDiff(oldArray, newArray) {
|
|
28
|
+
return {
|
|
29
|
+
added: newArray.filter((newItem) => !oldArray.includes(newItem)),
|
|
30
|
+
removed: oldArray.filter((oldItem) => !newArray.includes(oldItem)),
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
class ArrayWithOriginalIndices {
|
|
34
|
+
#array = [];
|
|
35
|
+
#originalIndices = [];
|
|
36
|
+
#equalsFn;
|
|
37
|
+
constructor(array, equalsFn) {
|
|
38
|
+
this.#array = [...array];
|
|
39
|
+
this.#originalIndices = array.map((_, i) => i);
|
|
40
|
+
this.#equalsFn = equalsFn;
|
|
41
|
+
}
|
|
42
|
+
get length() {
|
|
43
|
+
return this.#array.length;
|
|
44
|
+
}
|
|
45
|
+
originalIndexAt(index) {
|
|
46
|
+
return this.#originalIndices[index];
|
|
47
|
+
}
|
|
48
|
+
isRemoval(index, newArray) {
|
|
49
|
+
if (index >= this.length) {
|
|
50
|
+
return false;
|
|
51
|
+
}
|
|
52
|
+
const item = this.#array[index];
|
|
53
|
+
const indexInNewArray = newArray.findIndex((newItem) =>
|
|
54
|
+
this.#equalsFn(item, newItem)
|
|
55
|
+
);
|
|
56
|
+
return indexInNewArray === -1;
|
|
57
|
+
}
|
|
58
|
+
removeItem(index) {
|
|
59
|
+
const operation = {
|
|
60
|
+
op: ARRAY_DIFF_OP.REMOVE,
|
|
61
|
+
index,
|
|
62
|
+
item: this.#array[index],
|
|
63
|
+
};
|
|
64
|
+
this.#array.splice(index, 1);
|
|
65
|
+
this.#originalIndices.splice(index, 1);
|
|
66
|
+
return operation;
|
|
67
|
+
}
|
|
68
|
+
isNoop(index, newArray) {
|
|
69
|
+
if (index >= this.length) {
|
|
70
|
+
return false;
|
|
71
|
+
}
|
|
72
|
+
const item = this.#array[index];
|
|
73
|
+
const newItem = newArray[index];
|
|
74
|
+
return this.#equalsFn(item, newItem);
|
|
75
|
+
}
|
|
76
|
+
noopItem(index) {
|
|
77
|
+
return {
|
|
78
|
+
op: ARRAY_DIFF_OP.NOOP,
|
|
79
|
+
originalIndex: this.originalIndexAt(index),
|
|
80
|
+
index,
|
|
81
|
+
item: this.#array[index],
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
findIndexFrom(item, fromIndex) {
|
|
85
|
+
for (let i = fromIndex; i < this.length; i++) {
|
|
86
|
+
if (this.#equalsFn(item, this.#array[i])) {
|
|
87
|
+
return i;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
return -1;
|
|
91
|
+
}
|
|
92
|
+
isAddition(item, fromIdx) {
|
|
93
|
+
return this.findIndexFrom(item, fromIdx) === -1;
|
|
94
|
+
}
|
|
95
|
+
addItem(item, index) {
|
|
96
|
+
const operation = {
|
|
97
|
+
op: ARRAY_DIFF_OP.ADD,
|
|
98
|
+
index,
|
|
99
|
+
item,
|
|
100
|
+
};
|
|
101
|
+
this.#array.splice(index, 0, item);
|
|
102
|
+
this.#originalIndices.splice(index, 0, -1);
|
|
103
|
+
return operation;
|
|
104
|
+
}
|
|
105
|
+
moveItem(item, toIndex) {
|
|
106
|
+
const fromIndex = this.findIndexFrom(item, toIndex);
|
|
107
|
+
const operation = {
|
|
108
|
+
op: ARRAY_DIFF_OP.MOVE,
|
|
109
|
+
originalIndex: this.originalIndexAt(fromIndex),
|
|
110
|
+
from: fromIndex,
|
|
111
|
+
index: toIndex,
|
|
112
|
+
item: this.#array[fromIndex],
|
|
113
|
+
};
|
|
114
|
+
const [_item] = this.#array.splice(fromIndex, 1);
|
|
115
|
+
this.#array.splice(toIndex, 0, _item);
|
|
116
|
+
const [originalIndex] = this.#originalIndices.splice(fromIndex, 1);
|
|
117
|
+
this.#originalIndices.splice(toIndex, 0, originalIndex);
|
|
118
|
+
return operation;
|
|
119
|
+
}
|
|
120
|
+
removeItemsAfter(index) {
|
|
121
|
+
const operations = [];
|
|
122
|
+
while (this.length > index) {
|
|
123
|
+
operations.push(this.removeItem(index));
|
|
124
|
+
}
|
|
125
|
+
return operations;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
function arraysDiffSequence(
|
|
129
|
+
oldArray,
|
|
130
|
+
newArray,
|
|
131
|
+
equalsFn = (a, b) => a === b
|
|
132
|
+
) {
|
|
133
|
+
const sequence = [];
|
|
134
|
+
const array = new ArrayWithOriginalIndices(oldArray, equalsFn);
|
|
135
|
+
for (let index = 0; index < newArray.length; index++) {
|
|
136
|
+
if (array.isRemoval(index, newArray)) {
|
|
137
|
+
sequence.push(array.removeItem(index));
|
|
138
|
+
index--;
|
|
139
|
+
continue;
|
|
140
|
+
}
|
|
141
|
+
if (array.isNoop(index, newArray)) {
|
|
142
|
+
sequence.push(array.noopItem(index));
|
|
143
|
+
continue;
|
|
144
|
+
}
|
|
145
|
+
const item = newArray[index];
|
|
146
|
+
if (array.isAddition(item, index)) {
|
|
147
|
+
sequence.push(array.addItem(item, index));
|
|
148
|
+
continue;
|
|
149
|
+
}
|
|
150
|
+
sequence.push(array.moveItem(item, index));
|
|
151
|
+
}
|
|
152
|
+
sequence.push(...array.removeItemsAfter(newArray.length));
|
|
153
|
+
return sequence;
|
|
154
|
+
}
|
|
21
155
|
|
|
22
156
|
const DOM_TYPES = {
|
|
23
157
|
TEXT: "text",
|
|
@@ -46,6 +180,20 @@ function hFragment(vNodes) {
|
|
|
46
180
|
children: mapTextNodes(withoutNulls(vNodes)),
|
|
47
181
|
};
|
|
48
182
|
}
|
|
183
|
+
function extractChildren(vdom) {
|
|
184
|
+
if (vdom.children == null) {
|
|
185
|
+
return [];
|
|
186
|
+
}
|
|
187
|
+
const children = [];
|
|
188
|
+
for (const child of vdom.children) {
|
|
189
|
+
if (child.type === DOM_TYPES.FRAGMENT) {
|
|
190
|
+
children.push(...extractChildren(child));
|
|
191
|
+
} else {
|
|
192
|
+
children.push(child);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
return children;
|
|
196
|
+
}
|
|
49
197
|
|
|
50
198
|
function destroyDOM(vDom) {
|
|
51
199
|
const { type } = vDom;
|
|
@@ -146,6 +294,9 @@ function setClass(el, className) {
|
|
|
146
294
|
function setStyle(el, prop, value) {
|
|
147
295
|
el.style[prop] = value;
|
|
148
296
|
}
|
|
297
|
+
function removeStyle(el, prop) {
|
|
298
|
+
el.style[prop] = null;
|
|
299
|
+
}
|
|
149
300
|
function setAttribute(el, name, value) {
|
|
150
301
|
if (value == null) {
|
|
151
302
|
removeAttribute(el, name);
|
|
@@ -160,18 +311,18 @@ function removeAttribute(el, name) {
|
|
|
160
311
|
el.removeAttribute(name);
|
|
161
312
|
}
|
|
162
313
|
|
|
163
|
-
function mountDOM(vDom, parentElement) {
|
|
314
|
+
function mountDOM(vDom, parentElement, index) {
|
|
164
315
|
switch (vDom.type) {
|
|
165
316
|
case DOM_TYPES.TEXT: {
|
|
166
|
-
createTextNode(vDom, parentElement);
|
|
317
|
+
createTextNode(vDom, parentElement, index);
|
|
167
318
|
break;
|
|
168
319
|
}
|
|
169
320
|
case DOM_TYPES.ELEMENT: {
|
|
170
|
-
createElementNode(vDom, parentElement);
|
|
321
|
+
createElementNode(vDom, parentElement, index);
|
|
171
322
|
break;
|
|
172
323
|
}
|
|
173
324
|
case DOM_TYPES.FRAGMENT: {
|
|
174
|
-
createFragmentNode(vDom, parentElement);
|
|
325
|
+
createFragmentNode(vDom, parentElement, index);
|
|
175
326
|
break;
|
|
176
327
|
}
|
|
177
328
|
default: {
|
|
@@ -179,30 +330,222 @@ function mountDOM(vDom, parentElement) {
|
|
|
179
330
|
}
|
|
180
331
|
}
|
|
181
332
|
}
|
|
182
|
-
function createTextNode(vDom, parentElement) {
|
|
333
|
+
function createTextNode(vDom, parentElement, index) {
|
|
183
334
|
const { value } = vDom;
|
|
184
335
|
const textNode = document.createTextNode(value);
|
|
185
336
|
vDom.el = textNode;
|
|
186
|
-
|
|
337
|
+
insert(textNode, parentElement, index);
|
|
187
338
|
}
|
|
188
|
-
function createFragmentNode(vDom, parentElement) {
|
|
339
|
+
function createFragmentNode(vDom, parentElement, index) {
|
|
189
340
|
const { children } = vDom;
|
|
190
341
|
vDom.el = parentElement;
|
|
191
|
-
children.forEach((child) => mountDOM(child, parentElement));
|
|
342
|
+
children.forEach((child) => mountDOM(child, parentElement, index ? index + 1 : null));
|
|
192
343
|
}
|
|
193
|
-
function createElementNode(vDom, parentElement) {
|
|
344
|
+
function createElementNode(vDom, parentElement, index) {
|
|
194
345
|
const { tag, props, children } = vDom;
|
|
195
346
|
const element = document.createElement(tag);
|
|
196
347
|
addProps(element, props, vDom);
|
|
197
348
|
vDom.el = element;
|
|
198
349
|
children.forEach((child) => mountDOM(child, element));
|
|
199
|
-
|
|
350
|
+
insert(element, parentElement, index);
|
|
200
351
|
}
|
|
201
352
|
function addProps(el, props, vdom) {
|
|
202
353
|
const { on: events, ...attrs } = props;
|
|
203
354
|
vdom.listeners = addEventListeners(events, el);
|
|
204
355
|
setAttributes(el, attrs);
|
|
205
356
|
}
|
|
357
|
+
function insert(el, parentEl, index) {
|
|
358
|
+
if (index == null) {
|
|
359
|
+
parentEl.append(el);
|
|
360
|
+
return;
|
|
361
|
+
}
|
|
362
|
+
if (index < 0) {
|
|
363
|
+
throw new Error(`Index must be a positive integer, got ${index}`);
|
|
364
|
+
}
|
|
365
|
+
const children = parentEl.childNodes;
|
|
366
|
+
if (index >= children.length) {
|
|
367
|
+
parentEl.append(el);
|
|
368
|
+
} else {
|
|
369
|
+
parentEl.insertBefore(el, children[index]);
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
function areNodesEqual(nodeOne, nodeTwo) {
|
|
374
|
+
if (nodeOne.type !== nodeTwo.type) {
|
|
375
|
+
return false;
|
|
376
|
+
}
|
|
377
|
+
if (nodeOne.type === DOM_TYPES.ELEMENT) {
|
|
378
|
+
const { tag: tagOne } = nodeOne;
|
|
379
|
+
const { tag: tagTwo } = nodeTwo;
|
|
380
|
+
return tagOne === tagTwo;
|
|
381
|
+
}
|
|
382
|
+
return true;
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
function objectsDiff(oldObj, newObj) {
|
|
386
|
+
const oldKeys = Object.keys(oldObj);
|
|
387
|
+
const newKeys = Object.keys(newObj);
|
|
388
|
+
const added = [];
|
|
389
|
+
const updated = [];
|
|
390
|
+
newKeys.forEach(key => {
|
|
391
|
+
if (!(key in oldObj)) {
|
|
392
|
+
added.push(key);
|
|
393
|
+
}
|
|
394
|
+
if (key in oldObj && oldObj[key] !== newObj[key]) {
|
|
395
|
+
updated.push(key);
|
|
396
|
+
}
|
|
397
|
+
});
|
|
398
|
+
return {
|
|
399
|
+
added,
|
|
400
|
+
removed: oldKeys.filter((key) => !(key in newObj)),
|
|
401
|
+
updated,
|
|
402
|
+
};
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
function isNotEmptyString(str) {
|
|
406
|
+
return str !== ''
|
|
407
|
+
}
|
|
408
|
+
function isNotBlankOrEmptyString(str) {
|
|
409
|
+
return isNotEmptyString(str.trim())
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
function patchDOM(oldVdom, newVdom, parentEl) {
|
|
413
|
+
if (!areNodesEqual(oldVdom, newVdom)) {
|
|
414
|
+
const index = findIndexInParent(parentEl, oldVdom.el);
|
|
415
|
+
destroyDOM(oldVdom);
|
|
416
|
+
mountDOM(newVdom, parentEl, index);
|
|
417
|
+
return newVdom;
|
|
418
|
+
}
|
|
419
|
+
newVdom.el = oldVdom.el;
|
|
420
|
+
switch (newVdom.type) {
|
|
421
|
+
case DOM_TYPES.TEXT: {
|
|
422
|
+
patchText(oldVdom, newVdom);
|
|
423
|
+
return newVdom;
|
|
424
|
+
}
|
|
425
|
+
case DOM_TYPES.ELEMENT: {
|
|
426
|
+
patchElement(oldVdom, newVdom);
|
|
427
|
+
break;
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
patchChildren(oldVdom, newVdom);
|
|
431
|
+
return newVdom;
|
|
432
|
+
}
|
|
433
|
+
function findIndexInParent(parentEl, el) {
|
|
434
|
+
const index = Array.from(parentEl.childNodes).indexOf(el);
|
|
435
|
+
if (index < 0) {
|
|
436
|
+
return null;
|
|
437
|
+
}
|
|
438
|
+
return index;
|
|
439
|
+
}
|
|
440
|
+
function patchText(oldVdom, newVdom) {
|
|
441
|
+
const { value: oldText } = oldVdom;
|
|
442
|
+
const { value: newText } = newVdom;
|
|
443
|
+
if (oldText !== newText) {
|
|
444
|
+
newVdom.el.nodeValue = newText;
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
function patchElement(oldVdom, newVdom) {
|
|
448
|
+
const el = oldVdom.el;
|
|
449
|
+
const {
|
|
450
|
+
class: oldClass,
|
|
451
|
+
style: oldStyle,
|
|
452
|
+
on: oldEvents,
|
|
453
|
+
...oldAttrs
|
|
454
|
+
} = oldVdom.props;
|
|
455
|
+
const {
|
|
456
|
+
class: newClass,
|
|
457
|
+
style: newStyle,
|
|
458
|
+
on: newEvents,
|
|
459
|
+
...newAttrs
|
|
460
|
+
} = newVdom.props;
|
|
461
|
+
const { listeners: oldListeners } = oldVdom;
|
|
462
|
+
patchAttrs(el, oldAttrs, newAttrs);
|
|
463
|
+
patchClasses(el, oldClass, newClass);
|
|
464
|
+
patchStyles(el, oldStyle, newStyle);
|
|
465
|
+
newVdom.listeners = patchEvents(el, oldListeners, oldEvents, newEvents);
|
|
466
|
+
}
|
|
467
|
+
function patchAttrs(el, oldAttrs, newAttrs) {
|
|
468
|
+
const { added, removed, updated } = objectsDiff(oldAttrs, newAttrs);
|
|
469
|
+
for (const attr of removed) {
|
|
470
|
+
removeAttribute(el, attr);
|
|
471
|
+
}
|
|
472
|
+
for (const attr of added.concat(updated)) {
|
|
473
|
+
setAttribute(el, attr, newAttrs[attr]);
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
function patchClasses(el, oldClass, newClass) {
|
|
477
|
+
const oldClasses = toClassList(oldClass);
|
|
478
|
+
const newClasses = toClassList(newClass);
|
|
479
|
+
const { added, removed } = arraysDiff(oldClasses, newClasses);
|
|
480
|
+
if (removed.length > 0) {
|
|
481
|
+
el.classList.remove(...removed);
|
|
482
|
+
}
|
|
483
|
+
if (added.length > 0) {
|
|
484
|
+
el.classList.add(...added);
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
function toClassList(classes = "") {
|
|
488
|
+
return Array.isArray(classes)
|
|
489
|
+
? classes.filter(isNotBlankOrEmptyString)
|
|
490
|
+
: classes.split(/(\s+)/).filter(isNotBlankOrEmptyString);
|
|
491
|
+
}
|
|
492
|
+
function patchStyles(el, oldStyle = {}, newStyle = {}) {
|
|
493
|
+
const { added, removed, updated } = objectsDiff(oldStyle, newStyle);
|
|
494
|
+
for (const style of removed) {
|
|
495
|
+
removeStyle(el, style);
|
|
496
|
+
}
|
|
497
|
+
for (const style of added.concat(updated)) {
|
|
498
|
+
setStyle(el, style, newStyle[style]);
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
function patchEvents(el, oldListeners = {}, oldEvents = {}, newEvents = {}) {
|
|
502
|
+
const { removed, added, updated } = objectsDiff(oldEvents, newEvents);
|
|
503
|
+
for (const eventName of removed.concat(updated)) {
|
|
504
|
+
el.removeEventListener(eventName, oldListeners[eventName]);
|
|
505
|
+
}
|
|
506
|
+
const addedListeners = {};
|
|
507
|
+
for (const eventName of added.concat(updated)) {
|
|
508
|
+
const listener = addEventListener(eventName, newEvents[eventName], el);
|
|
509
|
+
addedListeners[eventName] = listener;
|
|
510
|
+
}
|
|
511
|
+
return addedListeners;
|
|
512
|
+
}
|
|
513
|
+
function patchChildren(oldVdom, newVdom) {
|
|
514
|
+
const oldChildren = extractChildren(oldVdom);
|
|
515
|
+
const newChildren = extractChildren(newVdom);
|
|
516
|
+
const parentEl = oldVdom.el;
|
|
517
|
+
const diffSeq = arraysDiffSequence(
|
|
518
|
+
oldChildren,
|
|
519
|
+
newChildren,
|
|
520
|
+
areNodesEqual
|
|
521
|
+
);
|
|
522
|
+
for (const operation of diffSeq) {
|
|
523
|
+
const { originalIndex, index, item } = operation;
|
|
524
|
+
switch (operation.op) {
|
|
525
|
+
case ARRAY_DIFF_OP.ADD: {
|
|
526
|
+
mountDOM(item, parentEl, index);
|
|
527
|
+
break;
|
|
528
|
+
}
|
|
529
|
+
case ARRAY_DIFF_OP.REMOVE: {
|
|
530
|
+
destroyDOM(item);
|
|
531
|
+
break;
|
|
532
|
+
}
|
|
533
|
+
case ARRAY_DIFF_OP.MOVE: {
|
|
534
|
+
const oldChild = oldChildren[originalIndex];
|
|
535
|
+
const newChild = newChildren[index];
|
|
536
|
+
const el = oldChild.el;
|
|
537
|
+
const elAtTargetIndex = parentEl.childNodes[index];
|
|
538
|
+
parentEl.insertBefore(el, elAtTargetIndex);
|
|
539
|
+
patchDOM(oldChild, newChild, parentEl);
|
|
540
|
+
break;
|
|
541
|
+
}
|
|
542
|
+
case ARRAY_DIFF_OP.NOOP: {
|
|
543
|
+
patchDOM(oldChildren[originalIndex], newChildren[index], parentEl);
|
|
544
|
+
break;
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
}
|
|
206
549
|
|
|
207
550
|
function createApp({ state, view, reducers = {} }) {
|
|
208
551
|
let parentEl = null;
|
|
@@ -220,16 +563,17 @@ function createApp({ state, view, reducers = {} }) {
|
|
|
220
563
|
subscriptions.push(subs);
|
|
221
564
|
}
|
|
222
565
|
function renderApp() {
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
}
|
|
226
|
-
vdom = view(state, emit);
|
|
227
|
-
mountDOM(vdom, parentEl);
|
|
566
|
+
const newVdom = view(state, emit);
|
|
567
|
+
vdom = patchDOM(vdom, newVdom, parentEl);
|
|
228
568
|
}
|
|
229
569
|
return {
|
|
230
570
|
mount(_parentEl) {
|
|
571
|
+
if (vdom) {
|
|
572
|
+
throw new Error('App is already mounted');
|
|
573
|
+
}
|
|
231
574
|
parentEl = _parentEl;
|
|
232
|
-
|
|
575
|
+
vdom = view(state, emit);
|
|
576
|
+
mountDOM(vdom, parentEl);
|
|
233
577
|
},
|
|
234
578
|
unmount() {
|
|
235
579
|
destroyDOM(vdom);
|