sdc_client 0.57.11 → 0.57.12
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/.idea/misc.xml +2 -2
- package/.idea/workspace.xml +133 -39
- package/SimpleDomControlClient.iml +5 -2
- package/dist/index.js +6 -6
- package/dist/ugly.index.js +1 -1
- package/package.json +1 -1
- package/src/simpleDomControl/AbstractSDC.js +5 -0
- package/src/simpleDomControl/sdc_main.js +10 -8
- package/src/simpleDomControl/sdc_socket.js +24 -6
- package/src/simpleDomControl/sdc_test_utils.js +34 -10
- package/src/simpleDomControl/sdc_utils.js +45 -23
- package/src/simpleDomControl/sdc_view.js +162 -30
- package/test/controller.test.js +1 -41
- package/test/utils.js +89 -0
- package/test/view.test.js +141 -0
@@ -1,4 +1,4 @@
|
|
1
|
-
import {controllerFactory, runControlFlowFunctions
|
1
|
+
import {controllerFactory, runControlFlowFunctions} from "./sdc_controller.js";
|
2
2
|
import {getUrlParam} from "./sdc_params.js";
|
3
3
|
import {app} from "./sdc_main.js";
|
4
4
|
import {trigger} from "./sdc_events.js";
|
@@ -23,7 +23,7 @@ export function cleanCache() {
|
|
23
23
|
* doms and returns a list of objects containing also the tag name the dom and the tag
|
24
24
|
* names of the super controller
|
25
25
|
*
|
26
|
-
* @param {
|
26
|
+
* @param {$} $container - jQuery container
|
27
27
|
* @param {Array<string>} tagNameList - a string list with tag names.
|
28
28
|
* @param {AbstractSDC} parentController - controller in surrounding
|
29
29
|
* @return {Array} - a array of objects with all register tags found
|
@@ -338,38 +338,86 @@ function _reloadMethodHTML(controller, $dom) {
|
|
338
338
|
}
|
339
339
|
|
340
340
|
|
341
|
-
|
342
|
-
|
343
|
-
|
344
|
-
$virtualNode.prop("nodeName") !== $realNode.prop("nodeName")) {
|
345
|
-
$realNode.safeReplace($virtualNode);
|
346
|
-
return;
|
341
|
+
function getNodeKey(node) {
|
342
|
+
if (node[0].nodeType === 3) {
|
343
|
+
return `TEXT__${node[0].nodeValue}`;
|
347
344
|
}
|
348
|
-
|
349
|
-
if (
|
350
|
-
|
351
|
-
|
352
|
-
|
353
|
-
|
345
|
+
const res = [node[0].tagName];
|
346
|
+
if (node[0].nodeName === 'INPUT') {
|
347
|
+
[['name', ''], ['type', 'text'], ['id', '']].forEach(([key, defaultValue]) => {
|
348
|
+
const attr = node.attr(key) ?? defaultValue;
|
349
|
+
if (attr) {
|
350
|
+
res.push(attr);
|
351
|
+
}
|
352
|
+
});
|
354
353
|
}
|
354
|
+
return res.join('__');
|
355
|
+
}
|
355
356
|
|
356
|
-
|
357
|
-
|
357
|
+
function reconcileTree($element, id = [], parent = null) {
|
358
|
+
id.push(getNodeKey($element));
|
359
|
+
const obj = {
|
360
|
+
$element,
|
361
|
+
id: id.join('::'),
|
362
|
+
depth: id.length,
|
363
|
+
idx: 0,
|
364
|
+
op: null,
|
365
|
+
parent
|
366
|
+
};
|
367
|
+
return [obj].concat($element.contents().toArray().map((x) => reconcileTree($(x), id.slice(), obj)).flat());
|
358
368
|
|
359
|
-
|
360
|
-
|
361
|
-
|
369
|
+
}
|
370
|
+
|
371
|
+
|
372
|
+
export function reconcile($virtualNode, $realNode) {
|
373
|
+
const $old = reconcileTree($realNode);
|
374
|
+
const $new = reconcileTree($virtualNode);
|
375
|
+
$old.map((x, i) => x.idx = i);
|
376
|
+
$new.map((x, i) => x.idx = i);
|
377
|
+
const depth = Math.max(...$new.concat($old).map(x => x.depth));
|
378
|
+
const op_steps = lcbDiff($old, $new, depth);
|
379
|
+
let opIdx = 0;
|
380
|
+
let toRemove = [];
|
381
|
+
|
382
|
+
op_steps.forEach(op_step => {
|
383
|
+
const {op, $element, idx} = op_step;
|
384
|
+
if (op.type === 'keep_counterpart') {
|
385
|
+
|
386
|
+
if (op.counterpart.idx + opIdx !== idx) {
|
387
|
+
const elemBefore = op_step.getBefore();
|
388
|
+
if (!elemBefore) {
|
389
|
+
op_step.getRealParent().$element.prepend(op.counterpart.$element);
|
390
|
+
} else {
|
391
|
+
op.counterpart.$element.insertAfter(elemBefore.$element);
|
392
|
+
}
|
393
|
+
}
|
394
|
+
|
395
|
+
syncAttributes(op.counterpart.$element, $element);
|
396
|
+
if ($element.hasClass(CONTROLLER_CLASS)) {
|
397
|
+
$element.data(DATA_CONTROLLER_KEY).$container = op.counterpart.$element;
|
398
|
+
$element.data(DATA_CONTROLLER_KEY, null);
|
399
|
+
}
|
400
|
+
|
401
|
+
toRemove.push($element);
|
402
|
+
|
403
|
+
} else if (op.type === 'delete') {
|
404
|
+
$element.safeRemove();
|
405
|
+
opIdx--;
|
406
|
+
} else if (op.type === 'insert_ignore') {
|
407
|
+
opIdx++;
|
408
|
+
} else if (op.type === 'insert') {
|
409
|
+
opIdx++;
|
410
|
+
const {after, target} = op_step.op;
|
411
|
+
if (after) {
|
412
|
+
$element.insertAfter(after.$element);
|
413
|
+
} else if (target) {
|
414
|
+
target.$element.prepend($element);
|
415
|
+
}
|
362
416
|
|
363
|
-
const len = Math.max(virtualChildren.length, realChildren.length);
|
364
|
-
for (let i = 0; i < len; i++) {
|
365
|
-
if (i >= realChildren.length) {
|
366
|
-
$realNode.append($(virtualChildren[i]));
|
367
|
-
} else if (i >= virtualChildren.length) {
|
368
|
-
$(realChildren[i]).safeRemove();
|
369
|
-
} else {
|
370
|
-
reconcile($(virtualChildren[i]), $(realChildren[i]), controller);
|
371
417
|
}
|
372
|
-
}
|
418
|
+
});
|
419
|
+
|
420
|
+
toRemove.forEach(($element) => $element.safeRemove());
|
373
421
|
}
|
374
422
|
|
375
423
|
function syncAttributes($real, $virtual) {
|
@@ -390,8 +438,92 @@ function syncAttributes($real, $virtual) {
|
|
390
438
|
});
|
391
439
|
|
392
440
|
Object.entries($virtual.data()).forEach(([key, value]) => {
|
393
|
-
|
394
|
-
|
441
|
+
$real.data(key, value);
|
442
|
+
});
|
443
|
+
}
|
444
|
+
|
445
|
+
/**
|
446
|
+
* LCB (Longest Common Branch) finds matching branches and reserves them!
|
447
|
+
*
|
448
|
+
* @param oldNodes
|
449
|
+
* @param newNodes
|
450
|
+
* @param depth
|
451
|
+
* @returns {*|*[]}
|
452
|
+
*/
|
453
|
+
function lcbDiff(oldNodes, newNodes, depth) {
|
454
|
+
newNodes.filter(x => x.depth === depth && !x.op).forEach((newNode) => {
|
455
|
+
const oldNode = oldNodes.find((tempOldNode) => {
|
456
|
+
return !tempOldNode.op && tempOldNode.id === newNode.id;
|
457
|
+
});
|
458
|
+
|
459
|
+
if (oldNode) {
|
460
|
+
const keepTreeBranch = (oldNode, newNode) => {
|
461
|
+
oldNode.op = {type: 'keep', idx: newNode.idx};
|
462
|
+
newNode.op = {type: 'keep_counterpart', counterpart: oldNode};
|
463
|
+
oldNode = oldNode.parent;
|
464
|
+
if (!oldNode || oldNode.op) {
|
465
|
+
return;
|
466
|
+
}
|
467
|
+
newNode = newNode.parent;
|
468
|
+
keepTreeBranch(oldNode, newNode);
|
469
|
+
|
470
|
+
}
|
471
|
+
keepTreeBranch(oldNode, newNode);
|
472
|
+
}
|
473
|
+
});
|
474
|
+
if (depth > 1) {
|
475
|
+
return lcbDiff(oldNodes, newNodes, depth - 1);
|
476
|
+
}
|
477
|
+
|
478
|
+
oldNodes.forEach((x, i) => {
|
479
|
+
if (!x.op) {
|
480
|
+
const idx = (oldNodes[i - 1]?.op.idx ?? -1) + 1;
|
481
|
+
x.op = {type: 'delete', idx}
|
482
|
+
}
|
483
|
+
});
|
484
|
+
|
485
|
+
function getRealParent(element) {
|
486
|
+
if (!element.parent) {
|
487
|
+
return null;
|
488
|
+
}
|
489
|
+
return element.parent.op.type === 'keep_counterpart' ? element.parent.op.counterpart : element.parent;
|
490
|
+
}
|
491
|
+
|
492
|
+
function getBefore(element, idx) {
|
493
|
+
const startDepth = element.depth;
|
494
|
+
while (idx >= 0 && element.depth >= startDepth) {
|
495
|
+
idx -= 1;
|
496
|
+
element = newNodes[idx];
|
497
|
+
if (element.depth === startDepth) {
|
498
|
+
return element.op.type === 'keep_counterpart' ? element.op.counterpart : element;
|
499
|
+
}
|
500
|
+
}
|
501
|
+
|
502
|
+
return null
|
503
|
+
|
504
|
+
}
|
505
|
+
|
506
|
+
newNodes.forEach((x, i) => {
|
507
|
+
x.getBefore = () => getBefore(x, i);
|
508
|
+
x.getRealParent = () => getRealParent(x);
|
509
|
+
|
510
|
+
if (!x.op) {
|
511
|
+
const target = x.getRealParent();
|
512
|
+
const type = target?.op.type === 'insert' ? 'insert_ignore' : 'insert';
|
513
|
+
x.op = {type, target, after: x.getBefore()}
|
395
514
|
}
|
396
515
|
});
|
516
|
+
|
517
|
+
const tagged = [
|
518
|
+
...oldNodes,
|
519
|
+
...newNodes,
|
520
|
+
];
|
521
|
+
|
522
|
+
|
523
|
+
return tagged.sort((a, b) => {
|
524
|
+
const aVal = a.op?.idx ?? a.idx;
|
525
|
+
const bVal = b.op?.idx ?? b.idx;
|
526
|
+
|
527
|
+
return aVal - bVal;
|
528
|
+
});
|
397
529
|
}
|
package/test/controller.test.js
CHANGED
@@ -9,49 +9,9 @@ import * as sdc_view from '../src/simpleDomControl/sdc_view.js';
|
|
9
9
|
const app = sdc.app;
|
10
10
|
|
11
11
|
import $ from 'jquery';
|
12
|
+
import {TestCtr, TestCtrA} from "./utils.js";
|
12
13
|
window.$ = $;
|
13
14
|
|
14
|
-
const TestControllerInfo = {
|
15
|
-
name: 'TestCtr',
|
16
|
-
tag: 'test-ctr'
|
17
|
-
};
|
18
|
-
|
19
|
-
class TestCtr extends sdc.AbstractSDC {
|
20
|
-
constructor() {
|
21
|
-
super();
|
22
|
-
this.contentUrl = TestControllerInfo.name; //<test-ctr>
|
23
|
-
this.events.unshift({});
|
24
|
-
this.val = 0;
|
25
|
-
this.contentReload = true;
|
26
|
-
}
|
27
|
-
|
28
|
-
sayA() {
|
29
|
-
return 'A'
|
30
|
-
}
|
31
|
-
|
32
|
-
onInit() {}
|
33
|
-
}
|
34
|
-
|
35
|
-
class TestCtrA extends sdc.AbstractSDC {
|
36
|
-
constructor() {
|
37
|
-
super();
|
38
|
-
this.contentUrl = 'TestCtrA'; //<test-ctr-a>
|
39
|
-
this.events.unshift({});
|
40
|
-
this.val = 1;
|
41
|
-
this.val_2 = 2;
|
42
|
-
}
|
43
|
-
|
44
|
-
sayA() {
|
45
|
-
return 'B'
|
46
|
-
}
|
47
|
-
|
48
|
-
sayB() {
|
49
|
-
return 'B'
|
50
|
-
}
|
51
|
-
|
52
|
-
onInit() {}
|
53
|
-
}
|
54
|
-
|
55
15
|
describe('Controller', () => {
|
56
16
|
let ajaxSpy;
|
57
17
|
beforeEach(() => {
|
package/test/utils.js
ADDED
@@ -0,0 +1,89 @@
|
|
1
|
+
import {AbstractSDC} from "../src/index.js";
|
2
|
+
|
3
|
+
|
4
|
+
export const TestControllerInfo = {
|
5
|
+
name: 'TestCtr',
|
6
|
+
tag: 'test-ctr'
|
7
|
+
};
|
8
|
+
|
9
|
+
export class TestCtr extends AbstractSDC {
|
10
|
+
constructor() {
|
11
|
+
super();
|
12
|
+
this.contentUrl = TestControllerInfo.name; //<test-ctr>
|
13
|
+
this.events.unshift({});
|
14
|
+
this.val = 0;
|
15
|
+
this.contentReload = true;
|
16
|
+
}
|
17
|
+
|
18
|
+
sayA() {
|
19
|
+
return 'A'
|
20
|
+
}
|
21
|
+
|
22
|
+
onInit() {
|
23
|
+
}
|
24
|
+
}
|
25
|
+
|
26
|
+
export class TestCtrA extends AbstractSDC {
|
27
|
+
constructor() {
|
28
|
+
super();
|
29
|
+
this.contentUrl = 'TestCtrA'; //<test-ctr-a>
|
30
|
+
this.events.unshift({});
|
31
|
+
this.val = 1;
|
32
|
+
this.val_2 = 2;
|
33
|
+
}
|
34
|
+
|
35
|
+
sayA() {
|
36
|
+
return 'B'
|
37
|
+
}
|
38
|
+
|
39
|
+
sayB() {
|
40
|
+
return 'B'
|
41
|
+
}
|
42
|
+
|
43
|
+
onInit() {
|
44
|
+
}
|
45
|
+
}
|
46
|
+
|
47
|
+
export class TestList extends AbstractSDC {
|
48
|
+
constructor() {
|
49
|
+
super();
|
50
|
+
this.contentUrl = 'TestCtrA'; //<test-ctr-a>
|
51
|
+
this.events.unshift({});
|
52
|
+
this.number = 0;
|
53
|
+
}
|
54
|
+
|
55
|
+
onInit(number = 10) {
|
56
|
+
this.number = number;
|
57
|
+
}
|
58
|
+
|
59
|
+
onLoad(html) {
|
60
|
+
$(html).append('<div><this.listview></this.listview></div>');
|
61
|
+
return super.onLoad(html);
|
62
|
+
}
|
63
|
+
|
64
|
+
listview() {
|
65
|
+
const listItems = [];
|
66
|
+
for (let i = 0; i < this.number; i++) {
|
67
|
+
listItems.push(`<test-item data-idx="${i}"></test-item>`);
|
68
|
+
}
|
69
|
+
|
70
|
+
return `<div>${listItems.join('\n')}</div>`;
|
71
|
+
}
|
72
|
+
}
|
73
|
+
|
74
|
+
export class TestItem extends AbstractSDC {
|
75
|
+
constructor() {
|
76
|
+
super();
|
77
|
+
this.contentUrl = 'TestCtrA'; //<test-item>
|
78
|
+
this.events.unshift({});
|
79
|
+
}
|
80
|
+
|
81
|
+
onInit(idx) {
|
82
|
+
this.idx = idx;
|
83
|
+
}
|
84
|
+
|
85
|
+
onLoad(html) {
|
86
|
+
$(html).append(`<input name="i_${this.idx}" />`);
|
87
|
+
return super.onLoad(html);
|
88
|
+
}
|
89
|
+
}
|
@@ -0,0 +1,141 @@
|
|
1
|
+
/**
|
2
|
+
* @jest-environment jsdom
|
3
|
+
*/
|
4
|
+
|
5
|
+
import {TestItem, TestList} from "./utils.js";
|
6
|
+
import {reconcile} from "../src/simpleDomControl/sdc_view.js";
|
7
|
+
import {app} from "../src/index.js";
|
8
|
+
import $ from "jquery";
|
9
|
+
|
10
|
+
window.$ = $;
|
11
|
+
|
12
|
+
describe('Test reconcile', () => {
|
13
|
+
|
14
|
+
beforeAll(() => {
|
15
|
+
app.updateJquery();
|
16
|
+
});
|
17
|
+
|
18
|
+
test('Load Content', async () => {
|
19
|
+
const a = '<div>' +
|
20
|
+
'<h1>Test</h1>' +
|
21
|
+
'<ul>' +
|
22
|
+
'<li>B</li>' +
|
23
|
+
'<li><input name="TEST" /></li>' +
|
24
|
+
'<li>D</li>' +
|
25
|
+
'</ul>' +
|
26
|
+
'</div>';
|
27
|
+
|
28
|
+
const b = '<div class="class.1">' +
|
29
|
+
'<p>UPS</p>' +
|
30
|
+
'<p>UPS</p>' +
|
31
|
+
'<h1>Test 1</h1>' +
|
32
|
+
'<ul>' +
|
33
|
+
'<li>A</li>' +
|
34
|
+
'<li>A1 <input name="TEST" type="text"/></li>' +
|
35
|
+
'<li>B</li>' +
|
36
|
+
'<li><input name="TEST" type="text"/></li>' +
|
37
|
+
'<li>D</li>' +
|
38
|
+
'</ul>' +
|
39
|
+
'</div>';
|
40
|
+
const $b = $(b);
|
41
|
+
const $a = $(a);
|
42
|
+
const input_a = $a.find('[name=TEST]')[0]
|
43
|
+
reconcile($b, $a);
|
44
|
+
expect($a.html()).toBe($(b).html());
|
45
|
+
expect($a[0].className).toBe('class.1');
|
46
|
+
expect(input_a).toBe($a.find('[name=TEST]')[0]);
|
47
|
+
|
48
|
+
});
|
49
|
+
|
50
|
+
test('Load Content 2', async () => {
|
51
|
+
const a = '<div>' +
|
52
|
+
'<p>UPS</p>' +
|
53
|
+
'<p>UPS</p>' +
|
54
|
+
'<h1>Test</h1>' +
|
55
|
+
'<ul>' +
|
56
|
+
'<li>B</li>' +
|
57
|
+
'<li><input name="TEST" /></li>' +
|
58
|
+
'<li>D</li>' +
|
59
|
+
'</ul>' +
|
60
|
+
'</div>';
|
61
|
+
|
62
|
+
const b = '<div class="class.1">' +
|
63
|
+
'<h1>Test 1</h1>' +
|
64
|
+
'<ul>' +
|
65
|
+
'<li>A</li>' +
|
66
|
+
'<li>A1 <input name="TEST" type="text"/></li>' +
|
67
|
+
'<li>B</li>' +
|
68
|
+
'<li><input name="TEST" type="text"/></li>' +
|
69
|
+
'<li>D</li>' +
|
70
|
+
'</ul>' +
|
71
|
+
'</div>';
|
72
|
+
const $b = $(b);
|
73
|
+
const $a = $(a);
|
74
|
+
const input_a = $a.find('[name=TEST]')[0]
|
75
|
+
reconcile($b, $a);
|
76
|
+
expect($a.html()).toBe($(b).html());
|
77
|
+
expect($a[0].className).toBe('class.1');
|
78
|
+
expect(input_a).toBe($a.find('[name=TEST]')[0]);
|
79
|
+
|
80
|
+
});
|
81
|
+
|
82
|
+
|
83
|
+
test('Load Content Split', async () => {
|
84
|
+
const a = '<div>' +
|
85
|
+
'<ul>' +
|
86
|
+
'<li>X<input name="TEST" /></li>' +
|
87
|
+
'</ul>' +
|
88
|
+
'</div>';
|
89
|
+
|
90
|
+
const b = '<div class="class.1">' +
|
91
|
+
'<ul>' +
|
92
|
+
'<li>A1 <input name="TEST" type="text"/></li>' +
|
93
|
+
'<li>X</li>' +
|
94
|
+
'</ul>' +
|
95
|
+
'</div>';
|
96
|
+
const $b = $(b);
|
97
|
+
const $a = $(a);
|
98
|
+
const input_a = $a.find('[name=TEST]')[0]
|
99
|
+
reconcile($b, $a);
|
100
|
+
expect($a.html()).toBe($(b).html());
|
101
|
+
expect($a[0].className).toBe('class.1');
|
102
|
+
expect(input_a).toBe($a.find('[name=TEST]')[0]);
|
103
|
+
|
104
|
+
});
|
105
|
+
});
|
106
|
+
|
107
|
+
describe('Controller reconcile', () => {
|
108
|
+
let ajaxSpy;
|
109
|
+
beforeEach(async () => {
|
110
|
+
ajaxSpy = jest.spyOn($, 'ajax');
|
111
|
+
ajaxSpy.mockImplementation(() => {
|
112
|
+
return Promise.resolve('<div></div>');
|
113
|
+
});
|
114
|
+
app.register(TestList);
|
115
|
+
app.register(TestItem);
|
116
|
+
const $body = $('body');
|
117
|
+
const $ctr_div = $(document.createElement('test-list'));
|
118
|
+
$body.append($ctr_div);
|
119
|
+
await app.init_sdc();
|
120
|
+
});
|
121
|
+
|
122
|
+
afterEach(() => {
|
123
|
+
jest.restoreAllMocks();
|
124
|
+
$('body').safeEmpty();
|
125
|
+
});
|
126
|
+
|
127
|
+
test('Load Content Split', async () => {
|
128
|
+
const oldList = $('body').find('input').toArray();
|
129
|
+
const controller = app.getController($('body').children());
|
130
|
+
controller.number = 5;
|
131
|
+
await controller.refresh();
|
132
|
+
await new Promise((resolve) => setTimeout(resolve, 1000))
|
133
|
+
const newList = $('body').find('input').toArray();
|
134
|
+
expect(newList.length).toBe(5);
|
135
|
+
newList.forEach((x, i) => {
|
136
|
+
expect(x).toBe(oldList[i]);
|
137
|
+
});
|
138
|
+
|
139
|
+
|
140
|
+
});
|
141
|
+
});
|