neo.mjs 6.8.3 → 6.9.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 (65) hide show
  1. package/apps/ServiceWorker.mjs +2 -2
  2. package/apps/learnneo/view/home/MainContainer.mjs +4 -3
  3. package/apps/learnneo/view/home/MainContainerController.mjs +24 -2
  4. package/apps/route/app.mjs +6 -0
  5. package/apps/route/index.html +11 -0
  6. package/apps/route/neo-config.json +6 -0
  7. package/apps/route/view/ButtonBar.mjs +57 -0
  8. package/apps/route/view/CenterContainer.mjs +37 -0
  9. package/apps/route/view/FooterContainer.mjs +47 -0
  10. package/apps/route/view/HeaderContainer.mjs +47 -0
  11. package/apps/route/view/MainView.mjs +66 -0
  12. package/apps/route/view/MainViewController.mjs +210 -0
  13. package/apps/route/view/MetaContainer.mjs +52 -0
  14. package/apps/route/view/Viewport.mjs +15 -0
  15. package/apps/route/view/center/CardAdministration.mjs +36 -0
  16. package/apps/route/view/center/CardAdministrationDenied.mjs +26 -0
  17. package/apps/route/view/center/CardContact.mjs +29 -0
  18. package/apps/route/view/center/CardHome.mjs +26 -0
  19. package/apps/route/view/center/CardSection1.mjs +26 -0
  20. package/apps/route/view/center/CardSection2.mjs +27 -0
  21. package/examples/ConfigurationViewport.mjs +1 -1
  22. package/examples/ServiceWorker.mjs +2 -2
  23. package/examples/form/field/select/MainContainer.mjs +1 -2
  24. package/examples/table/container/MainContainer.mjs +4 -2
  25. package/examples/table/container/MainModel.mjs +3 -0
  26. package/examples/table/container/MainStore.mjs +10 -10
  27. package/examples/toolbar/paging/view/MainContainer.mjs +38 -3
  28. package/package.json +4 -4
  29. package/resources/data/learnneo/content.json +9 -9
  30. package/resources/data/learnneo/pages/whyneo.md +76 -0
  31. package/resources/scss/src/apps/route/CenterContainer.scss +29 -0
  32. package/resources/scss/src/apps/route/HeaderContainer.scss +122 -0
  33. package/resources/scss/src/apps/route/MainView.scss +3 -0
  34. package/resources/scss/src/apps/route/MetaContainer.scss +44 -0
  35. package/resources/scss/src/apps/route/_all.scss +1 -0
  36. package/resources/scss/src/component/Toast.scss +2 -2
  37. package/src/DefaultConfig.mjs +2 -2
  38. package/src/Neo.mjs +15 -14
  39. package/src/button/Base.mjs +2 -2
  40. package/src/collection/Filter.mjs +13 -2
  41. package/src/component/Base.mjs +41 -50
  42. package/src/component/Toast.mjs +2 -1
  43. package/src/container/Base.mjs +59 -2
  44. package/src/controller/Base.mjs +84 -4
  45. package/src/controller/Component.mjs +22 -7
  46. package/src/core/Observable.mjs +50 -9
  47. package/src/form/field/Range.mjs +8 -0
  48. package/src/form/field/Select.mjs +9 -10
  49. package/src/form/field/Text.mjs +13 -3
  50. package/src/form/field/trigger/Picker.mjs +1 -8
  51. package/src/main/DomEvents.mjs +9 -3
  52. package/src/manager/DomEvent.mjs +37 -24
  53. package/src/menu/List.mjs +1 -1
  54. package/src/table/View.mjs +78 -53
  55. package/src/toolbar/Paging.mjs +68 -76
  56. package/src/tooltip/Base.mjs +123 -11
  57. package/src/vdom/Helper.mjs +7 -0
  58. package/src/worker/App.mjs +4 -0
  59. package/test/components/app.mjs +8 -0
  60. package/test/components/files/button/Base.mjs +17 -0
  61. package/test/components/files/component/DateSelector.mjs +18 -0
  62. package/test/components/files/form/field/Select.mjs +35 -0
  63. package/test/components/index.html +17 -0
  64. package/test/components/neo-config.json +6 -0
  65. package/test/components/siesta.js +14 -0
@@ -367,8 +367,9 @@ class Text extends Base {
367
367
  { cls } = me;
368
368
 
369
369
  NeoArray.toggle(cls, 'neo-not-editable', !value);
370
- me.cls = cls
371
- me.changeInputElKey('readonly', value ? false : true);
370
+ me.cls = cls;
371
+
372
+ me.updateReadOnlyState()
372
373
  }
