neo.mjs 10.0.0-beta.3 → 10.0.0-beta.5

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 (76) hide show
  1. package/.github/RELEASE_NOTES/v10.0.0-beta.4.md +41 -0
  2. package/ServiceWorker.mjs +2 -2
  3. package/apps/portal/index.html +1 -1
  4. package/apps/portal/view/ViewportController.mjs +1 -1
  5. package/apps/portal/view/home/FooterContainer.mjs +1 -1
  6. package/apps/portal/view/learn/MainContainerController.mjs +6 -6
  7. package/examples/button/effect/MainContainer.mjs +207 -0
  8. package/examples/button/effect/app.mjs +6 -0
  9. package/examples/button/effect/index.html +11 -0
  10. package/examples/button/effect/neo-config.json +6 -0
  11. package/learn/guides/{Collections.md → datahandling/Collections.md} +6 -6
  12. package/learn/guides/datahandling/Grids.md +621 -0
  13. package/learn/guides/{Records.md → datahandling/Records.md} +4 -3
  14. package/learn/guides/{StateProviders.md → datahandling/StateProviders.md} +146 -1
  15. package/learn/guides/fundamentals/DeclarativeVDOMWithEffects.md +166 -0
  16. package/learn/guides/fundamentals/ExtendingNeoClasses.md +359 -0
  17. package/learn/guides/{Layouts.md → uibuildingblocks/Layouts.md} +40 -38
  18. package/learn/guides/{form_fields → userinteraction/form_fields}/ComboBox.md +3 -3
  19. package/learn/tree.json +64 -57
  20. package/package.json +3 -3
  21. package/src/DefaultConfig.mjs +2 -2
  22. package/src/Neo.mjs +244 -88
  23. package/src/button/Effect.mjs +435 -0
  24. package/src/collection/Base.mjs +35 -3
  25. package/src/component/Base.mjs +72 -61
  26. package/src/container/Base.mjs +28 -24
  27. package/src/controller/Base.mjs +87 -63
  28. package/src/core/Base.mjs +207 -33
  29. package/src/core/Compare.mjs +3 -13
  30. package/src/core/Config.mjs +230 -0
  31. package/src/core/ConfigSymbols.mjs +3 -0
  32. package/src/core/Effect.mjs +127 -0
  33. package/src/core/EffectBatchManager.mjs +68 -0
  34. package/src/core/EffectManager.mjs +38 -0
  35. package/src/core/Util.mjs +3 -18
  36. package/src/data/RecordFactory.mjs +22 -3
  37. package/src/grid/Container.mjs +8 -4
  38. package/src/grid/column/Component.mjs +1 -1
  39. package/src/state/Provider.mjs +343 -452
  40. package/src/state/createHierarchicalDataProxy.mjs +124 -0
  41. package/src/tab/header/EffectButton.mjs +75 -0
  42. package/src/util/Function.mjs +52 -5
  43. package/src/vdom/Helper.mjs +9 -10
  44. package/src/vdom/VNode.mjs +1 -1
  45. package/src/worker/App.mjs +0 -5
  46. package/test/siesta/siesta.js +32 -0
  47. package/test/siesta/tests/CollectionBase.mjs +10 -10
  48. package/test/siesta/tests/VdomHelper.mjs +22 -59
  49. package/test/siesta/tests/config/AfterSetConfig.mjs +100 -0
  50. package/test/siesta/tests/config/Basic.mjs +149 -0
  51. package/test/siesta/tests/config/CircularDependencies.mjs +166 -0
  52. package/test/siesta/tests/config/CustomFunctions.mjs +69 -0
  53. package/test/siesta/tests/config/Hierarchy.mjs +94 -0
  54. package/test/siesta/tests/config/MemoryLeak.mjs +92 -0
  55. package/test/siesta/tests/config/MultiLevelHierarchy.mjs +85 -0
  56. package/test/siesta/tests/core/Effect.mjs +131 -0
  57. package/test/siesta/tests/core/EffectBatching.mjs +322 -0
  58. package/test/siesta/tests/neo/MixinStaticConfig.mjs +138 -0
  59. package/test/siesta/tests/state/Provider.mjs +537 -0
  60. package/test/siesta/tests/state/createHierarchicalDataProxy.mjs +217 -0
  61. package/learn/guides/ExtendingNeoClasses.md +0 -331
  62. /package/learn/guides/{Tables.md → datahandling/Tables.md} +0 -0
  63. /package/learn/guides/{ApplicationBootstrap.md → fundamentals/ApplicationBootstrap.md} +0 -0
  64. /package/learn/guides/{ConfigSystemDeepDive.md → fundamentals/ConfigSystemDeepDive.md} +0 -0
  65. /package/learn/guides/{DeclarativeComponentTreesVsImperativeVdom.md → fundamentals/DeclarativeComponentTreesVsImperativeVdom.md} +0 -0
  66. /package/learn/guides/{InstanceLifecycle.md → fundamentals/InstanceLifecycle.md} +0 -0
  67. /package/learn/guides/{MainThreadAddons.md → fundamentals/MainThreadAddons.md} +0 -0
  68. /package/learn/guides/{Mixins.md → specificfeatures/Mixins.md} +0 -0
  69. /package/learn/guides/{MultiWindow.md → specificfeatures/MultiWindow.md} +0 -0
  70. /package/learn/guides/{PortalApp.md → specificfeatures/PortalApp.md} +0 -0
  71. /package/learn/guides/{ComponentsAndContainers.md → uibuildingblocks/ComponentsAndContainers.md} +0 -0
  72. /package/learn/guides/{CustomComponents.md → uibuildingblocks/CustomComponents.md} +0 -0
  73. /package/learn/guides/{WorkingWithVDom.md → uibuildingblocks/WorkingWithVDom.md} +0 -0
  74. /package/learn/guides/{Forms.md → userinteraction/Forms.md} +0 -0
  75. /package/learn/guides/{events → userinteraction/events}/CustomEvents.md +0 -0
  76. /package/learn/guides/{events → userinteraction/events}/DomEvents.md +0 -0
