neo.mjs 8.20.1 → 8.21.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.
@@ -20,9 +20,9 @@ class ServiceWorker extends ServiceBase {
20
20
  */
21
21
  singleton: true,
22
22
  /**
23
- * @member {String} version='8.20.1'
23
+ * @member {String} version='8.21.0'
24
24
  */
25
- version: '8.20.1'
25
+ version: '8.21.0'
26
26
  }
27
27
 
28
28
  /**
@@ -16,7 +16,7 @@
16
16
  "@type": "Organization",
17
17
  "name": "Neo.mjs"
18
18
  },
19
- "datePublished": "2025-02-09",
19
+ "datePublished": "2025-02-12",
20
20
  "publisher": {
21
21
  "@type": "Organization",
22
22
  "name": "Neo.mjs"
@@ -107,7 +107,7 @@ class FooterContainer extends Container {
107
107
  }, {
108
108
  module: Component,
109
109
  cls : ['neo-version'],
110
- html : 'v8.20.1'
110
+ html : 'v8.21.0'
111
111
  }]
112
112
  }],
113
113
  /**
@@ -20,9 +20,9 @@ class ServiceWorker extends ServiceBase {
20
20
  */
21
21
  singleton: true,
22
22
  /**
23
- * @member {String} version='8.20.1'
23
+ * @member {String} version='8.21.0'
24
24
  */
25
- version: '8.20.1'
25
+ version: '8.21.0'
26
26
  }
27
27
 
28
28
  /**
@@ -84,6 +84,33 @@ class ControlsContainer extends Container {
84
84
  style : {marginTop: '.3em'},
85
85
  value : 'neo-theme-light',
86
86
  valueLabelText: 'Light'
87
+ }, {
88
+ ntype: 'label',
89
+ style: {marginTop: '2em'},
90
+ text : 'Filters'
91
+ }, {
92
+ ntype : 'textfield',
93
+ clearable : true,
94
+ editable : true,
95
+ labelText : 'Firstname',
96
+ labelWidth: 90,
97
+ listeners : {change: 'up.onFilterFieldChange'},
98
+ name : 'firstname',
99
+ style : {marginTop: '.3em'},
100
+ width : 200
101
+ }, {
102
+ ntype : 'textfield',
103
+ clearable : true,
104
+ editable : true,
105
+ labelText : 'Lastname',
106
+ labelWidth: 90,
107
+ listeners : {change: 'up.onFilterFieldChange'},
108
+ name : 'lastname',
109
+ width : 200
110
+ }, {
111
+ ntype : 'label',
112
+ reference: 'count-rows-label',
113
+ style : {marginTop: '1em'}
87
114
  }]
88
115
  }],
89
116
  /**
@@ -154,6 +181,36 @@ class ControlsContainer extends Container {
154
181
 
155
182
  me.grid.toggleCls('neo-extend-margin-right');
156
183
  }
184
+
185
+ onConstructed() {
186
+ super.onConstructed();
187
+
188
+ let me = this,
189
+ {store} = me.grid;
190
+
191
+ store.on({
192
+ filter: me.updateRowsLabel,
193
+ load : me.updateRowsLabel,
194
+ scope : me
195
+ });
196
+
197
+ store.getCount() > 0 && me.updateRowsLabel()
198
+ }
199
+
200
+ /**
201
+ * @param {Object} data
202
+ */
203
+ onFilterFieldChange(data) {
204
+ this.grid.store.getFilter(data.component.name).value = data.value
205
+ }
206
+
207
+ updateRowsLabel() {
208
+ let {store} = this.grid;
209
+
210
+ if (!store.isLoading) {
211
+ this.getItem('count-rows-label').text = 'Filtered rows: ' + store.getCount()
212
+ }
213
+ }
157
214
  }
158
215
 
159
216
  export default Neo.setupClass(ControlsContainer);
