neo.mjs 6.8.2 → 6.9.0

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 (66) hide show
  1. package/.github/PULL_REQUEST_TEMPLATE.md +1 -1
  2. package/apps/ServiceWorker.mjs +2 -2
  3. package/apps/learnneo/app.mjs +6 -0
  4. package/apps/learnneo/index.html +11 -0
  5. package/apps/learnneo/model/Content.mjs +44 -0
  6. package/apps/learnneo/neo-config.json +6 -0
  7. package/apps/learnneo/store/Content.mjs +24 -0
  8. package/apps/learnneo/view/Viewport.mjs +34 -0
  9. package/apps/learnneo/view/ViewportController.mjs +19 -0
  10. package/apps/learnneo/view/home/ContentTreeList.mjs +41 -0
  11. package/apps/learnneo/view/home/MainContainer.mjs +51 -0
  12. package/apps/learnneo/view/home/MainContainerController.mjs +50 -0
  13. package/apps/route/app.mjs +6 -0
  14. package/apps/route/index.html +11 -0
  15. package/apps/route/neo-config.json +6 -0
  16. package/apps/route/view/ButtonBar.mjs +57 -0
  17. package/apps/route/view/CenterContainer.mjs +37 -0
  18. package/apps/route/view/FooterContainer.mjs +47 -0
  19. package/apps/route/view/HeaderContainer.mjs +47 -0
  20. package/apps/route/view/MainView.mjs +66 -0
  21. package/apps/route/view/MainViewController.mjs +210 -0
  22. package/apps/route/view/MetaContainer.mjs +52 -0
  23. package/apps/route/view/Viewport.mjs +15 -0
  24. package/apps/route/view/center/CardAdministration.mjs +36 -0
  25. package/apps/route/view/center/CardAdministrationDenied.mjs +26 -0
  26. package/apps/route/view/center/CardContact.mjs +29 -0
  27. package/apps/route/view/center/CardHome.mjs +26 -0
  28. package/apps/route/view/center/CardSection1.mjs +26 -0
  29. package/apps/route/view/center/CardSection2.mjs +27 -0
  30. package/examples/ConfigurationViewport.mjs +1 -1
  31. package/examples/ServiceWorker.mjs +2 -2
  32. package/examples/form/field/select/MainContainer.mjs +7 -2
  33. package/examples/table/container/MainContainer.mjs +4 -2
  34. package/examples/table/container/MainModel.mjs +3 -0
  35. package/examples/table/container/MainStore.mjs +10 -10
  36. package/examples/todoList/version1/MainComponent.mjs +5 -5
  37. package/examples/toolbar/paging/view/MainContainer.mjs +31 -3
  38. package/package.json +1 -1
  39. package/resources/data/learnneo/content.json +27 -0
  40. package/resources/data/learnneo/pages/whyneo.md +76 -0
  41. package/resources/scss/src/apps/route/CenterContainer.scss +29 -0
  42. package/resources/scss/src/apps/route/HeaderContainer.scss +122 -0
  43. package/resources/scss/src/apps/route/MainView.scss +3 -0
  44. package/resources/scss/src/apps/route/MetaContainer.scss +44 -0
  45. package/resources/scss/src/apps/route/_all.scss +1 -0
  46. package/resources/scss/src/form/field/Picker.scss +0 -1
  47. package/src/DefaultConfig.mjs +2 -2
  48. package/src/Neo.mjs +15 -14
  49. package/src/button/Base.mjs +2 -2
  50. package/src/component/Base.mjs +41 -50
  51. package/src/container/Base.mjs +59 -2
  52. package/src/controller/Base.mjs +84 -4
  53. package/src/controller/Component.mjs +22 -7
  54. package/src/core/Observable.mjs +50 -9
  55. package/src/dialog/Base.mjs +1 -4
  56. package/src/form/field/FileUpload.mjs +11 -0
  57. package/src/form/field/Picker.mjs +0 -17
  58. package/src/form/field/Range.mjs +8 -0
  59. package/src/form/field/Text.mjs +29 -0
  60. package/src/form/field/trigger/Picker.mjs +7 -0
  61. package/src/main/DomEvents.mjs +9 -3
  62. package/src/manager/DomEvent.mjs +3 -0
  63. package/src/menu/List.mjs +1 -1
  64. package/src/table/View.mjs +78 -53
  65. package/src/toolbar/Paging.mjs +68 -76
  66. package/src/tooltip/Base.mjs +111 -11
