neo.mjs 9.16.0 → 10.0.0-alpha.1

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 (72) hide show
  1. package/ServiceWorker.mjs +2 -2
  2. package/apps/email/view/Viewport.mjs +2 -2
  3. package/apps/form/view/Viewport.mjs +1 -1
  4. package/apps/portal/index.html +1 -1
  5. package/apps/portal/view/examples/List.mjs +1 -1
  6. package/apps/portal/view/home/FooterContainer.mjs +1 -1
  7. package/apps/realworld2/view/HomeContainer.mjs +1 -1
  8. package/apps/route/view/center/CardAdministration.mjs +3 -3
  9. package/apps/route/view/center/CardAdministrationDenied.mjs +2 -2
  10. package/apps/route/view/center/CardContact.mjs +2 -2
  11. package/apps/route/view/center/CardHome.mjs +2 -2
  12. package/apps/route/view/center/CardSection1.mjs +2 -2
  13. package/apps/route/view/center/CardSection2.mjs +2 -2
  14. package/buildScripts/createApp.mjs +2 -2
  15. package/docs/app/view/classdetails/HeaderComponent.mjs +3 -3
  16. package/docs/app/view/classdetails/MembersList.mjs +43 -46
  17. package/docs/app/view/classdetails/SourceViewComponent.mjs +1 -1
  18. package/docs/app/view/classdetails/TutorialComponent.mjs +1 -1
  19. package/examples/component/toast/MainContainer.mjs +16 -16
  20. package/examples/component/wrapper/googleMaps/MarkerDialog.mjs +4 -4
  21. package/examples/fields/MainContainer.mjs +1 -1
  22. package/examples/panel/MainContainer.mjs +2 -2
  23. package/examples/tab/container/MainContainer.mjs +3 -3
  24. package/examples/tabs/MainContainer.mjs +2 -2
  25. package/examples/tabs/MainContainer2.mjs +3 -3
  26. package/examples/viewport/MainContainer.mjs +2 -2
  27. package/package.json +3 -3
  28. package/resources/data/deck/learnneo/pages/benefits/FourEnvironments.md +1 -1
  29. package/resources/data/deck/learnneo/pages/benefits/Introduction.md +4 -3
  30. package/resources/data/deck/learnneo/pages/guides/events/DomEvents.md +5 -5
  31. package/resources/data/deck/training/pages/2022-12-27T21-55-23-144Z.md +2 -2
  32. package/resources/data/deck/training/pages/2022-12-29T18-36-08-226Z.md +1 -1
  33. package/resources/data/deck/training/pages/2022-12-29T18-36-56-893Z.md +2 -2
  34. package/resources/data/deck/training/pages/2022-12-29T20-37-08-919Z.md +2 -2
  35. package/resources/data/deck/training/pages/2022-12-29T20-37-20-344Z.md +2 -2
  36. package/resources/data/deck/training/pages/2023-01-13T21-48-17-258Z.md +2 -2
  37. package/resources/data/deck/training/pages/2023-02-05T17-44-53-815Z.md +9 -9
  38. package/resources/data/deck/training/pages/2023-10-14T19-25-08-153Z.md +1 -1
  39. package/src/DefaultConfig.mjs +14 -2
  40. package/src/Main.mjs +14 -5
  41. package/src/button/Base.mjs +1 -1
  42. package/src/calendar/view/calendars/List.mjs +1 -1
  43. package/src/component/Base.mjs +11 -11
  44. package/src/component/Chip.mjs +1 -1
  45. package/src/component/Helix.mjs +3 -3
  46. package/src/component/Process.mjs +2 -2
  47. package/src/component/StatusBadge.mjs +2 -2
  48. package/src/component/Timer.mjs +1 -1
  49. package/src/component/Toast.mjs +2 -2
  50. package/src/container/Base.mjs +1 -1
  51. package/src/form/field/CheckBox.mjs +2 -2
  52. package/src/form/field/FileUpload.mjs +14 -14
  53. package/src/form/field/Range.mjs +1 -1
  54. package/src/form/field/Text.mjs +1 -1
  55. package/src/form/field/trigger/Base.mjs +2 -2
  56. package/src/form/field/trigger/SpinUpDown.mjs +2 -2
  57. package/src/grid/View.mjs +1 -1
  58. package/src/main/DeltaUpdates.mjs +382 -0
  59. package/src/main/DomAccess.mjs +13 -36
  60. package/src/main/render/DomApiRenderer.mjs +138 -0
  61. package/src/main/render/StringBasedRenderer.mjs +58 -0
  62. package/src/table/View.mjs +1 -1
  63. package/src/table/plugin/CellEditing.mjs +1 -1
  64. package/src/tree/Accordion.mjs +11 -11
  65. package/src/tree/List.mjs +12 -5
  66. package/src/vdom/Helper.mjs +174 -292
  67. package/src/vdom/VNode.mjs +47 -11
  68. package/src/vdom/domConstants.mjs +65 -0
  69. package/src/vdom/util/DomApiVnodeCreator.mjs +51 -0
  70. package/src/vdom/util/StringFromVnode.mjs +123 -0
  71. package/src/worker/mixin/RemoteMethodAccess.mjs +13 -1
  72. package/src/main/mixin/DeltaUpdates.mjs +0 -352