@@ -47,7 +47,7 @@ class GridContainer extends BaseGridContainer {
47
47
  afterSetAmountColumns(value, oldValue) {
48
48
  let i = 4,
49
49
  columns = [
50
- {dataField: 'id', text: '#', width: 60},
50
+ {dataField: 'id', text: '#', width: 60, renderer({record, store}) {return store.indexOf(record) + 1}},
51
51
  {cellAlign: 'left', dataField: 'firstname', defaultSortDirection: 'ASC', text: 'Firstname', width: 150},
52
52
  {cellAlign: 'left', dataField: 'lastname', defaultSortDirection: 'ASC', text: 'Lastname', width: 150}
53
53
  ];
@@ -20,6 +20,18 @@ class MainStore extends Store {
20
20
  * @member {Number} amountRows_=1000
21
21
  */
22
22
  amountRows_: 1000,
23
+ /**
24
+ * @member {Object[]} filters
25
+ */
26
+ filters: [{
27
+ property: 'firstname',
28
+ operator: 'like',
29
+ value : null
30
+ }, {
31
+ property: 'lastname',
32
+ operator: 'like',
33
+ value : null
34
+ }],
23
35
  /**
24
36
  * @member {Neo.data.Model} model=Model
25
37
  */
@@ -44,7 +44,8 @@ class MainContainer extends ConfigurationViewport {
44
44
  * @returns {Object[]}
45
45
  */
46
46
  createConfigurationComponents() {
47
- let me = this;
47
+ let me = this,
48
+ {selectionModel} = me.exampleComponent.view;
48
49
 
49
50
  const selectionModelRadioDefaults = {
50
51
  module : Radio,
@@ -64,25 +65,25 @@ class MainContainer extends ConfigurationViewport {
64
65
  value : me.exampleComponent.height
65
66
  }, {
66
67
  ...selectionModelRadioDefaults,
67
- checked : me.exampleComponent.selectionModel.ntype === 'selection-grid-cellmodel',
68
+ checked : selectionModel.ntype === 'selection-grid-cellmodel',
68
69
  labelText : 'selectionModel',
69
- listeners : {change: me.onRadioChange.bind(me, 'selectionModel', CellModel)},
70
+ listeners : {change: me.onRadioViewChange.bind(me, 'selectionModel', CellModel)},
70
71
  style : {marginTop: '10px'},
71
72
  valueLabelText: 'Cell'
72
73
  }, {
73
74
  ...selectionModelRadioDefaults,
74
- checked : me.exampleComponent.selectionModel.ntype === 'selection-grid-cellcolumnmodel',
75
- listeners : {change: me.onRadioChange.bind(me, 'selectionModel', CellColumnModel)},
75
+ checked : selectionModel.ntype === 'selection-grid-cellcolumnmodel',
76
+ listeners : {change: me.onRadioViewChange.bind(me, 'selectionModel', CellColumnModel)},
76
77
  valueLabelText: 'Cell & Column'
77
78
  }, {
78
79
  ...selectionModelRadioDefaults,
79
- checked : me.exampleComponent.selectionModel.ntype === 'selection-grid-cellrowmodel',
80
- listeners : {change: me.onRadioChange.bind(me, 'selectionModel', CellRowModel)},
80
+ checked : selectionModel.ntype === 'selection-grid-cellrowmodel',
81
+ listeners : {change: me.onRadioViewChange.bind(me, 'selectionModel', CellRowModel)},
81
82
  valueLabelText: 'Cell & Row'
82
83
  }, {
83
84
  ...selectionModelRadioDefaults,
84
- checked : me.exampleComponent.selectionModel.ntype === 'selection-grid-cellcolumnrowmodel',
85
- listeners : {change: me.onRadioChange.bind(me, 'selectionModel', CellColumnRowModel)},
85
+ checked : selectionModel.ntype === 'selection-grid-cellcolumnrowmodel',
86
+ listeners : {change: me.onRadioViewChange.bind(me, 'selectionModel', CellColumnRowModel)},
86
87
  valueLabelText: 'Cell & Column & Row'
87
88
  }, {
88
89
  module : CheckBox,
@@ -173,6 +174,17 @@ class MainContainer extends ConfigurationViewport {
173
174
  onPluginConfigChange(config, opts) {
174
175
  this.exampleComponent.getPlugin('grid-cell-editing')[config] = opts.value
175
176
  }
177
+
178
+ /**
179
+ * @param {String} config
180
+ * @param {String} value
181
+ * @param {Object} opts
182
+ */
183
+ onRadioViewChange(config, value, opts) {
184
+ if (opts.value === true) { // we only want to listen to check events, not uncheck
185
+ this.exampleComponent.view[config] = value
186
+ }
187
+ }
176
188
  }
177
189
 
178
190
  export default Neo.setupClass(MainContainer);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "neo.mjs",
3
- "version": "8.20.1",
3
+ "version": "8.21.0",
4
4
  "description": "The webworkers driven UI framework",
5
5
  "type": "module",
6
6
  "repository": {
@@ -55,11 +55,11 @@
55
55
  "fs-extra": "^11.3.0",
56
56
  "highlightjs-line-numbers.js": "^2.9.0",
57
57
  "inquirer": "^12.4.1",
58
- "marked": "^15.0.6",
58
+ "marked": "^15.0.7",
59
59
  "monaco-editor": "0.50.0",
60
60
  "neo-jsdoc": "1.0.1",
61
61
  "neo-jsdoc-x": "1.0.5",
62
- "postcss": "^8.5.1",
62
+ "postcss": "^8.5.2",
63
63
  "sass": "^1.84.0",
64
64
  "siesta-lite": "5.5.2",
65
65
  "url": "^0.11.4",
@@ -98,9 +98,11 @@
98
98
 
99
99
  .neo-mouse {
100
100
  .neo-grid-row {
101
- &:hover {
102
- .neo-grid-cell {
103
- background-color: var(--grid-cell-background-color-hover);
101
+ &:not(.neo-selected) {
102
+ &:hover {
103
+ .neo-grid-cell {
104
+ background-color: var(--grid-cell-background-color-hover);
105
+ }
104
106
  }
105
107
  }
106
108
  }
@@ -7,5 +7,6 @@
7
7
 
8
8
  .neo-grid-editor {
9
9
  margin: 0;
10
+ width : 100%;
10
11
  }
11
12
  }
@@ -18,6 +18,15 @@ const DefaultConfig = {
18
18
  * @type Boolean
19
19
  */
20
20
  applyBodyCls: true,
21
+ /**
22
+ * true will apply 'position: fixed' to the html tag itself
23
+ * See: https://github.com/neomjs/neo/issues/6429
24
+ * @default true
25
+ * @memberOf! module:Neo
26
+ * @name config.applyFixedPositionToHtmlTag
27
+ * @type Boolean
28
+ */
29
+ applyFixedPositionToHtmlTag: true,
21
30
  /**
22
31
  * Path to your app.mjs file. You can create multiple apps there if needed.
23
32
  * @default null
@@ -262,12 +271,12 @@ const DefaultConfig = {
262
271
  useVdomWorker: true,
263
272
  /**
264
273
  * buildScripts/injectPackageVersion.mjs will update this value
265
- * @default '8.20.1'
274
+ * @default '8.21.0'
266
275
  * @memberOf! module:Neo
267
276
  * @name config.version
268
277
  * @type String
269
278
  */
270
- version: '8.20.1'
279
+ version: '8.21.0'
271
280
  };
272
281
 
273
282
  Object.assign(DefaultConfig, {
@@ -234,7 +234,7 @@ class Collection extends Base {
234
234
  me.sortDirections = [];
235
235
  me.sortProperties = [];
236
236
 
237
- me.sorters.forEach(sorter => {//console.log('forEach', sorter);
237
+ me.sorters.forEach(sorter => {
238
238
  me.sortDirections.push(sorter.directionMultiplier);
239
239
  me.sortProperties.push(sorter.property)
240
240
  })
@@ -1050,9 +1050,12 @@ class Collection extends Base {
1050
1050
  onMutate(opts) {
1051
1051
  let me = this;
1052
1052
 
1053
- if (opts.preventBubbleUp) {
1053
+ // todo: inspect the bubbling chain
1054
+ /*if (opts.preventBubbleUp) {
1054
1055
  me.preventBubbleUp = true
1055
- }
1056
+ }*/
1057
+
1058
+ me.preventBubbleUp = true;
1056
1059
 
1057
1060
  me.splice(null, opts.removedItems, opts.addedItems)
1058
1061
  }
@@ -132,7 +132,7 @@ class Store extends Base {
132
132
  * @returns {Number} the collection count
133
133
  */
134
134
  add(item) {
135
- return super.add(this.beforeSetData(item))
135
+ return super.add(this.createRecord(item))
136
136
  }
137
137
 
138
138
  /**
@@ -161,6 +161,8 @@ class Store extends Base {
161
161
  me.initialData = [...value]
162
162
  }
163
163
 
164
+ me.isLoading = false;
165
+
164
166
  me.add(value)
165
167
  }
166
168
  }
@@ -256,22 +258,9 @@ class Store extends Base {
256
258
  */
257
259
  beforeSetData(value, oldValue) {
258
260
  if (value) {
259
- if (!Array.isArray(value)) {
260
- value = [value]
261
- }
262
-
263
- let me = this,
264
- i = 0,
265
- len = value.length,
266
- item;
267
-
268
- for (; i < len; i++) {
269
- item = value[i]
261
+ this.isLoading = true;
270
262
 
271
- if (!RecordFactory.isRecord(item)) {
272
- value[i] = RecordFactory.createRecord(me.model, item)
273
- }
274
- }
263
+ value = this.createRecord(value)
275
264
  }
276
265
 
277
266
  return value
@@ -304,10 +293,34 @@ class Store extends Base {
304
293
  }
305
294
 
306
295
  /**
307
- * @param {Object} config
296
+ * Converts an object or array of objects into records
297
+ * @param {Object|Object[]} config
298
+ * @returns {Object|Object[]} Array in case an array was passed
308
299
  */
309
300
  createRecord(config) {
310
- RecordFactory.createRecord(config)
301
+ let isArray = true;
302
+
303
+ if (config) {
304
+ if (!Array.isArray(config)) {
305
+ isArray = false;
306
+ config = [config]
307
+ }
308
+
309
+ let me = this,
310
+ i = 0,
311
+ len = config.length,
312
+ item;
313
+
314
+ for (; i < len; i++) {
315
+ item = config[i]
316
+
317
+ if (!RecordFactory.isRecord(item)) {
318
+ config[i] = RecordFactory.createRecord(me.model, item)
319
+ }
320
+ }
321
+ }
322
+
323
+ return isArray ? config : config[0]
311
324
  }
312
325
 
313
326
  /**
@@ -375,8 +388,7 @@ class Store extends Base {
375
388
  onCollectionMutate(opts) {
376
389
  let me = this;
377
390
 
378
- if (me.configsApplied) {
379
- // console.log('onCollectionMutate', opts);
391
+ if (me.configsApplied && !me.isLoading) {
380
392
  me.fire('load', me.items)
381
393
  }
382
394
  }
@@ -33,6 +33,10 @@ class Field extends Component {
33
33
  * @member {String|null} formGroup_=null
34
34
  */
35
35
  formGroup_: null,
36
+ /**
37
+ * @member {String|null} keys={}
38
+ */
39
+ keys: {},
36
40
  /**
37
41
  * True indicates that a user has interacted with the form field
38
42
  * @member {Boolean} isTouched_=false
@@ -93,8 +93,9 @@ class GridScrollbar extends Component {
93
93
  let me = this;
94
94
 
95
95
  value.on({
96
- load : me.updateScrollHeight,
97
- scope: me
96
+ filter: me.updateScrollHeight,
97
+ load : me.updateScrollHeight,
98
+ scope : me
98
99
  });
99
100
 
100
101
  value.getCount() > 0 && me.updateScrollHeight()
package/src/grid/View.mjs CHANGED
@@ -336,25 +336,6 @@ class GridView extends Component {
336
336
  oldValue !== undefined && this.createViewData()
337
337
  }
338
338
 
339
- /**
340
- * Triggered after the store config got changed
341
- * @param {Neo.data.Store|null} value
342
- * @param {Neo.data.Store|null} oldValue
343
- * @protected
344
- */
345
- afterSetStore(value, oldValue) {
346
- if (value) {
347
- let me = this;
348
-
349
- value.on({
350
- load : me.updateScrollHeight,
351
- scope: me
352
- });
353
-
354
- value.getCount() > 0 && me.updateScrollHeight()
355
- }
356
- }
357
-
358
339
  /**
359
340
  * Triggered after the visibleColumns config got changed
360
341
  * @param {Number[]} value
@@ -372,18 +353,19 @@ class GridView extends Component {
372
353
  * @param {String} [data.cellId]
373
354
  * @param {Object} data.column
374
355
  * @param {Number} data.columnIndex
375
- * @param {Neo.grid.Container} data.gridContainer
376
356
  * @param {Object} data.record
377
357
  * @param {Number} data.rowIndex
378
358
  * @returns {Object}
379
359
  */
380
360
  applyRendererOutput(data) {
381
- let {cellId, column, columnIndex, gridContainer, record, rowIndex} = data,
382
- me = this,
383
- cellCls = ['neo-grid-cell'],
384
- colspan = record[me.colspanField],
385
- {dataField} = column,
386
- fieldValue = record[dataField],
361
+ let {cellId, column, columnIndex, record, rowIndex} = data,
362
+ me = this,
363
+ gridContainer = me.parent,
364
+ {store} = me,
365
+ cellCls = ['neo-grid-cell'],
366
+ colspan = record[me.colspanField],
367
+ {dataField} = column,
368
+ fieldValue = record[dataField],
387
369
  cellConfig, rendererOutput;
388
370
 
389
371
  if (fieldValue === null || fieldValue === undefined) {
@@ -397,6 +379,7 @@ class GridView extends Component {
397
379
  gridContainer,
398
380
  record,
399
381
  rowIndex,
382
+ store,
400
383
  value: fieldValue
401
384
  });
402
385
 
@@ -541,7 +524,7 @@ class GridView extends Component {
541
524
 
542
525
  for (i=startIndex; i <= endIndex; i++) {
543
526
  column = columns[i];
544
- config = me.applyRendererOutput({column, columnIndex: i, gridContainer, record, rowIndex});
527
+ config = me.applyRendererOutput({column, columnIndex: i, record, rowIndex});
545
528
 
546
529
  if (column.dock) {
547
530
  config.cls = ['neo-locked', ...config.cls || []]
@@ -577,7 +560,7 @@ class GridView extends Component {
577
560
  endIndex, i;
578
561
 
579
562
  if (
580
- countRecords < 1 ||
563
+ store.isLoading ||
581
564
  me.availableRows < 1 ||
582
565
  me._containerWidth < 1 || // we are not checking me.containerWidth, since we want to ignore the config symbol
583
566
  me.columnPositions.getCount() < 1 ||
@@ -597,6 +580,7 @@ class GridView extends Component {
597
580
 
598
581
  me.parent.isLoading = false;
599
582
 
583
+ me.updateScrollHeight(true); // silent
600
584
  me.update()
601
585
  }
602
586
 
@@ -887,7 +871,6 @@ class GridView extends Component {
887
871
  let me = this,
888
872
  fieldNames = fields.map(field => field.name),
889
873
  needsUpdate = false,
890
- gridContainer = me.parent,
891
874
  rowIndex = me.store.indexOf(record),
892
875
  {selectionModel, vdom} = me,
893
876
  cellId, cellNode, cellStyle, cellVdom, column, columnIndex;
@@ -910,7 +893,7 @@ class GridView extends Component {
910
893
  cellStyle = cellNode.vdom.style;
911
894
  column = me.getColumn(field.name);
912
895
  columnIndex = cellNode.index;
913
- cellVdom = me.applyRendererOutput({cellId, column, columnIndex, gridContainer, record, rowIndex});
896
+ cellVdom = me.applyRendererOutput({cellId, column, columnIndex, record, rowIndex});
914
897
  needsUpdate = true;
915
898
 
916
899
  // The cell-positioning logic happens outside applyRendererOutput()
@@ -958,16 +941,16 @@ class GridView extends Component {
958
941
  }
959
942
 
960
943
  /**
961
- *
944
+ * @param {Boolean} silent=false
962
945
  */
963
- updateScrollHeight() {
946
+ updateScrollHeight(silent=false) {
964
947
  let me = this,
965
948
  countRecords = me.store.getCount(),
966
949
  {rowHeight} = me;
967
950
 
968
951
  if (countRecords > 0 && rowHeight > 0) {
969
952
  me.vdom.cn[0].height = `${(countRecords + 1) * rowHeight}px`;
970
- me.update()
953
+ !silent && me.update()
971
954
  }
972
955
  }
973
956
 
@@ -235,6 +235,7 @@ class Button extends BaseButton {
235
235
  * @param {Neo.grid.Container} data.gridContainer
236
236
  * @param {Object} data.record
237
237
  * @param {Number} data.rowIndex
238
+ * @param {Neo.data.Store} data.store
238
239
  * @param {Number|String} data.value
239
240
  * @returns {*}
240
241
  */
@@ -23,7 +23,11 @@ class CellEditing extends BaseCellEditing {
23
23
  /**
24
24
  * @member {String[]} editorCls=['neo-grid-editor']
25
25
  */
26
- editorCls: ['neo-grid-editor']
26
+ editorCls: ['neo-grid-editor'],
27
+ /**
28
+ * @member {Boolean} focusCells=true
29
+ */
30
+ focusCells: true
27
31
  }
28
32
  }
29
33
 
@@ -165,47 +165,6 @@ class DomAccess extends Base {
165
165
  me.syncAligns = me.syncAligns.bind(me)
166
166
  }
167
167
 
168
- /**
169
- *
170
- */
171
- initGlobalListeners() {
172
- let me = this;
173
-
174
- document.addEventListener('blur', me.onDocumentBlur .bind(me), capturePassive);
175
- document.addEventListener('keydown', me.onDocumentKeyDown .bind(me), capturePassive);
176
- document.addEventListener('keyup', me.onDocumentKeyUp .bind(me), capturePassive);
177
- document.addEventListener('mousedown', me.onDocumentMouseDown.bind(me), {capture : true})
178
- }
179
-
180
- onDocumentMouseDown(e) {
181
- let focusController = e.target?.closest('[data-focus]');
182
-
183
- // data-focus on an element means reject mousedown gestures, and move focus
184
- // to the referenced element.
185
- if (focusController) {
186
- e.preventDefault();
187
- document.getElementById(focusController.dataset.focus)?.focus()
188
- }
189
- }
190
-
191
- onDocumentKeyDown(keyEvent) {
192
- if (modifierKeys[keyEvent.key]) {
193
- // e.g. Neo.isShiftKeyDown = true or Neo.isControlKeyDown = true.
194
- // Selection can consult this value
195
- Neo[`${StringUtil.uncapitalize(keyEvent.key)}KeyDown`] = true;
196
- }
197
- }
198
-
199
- onDocumentKeyUp(keyEvent) {
200
- if (modifierKeys[keyEvent.key]) {
201
- Neo[`${StringUtil.uncapitalize(keyEvent.key)}KeyDown`] = false;
202
- }
203
- }
204
-
205
- onDocumentBlur() {
206
- Neo.altKeyDown = Neo.controlKeyDown = Neo.metaKeyDown = Neo.shiftKeyDown = false;
207
- }
208
-
209
168
  /**
210
169
  * @param {Object} alignSpec
211
170
  */
@@ -564,6 +523,18 @@ class DomAccess extends Base {
564
523
  }
565
524
  }
566
525
 
526
+ /**
527
+ *
528
+ */
529
+ initGlobalListeners() {
530
+ let me = this;
531
+
532
+ document.addEventListener('blur', me.onDocumentBlur .bind(me), capturePassive);
533
+ document.addEventListener('keydown', me.onDocumentKeyDown .bind(me), capturePassive);
534
+ document.addEventListener('keyup', me.onDocumentKeyUp .bind(me), capturePassive);
535
+ document.addEventListener('mousedown', me.onDocumentMouseDown.bind(me), {capture : true})
536
+ }
537
+
567
538
  /**
568
539
  * @param {HTMLElement} el
569
540
  * @returns {Boolean}
@@ -715,6 +686,33 @@ class DomAccess extends Base {
715
686
  }
716
687
  }
717
688
 
689
+ /**
690
+ *
691
+ */
692
+ onDocumentBlur() {
693
+ Neo.altKeyDown = Neo.controlKeyDown = Neo.metaKeyDown = Neo.shiftKeyDown = false
694
+ }
695
+
696
+ /**
697
+ * @param {KeyboardEvent} keyEvent
698
+ */
699
+ onDocumentKeyDown(keyEvent) {
700
+ if (modifierKeys[keyEvent.key]) {
701
+ // e.g. Neo.isShiftKeyDown = true or Neo.isControlKeyDown = true.
702
+ // Selection can consult this value
703
+ Neo[`${StringUtil.uncapitalize(keyEvent.key)}KeyDown`] = true
704
+ }
705
+ }
706
+
707
+ /**
708
+ * @param {KeyboardEvent} keyEvent
709
+ */
710
+ onDocumentKeyUp(keyEvent) {
711
+ if (modifierKeys[keyEvent.key]) {
712
+ Neo[`${StringUtil.uncapitalize(keyEvent.key)}KeyDown`] = false
713
+ }
714
+ }
715
+
718
716
  /**
719
717
  * @param {Array} mutations
720
718
  */
@@ -733,11 +731,26 @@ class DomAccess extends Base {
733
731
  }
734
732
  }
735
733
 
734
+ /**
735
+ * @param {MouseEvent} e
736
+ */
737
+ onDocumentMouseDown(e) {
738
+ let focusController = e.target?.closest('[data-focus]');
739
+
740
+ // data-focus on an element means reject mousedown gestures, and move focus
741
+ // to the referenced element.
742
+ if (focusController) {
743
+ e.preventDefault();
744
+ document.getElementById(focusController.dataset.focus)?.focus()
745
+ }
746
+ }
747
+
736
748
  /**
737
749
  *
738
750
  */
739
751
  onDomContentLoaded() {
740
- Neo.config.applyBodyCls && this.applyBodyCls({cls: ['neo-body']})
752
+ Neo.config.applyBodyCls && this.applyBodyCls({cls: ['neo-body']});
753
+ Neo.config.applyFixedPositionToHtmlTag && document.documentElement.style.setProperty('position', 'fixed')
741
754
  }
742
755
 
743
756
  /**
@@ -514,7 +514,7 @@ class DomEvents extends Base {
514
514
  if (
515
515
  isInput &&
516
516
  event.key === 'Tab' &&
517
- me.testPathInclusion(event, ['neo-table-editor'], true)
517
+ me.testPathInclusion(event, ['neo-grid-editor', 'neo-table-editor'], true)
518
518
  ) {
519
519
  event.preventDefault()
520
520
  }
@@ -26,14 +26,16 @@ class DeltaUpdates extends Base {
26
26
  len = attributes.length,
27
27
  attribute;
28
28
 
29
- for (; i < len; i++) {
30
- attribute = attributes.item(i);
31
- clone.setAttribute(attribute.nodeName, attribute.nodeValue)
32
- }
29
+ if (node) {
30
+ for (; i < len; i++) {
31
+ attribute = attributes.item(i);
32
+ clone.setAttribute(attribute.nodeName, attribute.nodeValue)
33
+ }
33
34
 
34
- clone.innerHTML= node.innerHTML;
35
+ clone.innerHTML= node.innerHTML;
35
36
 
36
- node.parentNode.replaceChild(clone, node)
37
+ node.parentNode.replaceChild(clone, node)
38
+ }
37
39
  }
38
40
 
39
41
  /**
@@ -41,7 +43,7 @@ class DeltaUpdates extends Base {
41
43
  * @param {String} delta.id
42
44
  */
43
45
  du_focusNode(delta) {
44
- this.getElement(delta.id).focus()
46
+ this.getElement(delta.id)?.focus()
45
47
  }
46
48
 
47
49
  /**
@@ -183,7 +185,7 @@ class DeltaUpdates extends Base {
183
185
  let me = this,
184
186
  node = me.getElement(delta.parentId);
185
187
 
186
- node.replaceChild(me.getElement(delta.toId), me.getElement(delta.fromId))
188
+ node?.replaceChild(me.getElement(delta.toId), me.getElement(delta.fromId))
187
189
  }
188
190
 
189
191
  /**
@@ -195,7 +197,9 @@ class DeltaUpdates extends Base {
195
197
  let me = this,
196
198
  node = me.getElement(delta.id);
197
199
 
198
- node.textContent = delta.value
200
+ if (node) {
201
+ node.textContent = delta.value
202
+ }
199
203
  }
200
204
 
201
205
  /**
@@ -211,11 +215,7 @@ class DeltaUpdates extends Base {
211
215
  let me = this,
212
216
  node = me.getElementOrBody(delta.id);
213
217
 
214
- if (!node) {
215
- if (Neo.config.environment === 'development') {
216
- console.warn('du_updateNode: node not found for id', delta.id)
217
- }
218
- } else {
218
+ if (node) {
219
219
  Object.entries(delta).forEach(([prop, value]) => {
220
220
  switch(prop) {
221
221
  case 'attributes':
@@ -22,6 +22,22 @@ class BaseModel extends Model {
22
22
  get dataFields() {
23
23
  return this.view.parent.columns.map(column => column.dataField)
24
24
  }
25
+
26
+ /**
27
+ * Checks if an event path contains a grid cell editor
28
+ * @param {Object} data
29
+ * @param {Object[]} data.path
30
+ * @returns {Boolean}
31
+ */
32
+ hasEditorFocus({path}) {
33
+ for (const node of path) {
34
+ if (node.cls?.includes('neo-grid-editor')) {
35
+ return true
36
+ }
37
+ }
38
+
39
+ return false
40
+ }
25
41
  }
26
42
 
27
43
  export default Neo.setupClass(BaseModel);
@@ -54,28 +54,28 @@ class CellModel extends BaseModel {
54
54
  * @param {Object} data
55
55
  */
56
56
  onKeyDownDown(data) {
57
- this.onNavKeyRow(1)
57
+ !this.hasEditorFocus(data) && this.onNavKeyRow(1)
58
58
  }
59
59
 
60
60
  /**
61
61
  * @param {Object} data
62
62
  */
63
63
  onKeyDownLeft(data) {
64
- this.onNavKeyColumn(-1)
64
+ !this.hasEditorFocus(data) && this.onNavKeyColumn(-1)
65
65
  }
66
66
 
67
67
  /**
68
68
  * @param {Object} data
69
69
  */
70
70
  onKeyDownRight(data) {
71
- this.onNavKeyColumn(1)
71
+ !this.hasEditorFocus(data) && this.onNavKeyColumn(1)
72
72
  }
73
73
 
74
74
  /**
75
75
  * @param {Object} data
76
76
  */
77
77
  onKeyDownUp(data) {
78
- this.onNavKeyRow(-1)
78
+ !this.hasEditorFocus(data) && this.onNavKeyRow(-1)
79
79
  }
80
80
 
81
81
  /**
@@ -66,14 +66,14 @@ class ColumnModel extends BaseModel {
66
66
  * @param {Object} data
67
67
  */
68
68
  onKeyDownLeft(data) {
69
- this.onNavKeyColumn(-1)
69
+ !this.hasEditorFocus(data) && this.onNavKeyColumn(-1)
70
70
  }
71
71
 
72
72
  /**
73
73
  * @param {Object} data
74
74
  */
75
75
  onKeyDownRight(data) {
76
- this.onNavKeyColumn(1)
76
+ !this.hasEditorFocus(data) && this.onNavKeyColumn(1)
77
77
  }
78
78
 
79
79
  /**
@@ -55,14 +55,14 @@ class RowModel extends BaseModel {
55
55
  * @param {Object} data
56
56
  */
57
57
  onKeyDownDown(data) {
58
- this.onNavKeyRow(1)
58
+ !this.hasEditorFocus(data) && this.onNavKeyRow(1)
59
59
  }
60
60
 
61
61
  /**
62
62
  * @param {Object} data
63
63
  */
64
64
  onKeyDownUp(data) {
65
- this.onNavKeyRow(-1)
65
+ !this.hasEditorFocus(data) && this.onNavKeyRow(-1)
66
66
  }
67
67
 
68
68
  /**
@@ -29,7 +29,11 @@ class CellEditing extends Plugin {
29
29
  /**
30
30
  * @member {String[]} editorCls=['neo-table-editor']
31
31
  */
32
- editorCls: ['neo-table-editor']
32
+ editorCls: ['neo-table-editor'],
33
+ /**
34
+ * @member {Boolean} focusCells=true
35
+ */
36
+ focusCells: true
33
37
  }
34
38
 
35
39
  /**
@@ -110,7 +114,8 @@ class CellEditing extends Plugin {
110
114
  cellNode = VdomUtil.find(view.vdom, cellId).vdom,
111
115
  column = me.owner.headerToolbar.getColumn(dataField),
112
116
  editor = me.editors[dataField],
113
- value = record[dataField];
117
+ value = record[dataField],
118
+ keys;
114
119
 
115
120
  if (me.mountedEditor) {
116
121
  await me.unmountEditor();
@@ -133,15 +138,21 @@ class CellEditing extends Plugin {
133
138
  value,
134
139
  windowId,
135
140
 
136
- keys: {
137
- Enter : 'onEditorKeyEnter',
138
- Escape: 'onEditorKeyEscape',
139
- Tab : 'onEditorKeyTab',
140
- scope : me
141
- },
142
-
143
141
  ...column.editor
144
- })
142
+ });
143
+
144
+ keys = {
145
+ Enter : 'onEditorKeyEnter',
146
+ Escape: 'onEditorKeyEscape',
147
+ Tab : 'onEditorKeyTab',
148
+ scope : me
149
+ };
150
+
151
+ if (editor.keys) {
152
+ editor.keys.add(keys)
153
+ } else {
154
+ editor.keys = keys
155
+ }
145
156
  } else {
146
157
  editor.originalConfig.value = value;
147
158
  editor.setSilent({record, value})
@@ -249,14 +260,14 @@ class CellEditing extends Plugin {
249
260
  * @returns {Promise<void>}
250
261
  */
251
262
  async onTableKeyDown(data) {
252
- let me = this,
253
- {target} = data,
254
- tableView = me.owner.view,
263
+ let me = this,
264
+ {target} = data,
265
+ {view} = me.owner,
255
266
  dataField, record;
256
267
 
257
268
  if (!me.mountedEditor && target.cls?.includes('neo-selected')) {
258
- dataField = tableView.getCellDataField(target.id);
259
- record = tableView.getRecord(target.id);
269
+ dataField = view.getCellDataField(target.id);
270
+ record = view.getRecord(target.id);
260
271
 
261
272
  await me.mountEditor(record, dataField)
262
273
  }
@@ -283,7 +294,7 @@ class CellEditing extends Plugin {
283
294
  if (cellId) {
284
295
  selectionModel?.deselect(cellId, true); // the cell might still count as selected => silent deselect first
285
296
  selectionModel?.select(cellId);
286
- me.owner.focus(cellId)
297
+ me.focusCells && me.owner.focus(cellId)
287
298
  }
288
299
  }
289
300
 
@@ -315,15 +326,15 @@ class CellEditing extends Plugin {
315
326
  return
316
327
  }
317
328
 
318
- let me = this,
319
- record = me.mountedEditor.record,
320
- tableView = me.owner.view,
321
- rowIndex = tableView.store.indexOf(record);
329
+ let me = this,
330
+ record = me.mountedEditor.record,
331
+ {view} = me.owner,
332
+ rowIndex = view.store.indexOf(record);
322
333
 
323
334
  me.mountedEditor = null;
324
335
 
325
- tableView.vdom.cn[rowIndex] = tableView.createRow({record, rowIndex});
326
- await tableView.promiseUpdate()
336
+ view.getVdomRoot().cn[rowIndex] = view.createRow({record, rowIndex});
337
+ await view.promiseUpdate()
327
338
  }
328
339
  }
329
340