neo.mjs 10.4.0 → 10.5.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.
- package/.github/RELEASE_NOTES/v10.4.1.md +16 -0
- package/.github/RELEASE_NOTES/v10.5.0.md +51 -0
- package/ServiceWorker.mjs +2 -2
- package/apps/finance/view/ViewportController.mjs +1 -1
- package/apps/form/view/SideNavList.mjs +1 -1
- package/apps/portal/index.html +1 -1
- package/apps/portal/view/home/FooterContainer.mjs +1 -1
- package/apps/realworld2/view/article/PreviewList.mjs +1 -1
- package/docs/app/view/classdetails/MainContainerController.mjs +1 -1
- package/docs/app/view/classdetails/MembersList.mjs +5 -5
- package/examples/grid/bigData/ControlsContainer.mjs +6 -6
- package/examples/grid/bigData/MainStore.mjs +4 -4
- package/examples/grid/cellEditing/MainContainerStateProvider.mjs +1 -1
- package/examples/grid/nestedRecordFields/EditUserDialog.mjs +1 -1
- package/examples/grid/nestedRecordFields/ViewportController.mjs +1 -1
- package/examples/table/cellEditing/MainContainerStateProvider.mjs +1 -1
- package/examples/table/nestedRecordFields/EditUserDialog.mjs +1 -1
- package/examples/table/nestedRecordFields/ViewportStateProvider.mjs +1 -1
- package/examples/tableStore/MainContainer.mjs +2 -2
- package/package.json +1 -1
- package/src/DefaultConfig.mjs +2 -2
- package/src/collection/Base.mjs +24 -6
- package/src/component/Gallery.mjs +3 -3
- package/src/component/Helix.mjs +4 -4
- package/src/component/wrapper/GoogleMaps.mjs +1 -1
- package/src/data/Store.mjs +105 -12
- package/src/draggable/list/DragZone.mjs +1 -1
- package/src/draggable/tree/DragZone.mjs +1 -1
- package/src/form/field/ComboBox.mjs +10 -2
- package/src/grid/Body.mjs +99 -14
- package/src/grid/Container.mjs +13 -8
- package/src/grid/VerticalScrollbar.mjs +7 -6
- package/src/grid/column/Component.mjs +7 -2
- package/src/list/Base.mjs +1 -1
- package/src/selection/TreeAccordionModel.mjs +9 -3
- package/src/table/Body.mjs +29 -2
- package/src/tree/Accordion.mjs +2 -2
- package/src/tree/List.mjs +3 -3
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# Neo.mjs v10.4.1 Release Notes
|
|
2
|
+
|
|
3
|
+
## Highlights
|
|
4
|
+
|
|
5
|
+
This patch release polishes the "instant preview" feature for grids introduced in v10.4.0, ensuring the grid's scrollbar and accessibility properties are correctly sized from the very first paint when loading large datasets. The non-buffered `table.Body` also receives a major performance boost for clearing data.
|
|
6
|
+
|
|
7
|
+
## Enhancements & Bug Fixes
|
|
8
|
+
|
|
9
|
+
### 1. Polished "Instant Preview" for Large Datasets in Grids
|
|
10
|
+
The chunked loading mechanism for stores and grids has been refined. When adding a large dataset, the initial `load` event now carries the final total record count. The `grid.Container` uses this information immediately to set the correct `aria-rowcount` and to size the vertical scrollbar accurately, preventing layout shifts and providing a more stable and accessible user experience from the very first render.
|
|
11
|
+
|
|
12
|
+
### 2. Standardized Internal Store `load` Event
|
|
13
|
+
To support the "instant preview" refinements, the `data.Store` `load` event payload has been standardized internally. It now consistently uses an object containing an `items` property (e.g., `{items: [...]}`). Framework components have been updated to use this new signature.
|
|
14
|
+
|
|
15
|
+
### 3. Performance Fast Path for `table.Body`
|
|
16
|
+
The non-buffered `table.Body` is now significantly faster when clearing its data. It uses the same "fast path" optimization as `grid.Body`, directly clearing the DOM instead of processing a large and unnecessary VDOM diff when an empty dataset is loaded. This aligns the performance of the two components.
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# Neo.mjs v10.5.0 Release Notes
|
|
2
|
+
|
|
3
|
+
## Major Performance Enhancements for Grids and Data Handling
|
|
4
|
+
|
|
5
|
+
This release introduces a groundbreaking set of performance optimizations for `Neo.data.Store` and `Neo.grid.Container`, fundamentally transforming how large datasets are handled within Neo.mjs applications. These enhancements dramatically reduce initial load times, improve UI responsiveness, and resolve critical VDom reconciliation issues, making it feasible to work with millions of data points with unprecedented fluidity.
|
|
6
|
+
|
|
7
|
+
### Key Highlights:
|
|
8
|
+
|
|
9
|
+
* **Lazy Record Instantiation (GET-driven approach):**
|
|
10
|
+
* `Neo.data.Store` now defers the creation of `Neo.data.Record` instances. Raw data objects are stored directly, and `Record` instances are only created on-demand when an item is explicitly accessed (e.g., via `store.get()`, `store.getAt()`, or during VDom rendering of visible rows).
|
|
11
|
+
* **Impact:** This eliminates the massive upfront cost of instantiating millions of `Record` objects, leading to **up to 97% reduction in initial data processing time** for large datasets.
|
|
12
|
+
|
|
13
|
+
* **Configurable Data Chunking for UI Responsiveness:**
|
|
14
|
+
* Introduced an `initialChunkSize` config in `Neo.data.Store` (default `0`). When enabled, `Store.add()` processes data in chunks, significantly mitigating UI freezes during synchronous loading of extremely large datasets (e.g., 1,000,000+ rows).
|
|
15
|
+
* **Impact:** Provides a smoother perceived user experience during initial data loads, even for datasets that would otherwise cause multi-second UI blocks.
|
|
16
|
+
|
|
17
|
+
* **Robust Component ID Management for VDom Stability:**
|
|
18
|
+
* Resolved critical VDom reconciliation errors (e.g., `RangeError`, infinite loops) that could occur with component columns when chunking was active. `Neo.grid.column.Component` now intelligently generates unique IDs based on the store's chunking state, ensuring VDom stability.
|
|
19
|
+
* **Impact:** Eliminates a major source of instability and crashes when using component columns with large, dynamically loaded datasets.
|
|
20
|
+
|
|
21
|
+
* **Automated Component Instance Cleanup:**
|
|
22
|
+
* Enhanced `Neo.grid.Body` with sophisticated component instance management. Components are now automatically destroyed when the grid body is destroyed, the store is cleared, the store changes, or when they scroll out of view after a chunked load.
|
|
23
|
+
* **Impact:** Reduces memory overhead and improves long-term performance by preventing the accumulation of unused component instances.
|
|
24
|
+
|
|
25
|
+
* **GPU-Accelerated Vertical Scrolling (`translate3d`):**
|
|
26
|
+
* Grid rows now utilize `transform: translate3d(0px, Ypx, 0px)` for vertical positioning. This hints to the browser to promote rows to their own composite layers, offloading rendering to the GPU.
|
|
27
|
+
* **Impact:** Leads to noticeably smoother and more fluid vertical scrolling, especially during rapid movements through large grids.
|
|
28
|
+
|
|
29
|
+
### Other Enhancements:
|
|
30
|
+
|
|
31
|
+
* **ComboBox Initial Display Fix:** Resolved an issue where `Neo.form.field.ComboBox` instances would appear blank on initial load when backed by lazily instantiated stores. The `updateInputValueFromValue` method now correctly displays values from raw data or `Record` instances.
|
|
32
|
+
* **Enhanced BigData Grid Example:** The `examples/grid/bigData` demo has been updated to include control options for up to 200 columns, allowing for testing with up to 20,000,000 cells. This provides a more extreme and comprehensive benchmark for grid performance.
|
|
33
|
+
|
|
34
|
+
These collective improvements mark a significant leap forward in Neo.mjs's capability to handle and display massive amounts of data with high performance and a superior user experience.
|
|
35
|
+
|
|
36
|
+
---
|
|
37
|
+
|
|
38
|
+
### Performance Benchmarks (from `examples/grid/bigData`):
|
|
39
|
+
|
|
40
|
+
| Scenario | Before (Eager Instantiation) | After (Lazy Instantiation, with chunking) | After (Lazy Instantiation, no chunking) |
|
|
41
|
+
| :------------------------------------- | :--------------------------- | :---------------------------------------- | :-------------------------------------- |
|
|
42
|
+
| 1,000 rows, 50 columns | 49ms (Record creation) | 2ms (Data gen + add) | 2ms (Data gen + add) |
|
|
43
|
+
| 50,000 rows, 50 columns | 2391ms (Record creation) | 1252ms (Data gen + add) | 93ms (Data gen + add) |
|
|
44
|
+
| 50,000 rows, 100 columns | 4377ms (Record creation) | 1289ms (Data gen + add) | 95ms (Data gen + add) |
|
|
45
|
+
| 100,000 rows, 50 columns | N/A (too slow) | 1299ms (Data gen + add) | 156ms (Data gen + add) |
|
|
46
|
+
| 100,000 rows, 200 columns | N/A (too slow) | 1427ms (Data gen + add) | 174ms (Data gen + add) |
|
|
47
|
+
|
|
48
|
+
*Note: "Record creation" refers to the time taken for `Neo.data.Record` instantiation. "Data gen + add" refers to the time taken to generate raw data and add it to the store's collection.*
|
|
49
|
+
|
|
50
|
+
**Important Note on Chunking:**
|
|
51
|
+
The introduction of lazy record instantiation significantly reduces the need for chunking in most common use cases, as the initial data loading is now extremely fast even for large datasets. However, Neo.mjs still optionally supports data chunking via the `initialChunkSize` config for scenarios involving truly massive synchronous data additions where mitigating UI freezes is paramount.
|
package/ServiceWorker.mjs
CHANGED
|
@@ -35,7 +35,7 @@ class ViewportController extends Controller {
|
|
|
35
35
|
companiesStore = me.getStore('companies'),
|
|
36
36
|
items = [];
|
|
37
37
|
|
|
38
|
-
companiesStore.
|
|
38
|
+
companiesStore.forEach(record => {
|
|
39
39
|
items.push({
|
|
40
40
|
symbol: record.symbol,
|
|
41
41
|
value : Math.random() * 1000
|
package/apps/portal/index.html
CHANGED
|
@@ -128,7 +128,7 @@ class MembersList extends Base {
|
|
|
128
128
|
* @returns {Object} vdom
|
|
129
129
|
*/
|
|
130
130
|
applyConfigsHeader(store, vdom) {
|
|
131
|
-
if (store.
|
|
131
|
+
if (store.getAt(0)?.kind === 'member') {
|
|
132
132
|
vdom.cn.push({
|
|
133
133
|
// scrolling placeholder
|
|
134
134
|
}, {
|
|
@@ -152,7 +152,7 @@ class MembersList extends Base {
|
|
|
152
152
|
applyEventsHeader(item, index, store, vdom) {
|
|
153
153
|
if (
|
|
154
154
|
item.kind === 'event' &&
|
|
155
|
-
store.
|
|
155
|
+
store.getAt(index -1)?.kind !== 'event'
|
|
156
156
|
) {
|
|
157
157
|
vdom.cn.push({
|
|
158
158
|
// scrolling placeholder
|
|
@@ -179,8 +179,8 @@ class MembersList extends Base {
|
|
|
179
179
|
if (
|
|
180
180
|
item.kind === 'function' &&
|
|
181
181
|
(
|
|
182
|
-
!store.
|
|
183
|
-
store.
|
|
182
|
+
!store.getAt(index -1) || (
|
|
183
|
+
store.getAt(index -1)?.kind !== 'function'
|
|
184
184
|
)
|
|
185
185
|
)
|
|
186
186
|
) {
|
|
@@ -211,7 +211,7 @@ class MembersList extends Base {
|
|
|
211
211
|
vdom.cn = [];
|
|
212
212
|
vdom = me.applyConfigsHeader(me.store, vdom);
|
|
213
213
|
|
|
214
|
-
me.store.
|
|
214
|
+
me.store.forEach((item, index) => {
|
|
215
215
|
vdom = me.applyEventsHeader( item, index, me.store, vdom);
|
|
216
216
|
vdom = me.applyMethodsHeader(item, index, me.store, vdom);
|
|
217
217
|
|
|
@@ -17,10 +17,10 @@ class ControlsContainer extends Container {
|
|
|
17
17
|
* @static
|
|
18
18
|
*/
|
|
19
19
|
static delayable = {
|
|
20
|
-
onAmountColumnsChange : {type: 'buffer', timer:
|
|
21
|
-
onAmountRowsChange : {type: 'buffer', timer:
|
|
22
|
-
onBufferColumnRangeChange: {type: 'buffer', timer:
|
|
23
|
-
onBufferRowRangeChange : {type: 'buffer', timer:
|
|
20
|
+
onAmountColumnsChange : {type: 'buffer', timer: 15},
|
|
21
|
+
onAmountRowsChange : {type: 'buffer', timer: 15},
|
|
22
|
+
onBufferColumnRangeChange: {type: 'buffer', timer: 15},
|
|
23
|
+
onBufferRowRangeChange : {type: 'buffer', timer: 15}
|
|
24
24
|
}
|
|
25
25
|
|
|
26
26
|
static config = {
|
|
@@ -69,14 +69,14 @@ class ControlsContainer extends Container {
|
|
|
69
69
|
labelText : 'Amount Rows',
|
|
70
70
|
labelWidth: 120,
|
|
71
71
|
listeners : {change: 'up.onAmountRowsChange'},
|
|
72
|
-
store : ['1000', '5000', '10000', '20000', '50000'],
|
|
72
|
+
store : ['1000', '5000', '10000', '20000', '50000', '100000'],
|
|
73
73
|
value : '1000',
|
|
74
74
|
width : 200
|
|
75
75
|
}, {
|
|
76
76
|
labelText : 'Amount Columns',
|
|
77
77
|
labelWidth: 145,
|
|
78
78
|
listeners : {change: 'up.onAmountColumnsChange'},
|
|
79
|
-
store : ['10', '25', '50', '75', '100'],
|
|
79
|
+
store : ['10', '25', '50', '75', '100', '200'],
|
|
80
80
|
value : '50',
|
|
81
81
|
width : 200
|
|
82
82
|
}, {
|
|
@@ -74,7 +74,7 @@ class MainStore extends Store {
|
|
|
74
74
|
|
|
75
75
|
me.model.amountColumns = value;
|
|
76
76
|
|
|
77
|
-
console.log('Start
|
|
77
|
+
console.log('Start generating data and adding to collection');
|
|
78
78
|
|
|
79
79
|
if (me.items?.length > 0) {
|
|
80
80
|
me.clear()
|
|
@@ -82,7 +82,7 @@ class MainStore extends Store {
|
|
|
82
82
|
|
|
83
83
|
me.add(data);
|
|
84
84
|
|
|
85
|
-
console.log(`
|
|
85
|
+
console.log(`Data generation and collection add total time: ${Math.round(performance.now() - start)}ms`)
|
|
86
86
|
}
|
|
87
87
|
}
|
|
88
88
|
|
|
@@ -97,7 +97,7 @@ class MainStore extends Store {
|
|
|
97
97
|
data = me.generateData(value, me.amountColumns),
|
|
98
98
|
start = performance.now();
|
|
99
99
|
|
|
100
|
-
console.log('Start
|
|
100
|
+
console.log('Start generating data and adding to collection');
|
|
101
101
|
|
|
102
102
|
if (me.items?.length > 0) {
|
|
103
103
|
me.clear()
|
|
@@ -105,7 +105,7 @@ class MainStore extends Store {
|
|
|
105
105
|
|
|
106
106
|
me.add(data);
|
|
107
107
|
|
|
108
|
-
console.log(`
|
|
108
|
+
console.log(`Data generation and collection add total time: ${Math.round(performance.now() - start)}ms`)
|
|
109
109
|
}
|
|
110
110
|
|
|
111
111
|
/**
|
|
@@ -47,7 +47,7 @@ class MainContainerStateProvider extends StateProvider {
|
|
|
47
47
|
|
|
48
48
|
// if the main table store is already loaded, the country field renderer had no data
|
|
49
49
|
if (mainStore.getCount() > 0) {
|
|
50
|
-
mainStore.
|
|
50
|
+
mainStore.forEach(record => {
|
|
51
51
|
country = record.country;
|
|
52
52
|
|
|
53
53
|
// hack resetting the current value to get a new record change
|
|
@@ -163,7 +163,7 @@ class EditUserDialog extends Dialog {
|
|
|
163
163
|
me.record.set({annotations: {selected: false}})
|
|
164
164
|
} else {
|
|
165
165
|
// Assuming we want to support a single row selection
|
|
166
|
-
store.
|
|
166
|
+
store.forEach(record => {
|
|
167
167
|
record.set({annotations: {
|
|
168
168
|
selected: record === me.record ? data.value : false
|
|
169
169
|
}})
|
|
@@ -71,7 +71,7 @@ class ViewportController extends Component {
|
|
|
71
71
|
|
|
72
72
|
// if the main table store is already loaded, the country field renderer had no data
|
|
73
73
|
if (mainStore.getCount() > 0) {
|
|
74
|
-
mainStore.
|
|
74
|
+
mainStore.forEach(record => {
|
|
75
75
|
country = record.country;
|
|
76
76
|
|
|
77
77
|
// hack resetting the current value to get a new record change
|
|
@@ -47,7 +47,7 @@ class MainContainerStateProvider extends StateProvider {
|
|
|
47
47
|
|
|
48
48
|
// if the main table store is already loaded, the country field renderer had no data
|
|
49
49
|
if (mainStore.getCount() > 0) {
|
|
50
|
-
mainStore.
|
|
50
|
+
mainStore.forEach(record => {
|
|
51
51
|
country = record.country;
|
|
52
52
|
|
|
53
53
|
// hack resetting the current value to get a new record change
|
|
@@ -145,7 +145,7 @@ class EditUserDialog extends Dialog {
|
|
|
145
145
|
me.record.set({annotations: {selected: false}})
|
|
146
146
|
} else {
|
|
147
147
|
// Assuming we want to support a single row selection
|
|
148
|
-
store.
|
|
148
|
+
store.forEach(record => {
|
|
149
149
|
record.set({annotations: {
|
|
150
150
|
selected: record === me.record ? data.value : false
|
|
151
151
|
}})
|
|
@@ -47,7 +47,7 @@ class ViewportStateProvider extends StateProvider {
|
|
|
47
47
|
|
|
48
48
|
// if the main table store is already loaded, the country field renderer had no data
|
|
49
49
|
if (mainStore.getCount() > 0) {
|
|
50
|
-
mainStore.
|
|
50
|
+
mainStore.forEach(record => {
|
|
51
51
|
country = record.country;
|
|
52
52
|
|
|
53
53
|
// hack resetting the current value to get a new record change
|
|
@@ -39,7 +39,7 @@ class MainContainer extends Viewport {
|
|
|
39
39
|
handler() {
|
|
40
40
|
let tabContainer = Neo.getComponent('myTableStoreContainer'),
|
|
41
41
|
store = tabContainer.store,
|
|
42
|
-
record = store.
|
|
42
|
+
record = store.getAt(0);
|
|
43
43
|
|
|
44
44
|
record.firstname = record.firstname + '<span style="color:red;"> Foo</span>';
|
|
45
45
|
}
|
|
@@ -61,7 +61,7 @@ class MainContainer extends Viewport {
|
|
|
61
61
|
|
|
62
62
|
for (; j < repeats; j++) {
|
|
63
63
|
for (i=0; i < countRecords; i++) {
|
|
64
|
-
record = store.
|
|
64
|
+
record = store.getAt(i);
|
|
65
65
|
|
|
66
66
|
Object.entries(record.toJSON()).forEach(([field, value]) => {
|
|
67
67
|
if (field !== 'githubId') {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "neo.mjs",
|
|
3
|
-
"version": "10.
|
|
3
|
+
"version": "10.5.0",
|
|
4
4
|
"description": "Neo.mjs: The multi-threaded UI framework for building ultra-fast, desktop-like web applications with uncompromised responsiveness, inherent security, and a transpilation-free dev mode.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"repository": {
|
package/src/DefaultConfig.mjs
CHANGED
|
@@ -299,12 +299,12 @@ const DefaultConfig = {
|
|
|
299
299
|
useVdomWorker: true,
|
|
300
300
|
/**
|
|
301
301
|
* buildScripts/injectPackageVersion.mjs will update this value
|
|
302
|
-
* @default '10.
|
|
302
|
+
* @default '10.5.0'
|
|
303
303
|
* @memberOf! module:Neo
|
|
304
304
|
* @name config.version
|
|
305
305
|
* @type String
|
|
306
306
|
*/
|
|
307
|
-
version: '10.
|
|
307
|
+
version: '10.5.0'
|
|
308
308
|
};
|
|
309
309
|
|
|
310
310
|
Object.assign(DefaultConfig, {
|
package/src/collection/Base.mjs
CHANGED
|
@@ -849,7 +849,7 @@ class Collection extends Base {
|
|
|
849
849
|
* @returns {Object}
|
|
850
850
|
*/
|
|
851
851
|
first() {
|
|
852
|
-
return this.
|
|
852
|
+
return this.getAt(0)
|
|
853
853
|
}
|
|
854
854
|
|
|
855
855
|
/**
|
|
@@ -1041,7 +1041,7 @@ class Collection extends Base {
|
|
|
1041
1041
|
* @returns {Object}
|
|
1042
1042
|
*/
|
|
1043
1043
|
last() {
|
|
1044
|
-
return this.
|
|
1044
|
+
return this.getAt(this.count -1)
|
|
1045
1045
|
}
|
|
1046
1046
|
|
|
1047
1047
|
/**
|
|
@@ -1171,6 +1171,15 @@ class Collection extends Base {
|
|
|
1171
1171
|
return this._items.some(...args)
|
|
1172
1172
|
}
|
|
1173
1173
|
|
|
1174
|
+
/**
|
|
1175
|
+
* Executes a provided function once for each array element.
|
|
1176
|
+
* @param {Function} fn The function to execute for each element.
|
|
1177
|
+
* @param {Object} [scope] Value to use as `this` when executing `fn`.
|
|
1178
|
+
*/
|
|
1179
|
+
forEach(fn, scope) {
|
|
1180
|
+
this._items.forEach(fn, scope);
|
|
1181
|
+
}
|
|
1182
|
+
|
|
1174
1183
|
/**
|
|
1175
1184
|
* Removes items from and/or adds items to this collection
|
|
1176
1185
|
* If the toRemoveArray is used, then the index is not used for removing, the entries are found by key and removed from where they are.
|
|
@@ -1214,10 +1223,19 @@ class Collection extends Base {
|
|
|
1214
1223
|
}
|
|
1215
1224
|
}
|
|
1216
1225
|
} else if (removeCountAtIndex && removeCountAtIndex > 0) {
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1226
|
+
// Optimization: If this is a full clear operation, use map.clear()
|
|
1227
|
+
if (index === 0 && removeCountAtIndex === me.count) {
|
|
1228
|
+
removedItems = items;
|
|
1229
|
+
me._items = [];
|
|
1230
|
+
map.clear()
|
|
1231
|
+
} else {
|
|
1232
|
+
removedItems = items.splice(index, removeCountAtIndex);
|
|
1233
|
+
|
|
1234
|
+
// For partial removals, iterate and delete individual items from the map
|
|
1235
|
+
removedItems.forEach(e => {
|
|
1236
|
+
map.delete(e[keyProperty])
|
|
1237
|
+
})
|
|
1238
|
+
}
|
|
1221
1239
|
}
|
|
1222
1240
|
|
|
1223
1241
|
if (toAddArray && (len = toAddArray.length) > 0) {
|
|
@@ -307,7 +307,7 @@ class Gallery extends Component {
|
|
|
307
307
|
if (Neo.isBoolean(oldValue)) {
|
|
308
308
|
let me = this,
|
|
309
309
|
i = 0,
|
|
310
|
-
len = Math.min(me.maxItems, me.store.
|
|
310
|
+
len = Math.min(me.maxItems, me.store.count),
|
|
311
311
|
view = me.getItemsRoot();
|
|
312
312
|
|
|
313
313
|
if (me.vnodeInitialized) {
|
|
@@ -424,7 +424,7 @@ class Gallery extends Component {
|
|
|
424
424
|
vdom = me.vdom,
|
|
425
425
|
itemsRoot = me.getItemsRoot(),
|
|
426
426
|
i = startIndex || 0,
|
|
427
|
-
len = Math.min(me.maxItems, me.store.
|
|
427
|
+
len = Math.min(me.maxItems, me.store.count),
|
|
428
428
|
amountColumns, item, vdomItem;
|
|
429
429
|
|
|
430
430
|
if (orderByRow) {
|
|
@@ -432,7 +432,7 @@ class Gallery extends Component {
|
|
|
432
432
|
}
|
|
433
433
|
|
|
434
434
|
for (; i < len; i++) {
|
|
435
|
-
item = me.store.
|
|
435
|
+
item = me.store.getAt(i);
|
|
436
436
|
vdomItem = me.createItem(me.itemTpl, item, i);
|
|
437
437
|
|
|
438
438
|
vdomItem. style = vdomItem.style || {};
|
package/src/component/Helix.mjs
CHANGED
|
@@ -581,7 +581,7 @@ class Helix extends Component {
|
|
|
581
581
|
{deltaY, itemAngle, matrix, radius, rotationAngle, translateX, translateY, translateZ, vdom} = me,
|
|
582
582
|
group = me.getItemsRoot(),
|
|
583
583
|
i = startIndex || 0,
|
|
584
|
-
len = Math.min(me.maxItems, me.store.
|
|
584
|
+
len = Math.min(me.maxItems, me.store.count),
|
|
585
585
|
angle, item, matrixItems, transformStyle, vdomItem, c, s, x, y, z;
|
|
586
586
|
|
|
587
587
|
if (!me.mounted) {
|
|
@@ -592,7 +592,7 @@ class Helix extends Component {
|
|
|
592
592
|
}, me, {once: true})
|
|
593
593
|
} else {
|
|
594
594
|
for (; i < len; i++) {
|
|
595
|
-
item = me.store.
|
|
595
|
+
item = me.store.getAt(i);
|
|
596
596
|
|
|
597
597
|
angle = -rotationAngle + i * itemAngle;
|
|
598
598
|
|
|
@@ -952,7 +952,7 @@ class Helix extends Component {
|
|
|
952
952
|
}
|
|
953
953
|
|
|
954
954
|
for (; index < len; index++) {
|
|
955
|
-
item = me.store.
|
|
955
|
+
item = me.store.getAt(index);
|
|
956
956
|
vdomItem = vdom.cn[0].cn[0].cn[index];
|
|
957
957
|
|
|
958
958
|
angle = -rotationAngle + index * itemAngle;
|
|
@@ -1024,7 +1024,7 @@ class Helix extends Component {
|
|
|
1024
1024
|
for (; i < len; i++) {
|
|
1025
1025
|
deltas.push({
|
|
1026
1026
|
action: 'moveNode',
|
|
1027
|
-
id : me.getItemVnodeId(me.store.
|
|
1027
|
+
id : me.getItemVnodeId(me.store.getAt(i)[me.keyProperty]),
|
|
1028
1028
|
index : i,
|
|
1029
1029
|
parentId
|
|
1030
1030
|
})
|
package/src/data/Store.mjs
CHANGED
|
@@ -65,6 +65,11 @@ class Store extends Base {
|
|
|
65
65
|
* @reactive
|
|
66
66
|
*/
|
|
67
67
|
initialData_: null,
|
|
68
|
+
/**
|
|
69
|
+
* The initial chunk size for adding large datasets. Set to 0 to disable chunking.
|
|
70
|
+
* @member {Number} initialChunkSize=0
|
|
71
|
+
*/
|
|
72
|
+
initialChunkSize: 0,
|
|
68
73
|
/**
|
|
69
74
|
* @member {Boolean} isGrouped=false
|
|
70
75
|
*/
|
|
@@ -140,31 +145,37 @@ class Store extends Base {
|
|
|
140
145
|
*/
|
|
141
146
|
add(item) {
|
|
142
147
|
let items = Array.isArray(item) ? item : [item];
|
|
143
|
-
const threshold =
|
|
148
|
+
const threshold = this.initialChunkSize;
|
|
144
149
|
|
|
145
|
-
if (items.length > threshold) {
|
|
150
|
+
if (threshold > 0 && items.length > threshold) {
|
|
146
151
|
const me = this,
|
|
152
|
+
total = me.count + items.length,
|
|
147
153
|
chunk = items.splice(0, threshold);
|
|
148
154
|
|
|
149
|
-
|
|
150
|
-
|
|
155
|
+
me.chunkingTotal = total;
|
|
156
|
+
|
|
157
|
+
// 1. Add the first chunk. This fires 'mutate' -> 'load' (via onCollectionMutate)
|
|
158
|
+
// and triggers the initial grid render. The 'load' event will contain the final total count.
|
|
159
|
+
super.add(chunk); // Pass raw chunk directly
|
|
151
160
|
|
|
152
161
|
// 2. Suspend events to prevent the next 'add' from firing 'load'.
|
|
153
162
|
me.suspendEvents = true;
|
|
154
163
|
|
|
155
164
|
// 3. Add the rest of the items silently.
|
|
156
|
-
super.add(
|
|
165
|
+
super.add(items); // Pass raw items directly
|
|
157
166
|
|
|
158
167
|
// 4. Resume events.
|
|
159
168
|
me.suspendEvents = false;
|
|
160
169
|
|
|
161
|
-
// 5. Manually fire a final 'load' event to update the grid's scrollbar.
|
|
162
|
-
me.fire('load', me.items);
|
|
170
|
+
// 5. Manually fire a final 'load' event to update the grid's scrollbar and notify other listeners.
|
|
171
|
+
me.fire('load', {items: me.items, postChunkLoad: true, total: me.chunkingTotal});
|
|
172
|
+
|
|
173
|
+
delete me.chunkingTotal;
|
|
163
174
|
|
|
164
|
-
return me.count
|
|
175
|
+
return me.count;
|
|
165
176
|
}
|
|
166
177
|
|
|
167
|
-
return super.add(
|
|
178
|
+
return super.add(item); // Pass raw item directly
|
|
168
179
|
}
|
|
169
180
|
|
|
170
181
|
/**
|
|
@@ -294,7 +305,7 @@ class Store extends Base {
|
|
|
294
305
|
if (value) {
|
|
295
306
|
this.isLoading = true;
|
|
296
307
|
|
|
297
|
-
value = this.createRecord(value)
|
|
308
|
+
// value = this.createRecord(value)
|
|
298
309
|
}
|
|
299
310
|
|
|
300
311
|
return value
|
|
@@ -357,6 +368,88 @@ class Store extends Base {
|
|
|
357
368
|
return isArray ? config : config[0]
|
|
358
369
|
}
|
|
359
370
|
|
|
371
|
+
/**
|
|
372
|
+
* Overrides collection.Base:find() to ensure the returned item(s) are Record instances.
|
|
373
|
+
* @param {Object|String} property
|
|
374
|
+
* @param {String|Number} [value] Only required in case the first param is a string
|
|
375
|
+
* @param {Boolean} returnFirstMatch=false
|
|
376
|
+
* @returns {Object|Object[]|null}
|
|
377
|
+
*/
|
|
378
|
+
find(property, value, returnFirstMatch=false) {
|
|
379
|
+
const result = super.find(property, value, returnFirstMatch);
|
|
380
|
+
|
|
381
|
+
if (returnFirstMatch) {
|
|
382
|
+
return result ? this.get(result[this.keyProperty]) : null;
|
|
383
|
+
} else {
|
|
384
|
+
return result.map(item => this.get(item[this.keyProperty]));
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
/**
|
|
389
|
+
* Overrides collection.Base:findBy() to ensure the returned item(s) are Record instances.
|
|
390
|
+
* @param {function} fn The function to run for each item inside the start-end range. Return true for a match.
|
|
391
|
+
* @param {Object} scope=this The scope in which the passed function gets executed
|
|
392
|
+
* @param {Number} start=0 The start index
|
|
393
|
+
* @param {Number} end=this.count The end index (up to, last value excluded)
|
|
394
|
+
* @returns {Array}
|
|
395
|
+
*/
|
|
396
|
+
findBy(fn, scope=this, start=0, end=this.count) {
|
|
397
|
+
const result = super.findBy(fn, scope, start, end);
|
|
398
|
+
return result.map(item => this.get(item[this.keyProperty]));
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
/**
|
|
402
|
+
* Overrides collection.Base:forEach() to ensure the iterated item is a Record instance.
|
|
403
|
+
* @param {Function} fn The function to execute for each record.
|
|
404
|
+
* @param {Object} [scope] Value to use as `this` when executing `fn`.
|
|
405
|
+
*/
|
|
406
|
+
forEach(fn, scope) {
|
|
407
|
+
const me = this;
|
|
408
|
+
for (let i = 0; i < me.count; i++) {
|
|
409
|
+
fn.call(scope || me, me.getAt(i), i, me.items);
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
/**
|
|
414
|
+
* Overrides collection.Base:get() to ensure the returned item is a Record instance.
|
|
415
|
+
* @param {Number|String} key
|
|
416
|
+
* @returns {Object|null}
|
|
417
|
+
*/
|
|
418
|
+
get(key) {
|
|
419
|
+
let item = super.get(key); // Get item from Collection.Base (could be raw data)
|
|
420
|
+
|
|
421
|
+
if (item && !RecordFactory.isRecord(item)) {
|
|
422
|
+
const record = RecordFactory.createRecord(this.model, item);
|
|
423
|
+
// Replace the raw data with the record instance in the collection
|
|
424
|
+
this.map.set(key, record);
|
|
425
|
+
const index = this._items.indexOf(item); // Find the index of the raw item
|
|
426
|
+
if (index !== -1) {
|
|
427
|
+
this._items[index] = record; // Replace it with the record
|
|
428
|
+
}
|
|
429
|
+
return record;
|
|
430
|
+
}
|
|
431
|
+
return item; // Already a record or null
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
/**
|
|
435
|
+
* Overrides collection.Base:getAt() to ensure the returned item is a Record instance.
|
|
436
|
+
* @param {Number} index
|
|
437
|
+
* @returns {Object|undefined}
|
|
438
|
+
*/
|
|
439
|
+
getAt(index) {
|
|
440
|
+
let item = super.getAt(index); // Get item from Collection.Base (could be raw data)
|
|
441
|
+
|
|
442
|
+
if (item && !RecordFactory.isRecord(item)) {
|
|
443
|
+
const record = RecordFactory.createRecord(this.model, item);
|
|
444
|
+
// Replace the raw data with the record instance in the collection
|
|
445
|
+
this._items[index] = record;
|
|
446
|
+
// Also update the map, as the key might be derived from the item
|
|
447
|
+
this.map.set(record[this.keyProperty], record);
|
|
448
|
+
return record;
|
|
449
|
+
}
|
|
450
|
+
return item; // Already a record or undefined
|
|
451
|
+
}
|
|
452
|
+
|
|
360
453
|
/**
|
|
361
454
|
* @returns {String}
|
|
362
455
|
*/
|
|
@@ -447,7 +540,7 @@ class Store extends Base {
|
|
|
447
540
|
let me = this;
|
|
448
541
|
|
|
449
542
|
if (me.isConstructed && !me.isLoading) {
|
|
450
|
-
me.fire('load', me.items)
|
|
543
|
+
me.fire('load', {items: me.items, total: me.chunkingTotal});
|
|
451
544
|
}
|
|
452
545
|
}
|
|
453
546
|
|
|
@@ -478,7 +571,7 @@ class Store extends Base {
|
|
|
478
571
|
// => break the sync flow to ensure potential listeners got applied
|
|
479
572
|
Promise.resolve().then(() => {
|
|
480
573
|
if (me.isLoaded) {
|
|
481
|
-
me.fire('load', me.items)
|
|
574
|
+
me.fire('load', {items: me.items})
|
|
482
575
|
} else if (me.autoLoad) {
|
|
483
576
|
me.load()
|
|
484
577
|
}
|
|
@@ -695,7 +695,7 @@ class ComboBox extends Picker {
|
|
|
695
695
|
updateInputValueFromValue(value) {
|
|
696
696
|
let inputValue = null;
|
|
697
697
|
|
|
698
|
-
if (Neo.isRecord(value)) {
|
|
698
|
+
if (Neo.isObject(value) || Neo.isRecord(value)) {
|
|
699
699
|
inputValue = value[this.displayField]
|
|
700
700
|
}
|
|
701
701
|
|
|
@@ -745,7 +745,15 @@ class ComboBox extends Picker {
|
|
|
745
745
|
if (me.typeAhead) {
|
|
746
746
|
if (!me.value && value?.length > 0) {
|
|
747
747
|
const search = value.toLocaleLowerCase();
|
|
748
|
-
match =
|
|
748
|
+
let match = null;
|
|
749
|
+
|
|
750
|
+
for (let i = 0; i < store.count; i++) {
|
|
751
|
+
const r = store.getAt(i);
|
|
752
|
+
if (r[displayField]?.toLowerCase?.()?.startsWith(search)) {
|
|
753
|
+
match = r;
|
|
754
|
+
break;
|
|
755
|
+
}
|
|
756
|
+
}
|
|
749
757
|
|
|
750
758
|
if (match && inputHintEl) {
|
|
751
759
|
inputHintEl.value = value + match[displayField].substr(value.length);
|
package/src/grid/Body.mjs
CHANGED
|
@@ -181,6 +181,17 @@ class GridBody extends Component {
|
|
|
181
181
|
]}
|
|
182
182
|
}
|
|
183
183
|
|
|
184
|
+
/**
|
|
185
|
+
* Internal flag to adopt to store.add() passing an initial chunk.
|
|
186
|
+
* @member {Number} #initialChunkSize=0
|
|
187
|
+
*/
|
|
188
|
+
#initialChunkSize = 0
|
|
189
|
+
/**
|
|
190
|
+
* Internal flag to adopt to store.add() passing an initial chunk.
|
|
191
|
+
* @member {Number} #initialChunkSize=0
|
|
192
|
+
*/
|
|
193
|
+
#initialTotalSize = 0
|
|
194
|
+
|
|
184
195
|
/**
|
|
185
196
|
* @member {String[]} selectedCells
|
|
186
197
|
*/
|
|
@@ -427,6 +438,11 @@ class GridBody extends Component {
|
|
|
427
438
|
|
|
428
439
|
oldValue?.un(listeners);
|
|
429
440
|
value ?.on(listeners);
|
|
441
|
+
|
|
442
|
+
// Clear component instances when the store changes or is replaced
|
|
443
|
+
if (oldValue) {
|
|
444
|
+
me.clearComponentColumnMaps();
|
|
445
|
+
}
|
|
430
446
|
}
|
|
431
447
|
|
|
432
448
|
/**
|
|
@@ -580,6 +596,46 @@ class GridBody extends Component {
|
|
|
580
596
|
return ClassSystemUtil.beforeSetInstance(value, RowModel)
|
|
581
597
|
}
|
|
582
598
|
|
|
599
|
+
/**
|
|
600
|
+
* Destroys all component instances created by component columns.
|
|
601
|
+
* @protected
|
|
602
|
+
*/
|
|
603
|
+
clearComponentColumnMaps() {
|
|
604
|
+
let me = this,
|
|
605
|
+
columns = me.parent.columns.items;
|
|
606
|
+
|
|
607
|
+
columns.forEach(column => {
|
|
608
|
+
if (column instanceof Neo.grid.column.Component) {
|
|
609
|
+
column.map.forEach(component => {
|
|
610
|
+
component.destroy()
|
|
611
|
+
});
|
|
612
|
+
column.map.clear()
|
|
613
|
+
}
|
|
614
|
+
});
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
/**
|
|
618
|
+
* Cleans up component instances that are no longer visible or needed.
|
|
619
|
+
* @protected
|
|
620
|
+
*/
|
|
621
|
+
cleanupComponentInstances() {
|
|
622
|
+
let me = this;
|
|
623
|
+
|
|
624
|
+
me.parent.columns.items.forEach(column => {
|
|
625
|
+
if (column instanceof Neo.grid.column.Component) {
|
|
626
|
+
column.map.forEach((component, id) => {
|
|
627
|
+
// Extract rowIndex from component ID (e.g., "grid-body-1-component-950")
|
|
628
|
+
const componentRowIndex = parseInt(id.split('-').pop());
|
|
629
|
+
|
|
630
|
+
if (componentRowIndex < me.mountedRows[0] || componentRowIndex > me.mountedRows[1]) {
|
|
631
|
+
component.destroy();
|
|
632
|
+
column.map.delete(id)
|
|
633
|
+
}
|
|
634
|
+
});
|
|
635
|
+
}
|
|
636
|
+
});
|
|
637
|
+
}
|
|
638
|
+
|
|
583
639
|
/**
|
|
584
640
|
* @param {Object} opts
|
|
585
641
|
* @param {Object} opts.record
|
|
@@ -618,7 +674,7 @@ class GridBody extends Component {
|
|
|
618
674
|
|
|
619
675
|
style: {
|
|
620
676
|
height : me.rowHeight + 'px',
|
|
621
|
-
transform: `
|
|
677
|
+
transform: `translate3d(0px, ${rowIndex * me.rowHeight}px, 0px)`
|
|
622
678
|
}
|
|
623
679
|
};
|
|
624
680
|
|
|
@@ -662,7 +718,7 @@ class GridBody extends Component {
|
|
|
662
718
|
let me = this,
|
|
663
719
|
{mountedRows, store} = me,
|
|
664
720
|
rows = [],
|
|
665
|
-
i;
|
|
721
|
+
endIndex, i, range;
|
|
666
722
|
|
|
667
723
|
if (
|
|
668
724
|
store.isLoading ||
|
|
@@ -674,18 +730,24 @@ class GridBody extends Component {
|
|
|
674
730
|
return
|
|
675
731
|
}
|
|
676
732
|
|
|
677
|
-
|
|
678
|
-
|
|
733
|
+
if (me.#initialChunkSize > 0) {
|
|
734
|
+
endIndex = me.#initialChunkSize;
|
|
735
|
+
range = endIndex;
|
|
736
|
+
} else {
|
|
737
|
+
// Creates the new start & end indexes
|
|
738
|
+
me.updateMountedAndVisibleRows();
|
|
739
|
+
endIndex = mountedRows[1]
|
|
740
|
+
}
|
|
679
741
|
|
|
680
|
-
for (i=mountedRows[0]; i <
|
|
681
|
-
rows.push(me.createRow({record: store.
|
|
742
|
+
for (i=mountedRows[0]; i < endIndex; i++) {
|
|
743
|
+
rows.push(me.createRow({record: store.getAt(i), rowIndex: i}))
|
|
682
744
|
}
|
|
683
745
|
|
|
684
746
|
me.getVdomRoot().cn = rows;
|
|
685
747
|
|
|
686
748
|
me.parent.isLoading = false;
|
|
687
749
|
|
|
688
|
-
me.updateScrollHeight(true); // silent
|
|
750
|
+
me.updateScrollHeight(true, range); // silent
|
|
689
751
|
!silent && me.update()
|
|
690
752
|
}
|
|
691
753
|
|
|
@@ -694,6 +756,7 @@ class GridBody extends Component {
|
|
|
694
756
|
*/
|
|
695
757
|
destroy(...args) {
|
|
696
758
|
this.store = null; // remove the listeners
|
|
759
|
+
this.clearComponentColumnMaps(); // Destroy component instances
|
|
697
760
|
|
|
698
761
|
super.destroy(...args)
|
|
699
762
|
}
|
|
@@ -862,7 +925,11 @@ class GridBody extends Component {
|
|
|
862
925
|
getRowId(rowIndex) {
|
|
863
926
|
let me = this;
|
|
864
927
|
|
|
865
|
-
|
|
928
|
+
if (me.#initialChunkSize > 0) {
|
|
929
|
+
return `${me.id}__row-${rowIndex}`
|
|
930
|
+
} else {
|
|
931
|
+
return `${me.id}__row-${rowIndex % (me.availableRows + 2 * me.bufferRowRange)}`
|
|
932
|
+
}
|
|
866
933
|
}
|
|
867
934
|
|
|
868
935
|
/**
|
|
@@ -932,10 +999,13 @@ class GridBody extends Component {
|
|
|
932
999
|
}
|
|
933
1000
|
|
|
934
1001
|
/**
|
|
935
|
-
* @param {Object
|
|
1002
|
+
* @param {Object} data
|
|
1003
|
+
* @param {Object[]} data.items
|
|
1004
|
+
* @param {Boolean} [data.postChunkLoad]
|
|
1005
|
+
* @param {Number} [data.total]
|
|
936
1006
|
* @protected
|
|
937
1007
|
*/
|
|
938
|
-
onStoreLoad(
|
|
1008
|
+
onStoreLoad({items, postChunkLoad, total}) {
|
|
939
1009
|
let me = this;
|
|
940
1010
|
|
|
941
1011
|
/*
|
|
@@ -944,7 +1014,7 @@ class GridBody extends Component {
|
|
|
944
1014
|
* This logic bypasses the standard update() cycle by directly clearing the vdom,
|
|
945
1015
|
* vnode cache and the real DOM via textContent.
|
|
946
1016
|
*/
|
|
947
|
-
if (
|
|
1017
|
+
if (items?.length < 1) {
|
|
948
1018
|
const vdomRoot = me.getVdomRoot();
|
|
949
1019
|
|
|
950
1020
|
// No change, opt out
|
|
@@ -963,9 +1033,19 @@ class GridBody extends Component {
|
|
|
963
1033
|
return
|
|
964
1034
|
}
|
|
965
1035
|
|
|
966
|
-
|
|
1036
|
+
// If it's the first chunked load (data.total exists and data.items is a subset of total)
|
|
1037
|
+
// Render the entire chunk for immediate scrollability
|
|
1038
|
+
if (total && items.length < total) {
|
|
1039
|
+
me.#initialChunkSize = items.length;
|
|
1040
|
+
me.#initialTotalSize = total;
|
|
1041
|
+
me.createViewData();
|
|
1042
|
+
me.#initialChunkSize = 0
|
|
1043
|
+
me.#initialTotalSize = 0
|
|
1044
|
+
} else {
|
|
1045
|
+
me.createViewData()
|
|
1046
|
+
}
|
|
967
1047
|
|
|
968
|
-
if (me.mounted) {
|
|
1048
|
+
if (me.mounted && !postChunkLoad) {
|
|
969
1049
|
me.timeout(50).then(() => {
|
|
970
1050
|
Neo.main.DomAccess.scrollTo({
|
|
971
1051
|
direction: 'top',
|
|
@@ -974,6 +1054,11 @@ class GridBody extends Component {
|
|
|
974
1054
|
})
|
|
975
1055
|
})
|
|
976
1056
|
}
|
|
1057
|
+
|
|
1058
|
+
// Cleanup component instances after chunked load
|
|
1059
|
+
if (postChunkLoad) {
|
|
1060
|
+
me.cleanupComponentInstances()
|
|
1061
|
+
}
|
|
977
1062
|
}
|
|
978
1063
|
|
|
979
1064
|
/**
|
|
@@ -1172,7 +1257,7 @@ class GridBody extends Component {
|
|
|
1172
1257
|
*/
|
|
1173
1258
|
updateScrollHeight(silent=false) {
|
|
1174
1259
|
let me = this,
|
|
1175
|
-
countRecords = me.store?.
|
|
1260
|
+
countRecords = me.#initialTotalSize || me.store?.count || 0,
|
|
1176
1261
|
{rowHeight} = me;
|
|
1177
1262
|
|
|
1178
1263
|
if (countRecords > 0 && rowHeight > 0) {
|
package/src/grid/Container.mjs
CHANGED
|
@@ -565,13 +565,16 @@ class GridContainer extends BaseContainer {
|
|
|
565
565
|
}
|
|
566
566
|
|
|
567
567
|
/**
|
|
568
|
-
* @param {Object
|
|
568
|
+
* @param {Object} data
|
|
569
|
+
* @param {Object[]} data.items
|
|
570
|
+
* @param {Number} [data.total]
|
|
569
571
|
* @protected
|
|
570
572
|
*/
|
|
571
573
|
onStoreLoad(data) {
|
|
572
|
-
let me
|
|
574
|
+
let me = this,
|
|
575
|
+
totalCount = data.total ? data.total : this.store.count;
|
|
573
576
|
|
|
574
|
-
me.updateRowCount();
|
|
577
|
+
me.updateRowCount(totalCount);
|
|
575
578
|
|
|
576
579
|
if (me.store.sorters?.length < 1) {
|
|
577
580
|
me.removeSortingCss()
|
|
@@ -664,13 +667,15 @@ class GridContainer extends BaseContainer {
|
|
|
664
667
|
}
|
|
665
668
|
|
|
666
669
|
/**
|
|
667
|
-
* @param {
|
|
670
|
+
* @param {Number} [count] The total number of rows in the store. Optional, will use store.count if not provided.
|
|
671
|
+
* @param {Boolean} [silent=false]
|
|
668
672
|
*/
|
|
669
|
-
updateRowCount(silent=false) {
|
|
670
|
-
let me
|
|
673
|
+
updateRowCount(count, silent=false) {
|
|
674
|
+
let me = this,
|
|
675
|
+
finalCount = count ? count : me.store.count;
|
|
671
676
|
|
|
672
|
-
|
|
673
|
-
!silent &&
|
|
677
|
+
me.getVdomRoot()['aria-rowcount'] = finalCount + 2;
|
|
678
|
+
!silent && me.update()
|
|
674
679
|
}
|
|
675
680
|
}
|
|
676
681
|
|
|
@@ -100,18 +100,19 @@ class VerticalScrollbar extends Component {
|
|
|
100
100
|
filter: me.updateScrollHeight,
|
|
101
101
|
load : me.updateScrollHeight,
|
|
102
102
|
scope : me
|
|
103
|
-
})
|
|
104
|
-
|
|
105
|
-
value.getCount() > 0 && me.updateScrollHeight()
|
|
103
|
+
})
|
|
106
104
|
}
|
|
107
105
|
}
|
|
108
106
|
|
|
109
107
|
/**
|
|
110
|
-
*
|
|
108
|
+
* @param {Object} data
|
|
109
|
+
* @param {Object[]} data.items
|
|
110
|
+
* @param {Number} [data.total]
|
|
111
|
+
* @protected
|
|
111
112
|
*/
|
|
112
|
-
updateScrollHeight() {
|
|
113
|
+
updateScrollHeight(data) {
|
|
113
114
|
let me = this,
|
|
114
|
-
countRecords = me.store.
|
|
115
|
+
countRecords = data?.total ? data.total : me.store.count,
|
|
115
116
|
{rowHeight} = me;
|
|
116
117
|
|
|
117
118
|
if (countRecords > 0 && rowHeight > 0) {
|
|
@@ -132,9 +132,14 @@ class Component extends Column {
|
|
|
132
132
|
*/
|
|
133
133
|
getComponentId(rowIndex) {
|
|
134
134
|
let me = this,
|
|
135
|
-
{body} = me.parent
|
|
135
|
+
{body} = me.parent,
|
|
136
|
+
store = body.store; // Access the store from the body
|
|
136
137
|
|
|
137
|
-
|
|
138
|
+
if (store.chunkingTotal) { // Check if chunking is active
|
|
139
|
+
return `${me.id}-component-${rowIndex}`; // Use rowIndex directly
|
|
140
|
+
} else {
|
|
141
|
+
return `${me.id}-component-${rowIndex % (body.availableRows + 2 * body.bufferRowRange)}`
|
|
142
|
+
}
|
|
138
143
|
}
|
|
139
144
|
}
|
|
140
145
|
|
package/src/list/Base.mjs
CHANGED
|
@@ -609,7 +609,7 @@ class List extends Component {
|
|
|
609
609
|
if (!(me.animate && !me.getPlugin('list-animate'))) {
|
|
610
610
|
vdom.cn = [];
|
|
611
611
|
|
|
612
|
-
me.store.
|
|
612
|
+
me.store.forEach((item, index) => {
|
|
613
613
|
listItem = me.createItem(item, index);
|
|
614
614
|
listItem && vdom.cn.push(listItem)
|
|
615
615
|
});
|
|
@@ -29,7 +29,9 @@ class TreeAccordionModel extends TreeModel {
|
|
|
29
29
|
recordId = record[view.getKeyProperty()],
|
|
30
30
|
childRecord = null;
|
|
31
31
|
|
|
32
|
-
for (
|
|
32
|
+
for (let i = 0; i < view.store.count; i++) {
|
|
33
|
+
const item = view.store.getAt(i);
|
|
34
|
+
|
|
33
35
|
if (item.parentId === recordId) {
|
|
34
36
|
childRecord = item;
|
|
35
37
|
break
|
|
@@ -66,7 +68,9 @@ class TreeAccordionModel extends TreeModel {
|
|
|
66
68
|
nextItemRecord = null,
|
|
67
69
|
previousItemRecord;
|
|
68
70
|
|
|
69
|
-
for (let
|
|
71
|
+
for (let i = 0; i < store.count; i++) {
|
|
72
|
+
const item = store.getAt(i);
|
|
73
|
+
|
|
70
74
|
if (hasFoundNext && item.parentId === parentRecordId) {
|
|
71
75
|
nextItemRecord = item;
|
|
72
76
|
break
|
|
@@ -255,7 +259,9 @@ class TreeAccordionModel extends TreeModel {
|
|
|
255
259
|
{store} = view,
|
|
256
260
|
record, rootItemId;
|
|
257
261
|
|
|
258
|
-
for (
|
|
262
|
+
for (let i = 0; i < store.count; i++) {
|
|
263
|
+
const record = store.getAt(i);
|
|
264
|
+
|
|
259
265
|
if (!record.parentId) {
|
|
260
266
|
rootItemId = view.getItemId(record[view.getKeyProperty()]);
|
|
261
267
|
break
|
package/src/table/Body.mjs
CHANGED
|
@@ -357,7 +357,7 @@ class TableBody extends Component {
|
|
|
357
357
|
rows = [];
|
|
358
358
|
|
|
359
359
|
for (; i < countRecords; i++) {
|
|
360
|
-
rows.push(me.createRow({record: store.
|
|
360
|
+
rows.push(me.createRow({record: store.getAt(i), rowIndex: i}))
|
|
361
361
|
}
|
|
362
362
|
|
|
363
363
|
me.vdom.cn = rows;
|
|
@@ -558,12 +558,39 @@ class TableBody extends Component {
|
|
|
558
558
|
}
|
|
559
559
|
|
|
560
560
|
/**
|
|
561
|
-
* @param {Object
|
|
561
|
+
* @param {Object} data
|
|
562
|
+
* @param {Object[]} data.items
|
|
563
|
+
* @param {Number} [data.total]
|
|
562
564
|
* @protected
|
|
563
565
|
*/
|
|
564
566
|
onStoreLoad(data) {
|
|
565
567
|
let me = this;
|
|
566
568
|
|
|
569
|
+
/*
|
|
570
|
+
* Fast path to handle clearing all rows (e.g., store.removeAll()).
|
|
571
|
+
* A full vdom diff against all existing rows is a performance bottleneck.
|
|
572
|
+
* This logic bypasses the standard update() cycle by directly clearing the vdom,
|
|
573
|
+
* vnode cache and the real DOM via textContent.
|
|
574
|
+
*/
|
|
575
|
+
if (data?.items.length < 1) {
|
|
576
|
+
const vdomRoot = me.getVdomRoot();
|
|
577
|
+
|
|
578
|
+
// No change, opt out
|
|
579
|
+
if (vdomRoot.cn.length < 1) {
|
|
580
|
+
return
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
vdomRoot.cn = [];
|
|
584
|
+
me.getVnodeRoot().childNodes = [];
|
|
585
|
+
|
|
586
|
+
Neo.applyDeltas(me.appName, {
|
|
587
|
+
id : vdomRoot.id,
|
|
588
|
+
textContent: ''
|
|
589
|
+
});
|
|
590
|
+
|
|
591
|
+
return
|
|
592
|
+
}
|
|
593
|
+
|
|
567
594
|
me.createViewData();
|
|
568
595
|
|
|
569
596
|
if (me.mounted) {
|
package/src/tree/Accordion.mjs
CHANGED
|
@@ -133,7 +133,7 @@ class AccordionTree extends TreeList {
|
|
|
133
133
|
if (me.vnodeInitialized && value === false) {
|
|
134
134
|
let {store} = me;
|
|
135
135
|
|
|
136
|
-
store.
|
|
136
|
+
store.forEach(record => {
|
|
137
137
|
if (record.parentId === null && !record.isLeaf) {
|
|
138
138
|
me.expandItem(record)
|
|
139
139
|
}
|
|
@@ -153,7 +153,7 @@ class AccordionTree extends TreeList {
|
|
|
153
153
|
{store} = me,
|
|
154
154
|
hide = !value;
|
|
155
155
|
|
|
156
|
-
store.
|
|
156
|
+
store.forEach(record => {
|
|
157
157
|
const itemId = me.getItemId(record[me.getKeyProperty()]),
|
|
158
158
|
vdom = me.getVdomChild(itemId),
|
|
159
159
|
itemVdom = VDomUtil.getByFlag(vdom, 'iconCls');
|
package/src/tree/List.mjs
CHANGED
|
@@ -162,7 +162,7 @@ class Tree extends Base {
|
|
|
162
162
|
hasMatch = false,
|
|
163
163
|
node;
|
|
164
164
|
|
|
165
|
-
me.store.
|
|
165
|
+
me.store.forEach(item => {
|
|
166
166
|
if (!item.isLeaf) {
|
|
167
167
|
node = me.getVdomChild(me.getItemId(item.id), me.vdom);
|
|
168
168
|
|
|
@@ -303,7 +303,7 @@ class Tree extends Base {
|
|
|
303
303
|
hasMatch = false,
|
|
304
304
|
node;
|
|
305
305
|
|
|
306
|
-
me.store.
|
|
306
|
+
me.store.forEach(item => {
|
|
307
307
|
if (!item.isLeaf) {
|
|
308
308
|
node = me.getVdomChild(me.getItemId(item.id), me.vdom);
|
|
309
309
|
|
|
@@ -337,7 +337,7 @@ class Tree extends Base {
|
|
|
337
337
|
value = ''
|
|
338
338
|
}
|
|
339
339
|
|
|
340
|
-
me.store.
|
|
340
|
+
me.store.forEach(item => {
|
|
341
341
|
if (item.parentId === parentId) {
|
|
342
342
|
directMatch = false;
|
|
343
343
|
node = me.getVdomChild(me.getItemId(item.id), me.vdom);
|