@@ -0,0 +1,382 @@
1
+ import Base from '../core/Base.mjs';
2
+ import DomAccess from './DomAccess.mjs';
3
+ import {voidAttributes} from '../vdom/domConstants.mjs';
4
+
5
+ const NeoConfig = Neo.config;
6
+
7
+ /**
8
+ * Logic to apply the deltas generated by vdom.Helper to the real DOM
9
+ * @class Neo.main.DeltaUpdates
10
+ * @extends Neo.core.Base
11
+ * @singleton
12
+ */
13
+ class DeltaUpdates extends Base {
14
+ static config = {
15
+ /**
16
+ * @member {String} className='Neo.main.DeltaUpdates'
17
+ * @protected
18
+ */
19
+ className: 'Neo.main.DeltaUpdates',
20
+ /**
21
+ * @member {Boolean} singleton=true
22
+ */
23
+ singleton: true
24
+ }
25
+
26
+ /**
27
+ * Private property to store the dynamically loaded renderer module.
28
+ * @member {Neo.main.render.DomApiRenderer|Neo.main.render.DomApiRenderer|null} #renderer=null
29
+ * @private
30
+ */
31
+ #renderer = null
32
+
33
+ /**
34
+ * Private property to signal that the renderer module has been loaded.
35
+ * This will be a Promise that resolves when the module is ready.
36
+ * @private
37
+ * @member {Promise<void>|null} #_readyPromise
38
+ */
39
+ #_readyPromise = null
40
+
41
+ /**
42
+ * @param {Object} config
43
+ */
44
+ construct(config) {
45
+ super.construct(config);
46
+
47
+ // Initiate the asynchronous loading of the renderer here.
48
+ this.#_readyPromise = (async () => {
49
+ try {
50
+ let module;
51
+
52
+ if (NeoConfig.useStringBasedMounting) {
53
+ module = await import('./render/StringBasedRenderer.mjs')
54
+ } else {
55
+ module = await import('./render/DomApiRenderer.mjs')
56
+ }
57
+
58
+ this.#renderer = module.default
59
+ } catch (err) {
60
+ console.error('DeltaUpdates: Failed to load renderer module:', err);
61
+ throw err // Re-throw to propagate initialization failures
62
+ }
63
+ })()
64
+ }
65
+
66
+ /**
67
+ * @param {HTMLElement} node
68
+ * @param {String} nodeName
69
+ */
70
+ changeNodeName(node, nodeName) {
71
+ let {attributes} = node,
72
+ clone = document.createElement(nodeName),
73
+ i = 0,
74
+ len = attributes.length,
75
+ attribute;
76
+
77
+ if (node) {
78
+ for (; i < len; i++) {
79
+ attribute = attributes.item(i);
80
+ clone.setAttribute(attribute.nodeName, attribute.nodeValue)
81
+ }
82
+
83
+ clone.innerHTML= node.innerHTML;
84
+
85
+ node.parentNode.replaceChild(clone, node)
86
+ }
87
+ }
88
+
89
+ /**
90
+ * @param {Object} delta
91
+ * @param {String} delta.id
92
+ */
93
+ focusNode({id}) {
94
+ DomAccess.getElement(id)?.focus()
95
+ }
96
+
97
+ /**
98
+ * Inserts a new node into the DOM tree based on delta updates.
99
+ * This method handles both string-based (outerHTML) and direct DOM API (vnode) mounting.
100
+ * It ensures the node is inserted at the correct index within the parent.
101
+ *
102
+ * Implementation Details & Considerations:
103
+ * - `parentNode.children` contains only element nodes (tags).
104
+ * - `parentNode.childNodes` contains all nodes, including text and comment nodes.
105
+ * - Since every `vtype:'text'` is wrapped inside a comment block (as an ID),
106
+ * calculating a "realIndex" is necessary for string-based insertions to
107
+ * correctly account for non-element nodes.
108
+ * - `insertAdjacentHTML()` is generally faster than creating a node via template,
109
+ * but it's only available for manipulating children (elements), not `childNodes` (all nodes).
110
+ * - For performance, in cases where there are no comment nodes (i.e., no wrapped text nodes),
111
+ * the method prioritizes `insertAdjacentHTML()` when `useStringBasedMounting` is true.
112
+ *
113
+ * @param {Object} delta
114
+ * @param {Boolean} delta.hasLeadingTextChildren Flag to honor leading comments, which require special treatment.
115
+ * @param {Number} delta.index The index at which to insert the new node within its parent.
116
+ * @param {String} [delta.outerHTML] The string representation of the new node (for string-based mounting).
117
+ * @param {String} delta.parentId The ID of the parent DOM node.
118
+ * @param {Neo.vdom.VNode} [delta.vnode] The VNode representation of the new node (for direct DOM API mounting).
119
+ */
120
+ insertNode({hasLeadingTextChildren, index, outerHTML, parentId, vnode}) {
121
+ // This method is synchronous and *expects* the renderer to be loaded
122
+ if (!this.#renderer) {
123
+ console.error('DeltaUpdates renderer not ready during insertNode!');
124
+ return
125
+ }
126
+
127
+ const parentNode = DomAccess.getElementOrBody(parentId);
128
+
129
+ if (parentNode) {
130
+ if (!NeoConfig.useStringBasedMounting) {
131
+ this.#renderer.createDomTree({index, isRoot: true, parentNode, vnode})
132
+ } else {
133
+ this.#renderer.insertNodeAsString({hasLeadingTextChildren, index, outerHTML, parentNode})
134
+ }
135
+ }
136
+ }
137
+
138
+ /**
139
+ * Moves an existing DOM node to a new position within its parent
140
+ * or to a new parent.
141
+ * This method directly manipulates the DOM using the pre-calculated physical index.
142
+ *
143
+ * @param {Object} delta
144
+ * @param {String} delta.id The ID of the DOM node to move.
145
+ * @param {Number} delta.index The physical index at which to insert the node
146
+ * @param {String} delta.parentId The ID of the target parent DOM node.
147
+ */
148
+ moveNode({id, index, parentId}) {
149
+ let node = DomAccess.getElement(id),
150
+ parentNode = DomAccess.getElement(parentId);
151
+
152
+ if (node && parentNode) {
153
+ // If the target index is at or beyond the end of the parent's current childNodes, append the node.
154
+ if (index >= parentNode.childNodes.length) {
155
+ parentNode.appendChild(node)
156
+ } else {
157
+ // Get the reference node at the target physical index.
158
+ let referenceNode = parentNode.childNodes[index];
159
+
160
+ // Only proceed if the node is not already at its target position.
161
+ if (node !== referenceNode) {
162
+ // Perform a direct swap operation if immediate element siblings.
163
+ if (node.nodeType === 1 && node === referenceNode.nextElementSibling) {
164
+ node.replaceWith(referenceNode)
165
+ }
166
+
167
+ parentNode.insertBefore(node, referenceNode)
168
+ }
169
+ }
170
+ }
171
+ }
172
+
173
+ /**
174
+ * @param {Object} delta
175
+ * @param {String} delta.parentId
176
+ */
177
+ removeAll({parentId}) {
178
+ let node = DomAccess.getElement(parentId);
179
+
180
+ if (node) {
181
+ node.innerHTML = ''
182
+ }
183
+ }
184
+
185
+ /**
186
+ * @param {Object} delta
187
+ * @param {String} delta.id
188
+ * @param {String} delta.parentId
189
+ */
190
+ removeNode({id, parentId}) {
191
+ const node = DomAccess.getElement(id);
192
+
193
+ if (node) {
194
+ node.remove();
195
+ }
196
+ // Potentially a vtype: 'text' node (wrapped between 2 comments)
197
+ else if (parentId) {
198
+ const
199
+ parentNode = DomAccess.getElementOrBody(parentId),
200
+ isComment = Node.COMMENT_NODE;
201
+
202
+ if (parentNode) {
203
+ // Find the starting comment node using its id marker
204
+ const startComment = Array.from(parentNode.childNodes).find(n =>
205
+ n.nodeType === isComment && n.nodeValue.includes(` ${id} `)
206
+ );
207
+
208
+ if (startComment) {
209
+ const
210
+ textNode = startComment.nextSibling,
211
+ // Ensure endComment is a comment node before attempting to remove
212
+ endComment = textNode?.nextSibling?.nodeType === isComment ? textNode.nextSibling : null;
213
+
214
+ // Remove the three parts: start comment, text node, end comment
215
+ startComment.remove();
216
+ textNode?.remove();
217
+ endComment?.remove()
218
+ }
219
+ }
220
+ }
221
+ }
222
+
223
+ /**
224
+ * @param {Object} delta
225
+ * @param {String} delta.fromId
226
+ * @param {String} delta.parentId
227
+ * @param {String} delta.toId
228
+ */
229
+ replaceChild({fromId, parentId, toId}) {
230
+ let node = DomAccess.getElement(parentId);
231
+
232
+ node?.replaceChild(DomAccess.getElement(toId), DomAccess.getElement(fromId))
233
+ }
234
+
235
+ /**
236
+ * @param {Object} delta
237
+ * @param {String} [delta.id]
238
+ * @param {String} [delta.value
239
+ */
240
+ setTextContent({id, value}) {
241
+ let node = DomAccess.getElement(id);
242
+
243
+ if (node) {
244
+ node.textContent = value
245
+ }
246
+ }
247
+
248
+ /**
249
+ * @param {Object} delta
250
+ * @param {Object} [delta.attributes]
251
+ * @param {String} [delta.cls]
252
+ * @param {String} [delta.id]
253
+ * @param {String} [delta.innerHTML]
254
+ * @param {String} [delta.outerHTML]
255
+ * @param {Object} [delta.style]
256
+ */
257
+ updateNode(delta) {
258
+ let me = this,
259
+ node = DomAccess.getElementOrBody(delta.id);
260
+
261
+ if (!node) {
262
+ console.log('node not found', delta.id);
263
+ }
264
+
265
+ if (node) {
266
+ Object.entries(delta).forEach(([prop, value]) => {
267
+ switch(prop) {
268
+ case 'attributes':
269
+ Object.entries(value).forEach(([key, val]) => {
270
+ if (voidAttributes.has(key)) {
271
+ node[key] = val === 'true' // vnode attribute values get converted into strings
272
+ } else if (val === null || val === '') {
273
+ if (key === 'value') {
274
+ node[key] = '' // input fields => pseudo attribute can not be removed
275
+ } else {
276
+ node.removeAttribute(key)
277
+ }
278
+ } else if (key === 'id') {
279
+ node[NeoConfig.useDomIds ? 'id' : 'data-neo-id'] = val
280
+ } else if (key === 'spellcheck' && val === 'false') {
281
+ // see https://github.com/neomjs/neo/issues/1922
282
+ node[key] = false
283
+ } else {
284
+ if (key === 'value') {
285
+ node[key] = val
286
+ } else {
287
+ node.setAttribute(key, val)
288
+ }
289
+ }
290
+ });
291
+ break
292
+ case 'cls':
293
+ value.add && node.classList.add(...value.add);
294
+ value.remove && node.classList.remove(...value.remove);
295
+ break
296
+ case 'innerHTML':
297
+ node.innerHTML = value || '';
298
+ break
299
+ case 'nodeName':
300
+ me.changeNodeName(node, value);
301
+ break
302
+ case 'outerHTML':
303
+ node.outerHTML = value || '';
304
+ break
305
+ case 'style':
306
+ if (Neo.isObject(value)) {
307
+ Object.entries(value).forEach(([key, val]) => {
308
+ let important;
309
+
310
+ if (Neo.isString(val) && val.includes('!important')) {
311
+ val = val.replace('!important', '').trim();
312
+ important = 'important'
313
+ }
314
+
315
+ node.style.setProperty(Neo.decamel(key), val, important)
316
+ })
317
+ }
318
+ break
319
+ }
320
+ })
321
+ }
322
+ }
323
+
324
+ /**
325
+ * @param {Object} delta
326
+ * @param {String} delta.id
327
+ * @param {String} delta.parentId
328
+ * @param {String} delta.value
329
+ */
330
+ updateVtext({id, parentId, value}) {
331
+ let node = DomAccess.getElement(parentId),
332
+ innerHTML = node.innerHTML,
333
+ startTag = `<!-- ${id} -->`,
334
+ reg = new RegExp(startTag + '[\\s\\S]*?<!-- \/neo-vtext -->');
335
+
336
+ node.innerHTML = innerHTML.replace(reg, value)
337
+ }
338
+
339
+ /**
340
+ * @param {Object} data
341
+ * @param {Object|Object[]} data.deltas
342
+ * @param {String} data.id
343
+ * @param {String} [data.origin='app']
344
+ */
345
+ update(data) {
346
+ // This method is synchronous and *expects* the renderer to be loaded
347
+ if (!this.#renderer) {
348
+ console.error('DeltaUpdates renderer not ready during insertNode!');
349
+ return
350
+ }
351
+
352
+ let me = this,
353
+ {deltas} = data,
354
+ i = 0,
355
+ len;
356
+
357
+ deltas = Array.isArray(deltas) ? deltas : [deltas];
358
+ len = deltas.length;
359
+
360
+ if (NeoConfig.logDeltaUpdates && len > 0) {
361
+ me.countDeltas += len;
362
+ me.countUpdates++;
363
+ console.log('update ' + me.countUpdates, 'total deltas ', me.countDeltas, Neo.clone(data, true))
364
+ }
365
+
366
+ if (NeoConfig.renderCountDeltas && len > 0) {
367
+ me.countDeltasPer250ms += len
368
+ }
369
+
370
+ for (; i < len; i++) {
371
+ me[deltas[i].action || 'updateNode'](deltas[i])
372
+ }
373
+
374
+ Neo.worker.Manager.sendMessage(data.origin || 'app', {
375
+ action : 'reply',
376
+ replyId: data.id,
377
+ success: true
378
+ })
379
+ }
380
+ }
381
+
382
+ export default Neo.setupClass(DeltaUpdates);
@@ -1,9 +1,7 @@
1
- import Base from '../core/Base.mjs';
2
- import DeltaUpdates from './mixin/DeltaUpdates.mjs';
3
- import DomUtils from './DomUtils.mjs';
4
- import Observable from '../core/Observable.mjs';
5
- import Rectangle from '../util/Rectangle.mjs';
6
- import StringUtil from '../util/String.mjs';
1
+ import Base from '../core/Base.mjs';
2
+ import DomUtils from './DomUtils.mjs';
3
+ import Rectangle from '../util/Rectangle.mjs';
4
+ import StringUtil from '../util/String.mjs';
7
5
 