373
374
 
374
375
  /**
@@ -675,7 +676,7 @@ class Text extends Base {
675
676
  NeoArray[value ? 'add' : 'remove'](cls, 'neo-readonly');
676
677
  me.cls = cls;
677
678
 
678
- me.changeInputElKey('readonly', value ? value : null);
679
+ me.updateReadOnlyState();
679
680
 
680
681
  me.triggers?.forEach(trigger => {
681
682
  trigger.hidden = value ? true : trigger.getHiddenState?.() || false
@@ -1440,6 +1441,15 @@ class Text extends Base {
1440
1441
  me.update()
1441
1442
  }
1442
1443
 
1444
+ /**
1445
+ * The DOM based readonly attribute needs to honor the editable & readOnly configs
1446
+ */
1447
+ updateReadOnlyState() {
1448
+ let me = this;
1449
+
1450
+ me.changeInputElKey('readonly', !me.editable || me.readOnly || null);
1451
+ }
1452
+
1443
1453
  /**
1444
1454
  * Since triggers do not get rendered, assign the relevant props
1445
1455
  * todo: this could be handled by component.Base
@@ -34,14 +34,7 @@ class Picker extends Base {
34
34
  */
35
35
  onTriggerClick(data) {
36
36
  this.field.onPickerTriggerClick();
37
- }
38
-
39
- /**
40
- * @returns {Boolean} true in case the trigger should be hidden
41
- */
42
- getHiddenState() {
43
- return !this.field.editable;
44
- }
37
+ }
45
38
  }
46
39
 
