neo.mjs 8.7.1 → 8.8.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 (31) hide show
  1. package/apps/ServiceWorker.mjs +2 -2
  2. package/apps/portal/index.html +1 -1
  3. package/apps/portal/view/home/FooterContainer.mjs +1 -1
  4. package/examples/ServiceWorker.mjs +2 -2
  5. package/examples/grid/nestedRecordFields/EditUserDialog.mjs +154 -0
  6. package/examples/grid/nestedRecordFields/MainModel.mjs +54 -0
  7. package/examples/grid/nestedRecordFields/MainStore.mjs +54 -0
  8. package/examples/grid/nestedRecordFields/Viewport.mjs +152 -0
  9. package/examples/grid/nestedRecordFields/ViewportStateProvider.mjs +62 -0
  10. package/examples/grid/nestedRecordFields/app.mjs +6 -0
  11. package/examples/grid/nestedRecordFields/index.html +11 -0
  12. package/examples/grid/nestedRecordFields/neo-config.json +7 -0
  13. package/examples/table/nestedRecordFields/{MainContainer.mjs → Viewport.mjs} +50 -21
  14. package/examples/table/nestedRecordFields/{MainContainerStateProvider.mjs → ViewportStateProvider.mjs} +5 -5
  15. package/examples/table/nestedRecordFields/app.mjs +3 -3
  16. package/package.json +1 -1
  17. package/resources/scss/src/grid/View.scss +16 -0
  18. package/resources/scss/src/grid/header/Button.scss +3 -1
  19. package/resources/scss/src/table/View.scss +2 -2
  20. package/resources/scss/src/table/header/Button.scss +1 -2
  21. package/resources/scss/theme-dark/grid/View.scss +2 -0
  22. package/resources/scss/theme-dark/table/View.scss +2 -0
  23. package/resources/scss/theme-light/grid/View.scss +2 -0
  24. package/resources/scss/theme-light/table/View.scss +2 -0
  25. package/resources/scss/theme-neo-light/grid/View.scss +2 -0
  26. package/resources/scss/theme-neo-light/table/View.scss +2 -0
  27. package/src/DefaultConfig.mjs +2 -2
  28. package/src/Neo.mjs +48 -19
  29. package/src/data/RecordFactory.mjs +41 -48
  30. package/src/grid/View.mjs +23 -5
  31. package/src/table/View.mjs +1 -1
@@ -20,9 +20,9 @@ class ServiceWorker extends ServiceBase {
20
20
  */
21
21
  singleton: true,
22
22
  /**
23
- * @member {String} version='8.7.1'
23
+ * @member {String} version='8.8.0'
24
24
  */
25
- version: '8.7.1'
25
+ version: '8.8.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-01-23",
19
+ "datePublished": "2025-01-25",
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.7.1'
110
+ html : 'v8.8.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.7.1'
23
+ * @member {String} version='8.8.0'
24
24
  */
25
- version: '8.7.1'
25
+ version: '8.8.0'
26
26
  }
27
27
 
