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.
@@ -1,4 +1,4 @@
1
- import {controllerFactory, runControlFlowFunctions, tagList} from "./sdc_controller.js";
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 {jquery} $container - jQuery container
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
- export function reconcile($virtualNode, $realNode, controller) {
342
- // Step 1: Check node types (e.g., div vs span)
343
- if ($virtualNode[0].nodeType !== $realNode[0].nodeType ||
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 ($virtualNode[0].nodeType === 3) {
350
- if ($virtualNode[0].nodeValue !== $realNode[0].nodeValue) {
351
- $realNode[0].nodeValue = $virtualNode[0].nodeValue;
352
- }
353
- return;
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
- // Step 2: Compare attributes
357
- syncAttributes($realNode, $virtualNode);
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
- // Step 3: Recurse children
360
- const virtualChildren = $virtualNode.contents();
361
- const realChildren = $realNode.contents();
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
- if (key !== DATA_CONTROLLER_KEY) {
394
- $real.data(key, value);
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
  }
@@ -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
+ });