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.
- package/ServiceWorker.mjs +2 -2
- package/apps/email/view/Viewport.mjs +2 -2
- package/apps/form/view/Viewport.mjs +1 -1
- package/apps/portal/index.html +1 -1
- package/apps/portal/view/examples/List.mjs +1 -1
- package/apps/portal/view/home/FooterContainer.mjs +1 -1
- package/apps/realworld2/view/HomeContainer.mjs +1 -1
- package/apps/route/view/center/CardAdministration.mjs +3 -3
- package/apps/route/view/center/CardAdministrationDenied.mjs +2 -2
- package/apps/route/view/center/CardContact.mjs +2 -2
- package/apps/route/view/center/CardHome.mjs +2 -2
- package/apps/route/view/center/CardSection1.mjs +2 -2
- package/apps/route/view/center/CardSection2.mjs +2 -2
- package/buildScripts/createApp.mjs +2 -2
- package/docs/app/view/classdetails/HeaderComponent.mjs +3 -3
- package/docs/app/view/classdetails/MembersList.mjs +43 -46
- package/docs/app/view/classdetails/SourceViewComponent.mjs +1 -1
- package/docs/app/view/classdetails/TutorialComponent.mjs +1 -1
- package/examples/component/toast/MainContainer.mjs +16 -16
- package/examples/component/wrapper/googleMaps/MarkerDialog.mjs +4 -4
- package/examples/fields/MainContainer.mjs +1 -1
- package/examples/panel/MainContainer.mjs +2 -2
- package/examples/tab/container/MainContainer.mjs +3 -3
- package/examples/tabs/MainContainer.mjs +2 -2
- package/examples/tabs/MainContainer2.mjs +3 -3
- package/examples/viewport/MainContainer.mjs +2 -2
- package/package.json +3 -3
- package/resources/data/deck/learnneo/pages/benefits/FourEnvironments.md +1 -1
- package/resources/data/deck/learnneo/pages/benefits/Introduction.md +4 -3
- package/resources/data/deck/learnneo/pages/guides/events/DomEvents.md +5 -5
- package/resources/data/deck/training/pages/2022-12-27T21-55-23-144Z.md +2 -2
- package/resources/data/deck/training/pages/2022-12-29T18-36-08-226Z.md +1 -1
- package/resources/data/deck/training/pages/2022-12-29T18-36-56-893Z.md +2 -2
- package/resources/data/deck/training/pages/2022-12-29T20-37-08-919Z.md +2 -2
- package/resources/data/deck/training/pages/2022-12-29T20-37-20-344Z.md +2 -2
- package/resources/data/deck/training/pages/2023-01-13T21-48-17-258Z.md +2 -2
- package/resources/data/deck/training/pages/2023-02-05T17-44-53-815Z.md +9 -9
- package/resources/data/deck/training/pages/2023-10-14T19-25-08-153Z.md +1 -1
- package/src/DefaultConfig.mjs +14 -2
- package/src/Main.mjs +14 -5
- package/src/button/Base.mjs +1 -1
- package/src/calendar/view/calendars/List.mjs +1 -1
- package/src/component/Base.mjs +11 -11
- package/src/component/Chip.mjs +1 -1
- package/src/component/Helix.mjs +3 -3
- package/src/component/Process.mjs +2 -2
- package/src/component/StatusBadge.mjs +2 -2
- package/src/component/Timer.mjs +1 -1
- package/src/component/Toast.mjs +2 -2
- package/src/container/Base.mjs +1 -1
- package/src/form/field/CheckBox.mjs +2 -2
- package/src/form/field/FileUpload.mjs +14 -14
- package/src/form/field/Range.mjs +1 -1
- package/src/form/field/Text.mjs +1 -1
- package/src/form/field/trigger/Base.mjs +2 -2
- package/src/form/field/trigger/SpinUpDown.mjs +2 -2
- package/src/grid/View.mjs +1 -1
- package/src/main/DeltaUpdates.mjs +382 -0
- package/src/main/DomAccess.mjs +13 -36
- package/src/main/render/DomApiRenderer.mjs +138 -0
- package/src/main/render/StringBasedRenderer.mjs +58 -0
- package/src/table/View.mjs +1 -1
- package/src/table/plugin/CellEditing.mjs +1 -1
- package/src/tree/Accordion.mjs +11 -11
- package/src/tree/List.mjs +12 -5
- package/src/vdom/Helper.mjs +174 -292
- package/src/vdom/VNode.mjs +47 -11
- package/src/vdom/domConstants.mjs +65 -0
- package/src/vdom/util/DomApiVnodeCreator.mjs +51 -0
- package/src/vdom/util/StringFromVnode.mjs +123 -0
- package/src/worker/mixin/RemoteMethodAccess.mjs +13 -1
- 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);
|
package/src/main/DomAccess.mjs
CHANGED
@@ -1,9 +1,7 @@
|
|
1
|
-
import Base
|
2
|
-
import
|
3
|
-
import
|
4
|
-
import
|
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
|
-
|
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;
|
package/src/table/View.mjs
CHANGED