28
28
  /**
@@ -0,0 +1,154 @@
1
+ import CheckBox from '../../../src/form/field/CheckBox.mjs';
2
+ import CountryField from '../../../src/form/field/Country.mjs';
3
+ import Dialog from '../../../src/dialog/Base.mjs';
4
+ import TextField from '../../../src/form/field/Text.mjs';
5
+
6
+ /**
7
+ * @class Neo.examples.grid.nestedRecordFields.EditUserDialog
8
+ * @extends Neo.dialog.Base
9
+ */
10
+ class EditUserDialog extends Dialog {
11
+ static config = {
12
+ /**
13
+ * @member {String} className='Neo.examples.grid.nestedRecordFields.EditUserDialog'
14
+ * @protected
15
+ */
16
+ className: 'Neo.examples.grid.nestedRecordFields.EditUserDialog',
17
+ /**
18
+ * @member {String} closeAction='hide'
19
+ */
20
+ closeAction: 'hide',
21
+ /**
22
+ * @member {Object} containerConfig={style:{padding:'1em'}}
23
+ */
24
+ containerConfig: {
25
+ style: {
26
+ padding: '1em'
27
+ }
28
+ },
29
+ /**
30
+ * @member {Boolean} modal=true
31
+ */
32
+ modal: true,
33
+ /**
34
+ * @member {Record|null} record_=null
35
+ */
36
+ record_: null,
37
+ /**
38
+ * @member {String} title='Edit User'
39
+ */
40
+ title: 'Edit User',
41
+ /**
42
+ * @member {Object} itemDefaults
43
+ */
44
+ itemDefaults: {
45
+ flex : 'none',
46
+ labelWidth: 110
47
+ },
48
+ /**
49
+ * @member {Object[]} items
50
+ */
51
+ items: [{
52
+ module : TextField,
53
+ labelText: 'Firstname',
54
+ listeners: {change: 'up.onFirstnameFieldChange'},
55
+ reference: 'firstname-field'
56
+ }, {
57
+ module : TextField,
58
+ labelText: 'Lastname',
59
+ listeners: {change: 'up.onLastnameFieldChange'},
60
+ reference: 'lastname-field'
61
+ }, {
62
+ module : CountryField,
63
+ bind : {store: 'stores.countries'},
64
+ labelText : 'Country',
65
+ listeners : {change: 'up.onCountryFieldChange'},
66
+ reference : 'country-field',
67
+ valueField: 'code'
68
+ }, {
69
+ module : CheckBox,
70
+ labelText: 'Selected',
71
+ listeners: {change: 'up.onSelectedFieldChange'},
72
+ reference: 'selected-field',
73
+ style : {marginTop: '1em'}
74
+ }]
75
+ }
76
+
77
+ /**
78
+ * Triggered after the record config got changed
79
+ * @param {Record|null} value
80
+ * @param {Record|null} oldValue
81
+ * @protected
82
+ */
83
+ async afterSetRecord(value, oldValue) {
84
+ if (value) {
85
+ let me = this,
86
+ {record} = me;
87
+
88
+ // ensure the store has its data
89
+ await me.timeout(20);
90
+
91
+ me.getItem('country-field') .value = record.country;
92
+ me.getItem('firstname-field').value = record['user.firstname'];
93
+ me.getItem('lastname-field') .value = record['user.lastname'];
94
+ me.getItem('selected-field') .checked = record['annotations.selected'];
95
+ }
96
+ }
97
+
98
+ /**
99
+ * @param {Object} data
100
+ */
101
+ onCountryFieldChange(data) {
102
+ // You can also access the internal setter directly:
103
+ // this.record.country = data.value.code
104
+ // Using the API allows bulk changes
105
+
106
+ this.record.set({country: data.value.code})
107
+ }
108
+
109
+ /**
110
+ * @param {Object} data
111
+ */
112
+ onFirstnameFieldChange(data) {
113
+ // You can also access the internal setter directly:
114
+ // this.record['user.firstname'] = data.value
115
+ // Using the API allows bulk changes
116
+ this.record.set({user: {firstname: data.value}})
117
+
118
+
119
+ }
120
+
121
+ /**
122
+ * @param {Object} data
123
+ */
124
+ onLastnameFieldChange(data) {
125
+ // You can also access the internal setter directly:
126
+ // this.record['user.lastname'] = data.value
127
+ // Using the API allows bulk changes
128
+ this.record.set({user: {lastname: data.value}})
129
+ }
130
+
131
+ /**
132
+ * @param {Object} data
133
+ */
134
+ onSelectedFieldChange(data) {
135
+ let me = this,
136
+ store = me.getStateProvider().getStore('mainStore');
137
+
138
+ if (data.value === false) {
139
+ // You can also access the internal setter directly:
140
+ // me.record['annotations.selected'] = false
141
+ // Using the API allows bulk changes
142
+ me.record.set({annotations: {selected: false}})
143
+ } else {
144
+ // Assuming we want to support a single row selection
145
+ store.items.forEach(record => {
146
+ record.set({annotations: {
147
+ selected: record === me.record ? data.value : false
148
+ }})
149
+ })
150
+ }
151
+ }
152
+ }
153
+
154
+ export default Neo.setupClass(EditUserDialog);
@@ -0,0 +1,54 @@
1
+ import Model from '../../../src/data/Model.mjs';
2
+
3
+ /**
4
+ * @class Neo.examples.grid.nestedRecordFields.MainModel
5
+ * @extends Neo.data.Model
6
+ */
7
+ class MainModel extends Model {
8
+ static config = {
9
+ /**
10
+ * @member {String} className='Neo.examples.grid.nestedRecordFields.MainModel'
11
+ * @protected
12
+ */
13
+ className: 'Neo.examples.grid.nestedRecordFields.MainModel',
14
+ /**
15
+ * @member {Object[]} fields
16
+ */
17
+ fields: [{
18
+ name: 'annotations',
19
+ type: 'Object',
20
+
21
+ fields: [{
22
+ name : 'selected',
23
+ type : 'Boolean',
24
+ defaultValue: false
25
+ }]
26
+ }, {
27
+ name: 'country',
28
+ type: 'String'
29
+ }, {
30
+ name: 'edit',
31
+ type: 'String'
32
+ }, {
33
+ name: 'githubId',
34
+ type: 'String'
35
+ }, {
36
+ name: 'user',
37
+ type: 'Object',
38
+
39
+ fields: [{
40
+ name: 'firstname',
41
+ type: 'String'
42
+ }, {
43
+ name: 'lastname',
44
+ type: 'String'
45
+ }]
46
+ }],
47
+ /**
48
+ * @member {Boolean} trackModifiedFields=true
49
+ */
50
+ trackModifiedFields: true
51
+ }
52
+ }
53
+
54
+ export default Neo.setupClass(MainModel);
@@ -0,0 +1,54 @@
1
+ import Model from './MainModel.mjs';
2
+ import Store from '../../../src/data/Store.mjs';
3
+
4
+ /**
5
+ * @class Neo.examples.grid.nestedRecordFields.MainStore
6
+ * @extends Neo.data.Store
7
+ */
8
+ class MainStore extends Store {
9
+ static config = {
10
+ className : 'Neo.examples.grid.nestedRecordFields.MainStore',
11
+ keyProperty: 'githubId',
12
+ model : Model,
13
+
14
+ data: [{
15
+ country : 'DE',
16
+ githubId: 'tobiu',
17
+
18
+ user: {
19
+ firstname: 'Tobias',
20
+ lastname : 'Uhlig'
21
+ }
22
+ }, {
23
+ annotations: {
24
+ selected: true
25
+ },
26
+
27
+ country : 'US',
28
+ githubId: 'rwaters',
29
+
30
+ user: {
31
+ firstname: 'Rich',
32
+ lastname : 'Waters'
33
+ }
34
+ }, {
35
+ country : 'DE',
36
+ githubId: 'mrsunshine',
37
+
38
+ user: {
39
+ firstname: 'Nils',
40
+ lastname : 'Dehl'
41
+ }
42
+ }, {
43
+ country : 'US',
44
+ githubId: 'camtnbikerrwc',
45
+
46
+ user: {
47
+ firstname: 'Gerard',
48
+ lastname : 'Horan'
49
+ }
50
+ }]
51
+ }
52
+ }
53
+
54
+ export default Neo.setupClass(MainStore);
@@ -0,0 +1,152 @@
1
+ import BaseViewport from '../../../src/container/Viewport.mjs';
2
+ import Button from '../../../src/button/Base.mjs';
3
+ import GridContainer from '../../../src/grid/Container.mjs';
4
+ import ViewportStateProvider from './ViewportStateProvider.mjs';
5
+
6
+ /**
7
+ * @class Neo.examples.grid.nestedRecordFields.Viewport
8
+ * @extends Neo.container.Viewport
9
+ */
10
+ class Viewport extends BaseViewport {
11
+ static config = {
12
+ /**
13
+ * @member {String} className='Neo.examples.grid.nestedRecordFields.Viewport'
14
+ * @protected
15
+ */
16
+ className: 'Neo.examples.grid.nestedRecordFields.Viewport',
17
+ /**
18
+ * @member {Neo.state.Provider} stateProvider=ViewportStateProvider
19
+ */
20
+ stateProvider: ViewportStateProvider,
21
+ /**
22
+ * @member {Object} style={padding:'1em'}
23
+ */
24
+ style: {padding: '1em'},
25
+ /**
26
+ * @member {Object[]} items
27
+ */
28
+ items: [{
29
+ ntype : 'toolbar',
30
+ flex : 'none',
31
+ reference: 'switch-theme-button',
32
+ style : {marginBottom: '1em'},
33
+
34
+ items: ['->', {
35
+ handler: 'up.onSwitchThemeButtonClick',
36
+ iconCls: 'fas fa-moon',
37
+ text : 'Dark Theme'
38
+ }]
39
+ }, {
40
+ module: GridContainer,
41
+ bind : {store : 'stores.mainStore'},
42
+
43
+ columnDefaults: {
44
+ width: 200
45
+ },
46
+
47
+ columns: [
48
+ {dataField: 'user.firstname', text: 'Firstname'},
49
+ {dataField: 'user.lastname', text: 'Lastname'},
50
+ {dataField: 'githubId', text: 'Github Id'},
51
+ {dataField: 'country', text: 'Country', renderer: 'up.countryRenderer'},
52
+ {dataField: 'edit', text: 'Edit Action', renderer: 'up.editRenderer'}
53
+ ],
54
+
55
+ viewConfig: {
56
+ highlightModifiedCells: true
57
+ }
58
+ }]
59
+ }
60
+
61
+ /**
62
+ * @member {Neo.dialog.Base|null} dialog=null
63
+ */
64
+ dialog = null
65
+
66
+ /**
67
+ * @param {Object} data
68
+ */
69
+ countryRenderer({record}) {
70
+ let countryStore = this.getStateProvider().getStore('countries');
71
+
72
+ if (countryStore.getCount() > 0) {
73
+ return countryStore.get(record.country).name
74
+ }
75
+
76
+ return ''
77
+ }
78
+
79
+ /**
80
+ * @param {Object} data
81
+ */
82
+ editButtonHandler(data) {
83
+ let me = this,
84
+ button = data.component,
85
+ {appName, dialog, theme, windowId} = me,
86
+ {record} = button;
87
+
88
+ if (!dialog) {
89
+ import('./EditUserDialog.mjs').then(module => {
90
+ me.dialog = Neo.create({
91
+ module : module.default,
92
+ animateTargetId: button.id,
93
+ appName,
94
+ stateProvider : {parent: me.getStateProvider()},
95
+ record,
96
+ theme,
97
+ windowId
98
+ })
99
+ })
100
+ } else {
101
+ dialog.animateTargetId = button.id;
102
+ dialog.record = record;
103
+
104
+ dialog.show()
105
+ }
106
+ }
107
+
108
+ /**
109
+ * @param {Object} data
110
+ */
111
+ editRenderer({column, gridContainer, index, record}) {
112
+ let me = this,
113
+ {appName, windowId} = me,
114
+ widgetId = `${column.id}-widget-${index}`,
115
+ button = (column.widgetMap || (column.widgetMap = {}))[widgetId] || (column.widgetMap[widgetId] = Neo.create({
116
+ module : Button,
117
+ appName,
118
+ handler : 'up.editButtonHandler',
119
+ parentId: gridContainer.id,
120
+ record,
121
+ text : 'Edit',
122
+ windowId
123
+ }));
124
+
125
+ me.view.updateDepth = -1;
126
+
127
+ return button.createVdomReference()
128
+ }
129
+
130
+ /**
131
+ * @param {Object} data
132
+ */
133
+ onSwitchThemeButtonClick(data) {
134
+ let me = this,
135
+ button = data.component,
136
+ isDarkTheme = me.theme === 'neo-theme-dark',
137
+ theme = isDarkTheme ? 'neo-theme-light' : 'neo-theme-dark';
138
+
139
+ button.set({
140
+ iconCls: isDarkTheme ? 'fa fa-moon' : 'fa fa-sun',
141
+ text : isDarkTheme ? 'Dark Theme' : 'Light Theme'
142
+ });
143
+
144
+ me.theme = theme;
145
+
146
+ if (me.dialog) {
147
+ me.dialog.theme = theme
148
+ }
149
+ }
150
+ }
151
+
152
+ export default Neo.setupClass(Viewport);
@@ -0,0 +1,62 @@
1
+ import MainStore from './MainStore.mjs';
2
+ import StateProvider from '../../../src/state/Provider.mjs';
3
+ import Store from '../../../src/data/Store.mjs';
4
+
5
+ const dataSymbol = Symbol.for('data');
6
+
7
+ /**
8
+ * @class Neo.examples.grid.nestedRecordFields.ViewportStateProvider
9
+ * @extends Neo.state.Provider
10
+ */
11
+ class ViewportStateProvider extends StateProvider {
12
+ static config = {
13
+ /**
14
+ * @member {String} className='Neo.examples.grid.nestedRecordFields.ViewportStateProvider'
15
+ * @protected
16
+ */
17
+ className: 'Neo.examples.grid.nestedRecordFields.ViewportStateProvider',
18
+ /**
19
+ * @member {Object} stores
20
+ */
21
+ stores: {
22
+ countries: {
23
+ module : Store,
24
+ autoLoad : true,
25
+ keyProperty: 'code',
26
+ listeners : {load: 'onCountryStoreLoad'},
27
+ url : '../../../resources/examples/data/countries.json',
28
+
29
+ model: {
30
+ fields: [
31
+ {name: 'code'},
32
+ {name: 'name'}
33
+ ]
34
+ }
35
+ },
36
+ mainStore: MainStore
37
+ }
38
+ }
39
+
40
+ /**
41
+ * @param {Record[]} items
42
+ */
43
+ onCountryStoreLoad(items) {
44
+ let me = this,
45
+ mainStore = me.getStore('mainStore'),
46
+ country;
47
+
48
+ // if the main table store is already loaded, the country field renderer had no data
49
+ if (mainStore.getCount() > 0) {
50
+ mainStore.items.forEach(record => {
51
+ country = record.country;
52
+
53
+ // hack resetting the current value to get a new record change
54
+ record[dataSymbol].country = null;
55
+
56
+ record.country = country
57
+ })
58
+ }
59
+ }
60
+ }
61
+
62
+ export default Neo.setupClass(ViewportStateProvider);
@@ -0,0 +1,6 @@
1
+ import Viewport from './Viewport.mjs';
2
+
3
+ export const onStart = () => Neo.app({
4
+ mainView: Viewport,
5
+ name : 'Neo.examples.grid.nestedRecordFields'
6
+ })
@@ -0,0 +1,11 @@
1
+ <!DOCTYPE HTML>
2
+ <html>
3
+ <head>
4
+ <meta name="viewport" content="width=device-width, initial-scale=1">
5
+ <meta charset="UTF-8">
6
+ <title>Neo Grid: Nested Record Fields</title>
7
+ </head>
8
+ <body>
9
+ <script src="../../../src/MicroLoader.mjs" type="module"></script>
10
+ </body>
11
+ </html>
@@ -0,0 +1,7 @@
1
+ {
2
+ "appPath" : "examples/grid/nestedRecordFields/app.mjs",
3
+ "basePath" : "../../../",
4
+ "environment" : "development",
5
+ "mainPath" : "./Main.mjs",
6
+ "mainThreadAddons": ["DragDrop", "Navigator", "ResizeObserver", "Stylesheet"]
7
+ }
@@ -1,35 +1,42 @@
1
- import Button from '../../../src/button/Base.mjs';
2
- import MainContainerStateProvider from './MainContainerStateProvider.mjs';
3
- import TableContainer from '../../../src/table/Container.mjs';
4
- import Viewport from '../../../src/container/Viewport.mjs';
1
+ import BaseViewport from '../../../src/container/Viewport.mjs';
2
+ import Button from '../../../src/button/Base.mjs';
3
+ import TableContainer from '../../../src/table/Container.mjs';
4
+ import ViewportStateProvider from './ViewportStateProvider.mjs';
5
5
 
