neo.mjs 6.18.2 → 6.19.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.
Files changed (108) hide show
  1. package/README.md +28 -214
  2. package/apps/ServiceWorker.mjs +2 -2
  3. package/apps/colors/view/ViewportController.mjs +7 -3
  4. package/apps/portal/data/blog.json +13 -0
  5. package/apps/portal/view/HeaderToolbar.mjs +2 -2
  6. package/apps/portal/view/Viewport.mjs +4 -2
  7. package/apps/portal/view/ViewportController.mjs +89 -8
  8. package/apps/portal/view/blog/Container.mjs +8 -8
  9. package/apps/portal/view/blog/List.mjs +6 -6
  10. package/apps/portal/view/home/MainContainer.mjs +9 -10
  11. package/apps/portal/view/home/parts/BaseContainer.mjs +8 -1
  12. package/apps/portal/view/home/parts/Colors.mjs +4 -4
  13. package/apps/portal/view/home/parts/Helix.mjs +2 -2
  14. package/apps/portal/view/home/parts/How.mjs +3 -3
  15. package/apps/portal/view/home/parts/MainNeo.mjs +6 -7
  16. package/apps/portal/view/home/parts/References.mjs +88 -0
  17. package/apps/portal/view/learn/ContentView.mjs +3 -1
  18. package/apps/portal/view/learn/MainContainer.mjs +3 -2
  19. package/apps/portal/view/learn/MainContainerController.mjs +11 -0
  20. package/apps/portal/view/learn/PageContainer.mjs +5 -3
  21. package/apps/portal/view/services/Component.mjs +73 -0
  22. package/apps/website/data/blog.json +13 -0
  23. package/examples/ServiceWorker.mjs +2 -2
  24. package/examples/component/carousel/MainContainer.mjs +42 -33
  25. package/examples/layout/cube/MainContainer.mjs +217 -0
  26. package/examples/layout/cube/app.mjs +6 -0
  27. package/examples/layout/cube/index.html +11 -0
  28. package/examples/layout/cube/neo-config.json +6 -0
  29. package/package.json +7 -7
  30. package/resources/data/deck/learnneo/pages/2023-10-14T19-25-08-153Z.md +2 -2
  31. package/resources/data/deck/learnneo/pages/ComponentModels.md +6 -6
  32. package/resources/data/deck/learnneo/pages/ComponentsAndContainers.md +10 -10
  33. package/resources/data/deck/learnneo/pages/Config.md +6 -6
  34. package/resources/data/deck/learnneo/pages/CustomComponents.md +4 -4
  35. package/resources/data/deck/learnneo/pages/DescribingTheUI.md +4 -4
  36. package/resources/data/deck/learnneo/pages/Earthquakes-01-goals.md +32 -0
  37. package/resources/data/deck/learnneo/pages/Earthquakes-Lab-01-generate-a-workspace.md +47 -0
  38. package/resources/data/deck/learnneo/pages/Earthquakes-Lab-02-generate-the-starter-app.md +150 -0
  39. package/resources/data/deck/learnneo/pages/Earthquakes-Lab-03-debugging.md +136 -0
  40. package/resources/data/deck/learnneo/pages/Earthquakes-Lab-04-fetch-data.md +146 -0
  41. package/resources/data/deck/learnneo/pages/Earthquakes-Lab-05-refactor-the-table.md +146 -0
  42. package/resources/data/deck/learnneo/pages/Earthquakes-Lab-06-use-a-view-model.md +301 -0
  43. package/resources/data/deck/learnneo/pages/Earthquakes-Lab-07-use-the-google-maps-addon.md +175 -0
  44. package/resources/data/deck/learnneo/pages/Earthquakes-Lab-08-events.md +38 -0
  45. package/resources/data/deck/learnneo/pages/Earthquakes.md +10 -10
  46. package/resources/data/deck/learnneo/pages/Events.md +7 -7
  47. package/resources/data/deck/learnneo/pages/Extending.md +7 -7
  48. package/resources/data/deck/learnneo/pages/Glossary.md +0 -0
  49. package/resources/data/deck/learnneo/pages/GuideEvents.md +97 -19
  50. package/resources/data/deck/learnneo/pages/GuideViewModels.md +21 -21
  51. package/resources/data/deck/learnneo/pages/References.md +8 -8
  52. package/resources/data/deck/learnneo/pages/TestLivePreview.md +5 -4
  53. package/resources/data/deck/learnneo/pages/TodoList.md +9 -9
  54. package/resources/data/deck/learnneo/pages/Welcome.md +3 -3
  55. package/resources/data/deck/learnneo/pages/WhyNeo-Multi-Window.md +2 -2
  56. package/resources/data/deck/learnneo/pages/WhyNeo-Speed.md +2 -2
  57. package/resources/data/deck/learnneo/tree.json +2 -1
  58. package/resources/images/apps/portal/neo-references.png +0 -0
  59. package/resources/scss/src/apps/portal/HeaderToolbar.scss +0 -46
  60. package/resources/scss/src/apps/portal/Viewport.scss +16 -1
  61. package/resources/scss/src/apps/portal/blog/Container.scss +7 -7
  62. package/resources/scss/src/apps/portal/blog/List.scss +20 -16
  63. package/resources/scss/src/apps/portal/home/parts/BaseContainer.scss +33 -4
  64. package/resources/scss/src/apps/portal/home/parts/MainNeo.scss +4 -5
  65. package/resources/scss/src/apps/portal/home/parts/References.scss +46 -0
  66. package/resources/scss/src/apps/portal/learn/ContentTreeList.scss +20 -0
  67. package/resources/scss/src/apps/portal/learn/ContentView.scss +4 -0
  68. package/resources/scss/src/apps/portal/learn/MainContainer.scss +1 -1
  69. package/resources/scss/src/apps/portal/learn/PageContainer.scss +22 -16
  70. package/resources/scss/src/apps/portal/services/Component.scss +20 -0
  71. package/resources/scss/src/{apps/portal/learn → code}/LivePreview.scss +1 -1
  72. package/resources/scss/src/component/Carousel.scss +21 -0
  73. package/resources/scss/src/component/Helix.scss +1 -2
  74. package/resources/scss/src/examples/layout/cube/MainContainer.scss +7 -0
  75. package/resources/scss/src/layout/Cube.scss +80 -0
  76. package/resources/scss/src/tab/Container.scss +10 -10
  77. package/resources/scss/theme-neo-light/apps/portal/blog/Container.scss +3 -0
  78. package/resources/scss/theme-neo-light/form/field/Search.scss +1 -1
  79. package/resources/scss/theme-neo-light/tooltip/Base.scss +1 -1
  80. package/src/DefaultConfig.mjs +2 -2
  81. package/src/Main.mjs +15 -1
  82. package/src/Neo.mjs +14 -3
  83. package/{apps/portal/view/learn → src/code}/LivePreview.mjs +43 -27
  84. package/src/component/Base.mjs +18 -1
  85. package/src/container/Base.mjs +3 -1
  86. package/src/dialog/Base.mjs +1 -2
  87. package/src/layout/Base.mjs +43 -6
  88. package/src/layout/Card.mjs +21 -59
  89. package/src/layout/Cube.mjs +428 -0
  90. package/src/layout/Fit.mjs +9 -38
  91. package/src/layout/Flexbox.mjs +16 -17
  92. package/src/layout/Form.mjs +13 -70
  93. package/src/layout/Grid.mjs +6 -18
  94. package/src/main/addon/ResizeObserver.mjs +18 -2
  95. package/src/main/mixin/DeltaUpdates.mjs +16 -3
  96. package/src/util/Array.mjs +36 -0
  97. package/src/vdom/Helper.mjs +328 -445
  98. package/src/vdom/VNode.mjs +12 -1
  99. package/test/siesta/siesta.js +16 -1
  100. package/test/siesta/tests/VdomCalendar.mjs +2111 -37
  101. package/test/siesta/tests/VdomHelper.mjs +283 -47
  102. package/test/siesta/tests/vdom/Advanced.mjs +367 -0
  103. package/test/siesta/tests/vdom/layout/Cube.mjs +189 -0
  104. package/test/siesta/tests/vdom/table/Container.mjs +133 -0
  105. package/apps/portal/view/home/parts/HelloWorld.mjs +0 -83
  106. package/apps/portal/view/home/preview/PageCodeContainer.mjs +0 -55
  107. package/resources/scss/src/apps/portal/home/preview/PageCodeContainer.scss +0 -115
  108. package/resources/scss/theme-neo-light/apps/portal/learn/ContentTreeList.scss +0 -23