@@ -0,0 +1,435 @@
1
+ import Component from '../component/Base.mjs';
2
+ import Effect from '../core/Effect.mjs';
3
+ import NeoArray from '../util/Array.mjs';
4
+
5
+ /**
6
+ * @class Neo.button.Effect
7
+ * @extends Neo.component.Base
8
+ */
9
+ class EffectButton extends Component {
10
+ /**
11
+ * Valid values for badgePosition
12
+ * @member {String[]} badgePositions=['bottom-left','bottom-right','top-left','top-right']
13
+ * @protected
14
+ * @static
15
+ */
16
+ static badgePositions = ['bottom-left', 'bottom-right', 'top-left', 'top-right']
17
+ /**
18
+ * Valid values for iconPosition
19
+ * @member {String[]} iconPositions=['top','right','bottom','left']
20
+ * @protected
21
+ * @static
22
+ */
23
+ static iconPositions = ['top', 'right', 'bottom', 'left']
24
+
25
+ static config = {
26
+ /**
27
+ * @member {String} className='Neo.button.Effect'
28
+ * @protected
29
+ */
30
+ className: 'Neo.button.Effect',
31
+ /**
32
+ * @member {String} ntype='effect-button'
33
+ * @protected
34
+ */
35
+ ntype: 'effect-button',
36
+ /**
37
+ * @member {String} badgePosition_='top-right'
38
+ */
39
+ badgePosition_: 'top-right',
40
+ /**
41
+ * @member {String|null} badgeText_=null
42
+ */
43
+ badgeText_: null,
44
+ /**
45
+ * @member {String[]} baseCls=['neo-button']
46
+ */
47
+ baseCls: ['neo-button'],
48
+ /**
49
+ * @member {String[]} cls=[]
50
+ */
51
+ cls: [],
52
+ /**
53
+ * false calls Neo.Main.setRoute()
54
+ * @member {Boolean} editRoute=true
55
+ */
56
+ editRoute: true,
57
+ /**
58
+ * Shortcut for domListeners={click:handler}
59
+ * A string based value assumes that the handlerFn lives inside a controller.Component
60
+ * @member {Function|String|null} handler_=null
61
+ */
62
+ handler_: null,
63
+ /**
64
+ * The scope (this pointer) inside the handler function.
65
+ * Points to the button instance by default.
66
+ * You can use 'this' as a string for convenience reasons
67
+ * @member {Object|String|null} handlerScope=null
68
+ */
69
+ handlerScope: null,
70
+ /**
71
+ * The CSS class to use for an icon, e.g. 'fa fa-home'
72
+ * @member {String|null} [iconCls_=null]
73
+ */
74
+ iconCls_: null,
75
+ /**
76
+ * The color to use for an icon, e.g. '#ff0000' [optional]
77
+ * @member {String|null} iconColor_=null
78
+ */
79
+ iconColor_: null,
80
+ /**
81
+ * The position of the icon in case iconCls has a value.
82
+ * Valid values are: 'top', 'right', 'bottom', 'left'
83
+ * @member {String} iconPosition_='left'
84
+ */
85
+ iconPosition_: 'left',
86
+ /**
87
+ * An array representing the configuration of the menu items.
88
+ *
89
+ * Or a configuration object which adds custom configuration to the menu to be
90
+ * created and includes an `items` property to define the menu items.
91
+ * @member {Object|Object[]|null} menu_=null
92
+ */
93
+ menu_: null,
94
+ /**
95
+ * The pressed state of the Button
96
+ * @member {Boolean} pressed_=false
97
+ */
98
+ pressed_: false,
99
+ /**
100
+ * Change the browser hash value on click.
101
+ * Use route for internal navigation and url for external links. Do not use both on the same instance.
102
+ * Transforms the button tag into an a tag [optional]
103
+ * @member {String|null} route_=null
104
+ */
105
+ route_: null,
106
+ /**
107
+ * @member {String} tag='button'
108
+ */
109
+ tag: 'button',
110
+ /**
111
+ * Transforms the button tag into an a tag [optional]
112
+ * @member {String|null} url_=null
113
+ */
114
+ url_: null,
115
+ /**
116
+ * If url is set, applies the target attribute on the top level vdom node [optional]
117
+ * @member {String} urlTarget_='_blank'
118
+ */
119
+ urlTarget_: '_blank',
120
+ /**
121
+ * True adds an expanding circle on click
122
+ * @member {Boolean} useRippleEffect_=true
123
+ */
124
+ useRippleEffect_: true
125
+ }
126
+
127
+ /**
128
+ * Time in ms for the ripple effect when clicking on the button.
129
+ * Only active if useRippleEffect is set to true.
130
+ * @member {Number} rippleEffectDuration=400
131
+ */
132
+ rippleEffectDuration = 400
133
+ /**
134
+ * Internal flag to store the last setTimeout() id for ripple effect remove node callbacks
135
+ * @member {Number} #rippleTimeoutId=null
136
+ * @private
137
+ */
138
+ #rippleTimeoutId = null
139
+
140
+ /**
141
+ * @param {Object} config
142
+ */
143
+ construct(config) {
144
+ super.construct(config);
145
+
146
+ let me = this;
147
+
148
+ me.addDomListeners({
149
+ click: me.onClick,
150
+ scope: me
151
+ });
152
+
153
+ me.createVdomEffect()
154
+ }
155
+
156
+ /**
157
+ * Final. Should not be overridden.
158
+ * This is the core reactive effect.
159
+ * @returns {Neo.core.Effect}
160
+ * @protected
161
+ */
162
+ createVdomEffect() {
163
+ return new Effect({fn: () => {
164
+ // The effect's only job is to get the config and trigger an update.
165
+ this._vdom = this.getVdomConfig();
166
+ this.update();
167
+ }});
168
+ }
169
+
170
+ /**
171
+ * Builds the array of child nodes (the 'cn' property).
172
+ * Subclasses can override this to add or remove children.
173
+ * @returns {Object[]}
174
+ * @protected
175
+ */
176
+ getVdomChildren() {
177
+ return [
178
+ // iconNode
179
+ {tag: 'span', cls: ['neo-button-glyph', ...this._iconCls || []], removeDom: !this.iconCls, style: {color: this.iconColor || null}},
180
+ // textNode
181
+ {tag: 'span', cls: ['neo-button-text'], id: `${this.id}__text`, removeDom: !this.text, text: this.text},
182
+ // badgeNode
183
+ {cls: ['neo-button-badge', 'neo-' + this.badgePosition], removeDom: !this.badgeText, text: this.badgeText},
184
+ // rippleWrapper
185
+ {cls: ['neo-button-ripple-wrapper'], removeDom: !this.useRippleEffect, cn: [{cls: ['neo-button-ripple']}]}
186
+ ];
187
+ }
188
+
189
+ /**
190
+ * Builds the array of CSS classes for the root element.
191
+ * @returns {String[]}
192
+ * @protected
193
+ */
194
+ getVdomCls() {
195
+ let vdomCls = [...this.baseCls, ...this.cls];
196
+
197
+ NeoArray.toggle(vdomCls, 'no-text', !this.text);
198
+ NeoArray.toggle(vdomCls, 'pressed', this.pressed);
199
+ vdomCls.push('icon-' + this.iconPosition);
200
+
201
+ return vdomCls;
202
+ }
203
+
204
+ /**
205
+ * Builds the top-level VDOM object.
206
+ * Subclasses can override this to add or modify root properties.
207
+ * @returns {Object}
208
+ * @protected
209
+ */
210
+ getVdomConfig() {
211
+ let me = this,
212
+ link = !me.editRoute && me.route || me.url,
213
+ tag = link ? 'a' : 'button';
214
+
215
+ return {
216
+ tag,
217
+ cls : this.getVdomCls(),
218
+ href : link ? (link.startsWith('#') ? link : '#' + link) : null,
219
+ id : me.id,
220
+ style : me.style,
221
+ target : me.url ? me.urlTarget : null,
222
+ type : tag === 'button' ? 'button' : null,
223
+ cn : this.getVdomChildren()
224
+ };
225
+ }
226
+
227
+ /**
228
+ * Triggered after the menu config got changed
229
+ * @param {Object|Object[]|null} value
230
+ * @param {Object|Object[]|null} oldValue
231
+ * @protected
232
+ */
233
+ afterSetMenu(value, oldValue) {
234
+ if (value) {
235
+ import('../menu/List.mjs').then(module => {
236
+ let me = this,
237
+ isArray = Array.isArray(value),
238
+ items = isArray ? value : value.items,
239
+ menuConfig = isArray ? {} : value,
240
+ stateProvider = me.getStateProvider(),
241
+ {appName, theme, windowId} = me,
242
+
243
+ config = Neo.merge({
244
+ module : module.default,
245
+ align : {edgeAlign: 't0-b0', target: me.id},
246
+ appName,
247
+ displayField : 'text',
248
+ floating : true,
249
+ hidden : true,
250
+ parentComponent: me,
251
+ theme,
252
+ windowId
253
+ }, menuConfig);
254
+
255
+ if (items) {
256
+ config.items = items
257
+ }
258
+
259
+ if (stateProvider) {
260
+ config.stateProvider = {parent: stateProvider}
261
+ }
262
+
263
+ me.menuList = Neo.create(config)
264
+ })
265
+ }
266
+ }
267
+
268
+ /**
269
+ * Triggered after the theme config got changed
270
+ * @param {String|null} value
271
+ * @param {String|null} oldValue
272
+ * @protected
273
+ */
274
+ afterSetTheme(value, oldValue) {
275
+ super.afterSetTheme(value, oldValue);
276
+
277
+ let {menuList} = this;
278
+
279
+ if (menuList) {
280
+ menuList.theme = value
281
+ }
282
+ }
283
+
284
+ /**
285
+ * Triggered after the windowId config got changed
286
+ * @param {Number|null} value
287
+ * @param {Number|null} oldValue
288
+ * @protected
289
+ */
290
+ afterSetWindowId(value, oldValue) {
291
+ super.afterSetWindowId(value, oldValue);
292
+
293
+ let {menuList} = this;
294
+
295
+ if (menuList) {
296
+ menuList.windowId = value
297
+ }
298
+ }
299
+
300
+ /**
301
+ * Converts the iconCls array into a string on beforeGet
302
+ * @returns {String}
303
+ * @protected
304
+ */
305
+ beforeGetIconCls() {
306
+ let iconCls = this._iconCls;
307
+
308
+ if (Array.isArray(iconCls)) {
309
+ return iconCls.join(' ')
310
+ }
311
+
312
+ return iconCls
313
+ }
314
+
315
+ /**
316
+ * Triggered before the badgePosition config gets changed
317
+ * @param {String} value
318
+ * @param {String} oldValue
319
+ * @returns {String}
320
+ * @protected
321
+ */
322
+ beforeSetBadgePosition(value, oldValue) {
323
+ return this.beforeSetEnumValue(value, oldValue, 'badgePosition')
324
+ }
325
+
326
+ /**
327
+ * Triggered before the iconCls config gets changed. Converts the string into an array if needed.
328
+ * @param {Array|String|null} value
329
+ * @param {Array|String|null} oldValue
330
+ * @returns {Array}
331
+ * @protected
332
+ */
333
+ beforeSetIconCls(value, oldValue) {
334
+ if (value && !Array.isArray(value)) {
335
+ value = value.split(' ').filter(Boolean)
336
+ }
337
+
338
+ return value
339
+ }
340
+
341
+ /**
342
+ * Triggered before the iconPosition config gets changed
343
+ * @param {String} value
344
+ * @param {String} oldValue
345
+ * @protected
346
+ */
347
+ beforeSetIconPosition(value, oldValue) {
348
+ return this.beforeSetEnumValue(value, oldValue, 'iconPosition')
349
+ }
350
+
351
+ /**
352
+ * @protected
353
+ */
354
+ changeRoute() {
355
+ this.editRoute && Neo.Main.editRoute(this.route)
356
+ }
357
+
358
+ /**
359
+ * @param args
360
+ */
361
+ destroy(...args) {
362
+ this.menuList?.destroy(true, false);
363
+ super.destroy(...args)
364
+ }
365
+
366
+ /**
367
+ * @param {Object} data
368
+ */
369
+ onClick(data) {
370
+ let me = this;
371
+
372
+ me.bindCallback(me.handler, 'handler', me.handlerScope || me);
373
+ me.handler?.(data);
374
+
375
+ me.menu && me.toggleMenu();
376
+ me.route && me.changeRoute(); // only relevant for editRoute=true
377
+ me.useRippleEffect && me.showRipple(data)
378
+ }
379
+
380
+ /**
381
+ * @param {Object} data
382
+ */
383
+ async showRipple(data) {
384
+ let me = this,
385
+ buttonRect = data.path[0].rect,
386
+ diameter = Math.max(buttonRect.height, buttonRect.width),
387
+ radius = diameter / 2,
388
+ rippleEffectDuration = me.rippleEffectDuration,
389
+ rippleWrapper = me.getVdomRoot().cn[3],
390
+ rippleEl = rippleWrapper.cn[0],
391
+ rippleTimeoutId;
392
+
393
+ rippleEl.style = Object.assign(rippleEl.style || {}, {
394
+ animation: 'none',
395
+ height : `${diameter}px`,
396
+ left : `${data.clientX - buttonRect.left - radius}px`,
397
+ top : `${data.clientY - buttonRect.top - radius}px`,
398
+ width : `${diameter}px`
399
+ });
400
+
401
+ delete rippleWrapper.removeDom;
402
+ me.update();
403
+
404
+ await me.timeout(1);
405
+
406
+ rippleEl.style.animation = `ripple ${rippleEffectDuration}ms linear`;
407
+ me.update();
408
+
409
+ me.#rippleTimeoutId = rippleTimeoutId = setTimeout(() => {
410
+ // we do not want to break animations when clicking multiple times
411
+ if (me.#rippleTimeoutId === rippleTimeoutId) {
412
+ me.#rippleTimeoutId = null;
413
+
414
+ rippleWrapper.removeDom = true;
415
+ me.update()
416
+ }
417
+ }, rippleEffectDuration)
418
+ }
419
+
420
+ /**
421
+ *
422
+ */
423
+ async toggleMenu() {
424
+ let {menuList} = this,
425
+ hidden = !menuList.hidden;
426
+
427
+ menuList.hidden = hidden;
428
+
429
+ if (!hidden) {
430
+ await this.timeout(50)
431
+ }
432
+ }
433
+ }
434
+
435
+ export default Neo.setupClass(EffectButton);
@@ -48,6 +48,11 @@ class Collection extends Base {
48
48
  * @member {Boolean} autoSort=true
49
49
  */
50
50
  autoSort: true,
51
+ /**
52
+ * Stores the items.length of the items array in use
53
+ * @member {Number} count_=0
54
+ */
55
+ count_: 0,
51
56
  /**
52
57
  * Use 'primitive' for default filters, use 'advanced' for filters using a filterBy method
53
58
  * which need to iterate over other collection items
@@ -140,6 +145,17 @@ class Collection extends Base {
140
145
  }
141
146
 
142
147
  /**
148
+ * Triggered after the badgePosition config got changed
149
+ * @param {Number} value
150
+ * @param {Number} oldValue
151
+ * @protected
152
+ */
153
+ afterSetCount(value, oldValue) {
154
+ this.fire('countChange', {oldValue, value})
155
+ }
156
+
157
+ /**
158
+ * Triggered after the filters config got changed
143
159
  * @param {Array} value
144
160
  * @param {Array} oldValue
145
161
  * @protected
@@ -158,6 +174,7 @@ class Collection extends Base {
158
174
  }
159
175
 
160
176
  /**
177
+ * Triggered after the items config got changed
161
178
  * @param {Array} value
162
179
  * @param {Array} oldValue
163
180
  * @protected
@@ -174,10 +191,13 @@ class Collection extends Base {
174
191
  item = value[i];
175
192
  me.map.set(item[keyProperty], item)
176
193
  }
194
+
195
+ me.count = len
177
196
  }
178
197
  }
179
198
 
180
199
  /**
200
+ * Triggered after the sorters config got changed
181
201
  * @param {Array} value
182
202
  * @param {Array} oldValue
183
203
  * @protected
@@ -198,6 +218,7 @@ class Collection extends Base {
198
218
  }
199
219
 
200
220
  /**
221
+ * Triggered after the sourceId config got changed
201
222
  * @param {Number|String} value
202
223
  * @param {Number|String} oldValue
203
224
  * @protected
@@ -433,13 +454,17 @@ class Collection extends Base {
433
454
  filters = me._filters || [],
434
455
  sorters = me._sorters || [];
435
456
 
457
+ // Ensure the keyProperty does not get lost.
458
+ config.keyProperty = me.keyProperty;
459
+
436
460
  delete config.id;
437
461
  delete config.filters;
438
462
  delete config.items;
439
463
  delete config.sorters;
440
464
 
441
465
  if (me._items.length > 0) {
442
- config.items = [...me._items]
466
+ config.items = [...me._items];
467
+ config.count = config.items.length;
443
468
  }
444
469
 
445
470
  config.filters = [];
@@ -673,6 +698,8 @@ class Collection extends Base {
673
698
 
674
699
  me.allItems = Neo.create(Collection, {
675
700
  ...Neo.clone(config, true, true),
701
+ id : me.id + '-all',
702
+ items : [...me._items], // Initialize with a shallow copy of current items
676
703
  keyProperty: me.keyProperty,
677
704
  sourceId : me.id
678
705
  })
@@ -727,6 +754,8 @@ class Collection extends Base {
727
754
  me.doSort(me.items, true)
728
755
  }
729
756
 
757
+ me.count = me.items.length;
758
+
730
759
  me.fire('filter', {
731
760
  isFiltered: me[isFiltered],
732
761
  items : me.items,
@@ -845,11 +874,12 @@ class Collection extends Base {
845
874
  }
846
875
 
847
876
  /**
848
- * Returns the length of the internal items array
877
+ * Returns the config value of this.count
849
878
  * @returns {Number}
879
+ * @deprecated Use `this.count` directly instead.
850
880
  */
851
881
  getCount() {
852
- return this._items.length
882
+ return this._count || 0 // skipping beforeGetCount() on purpose
853
883
  }
854
884
 
855
885
  /**
@@ -1238,6 +1268,8 @@ class Collection extends Base {
1238
1268
  }
1239
1269
 
1240
1270
  if (me[updatingIndex] === 0) {
1271
+ me.count = me._items.length;
1272
+
1241
1273
  me.fire('mutate', {
1242
1274
  addedItems : toAddArray,
1243
1275
  preventBubbleUp: me.preventBubbleUp,