8
6
  const
9
7
  doPreventDefault = e => e.preventDefault(),
@@ -43,6 +41,13 @@ const
43
41
  * @singleton
44
42
  */
45
43
  class DomAccess extends Base {
44
+ /**
45
+ * True automatically applies the core.Observable mixin
46
+ * @member {Boolean} observable=true
47
+ * @static
48
+ */
49
+ static observable = true
50
+
46
51
  static config = {
47
52
  /**
48
53
  * @member {String} className='Neo.main.DomAccess'
@@ -64,13 +69,6 @@ class DomAccess extends Base {
64
69
  * @protected
65
70
  */
66
71
  countUpdates: 0,
67
- /**
68
- * @member {Array} mixins=[DeltaUpdates, Observable]
69
- */
70
- mixins: [
71
- DeltaUpdates,
72
- Observable
73
- ],
74
72
  /**
75
73
  * Remote method access for other workers
76
74
  * @member {Object} remote
@@ -113,16 +111,7 @@ class DomAccess extends Base {
113
111
  * @member {Boolean} singleton=true
114
112
  * @protected
115
113
  */
116
- singleton: true,
117
- /**
118
- * Void attributes inside html tags
119
- * @member {String[]} voidAttributes
120
- * @protected
121
- */
122
- voidAttributes: [
123
- 'checked',
124
- 'required'
125
- ]
114
+ singleton: true
126
115
  }
127
116
 
128
117
  /**
@@ -865,7 +854,7 @@ class DomAccess extends Base {
865
854
  * @protected
866
855
  */
867
856
  read(data) {
868
- typeof data === 'function' && data()
857
+ Neo.isFunction(data) && data()
869
858
  }
870
859
 
871
860
  /**
@@ -1160,18 +1149,6 @@ class DomAccess extends Base {
1160
1149
  top : data.top || 0
1161
1150
  })
1162
1151
  }
1163
-
1164
- /**
1165
- * @param {Object} data
1166
- * @protected
1167
- */
1168
- write(data) {
1169
- this.du_insertNode({
1170
- index : data.parentIndex,
1171
- outerHTML: data.html || data.outerHTML,
1172
- parentId : data.parentId
1173
- })
1174
- }
1175
1152
  }