@@ -3,7 +3,6 @@ import NeoArray from '../util/Array.mjs';
3
3
  import NeoString from '../util/String.mjs';
4
4
  import Style from '../util/Style.mjs';
5
5
  import VNode from './VNode.mjs';
6
- import VNodeUtil from '../util/VNode.mjs';
7
6
 
8
7
  /**
9
8
  * The central class for the VDom worker to create vnodes & delta updates.
@@ -97,7 +96,7 @@ class Helper extends Base {
97
96
  delete opts.parentIndex;
98
97
  delete opts.windowId;
99
98
 
100
- node = me.parseHelper(opts);
99
+ node = me.createVnode(opts);
101
100
  node.outerHTML = me.createStringFromVnode(node);
102
101
 
103
102
  if (autoMount) {
@@ -122,414 +121,186 @@ class Helper extends Base {
122
121
  }
123
122
 
124
123
  /**
125
- * @param {Object} config
126
- * @param {Array} config.deltas
127
- * @param {Number} config.index
128
- * @param {Object} config.newVnode
129
- * @param {Object} config.newVnodeRoot
130
- * @param {Object} config.oldVnode
131
- * @param {Object} config.oldVnodeRoot
132
- * @param {String} config.parentId
133
- * @returns {Array} deltas
124
+ * @param {Object} config
125
+ * @param {Object} config.deltas
126
+ * @param {Neo.vdom.VNode} config.oldVnode
127
+ * @param {Neo.vdom.VNode} config.vnode
128
+ * @param {Map} config.vnodeMap
129
+ * @returns {Object} deltas
134
130
  */
