neo.mjs 9.16.0 → 10.0.0-alpha.2
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/README.md +127 -121
- 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 +33 -5
- 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 +15 -6
- 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 +442 -0
- package/src/main/DomAccess.mjs +13 -95
- 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 +179 -309
- 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
package/src/vdom/VNode.mjs
CHANGED
@@ -1,5 +1,12 @@
|
|
1
|
+
import StringUtil from '../util/String.mjs';
|
2
|
+
|
1
3
|
/**
|
2
|
-
* Wrapper class for vnode objects.
|
4
|
+
* Wrapper class for vnode objects.
|
5
|
+
* For convenience, a VNode instance will always contain a childNodes array, which can be empty.
|
6
|
+
* A VNode can optionally have `innerHTML` xor `textContent`
|
7
|
+
* `textContent` is better from a XSS security perspective.
|
8
|
+
* If by accident both are set, `innerHTML` will get the priority.
|
9
|
+
*
|
3
10
|
* @class Neo.vdom.VNode
|
4
11
|
*/
|
5
12
|
class VNode {
|
@@ -8,7 +15,8 @@ class VNode {
|
|
8
15
|
*/
|
9
16
|
constructor(config) {
|
10
17
|
/**
|
11
|
-
*
|
18
|
+
* Not set for vtype='text' nodes
|
19
|
+
* @member {Object} attributes={}
|
12
20
|
*/
|
13
21
|
|
14
22
|
/**
|
@@ -16,6 +24,7 @@ class VNode {
|
|
16
24
|
*/
|
17
25
|
|
18
26
|
/**
|
27
|
+
* Not set for vtype='text' nodes
|
19
28
|
* @member {Array} className=[]
|
20
29
|
*/
|
21
30
|
|
@@ -37,33 +46,60 @@ class VNode {
|
|
37
46
|
*/
|
38
47
|
|
39
48
|
/**
|
49
|
+
* Not set for vtype='text' nodes
|
40
50
|
* @member {Object} style
|
41
51
|
*/
|
42
52
|
|
53
|
+
/**
|
54
|
+
* @member {String} textContent
|
55
|
+
*/
|
56
|
+
|
43
57
|
/**
|
44
58
|
* Valid values are "root", "text" & "vnode"
|
45
59
|
* @member {String} vtype='vnode'
|
46
60
|
*/
|
47
61
|
|
48
|
-
|
49
|
-
|
62
|
+
let me = this,
|
63
|
+
{textContent} = config,
|
64
|
+
hasInnerHtml = Object.hasOwn(config, 'innerHTML'),
|
65
|
+
isVText = config.vtype === 'text';
|
66
|
+
|
67
|
+
Object.assign(me, {
|
50
68
|
childNodes: config.childNodes || [],
|
51
|
-
|
52
|
-
id : config.id || Neo.getId('vnode'),
|
53
|
-
innerHTML : config.innerHTML,
|
54
|
-
nodeName : config.nodeName,
|
55
|
-
style : config.style,
|
69
|
+
id : config.id || Neo.getId(isVText ? 'vtext' : 'vnode'),
|
56
70
|
vtype : config.vtype || 'vnode'
|
57
71
|
});
|
58
72
|
|
73
|
+
if (isVText) {
|
74
|
+
// XSS Security: a pure text node is not supposed to contain HTML
|
75
|
+
me.textContent = StringUtil.escapeHtml(hasInnerHtml ? config.innerHTML : textContent)
|
76
|
+
} else {
|
77
|
+
Object.assign(me, {
|
78
|
+
attributes: config.attributes || {},
|
79
|
+
className : config.className || [],
|
80
|
+
nodeName : config.nodeName || 'div',
|
81
|
+
style : config.style
|
82
|
+
});
|
83
|
+
|
84
|
+
// Use vdom.html on your own risk, it is not fully XSS secure.
|
85
|
+
if (hasInnerHtml) {
|
86
|
+
me.innerHTML = config.innerHTML
|
87
|
+
}
|
88
|
+
|
89
|
+
// We only apply textContent, in case it has content
|
90
|
+
else if (Object.hasOwn(config, 'textContent')) {
|
91
|
+
me.textContent = Neo.config.useStringBasedMounting ? StringUtil.escapeHtml(textContent) : textContent
|
92
|
+
}
|
93
|
+
}
|
94
|
+
|
59
95
|
// We only apply the static attribute, in case the value is true
|
60
96
|
if (config.static) {
|
61
|
-
|
97
|
+
me.static = true
|
62
98
|
}
|
63
99
|
}
|
64
100
|
}
|
65
101
|
|
66
102
|
const ns = Neo.ns('Neo.vdom', true);
|
67
|
-
ns
|
103
|
+
ns.VNode = VNode;
|
68
104
|
|
69
105
|
export default VNode;
|
@@ -0,0 +1,65 @@
|
|
1
|
+
/**
|
2
|
+
* The following top-level attributes will get converted into styles:
|
3
|
+
* height, maxHeight, maxWidth, minHeight, minWidth, width
|
4
|
+
*
|
5
|
+
* Some tags must not do the transformation, so we add them here.
|
6
|
+
* @member {Set} rawDimensionTags
|
7
|
+
*/
|
8
|
+
export const rawDimensionTags = new Set([
|
9
|
+
'circle',
|
10
|
+
'clipPath',
|
11
|
+
'ellipse',
|
12
|
+
'filter',
|
13
|
+
'foreignObject',
|
14
|
+
'image',
|
15
|
+
'marker',
|
16
|
+
'mask',
|
17
|
+
'pattern',
|
18
|
+
'rect',
|
19
|
+
'svg',
|
20
|
+
'use'
|
21
|
+
]);
|
22
|
+
|
23
|
+
/**
|
24
|
+
* Void attributes inside html tags
|
25
|
+
* @member {Set} voidAttributes
|
26
|
+
* @protected
|
27
|
+
*/
|
28
|
+
export const voidAttributes = new Set([
|
29
|
+
'checked',
|
30
|
+
'defer',
|
31
|
+
'disabled',
|
32
|
+
'ismap',
|
33
|
+
'multiple',
|
34
|
+
'nohref',
|
35
|
+
'noresize',
|
36
|
+
'noshade',
|
37
|
+
'nowrap',
|
38
|
+
'open',
|
39
|
+
'readonly',
|
40
|
+
'required',
|
41
|
+
'reversed',
|
42
|
+
'selected'
|
43
|
+
]);
|
44
|
+
|
45
|
+
/**
|
46
|
+
* Void html tags
|
47
|
+
* @member {Set} voidElements
|
48
|
+
* @protected
|
49
|
+
*/
|
50
|
+
export const voidElements = new Set([
|
51
|
+
'area',
|
52
|
+
'base',
|
53
|
+
'br',
|
54
|
+
'col',
|
55
|
+
'embed',
|
56
|
+
'hr',
|
57
|
+
'img',
|
58
|
+
'input',
|
59
|
+
'link',
|
60
|
+
'meta',
|
61
|
+
'param',
|
62
|
+
'source',
|
63
|
+
'track',
|
64
|
+
'wbr'
|
65
|
+
]);
|
@@ -0,0 +1,51 @@
|
|
1
|
+
const DomApiVnodeCreator = {
|
2
|
+
/**
|
3
|
+
* Recursively creates a VNode tree suitable for direct DOM API insertion.
|
4
|
+
* This tree excludes any nodes that are marked as 'moved' within the movedNodes map,
|
5
|
+
* as their DOM manipulation will be handled by separate moveNode deltas.
|
6
|
+
*
|
7
|
+
* @param {Neo.vdom.VNode} vnode The VNode to process.
|
8
|
+
* @param {Map} [movedNodes] A map of VNodes that are being moved.
|
9
|
+
* @returns {Object|null} A new VNode tree (or subtree) with moved nodes pruned, or null if the root is a moved node.
|
10
|
+
*/
|
11
|
+
create(vnode, movedNodes) {
|
12
|
+
/*
|
13
|
+
* A vnode itself can be null (removeDom: true) => opt out.
|
14
|
+
*
|
15
|
+
* If the node has a componentId, there is nothing to do (scoped vdom updates), opt out.
|
16
|
+
*
|
17
|
+
* If this specific vnode is in the movedNodes map, it means its DOM element
|
18
|
+
* will be moved by a separate delta. So, we should not include it in this fragment.
|
19
|
+
*/
|
20
|
+
if (!vnode || vnode.componentId || (vnode.id && movedNodes?.get(vnode.id))) {
|
21
|
+
return null // Prune this branch
|
22
|
+
}
|
23
|
+
|
24
|
+
// For text nodes, we can return the original VNode directly, as they have no childNodes array to modify.
|
25
|
+
if (vnode.vtype === 'text') {
|
26
|
+
return vnode
|
27
|
+
}
|
28
|
+
|
29
|
+
// For other VNodes (vnode or root), create a shallow clone first.
|
30
|
+
let clonedVnode = {...vnode, childNodes: []};
|
31
|
+
|
32
|
+
// Recursively process children
|
33
|
+
if (vnode.childNodes.length > 0) {
|
34
|
+
vnode.childNodes.forEach(child => {
|
35
|
+
const processedChild = DomApiVnodeCreator.create(child, movedNodes);
|
36
|
+
|
37
|
+
// Only add if not pruned
|
38
|
+
if (processedChild) {
|
39
|
+
clonedVnode.childNodes.push(processedChild)
|
40
|
+
}
|
41
|
+
});
|
42
|
+
}
|
43
|
+
|
44
|
+
return clonedVnode
|
45
|
+
}
|
46
|
+
};
|
47
|
+
|
48
|
+
const ns = Neo.ns('Neo.vdom.util', true);
|
49
|
+
ns.DomApiVnodeCreator = DomApiVnodeCreator;
|
50
|
+
|
51
|
+
export default DomApiVnodeCreator;
|
@@ -0,0 +1,123 @@
|
|
1
|
+
import NeoString from '../../util/String.mjs';
|
2
|
+
import {voidAttributes, voidElements} from '../domConstants.mjs';
|
3
|
+
|
4
|
+
const StringFromVnode = {
|
5
|
+
/**
|
6
|
+
* @param {Object} vnode
|
7
|
+
* @protected
|
8
|
+
*/
|
9
|
+
createCloseTag(vnode) {
|
10
|
+
return voidElements.has(vnode.nodeName) ? '' : '</' + vnode.nodeName + '>'
|
11
|
+
},
|
12
|
+
|
13
|
+
/**
|
14
|
+
* @param {Object} vnode
|
15
|
+
* @protected
|
16
|
+
*/
|
17
|
+
createOpenTag(vnode) {
|
18
|
+
let string = '<' + vnode.nodeName,
|
19
|
+
{attributes} = vnode,
|
20
|
+
cls = vnode.className,
|
21
|
+
style;
|
22
|
+
|
23
|
+
if (vnode.style) {
|
24
|
+
style = Neo.createStyles(vnode.style);
|
25
|
+
|
26
|
+
if (style !== '') {
|
27
|
+
string += ` style="${style}"`
|
28
|
+
}
|
29
|
+
}
|
30
|
+
|
31
|
+
if (cls) {
|
32
|
+
if (Array.isArray(cls)) {
|
33
|
+
cls = cls.join(' ')
|
34
|
+
}
|
35
|
+
|
36
|
+
if (cls !== '') {
|
37
|
+
string += ` class="${cls}"`
|
38
|
+
}
|
39
|
+
}
|
40
|
+
|
41
|
+
if (vnode.id) {
|
42
|
+
if (Neo.config.useDomIds) {
|
43
|
+
string += ` id="${vnode.id}"`
|
44
|
+
} else {
|
45
|
+
string += ` data-neo-id="${vnode.id}"`
|
46
|
+
}
|
47
|
+
}
|
48
|
+
|
49
|
+
Object.entries(attributes).forEach(([key, value]) => {
|
50
|
+
if (voidAttributes.has(key)) {
|
51
|
+
if (value === 'true') { // vnode attribute values get converted into strings
|
52
|
+
string += ` ${key}`
|
53
|
+
}
|
54
|
+
} else if (key !== 'removeDom') {
|
55
|
+
if (key === 'value') {
|
56
|
+
value = NeoString.escapeHtml(value)
|
57
|
+
}
|
58
|
+
|
59
|
+
string += ` ${key}="${value?.replaceAll?.('"', '"') ?? value}"`
|
60
|
+
}
|
61
|
+
});
|
62
|
+
|
63
|
+
return string + '>'
|
64
|
+
},
|
65
|
+
|
66
|
+
/**
|
67
|
+
* @param {Neo.vdom.VNode} vnode
|
68
|
+
* @param {Map} [movedNodes]
|
69
|
+
*/
|
70
|
+
create(vnode, movedNodes) {
|
71
|
+
let me = this,
|
72
|
+
id = vnode?.id;
|
73
|
+
|
74
|
+
// If a content node will get moved by a delta update OP, there is no need to regenerate it. Opt out.
|
75
|
+
if (id && movedNodes?.get(id)) {
|
76
|
+
return ''
|
77
|
+
}
|
78
|
+
|
79
|
+
switch (vnode.vtype) {
|
80
|
+
case 'root':
|
81
|
+
return me.create(vnode.childNodes[0], movedNodes)
|
82
|
+
case 'text':
|
83
|
+
// For text VNodes, `vnode.textContent` holds the HTML-escaped content.
|
84
|
+
// Add the comment wrappers here for string output, aligning with main.mixin.DeltaUpdates.createDomTree().
|
85
|
+
// `vnode.textContent || ''` ensures robustness in case vnode.textContent is not a string (e.g., a number or null).
|
86
|
+
return `<!-- ${vnode.id} -->${vnode.textContent}<!-- /neo-vtext -->`
|
87
|
+
case 'vnode':
|
88
|
+
return me.createOpenTag(vnode) + me.createTagContent(vnode, movedNodes) + me.createCloseTag(vnode)
|
89
|
+
default:
|
90
|
+
return ''
|
91
|
+
}
|
92
|
+
},
|
93
|
+
|
94
|
+
/**
|
95
|
+
* @param {Neo.vdom.VNode} vnode
|
96
|
+
* @param {Map} [movedNodes]
|
97
|
+
* @protected
|
98
|
+
*/
|
99
|
+
createTagContent(vnode, movedNodes) {
|
100
|
+
const hasContent = vnode.innerHTML || vnode.textContent;
|
101
|
+
|
102
|
+
if (hasContent) {
|
103
|
+
return hasContent
|
104
|
+
}
|
105
|
+
|
106
|
+
let string = '',
|
107
|
+
len = vnode.childNodes ? vnode.childNodes.length : 0,
|
108
|
+
i = 0,
|
109
|
+
childNode;
|
110
|
+
|
111
|
+
for (; i < len; i++) {
|
112
|
+
childNode = vnode.childNodes[i];
|
113
|
+
string += this.create(childNode, movedNodes)
|
114
|
+
}
|
115
|
+
|
116
|
+
return string
|
117
|
+
}
|
118
|
+
};
|
119
|
+
|
120
|
+
const ns = Neo.ns('Neo.vdom.util', true);
|
121
|
+
ns.StringFromVnode = StringFromVnode;
|
122
|
+
|
123
|
+
export default StringFromVnode;
|
@@ -98,7 +98,19 @@ class RemoteMethodAccess extends Base {
|
|
98
98
|
|
99
99
|
if (out instanceof Promise) {
|
100
100
|
out
|
101
|
-
|
101
|
+
/*
|
102
|
+
* Intended logic:
|
103
|
+
* If the code of a remote method fails, it would not show any errors inside the console,
|
104
|
+
* so we want to manually log the error for debugging.
|
105
|
+
* Rejecting the Promise gives us the chance to recover.
|
106
|
+
*
|
107
|
+
* Example:
|
108
|
+
* Neo.vdom.Helper.update(opts).catch(err => {
|
109
|
+
* me.isVdomUpdating = false;
|
110
|
+
* reject?.()
|
111
|
+
* }).then(data => {...})
|
112
|
+
*/
|
113
|
+
.catch(err => {console.error(err); me.reject(msg, err)})
|
102
114
|
.then(data => {me.resolve(msg, data)})
|
103
115
|
} else {
|
104
116
|
me.resolve(msg, out)
|