@@ -16,7 +16,23 @@ class Base extends CoreBase {
16
16
  * @member {String} ntype='controller'
17
17
  * @protected
18
18
  */
19
- ntype: 'controller'
19
+ ntype: 'controller',
20
+
21
+ /**
22
+ * @member {Object} routes={}
23
+ */
24
+ routes: {},
25
+
26
+ /**
27
+ * @member {Object} handleRoutes={}
28
+ */
29
+ handleRoutes: {},
30
+
31
+ /**
32
+ * @member {String} defaultRoute=undefined
33
+ */
34
+ defaultRoute: null
35
+
20
36
  }
21
37
 
22
38
  /**
@@ -25,7 +41,21 @@ class Base extends CoreBase {
25
41
  construct(config) {
26
42
  super.construct(config);
27
43
 
28
- HashHistory.on('change', this.onHashChange, this);
44
+ const me = this;
45
+
46
+ me.handleRoutes = {};
47
+ if (Object.keys(me.routes).length > 0) {
48
+ Object.keys(me.routes).forEach(key => {
49
+ if (key.toLowerCase() === 'default'){
50
+ me.defaultRoute = me.routes[key];
51
+ } else {
52
+ me.handleRoutes[key] = new RegExp(key.replace(/{[^\s/]+}/g, '([\\w-]+)')+'$');
53
+ }
54
+
55
+ });
56
+ }
57
+
58
+ HashHistory.on('change', me.onHashChange, me);
29
59
  }
30
60
 
31
61
  /**
@@ -37,6 +67,15 @@ class Base extends CoreBase {
37
67
  super.destroy(...args);
38
68
  }
39
69
 
70
+ /**
71
+ * Placeholder method which gets triggered when an invalid route is called
72
+ * @param {Object} value
73
+ * @param {Object} oldValue
74
+ */
75
+ onNoRouteFound(value, oldValue) {
76
+
77
+ }
78
+
40
79
  /**
41
80
  * Placeholder method which gets triggered when the hash inside the browser url changes
42
81
  * @param {Object} value
@@ -44,6 +83,46 @@ class Base extends CoreBase {
44
83
  */
45
84
  onHashChange(value, oldValue) {
46
85
 
86
+ const me = this;
87
+ let hasRouteBeenFound = false;
88
+ Object.keys(me.handleRoutes).every( key => {
89
+ let preHandler = undefined;
90
+ let executeHandler = undefined;
91
+ let responsePreHandler = undefined;
92
+
93
+ const result = value.hashString.match(me.handleRoutes[key]);
94
+ if (result){
95
+ const target = me.routes[key];
96
+ if (Neo.isString(target)){
97
+ executeHandler = this.routes[key];
98
+ responsePreHandler = true;
99
+ }
100
+ if (Neo.isObject(target)){
101
+ executeHandler = this.routes[key].handler;
102
+ preHandler = this.routes[key].preHandler;
103
+ responsePreHandler = preHandler ? me[preHandler]?.call(this, value, oldValue, result.splice(1,result.length - 1)) : true;
104
+ }
105
+
106
+ hasRouteBeenFound = true;
107
+
108
+ if (responsePreHandler) {
109
+ this[executeHandler]?.call(this, value, oldValue, result.splice(1,result.length - 1));
110
+ } else {
111
+ console.warn('No preHandler defined for routes -> todo it better');
112
+ }
113
+ return false;
114
+
115
+ }
116
+ return true;
117
+ });
118
+
119
+ if (Object.keys(me.handleRoutes).length > 0 && !hasRouteBeenFound) {
120
+ if (me.defaultRoute) {
121
+ this[me.defaultRoute]?.call(this, value, oldValue);
122
+ } else {
123
+ this.onNoRouteFound(value, oldValue);
124
+ }
125
+ }
47
126
  }