135
- createDeltas(config) {
136
- let {deltas=[], index, newVnode, oldVnode, parentId} = config,
137
- me = this,
138
- newVnodeRoot = config.newVnodeRoot || newVnode,
139
- oldVnodeRoot = config.oldVnodeRoot || oldVnode,
140
- attributes, delta, value, i, indexDelta, keys, len, movedNode, movedOldNode, styles, add, remove, returnValue, tmp, wrappedNode;
141
-
142
- // console.log('createDeltas', newVnode && newVnode.id, oldVnode && oldVnode.id, newVnode, oldVnode);
143
-
144
- if (newVnode && !oldVnode) { // new node at top level or at the end of a child array
145
- if (oldVnodeRoot) {
146
- movedOldNode = me.findVnode(oldVnodeRoot, newVnode.id, oldVnode)
147
- }
148
-
149
- if (!movedOldNode) {
150
- // console.log('insertNode', newVnode);
151
-
152
- deltas.push({
153
- action : 'insertNode',
154
- id : newVnode.id,
155
- index,
156
- outerHTML: me.createStringFromVnode(newVnode),
157
- parentId
158
- })
159
- }
160
- } else if (!newVnode && oldVnode) {
161
- if (newVnodeRoot) {
162
- movedNode = me.findVnode(newVnodeRoot, oldVnode.id, newVnode)
163
- }
164
-
165
- // use case: calendar week view => move an event into a column on the right side
166
-
167
- if (movedNode) {
168
- deltas.push({
169
- action: 'moveNode',
170
- id : oldVnode.id,
171
- index : movedNode.index,
172
- parentId: movedNode.parentNode.id
173
- });
174
-
175
- movedOldNode = me.findVnode(oldVnodeRoot, movedNode.parentNode.id);
176
-
177
- me.createDeltas({
178
- deltas,
179
- newVnode: movedNode.vnode,
180
- newVnodeRoot,
181
- oldVnode,
182
- oldVnodeRoot,
183
- parentId: movedNode.parentNode.id
184
- });
185
-
186
- movedOldNode.vnode.childNodes.splice(movedNode.index, 0, movedNode.vnode)
187
- } else {
188
- // console.log('top level removed node', oldVnode.id, oldVnode);
189
-
190
- delta = {
191
- action: 'removeNode',
192
- id : oldVnode.id
193
- };
194
-
195
- // We only need a parentId for vtype text
196
- if (oldVnode.vtype === 'text') {
197
- let removedNodeDetails = me.findVnode(oldVnodeRoot, oldVnode.id);
198
-
199
- delta.parentId = removedNodeDetails?.parentNode.id
200
- }
201
-
202
- deltas.push(delta)
203
- }
131
+ compareAttributes(config) {
132
+ let {deltas, oldVnode, vnode, vnodeMap} = config,
133
+ attributes, delta, value, keys, styles, add, remove;
134
+
135
+ if (vnode.vtype === 'text' && vnode.innerHTML !== oldVnode.innerHTML) {
136
+ deltas.default.push({
137
+ action : 'updateVtext',
138
+ id : vnode.id,
139
+ parentId: vnodeMap.get(vnode.id).parentNode.id,
140
+ value : vnode.innerHTML
141
+ })
204
142
  } else {
205
- if (newVnode && oldVnode && newVnode.id !== oldVnode.id) {
206
- movedNode = me.findVnode(newVnodeRoot, oldVnode.id, newVnode);
207
- movedOldNode = me.findVnode(oldVnodeRoot, newVnode.id, oldVnode);
208
-
209
- // console.log('movedNode', movedNode);
210
- // console.log('movedOldNode', movedOldNode);
211
-
212
- if (!movedNode && !movedOldNode) {
213
- // console.log('replace node', oldVnode.id, '('+newVnode.id+')');
214
-
215
- deltas.push({
216
- action: 'removeNode',
217
- id : oldVnode.id,
218
- });
219
-
220
- deltas.push({
221
- action : 'insertNode',
222
- id : newVnode.id,
223
- index,
224
- outerHTML: me.createStringFromVnode(newVnode),
225
- parentId
226
- });
227
-
228
- return {
229
- indexDelta: 0
230
- }
143
+ keys = Object.keys(vnode);
144
+
145
+ Object.keys(oldVnode).forEach(prop => {
146
+ if (!vnode.hasOwnProperty(prop)) {
147
+ keys.push(prop)
148
+ } else if (prop === 'attributes') { // find removed attributes
149
+ Object.keys(oldVnode[prop]).forEach(attr => {
150
+ if (!vnode[prop].hasOwnProperty(attr)) {
151
+ vnode[prop][attr] = null
152
+ }
153
+ })
231
154
  }
155
+ });
232
156
 
233
- // this case matches a typical array re-sorting
234
- else if (movedNode && movedOldNode && movedNode.parentNode.id === movedOldNode.parentNode.id) {
235
- deltas.push({
236
- action: 'moveNode',
237
- id : movedOldNode.vnode.id,
238
- index,
239
- parentId
240
- });
241
-
242
- me.createDeltas({
243
- deltas,
244
- newVnode,
245
- newVnodeRoot,
246
- oldVnode: movedOldNode.vnode,
247
- oldVnodeRoot,
248
- parentId: movedNode.parentNode.id
249
- });
250
-
251
- // see: https://github.com/neomjs/neo/issues/3116
252
- movedOldNode.parentNode.childNodes.splice(index, 0, movedOldNode)
253
- } else if (!movedNode && movedOldNode) {
254
- if (newVnode.id === movedOldNode.vnode.id) {
255
- indexDelta = 0;
256
-
257
- if (VNodeUtil.findChildVnodeById(oldVnode, newVnode.id)) {
258
- // the old vnode replaced a parent vnode
259
- // e.g.: vdom.cn[1] = vdom.cn[1].cn[0];
260
-
261
- deltas.push({
262
- action: 'replaceChild',
263
- fromId: oldVnode.id,
264
- parentId,
265
- toId : newVnode.id
266
- })
267
- } else {
268
- // the old vnode got moved into a different higher level branch
269
- // and its parent got removed
270
- // e.g.:
271
- // vdom.cn[1] = vdom.cn[2].cn[0];
272
- // vdom.cn.splice(2, 1);
273
-
274
- let movedOldNodeDetails = VNodeUtil.findChildVnode(oldVnodeRoot, movedOldNode.vnode.id),
275
- oldVnodeDetails = VNodeUtil.findChildVnode(oldVnodeRoot, oldVnode.id);
157
+ keys.forEach(prop => {
158
+ delta = {};
159
+ value = vnode[prop];
276
160
 
277
- indexDelta = 1;
161
+ switch (prop) {
162
+ case 'attributes':
163
+ attributes = {};
278
164
 
279
- if (movedOldNodeDetails.parentNode.id === oldVnodeDetails.parentNode.id) {
280
- // console.log('potential move node', index, movedOldNodeDetails.index);
281
-
282
- let newVnodeDetails = VNodeUtil.findChildVnode(newVnodeRoot, newVnode.id),
283
- targetIndex = index + 1; // +1 since the current index will already get removed
284
-
285
- // console.log(newVnodeDetails.parentNode);
286
-
287
- i = index + 1;
288
- tmp = oldVnodeDetails.parentNode.childNodes;
289
- len = movedOldNodeDetails.index;
290
-
291
- for (; i < len; i++) {
292
- // console.log(tmp[i]);
293
- if (!VNodeUtil.findChildVnode(newVnodeDetails.parentNode, tmp[i].id)) {
294
- // console.log('not found');
295
- targetIndex ++
296
- }
165
+ Object.entries(value).forEach(([key, value]) => {
166
+ if (!(oldVnode.attributes.hasOwnProperty(key) && oldVnode.attributes[key] === value)) {
167
+ if (value !== null && !Neo.isString(value) && Neo.isEmpty(value)) {
168
+ // ignore empty arrays & objects
169
+ } else {
170
+ attributes[key] = value
297
171
  }
172
+ }
173
+ });
298
174
 
299
- // console.log(movedOldNodeDetails.index, targetIndex);
300
-
301
- movedOldNodeDetails.parentNode.childNodes.splice(movedOldNodeDetails.index, 1);
175
+ if (Object.keys(attributes).length > 0) {
176
+ delta.attributes = attributes;
302
177
 
303
- // do not move a node in case its previous sibling nodes will get removed
304
- if (movedOldNodeDetails.index !== targetIndex) {
305
- deltas.push({
306
- action: 'moveNode',
307
- id : movedOldNode.vnode.id,
308
- index,
309
- parentId
310
- })
178
+ Object.entries(attributes).forEach(([key, value]) => {
179
+ if (value === null || value === '') {
180
+ delete vnode.attributes[key]
311
181
  }
312
-
313
- // console.log(movedOldNodeDetails);
314
-
315
- indexDelta = 0
316
- }
317
-
318
- deltas.push({
319
- action: 'removeNode',
320
- id : oldVnode.id,
321
- parentId
322
182
  })
323
183
  }
184
+ break
185
+ case 'nodeName':
186
+ case 'innerHTML':
187
+ if (value !== oldVnode[prop]) {
188
+ delta[prop] = value
189
+ }
190
+ break
191
+ case 'style':
192
+ styles = Style.compareStyles(value, oldVnode.style);
193
+ if (styles) {
194
+ delta.style = styles
195
+ }
196
+ break
197
+ case 'className':
198
+ if (oldVnode.className) {
199
+ add = NeoArray.difference(value, oldVnode.className);
200
+ remove = NeoArray.difference(oldVnode.className, value)
201
+ } else {
202
+ add = value;
203
+ remove = []
204
+ }
324
205
 
325
- me.createDeltas({
326
- deltas,
327
- newVnode,
328
- newVnodeRoot,
329
- oldVnode: movedOldNode.vnode,
330
- oldVnodeRoot,
331
- parentId
332
- });
333
-
334
- return {indexDelta}
335
- } else {
336
- // console.log('removed node', oldVnode.id, '('+newVnode.id+')');
337
-
338
- deltas.push({
339
- action: 'removeNode',
340
- id : oldVnode.id
341
- });
206
+ if (add.length > 0 || remove.length > 0) {
207
+ delta.cls = {};
342
208
 
343
- return {
344
- indexDelta: 1
209
+ if (add .length > 0) {delta.cls.add = add}
210
+ if (remove.length > 0) {delta.cls.remove = remove}
345
211
  }
346
- }
347
- } else if (!movedOldNode) {
348
- // new node inside of a child array
349
- // console.log('new node', index, parentId, newVnode);
212
+ break
213
+ }
350
214
 
351
- wrappedNode = movedNode && VNodeUtil.findChildVnodeById(newVnode, oldVnode.id);
215
+ if (Object.keys(delta).length > 0) {
216
+ delta.id = vnode.id;
217
+ deltas.default.push(delta)
218
+ }
219
+ })
220
+ }
352
221
 
353
- if (wrappedNode) {
354
- // an existing vnode got wrapped into a new vnode
355
- // => we need to remove the old one, since it will get recreated
222
+ return deltas
223
+ }
356
224
 
357
- // console.log('movedNode removeNode', movedNode.vnode.id);
225
+ /**
226
+ * @param {Object} config
227
+ * @param {Object} [config.deltas={default: [], remove: []}]
228
+ * @param {Neo.vdom.VNode} config.oldVnode
229
+ * @param {Map} [config.oldVnodeMap]
230
+ * @param {Neo.vdom.VNode} config.vnode
231
+ * @param {Map} [config.vnodeMap]
232
+ * @returns {Object} deltas
233
+ */
234
+ createDeltas(config) {
235
+ let {deltas={default: [], remove: []}, oldVnode, vnode} = config,
236
+ oldVnodeId = oldVnode?.id,
237
+ vnodeId = vnode?.id;
238
+
239
+ // Edge case: setting `removeDom: true` on a top-level vdom node
240
+ if (!vnode && oldVnodeId) {
241
+ deltas.remove.push({action: 'removeNode', id: oldVnodeId});
242
+ return deltas
243
+ }
358
244
 
359
- deltas.push({
360
- action: 'removeNode',
361
- id : movedNode.vnode.id
362
- })
363
- }
245
+ if (vnode.static) {
246
+ return deltas
247
+ }
364
248
 
365
- deltas.push({
366
- action : 'insertNode',
367
- id : newVnode.id,
368
- index,
369
- outerHTML: me.createStringFromVnode(newVnode),
370
- parentId
371
- });
249
+ if (vnodeId !== oldVnodeId) {
250
+ throw new Error(`createDeltas() must get called for the same node. ${vnodeId}, ${oldVnodeId}`);
251
+ }
372
252
 
373
- return {
374
- indexDelta: wrappedNode ? 0 : -1
375
- }
376
- } else if (movedNode) {
377
- indexDelta = 0;
253
+ let me = this,
254
+ oldVnodeMap = config.oldVnodeMap || me.createVnodeMap({vnode: oldVnode}),
255
+ vnodeMap = config.vnodeMap || me.createVnodeMap({vnode}),
256
+ childNodes = vnode .childNodes || [],
257
+ oldChildNodes = oldVnode.childNodes || [],
258
+ i = 0,
259
+ indexDelta = 0,
260
+ len = Math.max(childNodes.length, oldChildNodes.length),
261
+ childNode, nodeInNewTree, oldChildNode;
262
+
263
+ me.compareAttributes({deltas, oldVnode, vnode, vnodeMap});
264
+
265
+ if (childNodes.length === 0 && oldChildNodes.length > 1) {
266
+ deltas.remove.push({action: 'removeAll', parentId: vnodeId});
267
+ return deltas
268
+ }
378
269
 
379
- // check if the vnode got moved inside the vnode tree
270
+ for (; i < len; i++) {
271
+ childNode = childNodes[i];
272
+ oldChildNode = oldChildNodes[i + indexDelta];
380
273
 
381
- let newVnodeDetails = VNodeUtil.findChildVnode(newVnodeRoot, newVnode.id);
274
+ if (!childNode && !oldChildNode) {
275
+ break
276
+ }
382
277
 
383
- let sameParent = newVnodeDetails.parentNode.id === movedNode.parentNode.id;
278
+ // Same node, continue recursively
279
+ if (childNode && childNode.id === oldChildNode?.id) {
280
+ me.createDeltas({deltas, oldVnode: oldChildNode, oldVnodeMap, vnode: childNode, vnodeMap});
281
+ continue
282
+ }
384
283
 
385
- if (sameParent) {
386
- if (newVnodeDetails.index > movedNode.index) {
387
- // todo: needs testing => index gaps > 1
388
- indexDelta = newVnodeDetails.index - movedNode.index
389
- }
390
- }
284
+ if (oldChildNode) {
285
+ nodeInNewTree = vnodeMap.get(oldChildNode.id);
391
286
 
392
- if (!sameParent || newVnodeDetails.parentNode.childNodes[movedNode.index].id !== movedNode.vnode.id) {
393
- deltas.push({
394
- action: 'moveNode',
395
- id : movedNode.vnode.id,
396
- index : movedNode.index,
397
- parentId: movedNode.parentNode.id
398
- })
399
- }
287
+ // Remove node, if no longer inside the new tree
288
+ if (!nodeInNewTree) {
289
+ me.removeNode({deltas, oldVnode: oldChildNode, oldVnodeMap});
290
+ i--;
291
+ continue
292
+ }
400
293
 
401
- me.createDeltas({
402
- deltas,
403
- newVnode: movedNode.vnode,
404
- newVnodeRoot,
405
- oldVnode,
406
- oldVnodeRoot,
407
- parentId: movedNode.parentNode.id
408
- });
409
-
410
- return {
411
- indexDelta: 0
412
- }
294
+ // The old child node got moved into a different not processed array. It will get picked up there.
295
+ if (childNode && vnodeId !== nodeInNewTree.parentNode.id) {
296
+ i--;
297
+ indexDelta++;
298
+ continue
413
299
  }
414
300
  }
415
301
 
416
- if (newVnode && oldVnode && newVnode.id === oldVnode.id) {
417
- if (newVnode.vtype === 'text' && newVnode.innerHTML !== oldVnode.innerHTML) {
418
- deltas.push({
419
- action : 'updateVtext',
420
- id : newVnode.id,
421
- parentId: VNodeUtil.findChildVnode(newVnodeRoot, newVnode.id).parentNode.id,
422
- value : newVnode.innerHTML
423
- })
424
- } else {
425
- keys = Object.keys(newVnode);
426
-
427
- Object.keys(oldVnode).forEach(prop => {
428
- if (!newVnode.hasOwnProperty(prop)) {
429
- keys.push(prop)
430
- } else if (prop === 'attributes') { // find removed attributes
431
- Object.keys(oldVnode[prop]).forEach(attr => {
432
- if (!newVnode[prop].hasOwnProperty(attr)) {
433
- newVnode[prop][attr] = null;
434
- }
435
- })
436
- }
437
- });
438
-
439
- keys.forEach(prop => {
440
- delta = {};
441
- value = newVnode[prop];
442
-
443
- switch (prop) {
444
- case 'attributes':
445
- attributes = {};
446
-
447
- Object.entries(value).forEach(([key, value]) => {
448
- if (!(oldVnode.attributes.hasOwnProperty(key) && oldVnode.attributes[key] === value)) {
449
- if (value !== null && !Neo.isString(value) && Neo.isEmpty(value)) {
450
- // ignore empty arrays & objects
451
- } else {
452
- attributes[key] = value
453
- }
454
- }
455
- });
456
-
457
- if (Object.keys(attributes).length > 0) {
458
- delta.attributes = attributes;
459
-
460
- Object.entries(attributes).forEach(([key, value]) => {
461
- if (value === null || value === '') {
462
- delete newVnode.attributes[key]
463
- }
464
- })
465
- }
466
- break
467
- case 'childNodes':
468
- i = 0;
469
- indexDelta = 0;
470
- len = Math.max(value.length, oldVnode.childNodes.length);
471
-
472
- for (; i < len; i++) {
473
- returnValue = me.createDeltas({
474
- deltas,
475
- index : i,
476
- newVnode: value[i],
477
- newVnodeRoot,
478
- oldVnode: oldVnode.childNodes[i + indexDelta],
479
- oldVnodeRoot,
480
- parentId: newVnode.id
481
- });
482
-
483
- if (returnValue && returnValue.indexDelta) {
484
- indexDelta += returnValue.indexDelta
485
- }
486
- }
487
-
488
- if (indexDelta < 0) {
489
- // this case happens for infinite scrolling upwards:
490
- // add new nodes at the start, remove nodes at the end
491
- for (i=value.length + indexDelta; i < oldVnode.childNodes.length; i++) {
492
- deltas.push({
493
- action: 'removeNode',
494
- id : oldVnode.childNodes[i].id
495
- })
496
- }
497
- }
498
-
499
- break
500
- case 'nodeName':
501
- case 'innerHTML':
502
- if (value !== oldVnode[prop]) {
503
- delta[prop] = value
504
- }
505
- break
506
- case 'style':
507
- styles = Style.compareStyles(value, oldVnode.style);
508
- if (styles) {
509
- delta.style = styles
510
- }
511
- break
512
- case 'className':
513
- if (oldVnode.className) {
514
- add = NeoArray.difference(value, oldVnode.className);
515
- remove = NeoArray.difference(oldVnode.className, value)
516
- } else {
517
- add = value;
518
- remove = []
519
- }
520
-
521
- if (add.length > 0 || remove.length > 0) {
522
- delta.cls = {add, remove}
523
- }
524
- break
525
- }
526
-
527
- if (Object.keys(delta).length > 0) {
528
- delta.id = newVnode.id;
529
- deltas.push(delta)
530
- }
531
- })
532
- }
302
+ if (childNode) {
303
+ me[oldVnodeMap.get(childNode.id) ? 'moveNode' : 'insertNode']({deltas, oldVnodeMap, vnode: childNode, vnodeMap})
533
304
  }
534
305
  }
535
306
 
@@ -590,28 +361,35 @@ class Helper extends Base {
590
361
  }
591
362
 
592
363
  /**
593
- * @param {Object} vnode
364
+ * @param {Neo.vdom.VNode} vnode
365
+ * @param {Map} [movedNodes]
594
366
  */
595
- createStringFromVnode(vnode) {
596
- let me = this;
367
+ createStringFromVnode(vnode, movedNodes) {
368
+ let me = this,
369
+ id = vnode?.id;
370
+
371
+ if (id && movedNodes?.get(id)) {
372
+ return ''
373
+ }
597
374
 
598
375
  switch (vnode.vtype) {
599
376
  case 'root':
600
- return me.createStringFromVnode(vnode.childNodes[0])
377
+ return me.createStringFromVnode(vnode.childNodes[0], movedNodes)
601
378
  case 'text':
602
379
  return vnode.innerHTML === undefined ? '' : String(vnode.innerHTML)
603
380
  case 'vnode':
604
- return me.createOpenTag(vnode) + me.createTagContent(vnode) + me.createCloseTag(vnode)
381
+ return me.createOpenTag(vnode) + me.createTagContent(vnode, movedNodes) + me.createCloseTag(vnode)
605
382
  default:
606
383
  return ''
607
384
  }
608
385
  }
609
386
 
610
387
  /**
611
- * @param {Object} vnode
388
+ * @param {Neo.vdom.VNode} vnode
389
+ * @param {Map} [movedNodes]
612
390
  * @protected
613
391
  */
614
- createTagContent(vnode) {
392
+ createTagContent(vnode, movedNodes) {
615
393
  if (vnode.innerHTML) {
616
394
  return vnode.innerHTML
617
395
  }
@@ -623,7 +401,7 @@ class Helper extends Base {
623
401
 
624
402
  for (; i < len; i++) {
625
403
  childNode = vnode.childNodes[i];
626
- outerHTML = this.createStringFromVnode(childNode);
404
+ outerHTML = this.createStringFromVnode(childNode, movedNodes);
627
405
 
628
406
  if (childNode.innerHTML !== outerHTML) {
629
407
  if (this.returnChildNodeOuterHtml) {
@@ -637,53 +415,11 @@ class Helper extends Base {
637
415
  return string
638
416
  }
639
417
 
640
- /**
641
- * @param {Neo.vdom.VNode} vnode
642
- * @param {String} id
643
- * @param {Neo.vdom.VNode} parentNode
644
- * @param {Number} index
645
- * @returns {Object}
646
- * {Number} index
647
- * {String} parentId
648
- * {Neo.vdom.VNode} vnode
649
- */
650
- findVnode(vnode, id, parentNode, index) {
651
- if (!index) {
652
- index = 0
653
- }
654
-
655
- let returnValue = null,
656
- children, childValue, i, len;
657
-
658
- if (vnode.id === id) {
659
- returnValue = {index, parentNode, vnode}
660
- } else if (vnode.vtype !== 'text') {
661
- children = vnode.childNodes;
662
- i = 0;
663
- len = children?.length || 0;
664
-
665
- for (; i < len; i++) {
666
- childValue = this.findVnode(children[i], id, vnode, i);
667
-
668
- if (childValue && childValue.vnode.id === id) {
669
- returnValue = childValue;
670
- break
671
- }
672
- }
673
- }
674
-
675
- if (returnValue && returnValue.parentId === 'root') {
676
- returnValue.index = null
677
- }
678
-
679
- return returnValue;
680
- }
681
-
682
418
  /**
683
419
  * @param {Object} opts
684
420
  * @returns {Object|Neo.vdom.VNode|null}
685
421
  */
686
- parseHelper(opts) {
422
+ createVnode(opts) {
687
423
  if (opts.removeDom === true) {
688
424
  return null
689
425
  }
@@ -735,7 +471,7 @@ class Helper extends Base {
735
471
  value.forEach(item => {
736
472
  if (item.removeDom !== true) {
737
473
  delete item.removeDom; // could be false
738
- potentialNode = me.parseHelper(item);
474
+ potentialNode = me.createVnode(item);
739
475
 
740
476
  if (potentialNode) { // don't add null values
741
477
  newValue.push(potentialNode)
@@ -771,6 +507,9 @@ class Helper extends Base {
771
507
  case 'id':
772
508
  node.id = value;
773
509
  break
510
+ case 'static':
511
+ node.static = value;
512
+ break
774
513
  case 'style':
775
514
  style = node.style;
776
515
  if (Neo.isString(value)) {
@@ -791,6 +530,154 @@ class Helper extends Base {
791
530
  return new VNode(node)
792
531
  }
793
532
 
533
+ /**
534
+ * Creates a flap map of the tree, containing ids as keys and infos as values
535
+ * @param {Object} config
536
+ * @param {Neo.vdom.VNode} config.vnode
537
+ * @param {Neo.vdom.VNode} [config.parentNode=null]
538
+ * @param {Number} [config.index=0]
539
+ * @param {Map} [config.map=new Map()]
540
+ * @returns {Map}
541
+ * {String} id vnode.id (convenience shortcut)
542
+ * {Number} index
543
+ * {String} parentId
544
+ * {Neo.vdom.VNode} vnode
545
+ */
546
+ createVnodeMap(config) {
547
+ let {vnode, parentNode=null, index=0, map=new Map()} = config,
548
+ id = vnode?.id;
549
+
550
+ map.set(id, {id, index, parentNode, vnode});
551
+
552
+ vnode?.childNodes?.forEach((childNode, index) => {
553
+ this.createVnodeMap({vnode: childNode, parentNode: vnode, index, map})
554
+ });
555
+
556
+ return map
557
+ }
558
+
559
+ /**
560
+ * The logic will parse the vnode (tree) to find existing items inside a given map.
561
+ * It will not search for further childNodes inside an already found vnode.
562
+ * @param {Object} config
563
+ * @param {Map} [config.movedNodes=new Map()]
564
+ * @param {Map} config.oldVnodeMap
565
+ * @param {Neo.vdom.VNode} config.vnode
566
+ * @param {Map} config.vnodeMap
567
+ * @returns {Map}
568
+ */
569
+ findMovedNodes(config) {
570
+ let {movedNodes=new Map(), oldVnodeMap, vnode, vnodeMap} = config,
571
+ id = vnode?.id;
572
+
573
+ if (id) {
574
+ let currentNode = oldVnodeMap.get(id)
575
+
576
+ if (currentNode) {
577
+ movedNodes.set(id, vnodeMap.get(id))
578
+ } else {
579
+ vnode.childNodes.forEach(childNode => {
580
+ if (childNode.vtype !== 'text') {
581
+ this.findMovedNodes({movedNodes, oldVnodeMap, vnode: childNode, vnodeMap})
582
+ }
583
+ })
584
+ }
585
+ }
586
+
587
+ return movedNodes
588
+ }
589
+
590
+ /**
591
+ * @param {Object} config
592
+ * @param {Object} config.deltas
593
+ * @param {Map} config.oldVnodeMap
594
+ * @param {Neo.vdom.VNode} config.vnode
595
+ * @param {Map} config.vnodeMap
596
+ */
597
+ insertNode(config) {
598
+ let {deltas, oldVnodeMap, vnode, vnodeMap} = config,
599
+ details = vnodeMap.get(vnode.id),
600
+ {index} = details,
601
+ parentId = details.parentNode.id,
602
+ me = this,
603
+ movedNodes = me.findMovedNodes({oldVnodeMap, vnode, vnodeMap}),
604
+ outerHTML = me.createStringFromVnode(vnode, movedNodes);
605
+
606
+ deltas.default.push({action: 'insertNode', index, outerHTML, parentId});
607
+
608
+ // Insert the new node into the old tree, to simplify future OPs
609
+ oldVnodeMap.get(parentId).vnode.childNodes.splice(index, 0, vnode);
610
+
611
+ movedNodes.forEach(details => {
612
+ let {id} = details,
613
+ parentId = details.parentNode.id;
614
+
615
+ deltas.default.push({action: 'moveNode', id, index: details.index, parentId});
616
+
617
+ me.createDeltas({deltas, oldVnode: oldVnodeMap.get(id).vnode, oldVnodeMap, vnode: details.vnode, vnodeMap})
618
+ })
619
+ }
620
+
621
+ /**
622
+ * @param {Object} config
623
+ * @param {Object} config.deltas
624
+ * @param {Map} config.oldVnodeMap
625
+ * @param {Neo.vdom.VNode} config.vnode
626
+ * @param {Map} config.vnodeMap
627
+ */
628
+ moveNode(config) {
629
+ let {deltas, oldVnodeMap, vnode, vnodeMap} = config,
630
+ details = vnodeMap.get(vnode.id),
631
+ {index, parentNode} = details,
632
+ parentId = parentNode.id,
633
+ movedNode = oldVnodeMap.get(vnode.id),
634
+ movedParentNode = movedNode.parentNode,
635
+ {childNodes} = movedParentNode;
636
+
637
+ if (parentId !== movedParentNode.id) {
638
+ // We need to remove the node from the old parent childNodes
639
+ // (which must not be the same as the node they got moved into)
640
+ NeoArray.remove(childNodes, movedNode.vnode);
641
+
642
+ let oldParentNode = oldVnodeMap.get(parentId);
643
+
644
+ if (oldParentNode) {
645
+ // If moved into a new parent node, update the reference inside the flat map
646
+ movedNode.parentNode = oldParentNode.vnode;
647
+
648
+ childNodes = movedNode.parentNode.childNodes
649
+ }
650
+ }
651
+
652
+ deltas.default.push({action: 'moveNode', id: vnode.id, index, parentId});
653
+
654
+ // Add the node into the old vnode tree to simplify future OPs.
655
+ // NeoArray.insert() will switch to move() in case the node already exists.
656
+ NeoArray.insert(childNodes, index, movedNode.vnode);
657
+
658
+ this.createDeltas({deltas, oldVnode: movedNode.vnode, oldVnodeMap, vnode, vnodeMap})
659
+ }
660
+
661
+ /**
662
+ * @param {Object} config
663
+ * @param {Object} config.deltas
664
+ * @param {Neo.vdom.VNode} config.oldVnode
665
+ * @param {Map} config.oldVnodeMap
666
+ */
667
+ removeNode(config) {
668
+ let {deltas, oldVnode, oldVnodeMap} = config,
669
+ delta = {action: 'removeNode', id: oldVnode.id},
670
+ {parentNode} = oldVnodeMap.get(oldVnode.id);
671
+
672
+ if (oldVnode.vtype === 'text') {
673
+ delta.parentId = parentNode.id
674
+ }
675
+
676
+ deltas.remove.push(delta);
677
+
678
+ NeoArray.remove(parentNode.childNodes, oldVnode)
679
+ }
680
+
794
681
  /**
795
682
  * Creates a Neo.vdom.VNode tree for the given vdom template and compares the new vnode with the current one
796
683
  * to calculate the vdom deltas.
@@ -800,19 +687,15 @@ class Helper extends Base {
800
687
  * @returns {Object|Promise<Object>}
801
688
  */
802
689
  update(opts) {
803
- let me = this,
804
- node = me.parseHelper(opts.vdom),
805
-
806
- deltas = me.createDeltas({
807
- newVnode: node,
808
- oldVnode: opts.vnode
809
- }),
810
-
811
- returnObj = {
812
- deltas,
813
- updateVdom: true,
814
- vnode : node
815
- };
690
+ let me = this,
691
+ vnode = me.createVnode(opts.vdom),
692
+ deltas = me.createDeltas({oldVnode: opts.vnode, vnode});
693
+
694
+ // Trees to remove could contain nodes which we want to re-use (move),
695
+ // so we need to execute the removeNode OPs last.
696
+ deltas = deltas.default.concat(deltas.remove);
697
+
698
+ let returnObj = {deltas, updateVdom: true, vnode};
816
699
 
817
700
  return Neo.config.useVdomWorker ? returnObj : Promise.resolve(returnObj)
818
701
  }