47
40
  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) {
@@ -429,34 +432,44 @@ class DomEvent extends Base {
429
432
  * @returns {Boolean|String} true in case the delegation string matches the event path
430
433
  */
431
434
  verifyDelegationPath(listener, path) {
432
- let delegationArray = listener.delegate.split(' '),
433
- j = 0,
434
- len = delegationArray.length,
435
- pathLen = path.length,
436
- hasMatch, i, item, isId, targetId;
437
-
438
- for (i=len-1; i >= 0; i--) {
439
- hasMatch = false;
440
- item = delegationArray[i];
441
- isId = item.startsWith('#');
442
-
443
- if (isId || item.startsWith('.')) {
444
- item = item.substr(1);
435
+ const { delegate } = listener;
436
+
437
+ let j = 0, pathLen = path.length, targetId;
438
+
439
+ if (typeof delegate === 'function') {
440
+ j = delegate(path);
441
+ if (j != null) {
442
+ targetId = path[j].id;
445
443
  }
444
+ }
445
+ else {
446
+ let delegationArray = delegate.split(' '),
447
+ len = delegationArray.length,
448
+ hasMatch, i, item, isId;
449
+
450
+ for (i=len-1; i >= 0; i--) {
451
+ hasMatch = false;
452
+ item = delegationArray[i];
453
+ isId = item.startsWith('#');
454
+
455
+ if (isId || item.startsWith('.')) {
456
+ item = item.substr(1);
457
+ }
446
458
 
447
- for (; j < pathLen; j++) {
448
- if (
449
- (isId && path[j].id === item) ||
450
- path[j].cls.includes(item)
451
- ) {
452
- hasMatch = true;
453
- targetId = path[j].id;
454
- break
459
+ for (; j < pathLen; j++) {
460
+ if (
461
+ (isId && path[j].id === item) ||
462
+ path[j].cls.includes(item)
463
+ ) {
464
+ hasMatch = true;
465
+ targetId = path[j].id;
466
+ break
467
+ }
455
468
  }
456
- }
457
469
 
458
- if (!hasMatch) {
459
- return false
470
+ if (!hasMatch) {
471
+ return false
472
+ }
460
473
  }
461
474
  }
462
475
 
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,
@@ -21,6 +21,11 @@ class View extends Component {
21
21
  * @member {String[]} baseCls=['neo-table-view']
22
22
  */
23
23
  baseCls: ['neo-table-view'],
24
+ /**
25
+ * Define which model field contains the value of colspan definitions
26
+ * @member {String} colspanField='colspan'
27
+ */
28
+ colspanField: 'colspan',
24
29
  /**
25
30
  * @member {String|null} containerId=null
26
31
  * @protected
@@ -46,16 +51,19 @@ class View extends Component {
46
51
  }
47
52
 
48
53
  /**
49
- * @param {String} cellId
50
- * @param {Object} column
51
- * @param {Object} record
52
- * @param {Number} index
53
- * @param {Neo.table.Container} tableContainer
54
+ * @param {Object} data
55
+ * @param {String} [data.cellId]
56
+ * @param {Object} data.column
57
+ * @param {Object} data.record
58
+ * @param {Number} data.index
59
+ * @param {Neo.table.Container} data.tableContainer
54
60
  * @returns {Object}
55
61
  */
56
- applyRendererOutput(cellId, column, record, index, tableContainer) {
57
- let me = this,
62
+ applyRendererOutput(data) {
63
+ let {cellId, column, record, index, tableContainer} = data,
64
+ me = this,
58
65
  cellCls = ['neo-table-cell'],
66
+ colspan = record[me.colspanField],
59
67
  dataField = column.dataField,
60
68
  fieldValue = record[dataField],
61
69
  hasStore = tableContainer.store?.model, // todo: remove as soon as all tables use stores (examples table)
@@ -77,8 +85,8 @@ class View extends Component {
77
85
 
78
86
  switch (Neo.typeOf(rendererOutput)) {
79
87
  case 'Object': {
80
- if (rendererOutput.cls && rendererOutput.html) {
81
- cellCls.push(...rendererOutput.cls);
88
+ if (rendererOutput.html) {
89
+ rendererOutput.cls && cellCls.push(...rendererOutput.cls);
82
90
  } else {
83
91
  rendererOutput = [rendererOutput];
84
92
  }
@@ -107,7 +115,7 @@ class View extends Component {
107
115
  if (hasStore) {
108
116
  cellId = me.getCellId(record, column.dataField)
109
117
  } else {
110
- cellId = vdom.cn[i]?.cn[j]?.id || Neo.getId('td')
118
+ cellId = vdom.cn[index]?.cn[me.getColumn(column.dataField, true)]?.id || Neo.getId('td')
111
119
  }
112
120
  }
113
121
 
@@ -119,6 +127,10 @@ class View extends Component {
119
127
  tabIndex: '-1'
120
128
  };
121
129
 
130
+ if (colspan && Object.keys(colspan).includes(dataField)) {
131
+ cellConfig.colspan = colspan[dataField]
132
+ }
133
+
122
134
  if (Neo.typeOf(rendererOutput) === 'Object') {
123
135
  cellConfig.innerHTML = rendererOutput.html || ''
124
136
  } else {
@@ -129,29 +141,28 @@ class View extends Component {
129
141
  }
130
142
 
131
143
  /**
132
- * @param {Array} inputData
144
+ * @param {Object[]} inputData
133
145
  */
134
146
  createViewData(inputData) {
135
- let me = this,
136
- amountRows = inputData.length,
137
- container = me.parent,
138
- columns = container.items[0].items,
139
- colCount = columns.length,
140
- data = [],
141
- i = 0,
142
- vdom = me.vdom,
143
- config, column, dockLeftMargin, dockRightMargin, id, index, j,
144
- record, selectedRows, trCls;
145
-
146
- me.recordVnodeMap = {}; // remove old data
147
-
148
- if (container.selectionModel.ntype === 'selection-table-rowmodel') {
149
- selectedRows = container.selectionModel.items || [];
147
+ let me = this,
148
+ amountRows = inputData.length,
149
+ tableContainer = me.parent,
150
+ columns = tableContainer.items[0].items,
151
+ colCount = columns.length,
152
+ data = [],
153
+ i = 0,
154
+ vdom = me.vdom,
155
+ config, colspan, colspanKeys, column, dockLeftMargin, dockRightMargin, id, index, j, record, selectedRows, trCls;
156
+
157
+ if (tableContainer.selectionModel.ntype === 'selection-table-rowmodel') {
158
+ selectedRows = tableContainer.selectionModel.items || [];
150
159
  }
151
160
 
152
161
  for (; i < amountRows; i++) {
153
- record = inputData[i];
154
- id = me.getRowId(record, i);
162
+ record = inputData[i];
163
+ colspan = record[me.colspanField];
164
+ colspanKeys = colspan && Object.keys(colspan);
165
+ id = me.getRowId(record, i);
155
166
 
156
167
  me.recordVnodeMap[id] = i;
157
168
 
@@ -180,7 +191,7 @@ class View extends Component {
180
191
 
181
192
  for (; j < colCount; j++) {
182
193
  column = columns[j];
183
- config = me.applyRendererOutput(null, column, record, i, container);
194
+ config = me.applyRendererOutput({column, record, index: i, tableContainer});
184
195
 
185
196
  if (column.dock) {
186
197
  config.cls = ['neo-locked', ...config.cls || []];
@@ -196,6 +207,10 @@ class View extends Component {
196
207
  }
197
208
 
198
209
  data[i].cn.push(config);
210
+
211
+ if (colspanKeys?.includes(column.dataField)) {
212
+ j += (colspan[column.dataField] - 1)
213
+ }
199
214
  }
200
215
 
201
216
  j = 0;
@@ -208,13 +223,16 @@ class View extends Component {
208
223
  data[i].cn[index].style.right = dockRightMargin + 'px';
209
224
  dockRightMargin += (column.width + 1); // todo: borders fix
210
225
  }
226
+
227
+ if (colspanKeys?.includes(column.dataField)) {
228
+ j += (colspan[column.dataField] - 1)
229
+ }
211
230
  }
212
231
  }
213
232
 
214
233
  vdom.cn = data;
215
234
 
216
- container.dockLeftMargin = dockLeftMargin;
217
- container.dockRightMargin = dockRightMargin;
235
+ Object.assign(tableContainer, {dockLeftMargin, dockRightMargin});
218
236
 
219
237
  me.promiseUpdate().then(() => {
220
238
  if (selectedRows?.length > 0) {
@@ -243,11 +261,12 @@ class View extends Component {
243
261
  }
244
262
 
245
263
  /**
246
- * Get a table column by a given field name
264
+ * Get a table column or column index by a given field name
247
265
  * @param {String} field
248
- * @returns {Object|null}
266
+ * @param {Boolean} returnIndex=false
267
+ * @returns {Object|Number|null}
249
268
  */
250
- getColumn(field) {
269
+ getColumn(field, returnIndex=false) {
251
270
  let container = this.parent,
252
271
  columns = container.items[0].items, // todo: we need a shortcut for accessing the header toolbar
253
272
  i = 0,
@@ -258,7 +277,7 @@ class View extends Component {
258
277
  column = columns[i];
259
278
 
260
279
  if (column.dataField === field) {
261
- return column
280
+ return returnIndex ? i : column
262
281
  }
263
282
  }
264
283
 
@@ -336,26 +355,32 @@ class View extends Component {
336
355
  * @param {Object} opts.record
337
356
  */
338
357
  onStoreRecordChange(opts) {
339
- let me = this,
340
- container = me.parent,
341
- needsUpdate = false,
342
- vdom = me.vdom,
358
+ let me = this,
359
+ fieldNames = opts.fields.map(field => field.name),
360
+ needsUpdate = false,
361
+ tableContainer = me.parent,
362
+ vdom = me.vdom,
343
363
  cellId, cellNode, column, index, scope;
344
364
 
345
- opts.fields.forEach(field => {
346
- cellId = me.getCellId(opts.record, field.name);
347
- cellNode = VDomUtil.findVdomChild(vdom, cellId);
348
-
349
- // the vdom might not exist yet => nothing to do in this case
350
- if (cellNode?.vdom) {
351
- column = me.getColumn(field.name);
352
- index = cellNode.index;
353
- needsUpdate = true;
354
- scope = column.rendererScope || container;
355
-
356
- cellNode.parentNode.cn[index] = me.applyRendererOutput(cellId, column, opts.record, index, container)
357
- }
358
- });
365
+ if (fieldNames.includes(me.colspanField)) {
366
+ // we should narrow it down to only update the current row
367
+ me.createViewData(me.store.items)
368
+ } else {
369
+ opts.fields.forEach(field => {
370
+ cellId = me.getCellId(opts.record, field.name);
371
+ cellNode = VDomUtil.findVdomChild(vdom, cellId);
372
+
373
+ // the vdom might not exist yet => nothing to do in this case
374
+ if (cellNode?.vdom) {
375
+ column = me.getColumn(field.name);
376
+ index = cellNode.index;
377
+ needsUpdate = true;
378
+ scope = column.rendererScope || tableContainer;
379
+
380
+ cellNode.parentNode.cn[index] = me.applyRendererOutput({cellId, column, record: opts.record, index, tableContainer})
381
+ }
382
+ })
383
+ }
359
384
 
360
385
  needsUpdate && me.update()
361
386
  }
@@ -41,15 +41,75 @@ class Paging extends Toolbar {
41
41
  /**
42
42
  * @member {Function} totalText_=count=>`Total: ${count} records`
43
43
  */
44
- totalText_: count => `Total: ${count} rows`
45
- }
44
+ totalText_: count => `Total: ${count} rows`,
45
+ /**
46
+ * @member {Object|Object[]} items
47
+ */
48
+ items: {
49
+ 'nav-button-first': {
50
+ handler : 'up.onFirstPageButtonClick',
51
+ iconCls : 'fa fa-angles-left'
52
+ },
53
+ 'nav-button-prev': {
54
+ handler : 'up.onPrevPageButtonClick',
55
+ iconCls : 'fa fa-angle-left',
56
+ style : {marginLeft: '2px'}
57
+ },
58
+ 'pages-text': {
59
+ ntype : 'label',
60
+ style : {marginLeft: '10px'}
61
+ },
62
+ 'nav-button-next': {
63
+ handler : 'up.onNextPageButtonClick',
64
+ iconCls : 'fa fa-angle-right',
65
+ style : {marginLeft: '10px'}
66
+ },
67
+ 'nav-button-last': {
68
+ handler : 'up.onLastPageButtonClick',
69
+ iconCls : 'fa fa-angles-right',
70
+ style : {marginLeft: '2px'}
71
+ },
72
+ label: {
73
+ ntype: 'label',
74
+ style: {marginLeft: '50px'},
75
+ text : 'Rows per page:'
76
+ },
77
+ rowsPerPage: {
78
+ module : SelectField,
79
+ clearable : false,
80
+ hideLabel : true,
81
+ listConfig : {highlightFilterValue: false},
82
+ listeners : {change: 'up.onPageSizeFieldChange'},
83
+ style : {margin: 0},
84
+ triggerAction: 'all',
85
+ useFilter : false,
86
+ value : 30,
87
+ width : 70,
46
88
 
47
- /**
48
- * @param config
49
- */
50
- construct(config) {
51
- super.construct(config);
52
- this.createToolbarItems();
89
+ store: {
90
+ model: {
91
+ fields: [
92
+ {name: 'id', type: 'Integer'},
93
+ {name: 'name', type: 'Integer'}
94
+ ]
95
+ },
96
+ data: [
97
+ {id: 1, name: 10},
98
+ {id: 2, name: 20},
99
+ {id: 3, name: 30},
100
+ {id: 4, name: 50},
101
+ {id: 5, name: 100}
102
+ ]
103
+ }
104
+ },
105
+ spacer: {
106
+ ntype: 'component',
107
+ flex : 1
108
+ },
109
+ 'total-text': {
110
+ ntype: 'label'
111
+ }
112
+ }
53
113
  }
54
114
 
55
115
  /**
@@ -115,74 +175,6 @@ class Paging extends Toolbar {
115
175
  return ClassSystemUtil.beforeSetInstance(value, null, {listeners});
116
176
  }
117
177
 
118
- /**
119
- *
120
- */
121
- createToolbarItems() {
122
- let me = this;
123
-
124
- me.items = [{
125
- handler : me.onFirstPageButtonClick.bind(me),
126
- iconCls : 'fa fa-angles-left',
127
- reference: 'nav-button-first'
128
- }, {
129
- handler : me.onPrevPageButtonClick.bind(me),
130
- iconCls : 'fa fa-angle-left',
131
- reference: 'nav-button-prev',
132
- style : {marginLeft: '2px'}
133
- }, {
134
- ntype : 'label',
135
- reference: 'pages-text',
136
- style : {marginLeft: '10px'},
137
- text : me.pagesText(me)
138
- }, {
139
- handler : me.onNextPageButtonClick.bind(me),
140
- iconCls : 'fa fa-angle-right',
141
- reference: 'nav-button-next',
142
- style : {marginLeft: '10px'}
143
- }, {
144
- handler : me.onLastPageButtonClick.bind(me),
145
- iconCls : 'fa fa-angles-right',
146
- reference: 'nav-button-last',
147
- style : {marginLeft: '2px'}
148
- }, {
149
- ntype: 'label',
150
- style: {marginLeft: '50px'},
151
- text : 'Rows per page:'
152
- }, {
153
- module : SelectField,
154
- clearable : false,
155
- hideLabel : true,
156
- listConfig : {highlightFilterValue: false},
157
- listeners : {change: me.onPageSizeFieldChange.bind(me)},
158
- style : {margin: 0},
159
- triggerAction: 'all',
160
- useFilter : false,
161
- value : 30,
162
- width : 70,
163
-
164
- store: {
165
- model: {
166
- fields: [
167
- {name: 'id', type: 'Integer'},
168
- {name: 'name', type: 'Integer'}
169
- ]
170
- },
171
- data: [
172
- {id: 1, name: 10},
173
- {id: 2, name: 20},
174
- {id: 3, name: 30},
175
- {id: 4, name: 50},
176
- {id: 5, name: 100}
177
- ]
178
- }
179
- }, '->', {
180
- ntype : 'label',
181
- reference: 'total-text',
182
- text : me.totalText(me.store.totalCount)
183
- }];
184
- }
185
-
186
178
  /**
187
179
  * @returns {Number}
188
180
  */