48
127
 
49
128
  /**
@@ -53,9 +132,10 @@ class Base extends CoreBase {
53
132
  let currentHash = HashHistory.first();
54
133
 
55
134
  currentHash && this.onHashChange(currentHash, null);
56
-
57
- super.onConstructed();
58
135
  }
136
+
137
+
138
+
59
139
  }
60
140
 
61
141
  Neo.applyClassConfig(Base);
@@ -69,10 +69,22 @@ class Component extends Base {
69
69
  * @param {String} handlerName
70
70
  * @returns {Neo.controller.Component|null}
71
71
  */
72
- getHandlerScope(handlerName) {
72
+ getHandlerScope(handlerName, component) {
73
73
  let me = this,
74
74
  parent = me.parent;
75
75
 
76
+ if (component) {
77
+ // Look for ths function *name* first in the Component itself.
78
+ // If we find it, return true so calling code knows not to continue to search.
79
+ const handlerCb = component.resolveCallback(handlerName, component);
80
+
81
+ // Handler fn is resolved in the Component or its own parent chain.
82
+ // Return a status indicating that we do not need an erly binding
83
+ if (handlerCb.fn) {
84
+ return true;
85
+ }
86
+ }
87
+
76
88
  return Neo.isFunction(me[handlerName]) ?
77
89
  me : parent ?
78
90
  parent.getHandlerScope(handlerName) : null;
@@ -140,9 +152,12 @@ class Component extends Base {
140
152
  eventHandler, handlerScope;
141
153
 
142
154
  if (handler && typeof handler === 'string') {
143
- handlerScope = me.getHandlerScope(handler);
155
+ handlerScope = me.getHandlerScope(handler, component);
144
156
 
145
- component.handler = handlerScope[handler].bind(component.handlerScope || handlerScope)
157
+ // If the handler name was not resolved in the Component itself, bind it
158
+ if (handlerScope !== true) {
159
+ component.handler = handlerScope[handler].bind(component.handlerScope || handlerScope);
160
+ }
146
161
  }
147
162
 
148
163
  if (listeners) {
@@ -150,11 +165,11 @@ class Component extends Base {
150
165
  if (key !== 'scope' && key !== 'delegate') {
151
166
  if (Neo.isString(value)) {
152
167
  eventHandler = value;
153
- handlerScope = me.getHandlerScope(eventHandler);
168
+ handlerScope = me.getHandlerScope(eventHandler, component);
154
169
 
155
170
  if (!handlerScope) {
156
171
  Logger.logError('Unknown event handler for', eventHandler, component)
157
- } else {
172
+ } else if (handlerScope !== true) {
158
173
  listeners[key] = {};
159
174
  listeners[key].fn = handlerScope[eventHandler].bind(handlerScope)
160
175
  }
@@ -162,11 +177,11 @@ class Component extends Base {
162
177
  value.forEach(listener => {
163
178
  if (Neo.isObject(listener) && listener.hasOwnProperty('fn') && Neo.isString(listener.fn)) {
164
179
  eventHandler = listener.fn;
165
- handlerScope = me.getHandlerScope(eventHandler);
180
+ handlerScope = me.getHandlerScope(eventHandler, component);
166
181
 
167
182
  if (!handlerScope) {
168
183
  Logger.logError('Unknown event handler for', eventHandler, component)
169
- } else {
184
+ } else if (handlerScope !== true) {
170
185
  listener.fn = handlerScope[eventHandler].bind(handlerScope)
171
186
  }
172
187
  }
@@ -98,31 +98,50 @@ class Observable extends Base {
98
98
  let me = this,
99
99
  args = [].slice.call(arguments, 1),
100
100
  listeners = me.listeners,
101
- eventConfig, events, i, len;
101
+ handler, handlers, i, len;
102
102
 
103
103
  if (listeners && listeners[name]) {
104
- events = [...listeners[name]];
105
- len = events.length;
104
+ handlers = [...listeners[name]];
105
+ len = handlers.length;
106
106
 
107
107
  for (i = 0; i < len; i++) {
108
- eventConfig = events[i];
108
+ handler = handlers[i];
109
109
 
110
- if (!Neo.isFunction(eventConfig.fn)) {
111
- eventConfig.fn = eventConfig.scope[eventConfig.fn];
112
- }
110
+ // Resolve function name on the scope (oe me), or, if it starts with 'up.'
111
+ // look in the ownership hierarchy from me.
112
+ const cb = me.resolveCallback(handler.fn, handler.scope || me);
113
113
 
114
114
  // remove the listener, in case the scope no longer exists
115
- if (eventConfig.scope && !eventConfig.scope.id) {
115
+ if (cb.scope && !cb.scope.id) {
116
116
  listeners[name].splice(i, 1);
117
117
  } else {
118
118
  if (!me.suspendEvents) {
119
- eventConfig.fn.apply(eventConfig.scope || me, eventConfig.data ? args.concat(eventConfig.data) : args);
119
+ // Object event format. Inject firer reference in as 'source'
120
+ if (args.length === 1 && typeof(args[0]) === 'object') {
121
+ args[0].source = me.id;
122
+ }
123
+ cb.fn.apply(cb.scope, handler.data ? args.concat(handler.data) : args);
120
124
  }
121
125
  }
122
126
  }
123
127
  }
124
128
  }
125
129
 
130
+ /**
131
+ * Call the passed function, or a function by *name* which exists in the passed scope's
132
+ * or this component's ownership chain.
133
+ * @param {Function|String} fn A function, or the name of a function to find in the passed scope object/
134
+ * @param {Object} scope The scope to find the function in if it is specified as a string.
135
+ * @param {Array} args Arguments to pass to the callback.
136
+ */
137
+ callback(fn, scope=this, args) {
138
+ if (fn) {
139
+ const handler = this.resolveCallback(fn, scope);
140
+
141
+ handler.fn.apply(handler.scope, args);
142
+ }
143
+ }
144
+
126
145
  /**
127
146
  * @param {Object} config
128
147
  */
@@ -240,6 +259,28 @@ class Observable extends Base {
240
259
 
241
260
  // }
242
261
 
262
+ /**
263
+ * Locate a callable function by name in the passed scope.
264
+ *
265
+ * If the name starts with 'up.', the parent Component chain is searched.
266
+ *
267
+ * This is used by Observable.fire and by 'handler' function calls to resolve
268
+ * string function names in the Component's own hierarchy.
269
+ * @param {Function|String} fn A function, or the name of a function to find in the passed scope object/
270
+ * @param {Object} scope The scope to find the function in if it is specified as a string.
271
+ * @returns {Object}
272
+ */
273
+ resolveCallback(fn, scope=this) {
274
+ if (typeof fn === 'string') {
275
+ if (!scope[fn] && fn.startsWith('up.')) {
276
+ fn = fn.slice(3);
277
+ while (!scope[fn] && (scope = scope.parent));
278
+ }
279
+ fn = scope[fn];
280
+ }
281
+ return { scope, fn };
282
+ }
283
+
243
284
  /**
244
285
  * Alias for removeListener
245
286
  * @param {Object|String} name
@@ -376,10 +376,7 @@ class Base extends Panel {
376
376
  me.closeOrHide(false);
377
377
 
378
378
  if (me.closeAction === 'hide') {
379
- await Neo.applyDeltas(appName, [
380
- {id, cls: {remove: ['animated-hiding-showing']}},
381
- {id, action: 'removeNode'}
382
- ])
379
+ await Neo.applyDeltas(appName, {id, action: 'removeNode'})
383
380
  }
384
381
  }
385
382
 
@@ -691,13 +691,17 @@ class FileUpload extends Base {
691
691
 
692
692
  delete vdom.inert;
693
693
 
694
+ let isChangeEventNeeded;
695
+
694
696
  switch (value) {
695
697
  case 'ready':
696
698
  anchor.tag = 'div';
697
699
  anchor.href = '';
700
+ isChangeEventNeeded = true;
698
701
  break;
699
702
  case 'upload-failed':
700
703
  status.innerHTML = `${me.uploadFailed}${isNaN(me.progress) ? '' : `... (${Math.round(me.progress * 100)}%)`}`;
704
+ isChangeEventNeeded = true;
701
705
  break;
702
706
  case 'processing':
703
707
  status.innerHTML = `${me.scanning}... (${me.formatSize(me.uploadSize)})`;
@@ -706,6 +710,7 @@ class FileUpload extends Base {
706
710
  case 'scan-failed':
707
711
  status.innerHTML = `${me.malwareFoundInFile}. \u2022 ${me.fileSize}`;
708
712
  me.error = me.pleaseCheck;
713
+ isChangeEventNeeded = true;
709
714
  break;
710
715
  case 'downloadable':
711
716
  anchor.tag = 'a';
@@ -713,14 +718,20 @@ class FileUpload extends Base {
713
718
  [me.documentIdParameter] : me.documentId
714
719
  });
715
720
  status.innerHTML = me.fileSize;
721
+ isChangeEventNeeded = true;
716
722
  break;
717
723
  case 'not-downloadable':
718
724
  status.innerHTML = me.document ? me.fileSize : `${me.successfullyUploaded} \u2022 ${me.fileSize}`;
725
+ isChangeEventNeeded = true;
719
726
  break;
720
727
  case 'deleted':
721
728
  status.innerHTML = me.fileWasDeleted;
729
+ isChangeEventNeeded = true;
722
730
  }
723
731
 
732
+ if (isChangeEventNeeded) {
733
+ me.fireChangeEvent(me.file)
734
+ }
724
735
  me.validate();
725
736
  me.update();
726
737
 
@@ -32,10 +32,6 @@ class Picker extends Text {
32
32
  * @protected
33
33
  */
34
34
  clientRects: null,
35
- /**
36
- * @member {Boolean} editable_=true
37
- */
38
- editable_: true,
39
35
  /**
40
36
  * Additional used keys for the selection model
41
37
  * @member {Object} keys
@@ -110,19 +106,6 @@ class Picker extends Text {
110
106
  })
111
107
  }
112
108
 
113
- /**
114
- * Triggered after the editable config got changed
115
- * @param {Boolean} value
116
- * @param {Boolean} oldValue
117
- * @protected
118
- */
119
- afterSetEditable(value, oldValue) {
120
- let cls = this.cls;
121
-
122
- NeoArray.toggle(cls, 'neo-not-editable', !value);
123
- this.cls = cls
124
- }
125
-
126
109
  /**
127
110
  * Triggered after the mounted config got changed
128
111
  * @param {Boolean} value
@@ -5,6 +5,14 @@ import Number from './Number.mjs';
5
5
  * @extends Neo.form.field.Number
6
6
  */
7
7
  class Range extends Number {
8
+ /**
9
+ * Removing the debounce for range fields
10
+ * @member {Object} delayable
11
+ * @protected
12
+ * @static
13
+ */
14
+ static delayable = {}
15
+
8
16
  static config = {
9
17
  /**
10
18
  * @member {String} className='Neo.form.field.Range'
@@ -75,6 +75,15 @@ class Text extends Base {
75
75
  * @member {String[]|null} disabledChars_=null
76
76
  */
77
77
  disabledChars_: null,
78
+ /**
79
+ * Setting `editable` to `false` means that the input field will be read-only
80
+ * but the field is still workable and may have its value changed by user interaction.
81
+ *
82
+ * For example picker fields such as `Date` and `Select` may still have their
83
+ * values changed by selecting from the picker using keyboard or pointer.
84
+ * @member {Boolean} editable_=true
85
+ */
86
+ editable_: true,
78
87
  /**
79
88
  * Configure the value of empty fields. null or an empty string is recommended.
80
89
  * @member {String|null} emptyValue=null
@@ -184,6 +193,10 @@ class Text extends Base {
184
193
  */
185
194
  placeholderText_: null,
186
195
  /**
196
+ * Setting `readOnly` means that the field may not be changed by user interaction.
197
+ *
198
+ * The input field will be read-only and other ways of changing the field's value
199
+ * (such as by operating pickers) will be disabled.
187
200
  * @member {Boolean} readOnly_=false
188
201
  */
189
202
  readOnly_: false,
@@ -342,6 +355,22 @@ class Text extends Base {
342
355
  }
343
356
  }
344
357
 
358
+ /**
359
+ * Triggered after the editable config got changed
360
+ * @param {Boolean} value
361
+ * @param {Boolean} oldValue
362
+ * @protected
363
+ */
364
+ afterSetEditable(value, oldValue) {
365
+ const
366
+ me = this,
367
+ { cls } = me;
368
+
369
+ NeoArray.toggle(cls, 'neo-not-editable', !value);
370
+ me.cls = cls
371
+ me.changeInputElKey('readonly', value ? false : true);
372
+ }
373
+
345
374
  /**
346
375
  * Triggered after the error config got changed
347
376
  * @param {String|null} value
@@ -35,6 +35,13 @@ class Picker extends Base {
35
35
  onTriggerClick(data) {
36
36
  this.field.onPickerTriggerClick();
37
37
  }
38
+
39
+ /**
40
+ * @returns {Boolean} true in case the trigger should be hidden
41
+ */
42
+ getHiddenState() {
43
+ return !this.field.editable;
44
+ }
38
45
  }
39
46
 
40
47
  Neo.applyClassConfig(Picker);
@@ -262,12 +262,18 @@ class DomEvents extends Base {
262
262
  path = event.path;
263
263
  }
264
264
 
265
- return {
265
+ const result = {
266
266
  path : path.map(e => this.getTargetData(e)),
267
267
  target : this.getTargetData(event.target),
268
268
  timeStamp: event.timeStamp,
269
269
  type : event.type
270
+ };
271
+
272
+ if (event.relatedTarget) {
273
+ result.relatedTarget = this.getTargetData(event.relatedTarget);
270
274
  }
275
+
276
+ return result;
271
277
  }
272
278
 
273
279
  /**
@@ -552,7 +558,7 @@ class DomEvents extends Base {
552
558
  */
553
559
  onMouseEnter(event) {
554
560
  let me = this,
555
- appEvent = {...me.getMouseEventData(event), fromElementId: event.fromElement?.id || null};
561
+ appEvent = {...me.getMouseEventData(event), fromElementId: event.fromElement?.id || null, toElementId: event.toElement?.id || null};
556
562
 
557
563
  me.sendMessageToApp(appEvent);
558
564
  me.fire('mouseEnter', appEvent)
@@ -563,7 +569,7 @@ class DomEvents extends Base {
563
569
  */
564
570
  onMouseLeave(event) {
565
571
  let me = this,
566
- appEvent = {...me.getMouseEventData(event), toElementId: event.toElement?.id || null};
572
+ appEvent = {...me.getMouseEventData(event), fromElementId: event.fromElement?.id || null, toElementId: event.toElement?.id || null};
567
573
 
568
574
  me.sendMessageToApp(appEvent);
569
575
  me.fire('mouseLeave', appEvent)
@@ -127,6 +127,9 @@ class DomEvent extends Base {
127
127
  data = Neo.clone(data, true, true);
128
128
 
129
129
  data.component = component;
130
+
131
+ // Handler needs to know which actual target matched the delegate
132
+ data.currentTarget = delegationTargetId;
130
133
  listener.fn.apply(listener.scope || globalThis, [data]);
131
134
 
132
135
  if (!listener.bubble) {
package/src/menu/List.mjs CHANGED
@@ -311,7 +311,7 @@ class List extends BaseList {
311
311
  record = me.store.get(recordId),
312
312
  submenu;
313
313
 
314
- record.handler?.call(me, record);
314
+ me.callback(record.handler, me, [record]);
315
315
 
316
316
  record.route && Neo.Main.setRoute({
317
317
  appName: me.appName,