sdc_client 0.57.10 → 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.
@@ -56,6 +56,7 @@ export function tagNameToCamelCase(str) {
56
56
  str = str.replace(/-./g, letter => `${letter[1].toUpperCase()}`);
57
57
  return str;
58
58
  }
59
+
59
60
  export function tagNameToReadableName(str) {
60
61
  str = str.replace(/-./g, letter => ` ${letter[1].toUpperCase()}`).replace(/^./g, letter => `${letter.toUpperCase()}`);
61
62
  return str;
@@ -83,31 +84,42 @@ const copyProps = (targetClass, sourceClass) => {
83
84
 
84
85
  /**
85
86
  *
86
- * @param {AbstractSDC} baseClass
87
- * @param {AbstractSDC} mixins
87
+ * @param {typeof AbstractSDC} baseClass
88
+ * @param {typeof AbstractSDC} mixins
88
89
  * @returns {AbstractSDC}
89
90
  */
90
91
  export function agileAggregation(baseClass, ...mixins) {
91
92
 
92
- let base = class _Combined {
93
- constructor(..._args) {
94
- let _mixins = {};
95
- mixins.forEach((mixin) => {
96
- let newMixin;
97
- Object.assign(this, (newMixin = new mixin()));
98
- newMixin._tagName = mixin.prototype._tagName;
99
- newMixin._isMixin = true;
100
- _mixins[mixin.name] = newMixin;
101
- });
93
+ let base = {
94
+ [baseClass.name]: class {
95
+ constructor(..._args) {
96
+ let _mixins = {};
97
+ mixins.forEach((mixin) => {
98
+ let newMixin;
99
+ Object.assign(this, (newMixin = new mixin()));
100
+ newMixin._tagName = mixin.prototype._tagName;
101
+ newMixin._isMixin = true;
102
+ _mixins[mixin.name] = newMixin;
103
+ });
104
+
105
+ Object.assign(this, new baseClass());
106
+ this._mixins = _mixins;
107
+ }
102
108
 
103
- Object.assign(this, new baseClass());
104
- this._mixins = _mixins;
105
- }
106
109
 
107
- get mixins() {
108
- return this._mixins;
110
+ static get name() {
111
+ return baseClass.name;
112
+ }
113
+
114
+ static className() {
115
+ return this.name
116
+ }
117
+
118
+ get mixins() {
119
+ return this._mixins;
120
+ }
109
121
  }
110
- };
122
+ }[baseClass.name];
111
123
 
112
124
  copyProps(base, baseClass);
113
125
 
@@ -141,7 +153,7 @@ export function uploadFileFormData(formData, url, method) {
141
153
  cache: false,
142
154
  contentType: false,
143
155
  processData: false,
144
- beforeSend: function(xhr, settings) {
156
+ beforeSend: function (xhr, settings) {
145
157
  if (!csrfSafeMethod(settings.type) && !this.crossDomain) {
146
158
  xhr.setRequestHeader("X-CSRFToken", window.CSRF_TOKEN);
147
159
  }
@@ -171,8 +183,8 @@ export function checkIfParamNumberBoolOrString(paramElement, controller = null)
171
183
  return paramElement;
172
184
  }
173
185
 
174
- if(controller && typeof controller[paramElement] !== 'undefined') {
175
- if(typeof controller[paramElement] === 'function') {
186
+ if (controller && typeof controller[paramElement] !== 'undefined') {
187
+ if (typeof controller[paramElement] === 'function') {
176
188
  return controller[paramElement].bind(controller);
177
189
  }
178
190
  return controller[paramElement];
@@ -213,7 +225,7 @@ export function clearErrorsInForm($form) {
213
225
  }
214
226
 
215
227
  export function setErrorsInForm($form, $resForm) {
216
- $resForm = $('<div>').append($resForm);
228
+ $resForm = $('<div>').append($resForm);
217
229
 
218
230
  $form.find('.has-error').removeClass('has-error').find('.alert-danger').safeRemove();
219
231
  $form.find('.non-field-errors').safeRemove();
@@ -233,4 +245,14 @@ export function setErrorsInForm($form, $resForm) {
233
245
  });
234
246
 
235
247
  return hasNoError;
236
- }
248
+ }
249
+
250
+ export function jqueryInsertAt($container, index, $newElement) {
251
+ let lastIndex = $container.children().size();
252
+ if (index < lastIndex) {
253
+ $container.children().eq(index).before($newElement);
254
+ } else {
255
+ $container.append($newElement);
256
+ }
257
+ return this;
258
+ }
@@ -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
@@ -107,7 +107,7 @@ function loadHTMLFile(path, args, tag, hardReload) {
107
107
  const data = err.responseJSON;
108
108
  trigger('_RedirectOnView', data['url-link']);
109
109
  }
110
- trigger('navLoaded', {'controller_name': ()=> err.status});
110
+ trigger('navLoaded', {'controller_name': () => err.status});
111
111
 
112
112
  throw `<sdc-error data-code="${err.status}">${err.responseText}</sdc-error>`;
113
113
  });
@@ -300,16 +300,17 @@ export function replaceTagElementsInContainer(tagList, $container, parentControl
300
300
  });
301
301
  }
302
302
 
303
- export function reloadMethodHTML(controller) {
304
- return _reloadMethodHTML(controller, controller.$container)
303
+ export function reloadMethodHTML(controller, $container) {
304
+ return _reloadMethodHTML(controller, $container ?? controller.$container)
305
305
  }
306
+
306
307
  function _reloadMethodHTML(controller, $dom) {
307
308
  const plist = [];
308
309
 
309
310
  $dom.find(`._bind_to_update_handler.sdc_uuid_${controller._uuid}`).each(function () {
310
311
  const $this = $(this);
311
312
  let result = undefined;
312
- if($this.hasClass(`_with_handler`)) {
313
+ if ($this.hasClass(`_with_handler`)) {
313
314
  result = $this.data('handler');
314
315
  } else {
315
316
  let controller_handler = this.tagName.toLowerCase().replace(/^this./, '');
@@ -324,18 +325,205 @@ function _reloadMethodHTML(controller, $dom) {
324
325
  }
325
326
  if (result !== undefined) {
326
327
  plist.push(Promise.resolve(result).then((x) => {
327
- const $new_content = $(`<div></div>`);
328
- $new_content.append(x);
329
- return replaceTagElementsInContainer(tagList(), $new_content, controller).then(()=> {
330
- return _reloadMethodHTML(controller, $new_content).then(()=> {
331
- $this.safeEmpty().text('').append(x);
332
- return true;
333
- });
334
- });
328
+ let $newContent = $(`<div></div>`);
329
+ $newContent.append(x);
330
+ $newContent = $this.clone().empty().append($newContent);
331
+ return controller.reconcile($newContent, $this);
335
332
  }));
336
333
  }
337
334
 
338
335
  });
339
336
 
340
337
  return Promise.all(plist);
338
+ }
339
+
340
+
341
+ function getNodeKey(node) {
342
+ if (node[0].nodeType === 3) {
343
+ return `TEXT__${node[0].nodeValue}`;
344
+ }
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
+ });
353
+ }
354
+ return res.join('__');
355
+ }
356
+
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());
368
+
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
+ }
416
+
417
+ }
418
+ });
419
+
420
+ toRemove.forEach(($element) => $element.safeRemove());
421
+ }
422
+
423
+ function syncAttributes($real, $virtual) {
424
+ const realAttrs = $real[0].attributes ?? [];
425
+ const virtualAttrs = $virtual[0].attributes ?? [];
426
+ // Remove missing attrs
427
+ [...realAttrs].forEach(attr => {
428
+ if (!$virtual.is(`[${attr.name}]`)) {
429
+ $real.removeAttr(attr.name);
430
+ }
431
+ });
432
+
433
+ // Add or update
434
+ [...virtualAttrs].forEach(attr => {
435
+ if (!attr.name.startsWith(`data`) && $real.attr(attr.name) !== attr.value) {
436
+ $real.attr(attr.name, attr.value);
437
+ }
438
+ });
439
+
440
+ Object.entries($virtual.data()).forEach(([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()}
514
+ }
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
+ });
341
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
+ });