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
@@ -1,5 +1,12 @@
1
+ import StringUtil from '../util/String.mjs';
2
+
1
3
  /**
2
- * Wrapper class for vnode objects. See the tutorials for further infos.
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
- * @member {Array} attributes=[]
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
- Object.assign(this, {
49
- attributes: config.attributes || [],
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
- className : config.className || [],
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
- this.static = true
97
+ me.static = true
62
98
  }
63
99
  }
64
100
  }
65
101
 
66
102
  const ns = Neo.ns('Neo.vdom', true);
67
- ns['VNode'] = VNode;
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?.('"', '&quot;') ?? 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
- .catch(err => {me.reject(msg, err)})
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)