1176
1153
 
1177
1154
  export default Neo.setupClass(DomAccess);
@@ -0,0 +1,138 @@
1
+ import {voidAttributes} from '../../vdom/domConstants.mjs';
2
+
3
+ const DomApiRenderer = {
4
+ /**
5
+ * Recursively creates a DOM element (or DocumentFragment) from a VNode tree.
6
+ * This method handles two primary modes based on `isRoot`:
7
+ * 1. If `isRoot` is true:
8
+ * a. Builds a detached DocumentFragment: if `parentNode` is null. Returns the fragment.
9
+ * b. Builds and inserts directly into a host DOM: if `parentNode` is provided. Inserts the fragment.
10
+ * 2. If `isRoot` is false (default for recursive calls):
11
+ * Appends created DOM nodes directly to the provided `parentNode` (which is the DOM element of the direct parent VNode).
12
+ *
13
+ * @param {Object} config
14
+ * @param {Number} [config.index] The index within `parentNode` to insert the root fragment (used when `isRoot` is true).
15
+ * @param {Boolean} [config.isRoot=false] If true, this is the root call for the VNode tree.
16
+ * @param {HTMLElement} [config.parentNode=null] The parent DOM node to insert into. Its role changes based on `isRoot`.
17
+ * @param {Object} config.vnode The VNode object to convert to a real DOM element.
18
+ * @returns {DocumentFragment|HTMLElement|null} The created DOM node, the root DocumentFragment, or null.
19
+ * @private
20
+ */
21
+ createDomTree({index, isRoot=false, parentNode, vnode}) {
22
+ let domNode;
23
+
24
+ // No node or just a reference node, opt out
25
+ if (!vnode || vnode.componentId) {
26
+ return null
27
+ }
28
+
29
+ // Handle text nodes
30
+ if (vnode.vtype === 'text') {
31
+ domNode = document.createTextNode(vnode.textContent || '');
32
+
33
+ // Wrap in comment for consistency with delta updates
34
+ const
35
+ commentStart = document.createComment(` ${vnode.id} `),
36
+ commentEnd = document.createComment(' /neo-vtext '),
37
+ fragment = document.createDocumentFragment();
38
+
39
+ fragment.append(commentStart, domNode, commentEnd);
40
+ domNode = fragment
41
+ }
42
+ // Handle regular elements
43
+ else if (vnode.nodeName) {
44
+ if (vnode.ns) { // For SVG, ensure correct namespace
45
+ domNode = document.createElementNS(vnode.ns, vnode.nodeName)
46
+ } else {
47
+ domNode = document.createElement(vnode.nodeName)
48
+ }
49
+
50
+ // Apply the top-level 'id' property first (guaranteed to exist)
51
+ domNode[Neo.config.useDomIds ? 'id' : 'data-neo-id'] = vnode.id;
52
+
53
+ // Apply Attributes
54
+ Object.entries(vnode.attributes).forEach(([key, value]) => {
55
+ if (voidAttributes.has(key)) {
56
+ domNode[key] = (value === 'true' || value === true)
57
+ } else if (key === 'value') {
58
+ domNode.value = value
59
+ } else if (value !== null && value !== undefined) {
60
+ domNode.setAttribute(key, value)
61
+ }
62
+ });
63
+
64
+ // Apply Classes
65
+ if (vnode.className.length > 0) {
66
+ domNode.classList.add(...vnode.className)
67
+ }
68
+
69
+ // Apply Styles
70
+ if (Neo.isObject(vnode.style)) {
71
+ Object.entries(vnode.style).forEach(([key, value]) => {
72
+ let important;
73
+
74
+ if (Neo.isString(value) && value.includes('!important')) {
75
+ value = value.replace('!important', '').trim();
76
+ domNode.style.setProperty(Neo.decamel(key), value, 'important');
77
+ important = 'important'
78
+ }
79
+
80
+ domNode.style.setProperty(Neo.decamel(key), value, important)
81
+ })
82
+ }
83
+
84
+ // Handle innerHTML & textContent
85
+ // This applies to elements that contain only plain text (e.g., <span>Hello</span>)
86
+ // If the VNode has childNodes, this block is skipped, and content is handled recursively.
87
+ if (vnode.childNodes.length < 1) {
88
+ if (vnode.innerHTML) {
89
+ domNode.innerHTML = vnode.innerHTML
90
+ } else if (vnode.textContent) {
91
+ domNode.textContent = vnode.textContent
92
+ }
93
+ }
94
+ } else {
95
+ console.error('Unhandled VNode type or missing nodeName:', vnode);
96
+ return null
97
+ }
98
+
99
+ // Recursively process children
100
+ vnode.childNodes.forEach(childVnode => {
101
+ this.createDomTree({parentNode: domNode, vnode: childVnode})
102
+ })
103
+
104
+ // Final step: handle insertion based on `isRoot` and `parentNode`
105
+ if (isRoot) {
106
+ // This will be either HTMLElement or a DocumentFragment (for text vnodes)
107
+ let nodeToInsert = domNode;
108
+
109
+ if (nodeToInsert && parentNode && index !== -1) {
110
+ // If a specific host and index are provided, perform the insertion directly
111
+ if (index < parentNode.childNodes.length) {
112
+ parentNode.insertBefore(nodeToInsert, parentNode.childNodes[index])
113
+ } else {
114
+ parentNode.appendChild(nodeToInsert)
115
+ }
116
+
117
+ // Return the actual root DOM node (or fragment for text) that was inserted
118
+ return domNode
119
+ } else {
120
+ // If no specific host or index, return the detached nodeToInsert (HTMLElement or DocumentFragment)
121
+ return nodeToInsert
122
+ }
123
+ } else {
124
+ // For recursive calls (isRoot is false), append directly to the provided parentNode.
125
+ if (parentNode) { // parentNode here is the intermediate DOM parent
126
+ parentNode.append(domNode)
127
+ }
128
+
129
+ // Return the appended node (or null)
130
+ return domNode
131
+ }
132
+ }
133
+ };
134
+
135
+ const ns = Neo.ns('Neo.main.render', true);
136
+ ns.DomApiRenderer = DomApiRenderer;
137
+
138
+ export default DomApiRenderer;
@@ -0,0 +1,58 @@
1
+ const StringBasedRenderer = {
2
+ /**
3
+ * @param {String} html representing a single element
4
+ * @returns {DocumentFragment}
5
+ */
6
+ htmlStringToElement(html) {
7
+ const template = document.createElement('template');
8
+ template.innerHTML = html;
9
+ return template.content
10
+ },
11
+
12
+ /**
13
+ * Handles string-based insertion of a new node into the DOM.
14
+ * This method is called by `insertNode()` when `NeoConfig.useStringBasedMounting` is true.
15
+ *
16
+ * @param {Object} data
17
+ * @param {Boolean} data.hasLeadingTextChildren Flag to honor leading comments.
18
+ * @param {Number} data.index The index at which to insert the new node.
19
+ * @param {String} data.outerHTML The HTML string of the node to insert.
20
+ * @param {HTMLElement} data.parentNode The parent DOM node to insert into.
21
+ * @private
22
+ */
23
+ insertNodeAsString({hasLeadingTextChildren, index, outerHTML, parentNode}) {
24
+ let me = this;
25
+
26
+ // If comments detected, parse HTML string to a node and use insertBefore/appendChild on childNodes.
27
+ if (hasLeadingTextChildren) {
28
+ let node = me.htmlStringToElement(outerHTML);
29
+
30
+ if (index < parentNode.childNodes.length) {
31
+ parentNode.insertBefore(node, parentNode.childNodes[index])
32
+ } else {
33
+ parentNode.appendChild(node)
34
+ }
35
+ }
36
+ // If no comments detected, use insertAdjacentHTML for element nodes.
37
+ else {
38
+ let countChildren = parentNode.children.length; // Use `children` for `insertAdjacentHTML` context
39
+
40
+ if (index > 0 && index >= countChildren) {
41
+ parentNode.insertAdjacentHTML('beforeend', outerHTML);
42
+ return
43
+ }
44
+ if (countChildren > 0 && countChildren > index) {
45
+ parentNode.children[index].insertAdjacentHTML('beforebegin', outerHTML)
46
+ } else if (countChildren > 0) {
47
+ parentNode.children[countChildren - 1].insertAdjacentHTML('afterend', outerHTML)
48
+ } else {
49
+ parentNode.insertAdjacentHTML('beforeend', outerHTML)
50
+ }
51
+ }
52
+ }
53
+ };
54
+
55
+ const ns = Neo.ns('Neo.main.render', true);
56
+ ns.StringBasedRenderer = StringBasedRenderer;
57
+
58
+ export default StringBasedRenderer;
@@ -231,7 +231,7 @@ class View extends Component {
231
231
  }
232
232
 
233
233
  if (Neo.typeOf(rendererOutput) === 'Object') {
234
- cellConfig.innerHTML = rendererOutput.html || ''
234
+ cellConfig.html = rendererOutput.html || ''
235
235
  } else {
236
236
  cellConfig.cn = rendererOutput
237
237
  }
@@ -161,7 +161,7 @@ class CellEditing extends Plugin {
161
161
  me.mountedEditor = editor;
162
162
 
163
163
  cellNode.cn = [editor.createVdomReference()];
164
- delete cellNode.innerHTML;
164
+ delete cellNode.html;
165
165
 
166
166
  view.updateDepth = -1;
167
167