6
6
  /**
7
- * @class Neo.examples.table.nestedRecordFields.MainContainer
7
+ * @class Neo.examples.table.nestedRecordFields.Viewport
8
8
  * @extends Neo.container.Viewport
9
9
  */
10
- class MainContainer extends Viewport {
10
+ class Viewport extends BaseViewport {
11
11
  static config = {
12
12
  /**
13
- * @member {String} className='Neo.examples.table.nestedRecordFields.MainContainer'
13
+ * @member {String} className='Neo.examples.table.nestedRecordFields.Viewport'
14
14
  * @protected
15
15
  */
16
- className: 'Neo.examples.table.nestedRecordFields.MainContainer',
16
+ className: 'Neo.examples.table.nestedRecordFields.Viewport',
17
17
  /**
18
- * @member {Object|String} layout='fit'
18
+ * @member {Neo.state.Provider} stateProvider=ViewportStateProvider
19
19
  */
20
- layout: 'fit',
20
+ stateProvider: ViewportStateProvider,
21
21
  /**
22
- * @member {Neo.state.Provider} stateProvider=MainContainerStateProvider
22
+ * @member {Object} style={padding:'1em'}
23
23
  */
24
- stateProvider: MainContainerStateProvider,
25
- /**
26
- * @member {Object} style={padding:'20px'}
27
- */
28
- style: {padding: '20px'},
24
+ style: {padding: '1em'},
29
25
  /**
30
26
  * @member {Object[]} items
31
27
  */
32
28
  items: [{
29
+ ntype : 'toolbar',
30
+ flex : 'none',
31
+ reference: 'switch-theme-button',
32
+ style : {marginBottom: '1em'},
33
+
34
+ items: ['->', {
35
+ handler: 'up.onSwitchThemeButtonClick',
36
+ iconCls: 'fas fa-moon',
37
+ text : 'Dark Theme'
38
+ }]
39
+ }, {
33
40
  module: TableContainer,
34
41
  bind : {store : 'stores.mainStore'},
35
42
 
@@ -69,10 +76,10 @@ class MainContainer extends Viewport {
69
76
  * @param {Object} data
70
77
  */
71
78
  editButtonHandler(data) {
72
- let me = this,
73
- button = data.component,
74
- {appName, dialog, windowId} = me,
75
- {record} = button;
79
+ let me = this,
80
+ button = data.component,
81
+ {appName, dialog, theme, windowId} = me,
82
+ {record} = button;
76
83
 
77
84
  if (!dialog) {
78
85
  import('./EditUserDialog.mjs').then(module => {
@@ -82,6 +89,7 @@ class MainContainer extends Viewport {
82
89
  appName,
83
90
  stateProvider : {parent: me.getStateProvider()},
84
91
  record,
92
+ theme,
85
93
  windowId
86
94
  })
87
95
  })
@@ -114,6 +122,27 @@ class MainContainer extends Viewport {
114
122
 
115
123
  return button.createVdomReference()
116
124
  }
125
+
126
+ /**
127
+ * @param {Object} data
128
+ */
129
+ onSwitchThemeButtonClick(data) {
130
+ let me = this,
131
+ button = data.component,
132
+ isDarkTheme = me.theme === 'neo-theme-dark',
133
+ theme = isDarkTheme ? 'neo-theme-light' : 'neo-theme-dark';
134
+
135
+ button.set({
136
+ iconCls: isDarkTheme ? 'fa fa-moon' : 'fa fa-sun',
137
+ text : isDarkTheme ? 'Dark Theme' : 'Light Theme'
138
+ });
139
+
140
+ me.theme = theme;
141
+
142
+ if (me.dialog) {
143
+ me.dialog.theme = theme
144
+ }
145
+ }
117
146
  }
118
147
 
119
- export default Neo.setupClass(MainContainer);
148
+ export default Neo.setupClass(Viewport);
@@ -5,16 +5,16 @@ import Store from '../../../src/data/Store.mjs';
5
5
  const dataSymbol = Symbol.for('data');
6
6
 
7
7
  /**
8
- * @class Neo.examples.table.nestedRecordFields.MainContainerStateProvider
8
+ * @class Neo.examples.table.nestedRecordFields.ViewportStateProvider
9
9
  * @extends Neo.state.Provider
10
10
  */
11
- class MainContainerStateProvider extends StateProvider {
11
+ class ViewportStateProvider extends StateProvider {
12
12
  static config = {
13
13
  /**
14
- * @member {String} className='Neo.examples.table.nestedRecordFields.MainContainerStateProvider'
14
+ * @member {String} className='Neo.examples.table.nestedRecordFields.ViewportStateProvider'
15
15
  * @protected
16
16
  */
17
- className: 'Neo.examples.table.nestedRecordFields.MainContainerStateProvider',
17
+ className: 'Neo.examples.table.nestedRecordFields.ViewportStateProvider',
18
18
  /**
19
19
  * @member {Object} stores
20
20
  */
@@ -59,4 +59,4 @@ class MainContainerStateProvider extends StateProvider {
59
59
  }
60
60
  }
61
61
 
62
- export default Neo.setupClass(MainContainerStateProvider);
62
+ export default Neo.setupClass(ViewportStateProvider);
@@ -1,6 +1,6 @@
1
- import MainContainer from './MainContainer.mjs';
1
+ import Viewport from './Viewport.mjs';
2
2
 
3
3
  export const onStart = () => Neo.app({
4
- mainView: MainContainer,
4
+ mainView: Viewport,
5
5
  name : 'Neo.examples.table.nestedRecordFields'
6
- });
6
+ })
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "neo.mjs",
3
- "version": "8.7.1",
3
+ "version": "8.8.0",
4
4
  "description": "The webworkers driven UI framework",
5
5
  "type": "module",
6
6
  "repository": {
@@ -22,6 +22,22 @@
22
22
  height : 100%;
23
23
  overflow-x: visible;
24
24
 
25
+ .neo-grid-cell {
26
+ &.neo-is-modified {
27
+ &:after {
28
+ border-color: transparent var(--grid-cell-ismodified-color) transparent transparent;
29
+ border-style: solid;
30
+ border-width: 0 var(--grid-cell-ismodified-size) var(--grid-cell-ismodified-size) 0;
31
+ content : '';
32
+ height : 0;
33
+ position : absolute;
34
+ right : 0;
35
+ top : 0;
36
+ width : 0;
37
+ }
38
+ }
39
+ }
40
+
25
41
  &.neo-is-scrolling * {
26
42
  pointer-events: none !important;
27
43
  }
@@ -2,6 +2,7 @@
2
2
  align-items : center;
3
3
  background-color: var(--grid-header-button-background-color);
4
4
  background-image: var(--grid-header-button-background-image);
5
+ border-radius : 0;
5
6
  border-width : 0;
6
7
  color : var(--grid-header-button-color);
7
8
  cursor : pointer;
@@ -13,7 +14,6 @@
13
14
  height : 29px; // Webkit => Safari can not handle 100%
14
15
  justify-content : flex-end;
15
16
  margin : 0;
16
- padding : 7px 10px 6px;
17
17
  white-space : nowrap;
18
18
 
19
19
  &:not(:last-child) {
@@ -69,6 +69,8 @@
69
69
  }
70
70
 
71
71
  .neo-button-text {
72
+ color : var(--grid-header-button-color);
72
73
  pointer-events: none;
74
+ text-transform: none;
73
75
  }
74
76
  }
@@ -13,9 +13,9 @@
13
13
 
14
14
  &.neo-is-modified {
15
15
  &:after {
16
- border-color: transparent orange transparent transparent;
16
+ border-color: transparent var(--table-cell-ismodified-color) transparent transparent;
17
17
  border-style: solid;
18
- border-width: 0 10px 10px 0;
18
+ border-width: 0 var(--table-cell-ismodified-size) var(--table-cell-ismodified-size) 0;
19
19
  content : '';
20
20
  height : 0;
21
21
  position : absolute;
@@ -1,6 +1,7 @@
1
1
  .neo-table-header-button {
2
2
  align-items : center;
3
3
  background-color: var(--table-header-button-background-color);
4
+ background-image: var(--table-header-button-background-image);
4
5
  border-width : 0;
5
6
  color : var(--table-header-button-color);
6
7
  cursor : pointer;
@@ -15,8 +16,6 @@
15
16
  white-space : nowrap;
16
17
  width : 100% !important;
17
18
 
18
- background-image: var(--table-header-button-background-image);
19
-
20
19
  &.neo-drag-over {
21
20
  background-image: linear-gradient(green, darkgreen);;
22
21
  }
@@ -1,5 +1,7 @@
1
1
  :root .neo-theme-dark { // .neo-grid-view
2
2
  --grid-cell-background-color-hover : #54595c;
3
+ --grid-cell-ismodified-color : orange;
4
+ --grid-cell-ismodified-size : 10px;
3
5
  --grid-cellmodel-selected-cell-background-color : #64B5F6;
4
6
  --grid-cellmodel-selected-cell-color : #2b2b2b;
5
7
  --grid-cellmodel-selected-column-cell-background-color: #4f558a;
@@ -1,5 +1,7 @@
1
1
  :root .neo-theme-dark { // .neo-table-view
2
2
  --table-cell-background-color-hover : #54595c;
3
+ --table-cell-ismodified-color : orange;
4
+ --table-cell-ismodified-size : 10px;
3
5
  --table-cellmodel-selected-cell-background-color : #64B5F6;
4
6
  --table-cellmodel-selected-cell-color : #2b2b2b;
5
7
  --table-cellmodel-selected-column-cell-background-color: #4f558a;
@@ -2,6 +2,8 @@
2
2
 
3
3
  :root .neo-theme-light { // .neo-grid-view
4
4
  --grid-cell-background-color-hover : #{color.adjust(#33343d, $lightness: 70%)};
5
+ --grid-cell-ismodified-color : orange;
6
+ --grid-cell-ismodified-size : 10px;
5
7
  --grid-cellmodel-selected-cell-background-color : #{color.adjust(#64B5F6, $lightness: 22%)};
6
8
  --grid-cellmodel-selected-cell-color : #2b2b2b;
7
9
  --grid-cellmodel-selected-column-cell-background-color: #{color.adjust(#4f558a, $lightness: 52%)};
@@ -2,6 +2,8 @@
2
2
 
3
3
  :root .neo-theme-light { // .neo-table-view
4
4
  --table-cell-background-color-hover : #{color.adjust(#33343d, $lightness: 70%)};
5
+ --table-cell-ismodified-color : orange;
6
+ --table-cell-ismodified-size : 10px;
5
7
  --table-cellmodel-selected-cell-background-color : #{color.adjust(#64B5F6, $lightness: 22%)};
6
8
  --table-cellmodel-selected-cell-color : #2b2b2b;
7
9
  --table-cellmodel-selected-column-cell-background-color: #{color.adjust(#4f558a, $lightness: 52%)};
@@ -2,6 +2,8 @@
2
2
 
3
3
  :root .neo-theme-neo-light { // .neo-grid-view
4
4
  --grid-cell-background-color-hover : #{color.adjust(#33343d, $lightness: 70%)};
5
+ --grid-cell-ismodified-color : orange;
6
+ --grid-cell-ismodified-size : 10px;
5
7
  --grid-cellmodel-selected-cell-background-color : #{color.adjust(#64B5F6, $lightness: 22%)};
6
8
  --grid-cellmodel-selected-cell-color : #2b2b2b;
7
9
  --grid-cellmodel-selected-column-cell-background-color: #{color.adjust(#4f558a, $lightness: 52%)};
@@ -2,6 +2,8 @@
2
2
 
3
3
  :root .neo-theme-neo-light { // .neo-table-view
4
4
  --table-cell-background-color-hover : #{color.adjust(#33343d, $lightness: 70%)};
5
+ --table-cell-ismodified-color : orange;
6
+ --table-cell-ismodified-size : 10px;
5
7
  --table-cellmodel-selected-cell-background-color : #{color.adjust(#64B5F6, $lightness: 22%)};
6
8
  --table-cellmodel-selected-cell-color : #2b2b2b;
7
9
  --table-cellmodel-selected-column-cell-background-color: #{color.adjust(#4f558a, $lightness: 52%)};
@@ -262,12 +262,12 @@ const DefaultConfig = {
262
262
  useVdomWorker: true,
263
263
  /**
264
264
  * buildScripts/injectPackageVersion.mjs will update this value
265
- * @default '8.7.1'
265
+ * @default '8.8.0'
266
266
  * @memberOf! module:Neo
267
267
  * @name config.version
268
268
  * @type String
269
269
  */
270
- version: '8.7.1'
270
+ version: '8.8.0'
271
271
  };
272
272
 
273
273
  Object.assign(DefaultConfig, {
package/src/Neo.mjs CHANGED
@@ -66,10 +66,10 @@ Neo = globalThis.Neo = Object.assign({
66
66
  *
67
67
  * // e.g. Neo.core.Util.isObject => Neo.isObject
68
68
  * @memberOf module:Neo
69
- * @param {Neo|Neo.core.Base} target The target class or singleton Instance or Neo
70
- * @param {Neo.core.Base} namespace The class containing the methods
71
- * @param {Object} config
72
- * @param {Boolean} [bind] set this to true in case you want to bind methods to the "from" namespace
69
+ * @param {Neo|Neo.core.Base} target The target class or singleton Instance or Neo
70
+ * @param {Neo.core.Base} namespace The class containing the methods
71
+ * @param {Object} config
72
+ * @param {Boolean} [bind] set this to true in case you want to bind methods to the "from" namespace
73
73
  * @returns {Object} target
74
74
  */
75
75
  applyFromNs(target, namespace, config, bind) {
@@ -104,7 +104,7 @@ Neo = globalThis.Neo = Object.assign({
104
104
  /**
105
105
  * Copies all keys of defaults into target, in case they don't already exist
106
106
  * @memberOf module:Neo
107
- * @param {Object} target The target object
107
+ * @param {Object} target The target object
108
108
  * @param {Object} defaults The object containing the keys you want to copy
109
109
  * @returns {Object} target
110
110
  */
@@ -120,6 +120,35 @@ Neo = globalThis.Neo = Object.assign({
120
120
  return target
121
121
  },
122
122
 
123
+ /**
124
+ * Assigns a new value to a given nested objects path.
125
+ * It will create the path structure or parts of it, in case it does not exist.
126
+ * @example
127
+ * Neo.assignToNs('annotations.selected', false, record)
128
+ *
129
+ * @memberOf module:Neo
130
+ * @param {String[]|String} path The path string containing dots or an Array of the string parts
131
+ * @param {*} value The new value to assign to the leaf node
132
+ * @param {Object} scope=globalThis Set a different starting point as globalThis
133
+ * @param {Boolean} force=true false will only assign default values (assign if old value === undefined)
134
+ */
135
+ assignToNs(path, value, scope=globalThis, force=true) {
136
+ path = Array.isArray(path) ? path : path.split('.');
137
+
138
+ let key;
139
+
140
+ if (path.length > 1) {
141
+ key = path.pop();
142
+ scope = Neo.ns(path, true, scope)
143
+ } else {
144
+ key = path
145
+ }
146
+
147
+ if (force || scope[key] === undefined) {
148
+ scope[key] = value
149
+ }
150
+ },
151
+
123
152
  /**
124
153
  * Converts kebab-case strings into camel-case
125
154
  * @memberOf module:Neo
@@ -143,7 +172,7 @@ Neo = globalThis.Neo = Object.assign({
143
172
  /**
144
173
  * @memberOf module:Neo
145
174
  * @param {Object|Array|*} obj
146
- * @param {Boolean} deep=false Set this to true in case you want to clone nested objects as well
175
+ * @param {Boolean} deep=false Set this to true in case you want to clone nested objects as well
147
176
  * @param {Boolean} ignoreNeoInstances=false returns existing instances if set to true
148
177
  * @returns {Object|Array|*} the cloned input
149
178
  */
@@ -214,7 +243,7 @@ Neo = globalThis.Neo = Object.assign({
214
243
  * });
215
244
  * @memberOf module:Neo
216
245
  * @param {String|Object|Neo.core.Base} className
217
- * @param {Object} [config]
246
+ * @param {Object} [config]
218
247
  * @returns {Neo.core.Base|null} The new class instance
219
248
  * @tutorial 02_ClassSystem
220
249
  */
@@ -263,7 +292,7 @@ Neo = globalThis.Neo = Object.assign({
263
292
  * Checks if there is a set method for a given property key inside the prototype chain
264
293
  * @memberOf module:Neo
265
294
  * @param {Neo.core.Base} proto The top level prototype of a class
266
- * @param {String} key the property key to test
295
+ * @param {String} key The property key to test
267
296
  * @returns {Boolean}
268
297
  */
269
298
  hasPropertySetter(proto, key) {
@@ -319,9 +348,9 @@ Neo = globalThis.Neo = Object.assign({
319
348
  * // return globalThis.Neo.button.Base;
320
349
  *
321
350
  * @memberOf module:Neo
322
- * @param {Array|String} names The class name string containing dots or an Array of the string parts
323
- * @param {Boolean} create=false Set create to true to create empty objects for non-existing parts
324
- * @param {Object} [scope] Set a different starting point as globalThis
351
+ * @param {String[]|String} names The class name string containing dots or an Array of the string parts
352
+ * @param {Boolean} create=false Set create to true to create empty objects for non-existing parts
353
+ * @param {Object} [scope] Set a different starting point as globalThis
325
354
  * @returns {Object} reference to the toplevel namespace
326
355
  */
327
356
  ns(names, create=false, scope) {
@@ -341,9 +370,9 @@ Neo = globalThis.Neo = Object.assign({
341
370
  /**
342
371
  * Extended version of Neo.ns() which supports mapping into arrays.
343
372
  * @memberOf module:Neo
344
- * @param {Array|String} names The class name string containing dots or an Array of the string parts
345
- * @param {Boolean} create=false Set create to true to create empty objects for non-existing parts
346
- * @param {Object} [scope] Set a different starting point as globalThis
373
+ * @param {Array|String} names The class name string containing dots or an Array of the string parts
374
+ * @param {Boolean} create=false Set create to true to create empty objects for non-existing parts
375
+ * @param {Object} [scope] Set a different starting point as globalThis
347
376
  * @returns {Object} reference to the toplevel namespace
348
377
  */
349
378
  nsWithArrays(names, create=false, scope) {
@@ -383,7 +412,7 @@ Neo = globalThis.Neo = Object.assign({
383
412
  * });
384
413
  * @memberOf module:Neo
385
414
  * @param {String|Object} ntype
386
- * @param {Object} [config]
415
+ * @param {Object} [config]
387
416
  * @returns {Neo.core.Base}
388
417
  * @see {@link module:Neo.create create}
389
418
  */
@@ -575,7 +604,7 @@ const ignoreMixin = [
575
604
 
576
605
  /**
577
606
  * @param {Neo.core.Base} cls
578
- * @param {Array} mixins
607
+ * @param {Array} mixins
579
608
  * @private
580
609
  */
581
610
  function applyMixins(cls, mixins) {
@@ -614,7 +643,7 @@ function applyMixins(cls, mixins) {
614
643
  /**
615
644
  * Creates get / set methods for class configs ending with an underscore
616
645
  * @param {Neo.core.Base} proto
617
- * @param {String} key
646
+ * @param {String} key
618
647
  * @private
619
648
  * @tutorial 02_ClassSystem
620
649
  */
@@ -708,8 +737,8 @@ function autoGenerateGetSet(proto, key) {
708
737
 
709
738
  /**
710
739
  * @param {Boolean} create
711
- * @param {Object} current
712
- * @param {Object} prev
740
+ * @param {Object} current
741
+ * @param {Object} prev
713
742
  * @returns {Object|undefined}
714
743
  */
715
744
  function createArrayNs(create, current, prev) {
@@ -4,6 +4,7 @@ import Model from './Model.mjs';
4
4
 
5
5
  const
6
6
  dataSymbol = Symbol.for('data'),
7
+ isModifiedSymbol = Symbol.for('isModified'),
7
8
  originalDataSymbol = Symbol.for('originalData');
8
9
 
9
10
  let instance;
@@ -34,27 +35,17 @@ class RecordFactory extends Base {
34
35
  /**
35
36
  * Assigns model based default values to a data object
36
37
  * @param {Object} data
37
- * @param {Record} record
38
38
  * @param {Neo.data.Model} model
39
39
  * @returns {Object}
40
40
  */
41
- assignDefaultValues(data, record, model) {
42
- let {hasNestedFields} = model,
43
- scope;
44
-
41
+ assignDefaultValues(data, model) {
45
42
  model.fieldsMap.forEach((field, fieldName) => {
46
43
  if (Object.hasOwn(field, 'defaultValue')) {
47
- if (hasNestedFields && fieldName.includes('.')) {
48
- let nsArray = fieldName.split('.');
49
-
50
- fieldName = nsArray.pop();
51
- scope = Neo.ns(nsArray, true, data)
52
- } else {
53
- scope = data
54
- }
55
-
56
- if (scope[fieldName] === undefined) {
57
- scope[fieldName] = field.defaultValue
44
+ // We could always use Neo.assignToNs() => the check is just for improving the performance
45
+ if (model.hasNestedFields) {
46
+ Neo.assignToNs(fieldName, field.defaultValue, data, false)
47
+ } else if (data[fieldName] === undefined) {
48
+ data[fieldName] = field.defaultValue
58
49
  }
59
50
  }
60
51
  });
@@ -65,18 +56,18 @@ class RecordFactory extends Base {
65
56
  /**
66
57
  * @param {Object} data
67
58
  * @param {Object} data.field
68
- * @param {Neo.data.RecordFactory} data.me
69
59
  * @param {Neo.data.Model} data.model
70
60
  * @param {String} data.path=''
61
+ * @param {Object} data.proto
71
62
  */
72
- createField({field, me, model, path=''}) {
63
+ createField({field, model, path='', proto}) {
73
64
  let fieldName = field.name,
74
65
  fieldPath = path === '' ? fieldName : `${path}.${fieldName}`,
75
66
  properties;
76
67
 
77
68
  if (field.fields) {
78
69
  field.fields.forEach(childField => {
79
- this.createField({field: childField, me, model, path: fieldPath})
70
+ this.createField({field: childField, model, path: fieldPath, proto})
80
71
  })
81
72
  } else {
82
73
  properties = {
@@ -99,7 +90,9 @@ class RecordFactory extends Base {
99
90
  if (!Neo.isEqual(value, oldValue)) {
100
91
  instance.setRecordData({fieldName: fieldPath, model, record: me, value});
101
92
 
102
- me._isModified = me.isModified;
93
+ if (!model.trackModifiedFields) {
94
+ me[isModifiedSymbol] = true
95
+ }
103
96
 
104
97
  instance.onRecordChange({
105
98
  fields: [{name: fieldPath, oldValue, value}],
@@ -111,7 +104,7 @@ class RecordFactory extends Base {
111
104
  }
112
105
  };
113
106
 
114
- Object.defineProperties(me, properties)
107
+ Object.defineProperties(proto, properties)
115
108
  }
116
109
  }
117
110
 
@@ -155,10 +148,10 @@ class RecordFactory extends Base {
155
148
  let me = this;
156
149
 
157
150
  if (model.trackModifiedFields) {
158
- return Neo.isEqual(me[dataSymbol], me[originalDataSymbol])
151
+ return !Neo.isEqual(me[dataSymbol], me[originalDataSymbol])
159
152
  }
160
153
 
161
- return me._isModified
154
+ return me[isModifiedSymbol]
162
155
  }
163
156
 
164
157
  /**
@@ -167,7 +160,7 @@ class RecordFactory extends Base {
167
160
  constructor(config) {
168
161
  let me = this;
169
162
 
170
- config = instance.assignDefaultValues(config, me, model);
163
+ config = instance.assignDefaultValues(config, model);
171
164
 
172
165
  if (model.trackModifiedFields) {
173
166
  me[originalDataSymbol] = {};
@@ -175,7 +168,7 @@ class RecordFactory extends Base {
175
168
  }
176
169
 
177
170
  me.setSilent(config); // We do not want to fire change events when constructing
178
- me._isModified = false
171
+ me[isModifiedSymbol] = false
179
172
  }
180
173
 
181
174
  /**
@@ -186,7 +179,7 @@ class RecordFactory extends Base {
186
179
  let me = this;
187
180
 
188
181
  // Check if the field getter does exist
189
- if (!me.__proto__.hasOwnProperty(fieldName)) {
182
+ if (!Object.hasOwn(me.__proto__, fieldName)) {
190
183
  Logger.logError('The record does not contain the field', fieldName, me)
191
184
  }
192
185
 
@@ -210,6 +203,15 @@ class RecordFactory extends Base {
210
203
  return null
211
204
  }
212
205
 
206
+ /**
207
+ * Bulk-update multiple record fields at once
208
+ * @param {Object} fields
209
+ */
210
+ reset(fields) {
211
+ this.setOriginal(fields);
212
+ this.set(fields)
213
+ }
214
+
213
215
  /**
214
216
  * Bulk-update multiple record fields at once
215
217
  * @param {Object} fields
@@ -247,7 +249,7 @@ class RecordFactory extends Base {
247
249
 
248
250
  if (Array.isArray(model.fields)) {
249
251
  model.fields.forEach(field => {
250
- instance.createField({field, me: cls.prototype, model})
252
+ instance.createField({field, model, proto: cls.prototype})
251
253
  })
252
254
  }
253
255
 
@@ -261,23 +263,6 @@ class RecordFactory extends Base {
261
263
  }
262
264
  }
263
265
 
264
- /**
265
- * @param {Object} record
266
- * @returns {Boolean} true in case a change was found
267
- */
268
- isModified(record) {
269
- return record.isModified
270
- }
271
-
272
- /**
273
- * @param {Object} record
274
- * @param {String} fieldName
275
- * @returns {Boolean|null} null in case the model does not use trackModifiedFields, true in case a change was found
276
- */
277
- isModifiedField(record, fieldName) {
278
- return record.isModifiedField(fieldName)
279
- }
280
-
281
266
  /**
282
267
  * Tests if a given object is an instance of a class created by this factory
283
268
  * @param {Object} record
@@ -386,6 +371,10 @@ class RecordFactory extends Base {
386
371
  * @protected
387
372
  */
388
373
  setRecordData({fieldName, model, record, useOriginalData=false, value}) {
374
+ if (useOriginalData && !model.trackModifiedFields) {
375
+ return
376
+ }
377
+
389
378
  let scope = useOriginalData ? originalDataSymbol : dataSymbol;
390
379
 
391
380
  if (model.hasNestedFields && fieldName.includes('.')) {
@@ -411,9 +400,13 @@ class RecordFactory extends Base {
411
400
  * @param {Boolean} data.useOriginalData=false true will apply changes to the originalData symbol
412
401
  */
413
402
  setRecordFields({changedFields=[], fields, model, record, silent=false, useOriginalData=false}) {
414
- let {fieldsMap} = model,
403
+ let {fieldsMap, trackModifiedFields} = model,
415
404
  fieldExists, oldValue;
416
405
 
406
+ if (!trackModifiedFields && useOriginalData) {
407
+ return
408
+ }
409
+
417
410
  Object.entries(fields).forEach(([key, value]) => {
418
411
  fieldExists = fieldsMap.has(key);
419
412
 
@@ -435,8 +428,8 @@ class RecordFactory extends Base {
435
428
  if (!Neo.isEqual(oldValue, value)) {
436
429
  instance.setRecordData({fieldName: key, model, record, useOriginalData, value});
437
430
 
438
- if (!useOriginalData) {
439
- record._isModified = true
431
+ if (!trackModifiedFields && !useOriginalData) {
432
+ record[isModifiedSymbol] = true
440
433
  }
441
434
 
442
435
  changedFields.push({name: key, oldValue, value})
@@ -444,7 +437,7 @@ class RecordFactory extends Base {
444
437
  }
445
438
  });
446
439
 
447
- if (!silent && Object.keys(changedFields).length > 0) {
440
+ if (!silent && !useOriginalData && Object.keys(changedFields).length > 0) {
448
441
  Neo.get(model.storeId)?.onRecordChange({fields: changedFields, model, record})
449
442
  }
450
443
  }
package/src/grid/View.mjs CHANGED
@@ -72,6 +72,10 @@ class GridView extends Component {
72
72
  * @protected
73
73
  */
74
74
  columnPositions_: [],
75
+ /**
76
+ * @member {Boolean} highlightModifiedCells_=false
77
+ */
78
+ highlightModifiedCells_: false,
75
79
  /**
76
80
  * @member {Boolean} isScrolling_=false
77
81
  */
@@ -432,6 +436,12 @@ class GridView extends Component {
432
436
  cellCls.push('neo-' + column.cellAlign)
433
437
  }
434
438
 
439
+ if (me.highlightModifiedCells) {
440
+ if (record.isModifiedField(dataField)) {
441
+ cellCls.push('neo-is-modified')
442
+ }
443
+ }
444
+
435
445
  if (!cellId) {
436
446
  cellId = me.getCellId(record, column.dataField)
437
447
  }
@@ -803,9 +813,9 @@ class GridView extends Component {
803
813
  fieldNames = fields.map(field => field.name),
804
814
  needsUpdate = false,
805
815
  {gridContainer} = me,
806
- {selectionModel} = gridContainer,
816
+ {selectionModel} = gridContainer.view,
807
817
  {vdom} = me,
808
- cellId, cellNode, column, index, scope;
818
+ cellId, cellNode, cellStyle, cellVdom, column, index;
809
819
 
810
820
  if (fieldNames.includes(me.colspanField)) {
811
821
  index = me.store.indexOf(record);
@@ -821,14 +831,22 @@ class GridView extends Component {
821
831
  cellId = me.getCellId(record, field.name);
822
832
  cellNode = VDomUtil.find(vdom, cellId);
823
833
 
824
- // the vdom might not exist yet => nothing to do in this case
834
+ // The vdom might not exist yet => nothing to do in this case
825
835
  if (cellNode?.vdom) {
836
+ cellStyle = cellNode.vdom.style;
826
837
  column = me.getColumn(field.name);
827
838
  index = cellNode.index;
839
+ cellVdom = me.applyRendererOutput({cellId, column, gridContainer, index, record});
828
840
  needsUpdate = true;
829
- scope = column.rendererScope || gridContainer;
830
841
 
831
- cellNode.parentNode.cn[index] = me.applyRendererOutput({cellId, column, gridContainer, index, record})
842
+ // The cell-positioning logic happens outside applyRendererOutput()
843
+ // We need to preserve these styles
844
+ Object.assign(cellVdom.style, {
845
+ left : cellStyle.left,
846
+ width: cellStyle.width
847
+ });
848
+
849
+ cellNode.parentNode.cn[index] = cellVdom
832
850
  }
833
851
  }
834
852
  })
@@ -154,7 +154,7 @@ class View extends Component {
154
154
  cellCls.push('neo-' + column.cellAlign)
155
155
  }
156
156
 
157
- if (me.highlightModifiedCells && me.store.model.trackModifiedFields) {
157
+ if (me.highlightModifiedCells) {
158
158
  if (record.isModifiedField(dataField)) {
159
159
  cellCls.push('neo-is-modified')
160
160
  }