mtrl-addons 0.2.2 → 0.2.4
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/{src/components/index.ts → dist/components/index.d.ts} +0 -2
- package/dist/components/vlist/config.d.ts +86 -0
- package/{src/components/vlist/constants.ts → dist/components/vlist/constants.d.ts} +10 -11
- package/dist/components/vlist/features/api.d.ts +7 -0
- package/{src/components/vlist/features/index.ts → dist/components/vlist/features/index.d.ts} +0 -2
- package/dist/components/vlist/features/selection.d.ts +6 -0
- package/dist/components/vlist/features/viewport.d.ts +9 -0
- package/dist/components/vlist/features.d.ts +31 -0
- package/{src/components/vlist/index.ts → dist/components/vlist/index.d.ts} +1 -10
- package/dist/components/vlist/types.d.ts +596 -0
- package/dist/components/vlist/vlist.d.ts +29 -0
- package/dist/core/compose/features/gestures/index.d.ts +86 -0
- package/dist/core/compose/features/gestures/longpress.d.ts +85 -0
- package/dist/core/compose/features/gestures/pan.d.ts +108 -0
- package/dist/core/compose/features/gestures/pinch.d.ts +111 -0
- package/dist/core/compose/features/gestures/rotate.d.ts +111 -0
- package/dist/core/compose/features/gestures/swipe.d.ts +149 -0
- package/dist/core/compose/features/gestures/tap.d.ts +79 -0
- package/{src/core/compose/features/index.ts → dist/core/compose/features/index.d.ts} +1 -2
- package/{src/core/compose/index.ts → dist/core/compose/index.d.ts} +2 -11
- package/{src/core/gestures/index.ts → dist/core/gestures/index.d.ts} +1 -20
- package/dist/core/gestures/longpress.d.ts +23 -0
- package/dist/core/gestures/manager.d.ts +14 -0
- package/dist/core/gestures/pan.d.ts +12 -0
- package/dist/core/gestures/pinch.d.ts +14 -0
- package/dist/core/gestures/rotate.d.ts +14 -0
- package/dist/core/gestures/swipe.d.ts +20 -0
- package/dist/core/gestures/tap.d.ts +12 -0
- package/dist/core/gestures/types.d.ts +320 -0
- package/dist/core/gestures/utils.d.ts +57 -0
- package/dist/core/index.d.ts +13 -0
- package/dist/core/layout/config.d.ts +33 -0
- package/dist/core/layout/index.d.ts +51 -0
- package/dist/core/layout/jsx.d.ts +65 -0
- package/dist/core/layout/schema.d.ts +112 -0
- package/dist/core/layout/types.d.ts +69 -0
- package/dist/core/viewport/constants.d.ts +105 -0
- package/dist/core/viewport/features/base.d.ts +14 -0
- package/dist/core/viewport/features/collection.d.ts +41 -0
- package/dist/core/viewport/features/events.d.ts +13 -0
- package/{src/core/viewport/features/index.ts → dist/core/viewport/features/index.d.ts} +0 -7
- package/dist/core/viewport/features/item-size.d.ts +30 -0
- package/dist/core/viewport/features/loading.d.ts +34 -0
- package/dist/core/viewport/features/momentum.d.ts +17 -0
- package/dist/core/viewport/features/performance.d.ts +53 -0
- package/dist/core/viewport/features/placeholders.d.ts +38 -0
- package/dist/core/viewport/features/rendering.d.ts +16 -0
- package/dist/core/viewport/features/scrollbar.d.ts +26 -0
- package/dist/core/viewport/features/scrolling.d.ts +16 -0
- package/dist/core/viewport/features/utils.d.ts +43 -0
- package/dist/core/viewport/features/virtual.d.ts +18 -0
- package/{src/core/viewport/index.ts → dist/core/viewport/index.d.ts} +1 -17
- package/dist/core/viewport/types.d.ts +96 -0
- package/dist/core/viewport/utils/speed-tracker.d.ts +22 -0
- package/dist/core/viewport/viewport.d.ts +11 -0
- package/{src/index.ts → dist/index.d.ts} +0 -4
- package/dist/index.js +5143 -0
- package/dist/index.mjs +5111 -0
- package/dist/styles.css +254 -0
- package/dist/styles.css.map +1 -0
- package/package.json +16 -2
- package/.cursorrules +0 -117
- package/AI.md +0 -39
- package/CLAUDE.md +0 -882
- package/build.js +0 -377
- package/index.ts +0 -7
- package/scripts/analyze-orphaned-functions.ts +0 -387
- package/scripts/debug/vlist-selection.ts +0 -121
- package/src/components/vlist/config.ts +0 -323
- package/src/components/vlist/features/api.ts +0 -626
- package/src/components/vlist/features/selection.ts +0 -436
- package/src/components/vlist/features/viewport.ts +0 -59
- package/src/components/vlist/features.ts +0 -112
- package/src/components/vlist/types.ts +0 -723
- package/src/components/vlist/vlist.ts +0 -92
- package/src/core/compose/features/gestures/index.ts +0 -227
- package/src/core/compose/features/gestures/longpress.ts +0 -383
- package/src/core/compose/features/gestures/pan.ts +0 -424
- package/src/core/compose/features/gestures/pinch.ts +0 -475
- package/src/core/compose/features/gestures/rotate.ts +0 -485
- package/src/core/compose/features/gestures/swipe.ts +0 -492
- package/src/core/compose/features/gestures/tap.ts +0 -334
- package/src/core/gestures/longpress.ts +0 -68
- package/src/core/gestures/manager.ts +0 -418
- package/src/core/gestures/pan.ts +0 -48
- package/src/core/gestures/pinch.ts +0 -58
- package/src/core/gestures/rotate.ts +0 -58
- package/src/core/gestures/swipe.ts +0 -66
- package/src/core/gestures/tap.ts +0 -45
- package/src/core/gestures/types.ts +0 -387
- package/src/core/gestures/utils.ts +0 -128
- package/src/core/index.ts +0 -43
- package/src/core/layout/config.ts +0 -102
- package/src/core/layout/index.ts +0 -168
- package/src/core/layout/jsx.ts +0 -174
- package/src/core/layout/schema.ts +0 -1044
- package/src/core/layout/types.ts +0 -95
- package/src/core/viewport/constants.ts +0 -145
- package/src/core/viewport/features/base.ts +0 -73
- package/src/core/viewport/features/collection.ts +0 -1182
- package/src/core/viewport/features/events.ts +0 -130
- package/src/core/viewport/features/item-size.ts +0 -271
- package/src/core/viewport/features/loading.ts +0 -263
- package/src/core/viewport/features/momentum.ts +0 -269
- package/src/core/viewport/features/performance.ts +0 -161
- package/src/core/viewport/features/placeholders.ts +0 -335
- package/src/core/viewport/features/rendering.ts +0 -962
- package/src/core/viewport/features/scrollbar.ts +0 -434
- package/src/core/viewport/features/scrolling.ts +0 -634
- package/src/core/viewport/features/utils.ts +0 -94
- package/src/core/viewport/features/virtual.ts +0 -525
- package/src/core/viewport/types.ts +0 -133
- package/src/core/viewport/utils/speed-tracker.ts +0 -79
- package/src/core/viewport/viewport.ts +0 -265
- package/test/benchmarks/layout/advanced.test.ts +0 -656
- package/test/benchmarks/layout/comparison.test.ts +0 -519
- package/test/benchmarks/layout/performance-comparison.test.ts +0 -274
- package/test/benchmarks/layout/real-components.test.ts +0 -733
- package/test/benchmarks/layout/simple.test.ts +0 -321
- package/test/benchmarks/layout/stress.test.ts +0 -990
- package/test/collection/basic.test.ts +0 -304
- package/test/components/vlist-selection.test.ts +0 -240
- package/test/components/vlist.test.ts +0 -63
- package/test/core/collection/adapter.test.ts +0 -161
- package/test/core/collection/collection.test.ts +0 -394
- package/test/core/layout/layout.test.ts +0 -201
- package/test/utils/dom-helpers.ts +0 -275
- package/test/utils/performance-helpers.ts +0 -392
- package/tsconfig.json +0 -20
|
@@ -1,626 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* API feature for VList
|
|
3
|
-
* Provides a clean public API for the VList component
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
import type { VListConfig, VListItem, RemoveItemOptions } from "../types";
|
|
7
|
-
|
|
8
|
-
// Re-export for convenience
|
|
9
|
-
export type { RemoveItemOptions } from "../types";
|
|
10
|
-
|
|
11
|
-
export const withAPI = <T extends VListItem = VListItem>(
|
|
12
|
-
config: VListConfig<T>,
|
|
13
|
-
) => {
|
|
14
|
-
return (component: any) => {
|
|
15
|
-
// Initialize viewport on creation
|
|
16
|
-
setTimeout(() => {
|
|
17
|
-
if (component.viewport && component.viewport.initialize) {
|
|
18
|
-
component.viewport.initialize();
|
|
19
|
-
}
|
|
20
|
-
}, 0);
|
|
21
|
-
|
|
22
|
-
// Track loading state
|
|
23
|
-
let isLoading = false;
|
|
24
|
-
let selectedIds = new Set<string | number>();
|
|
25
|
-
|
|
26
|
-
// Track pending removals to filter out items from stale server responses
|
|
27
|
-
// This prevents race conditions where server returns old data before updates are committed
|
|
28
|
-
const pendingRemovals = new Set<string | number>();
|
|
29
|
-
|
|
30
|
-
// Listen for collection events
|
|
31
|
-
component.on?.("collection:range-loaded", () => {
|
|
32
|
-
isLoading = false;
|
|
33
|
-
});
|
|
34
|
-
component.on?.("collection:range-failed", () => {
|
|
35
|
-
isLoading = false;
|
|
36
|
-
});
|
|
37
|
-
|
|
38
|
-
return {
|
|
39
|
-
...component,
|
|
40
|
-
|
|
41
|
-
// Data operations
|
|
42
|
-
setItems(items: T[]) {
|
|
43
|
-
if (component.viewport?.collection) {
|
|
44
|
-
component.viewport.collection.setItems(items);
|
|
45
|
-
} else {
|
|
46
|
-
component.items = items;
|
|
47
|
-
component.totalItems = items.length;
|
|
48
|
-
}
|
|
49
|
-
component.emit?.("items:set", { items, total: items.length });
|
|
50
|
-
},
|
|
51
|
-
|
|
52
|
-
getItems(): T[] {
|
|
53
|
-
// Collection is added directly to component, not to viewport.collection
|
|
54
|
-
if ((component as any).collection?.getItems) {
|
|
55
|
-
return (component as any).collection.getItems();
|
|
56
|
-
}
|
|
57
|
-
return component.items || [];
|
|
58
|
-
},
|
|
59
|
-
|
|
60
|
-
getVisibleItems(): T[] {
|
|
61
|
-
const range = component.viewport?.getVisibleRange() || {
|
|
62
|
-
start: 0,
|
|
63
|
-
end: 0,
|
|
64
|
-
};
|
|
65
|
-
const items = this.getItems();
|
|
66
|
-
return items.slice(range.start, range.end);
|
|
67
|
-
},
|
|
68
|
-
|
|
69
|
-
getItemCount(): number {
|
|
70
|
-
if (component.viewport?.collection) {
|
|
71
|
-
return component.viewport.collection.getTotalItems();
|
|
72
|
-
}
|
|
73
|
-
return component.totalItems || component.items?.length || 0;
|
|
74
|
-
},
|
|
75
|
-
|
|
76
|
-
/**
|
|
77
|
-
* Get item at a specific index
|
|
78
|
-
* @param index - The index of the item to retrieve
|
|
79
|
-
* @returns The item at the index, or undefined if not found
|
|
80
|
-
*/
|
|
81
|
-
getItem(index: number): T | undefined {
|
|
82
|
-
if ((component as any).collection?.getItem) {
|
|
83
|
-
return (component as any).collection.getItem(index);
|
|
84
|
-
}
|
|
85
|
-
const items = this.getItems();
|
|
86
|
-
return items[index];
|
|
87
|
-
},
|
|
88
|
-
|
|
89
|
-
/**
|
|
90
|
-
* Update item at a specific index
|
|
91
|
-
* @param index - The index of the item to update
|
|
92
|
-
* @param item - The new item data (full replacement)
|
|
93
|
-
*/
|
|
94
|
-
updateItem(index: number, item: T): void {
|
|
95
|
-
const items = this.getItems();
|
|
96
|
-
|
|
97
|
-
if (index < 0 || index >= items.length) {
|
|
98
|
-
console.warn(`[VList] updateItem: index ${index} out of bounds`);
|
|
99
|
-
return;
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
const previousItem = items[index];
|
|
103
|
-
|
|
104
|
-
// Update in collection items array
|
|
105
|
-
if ((component as any).collection?.items) {
|
|
106
|
-
(component as any).collection.items[index] = item;
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
// Emit event for rendering feature to handle DOM update
|
|
110
|
-
component.emit?.("item:update-request", {
|
|
111
|
-
index,
|
|
112
|
-
item,
|
|
113
|
-
previousItem,
|
|
114
|
-
});
|
|
115
|
-
},
|
|
116
|
-
|
|
117
|
-
/**
|
|
118
|
-
* Update item by ID
|
|
119
|
-
* Finds the item in the collection by its ID and updates it with new data
|
|
120
|
-
* Re-renders the item if currently visible in the viewport
|
|
121
|
-
* @param id - The item ID to find
|
|
122
|
-
* @param data - Partial data to merge with existing item, or full item replacement
|
|
123
|
-
* @param options - Update options
|
|
124
|
-
* @returns true if item was found and updated, false otherwise
|
|
125
|
-
*/
|
|
126
|
-
updateItemById(
|
|
127
|
-
id: string | number,
|
|
128
|
-
data: Partial<T>,
|
|
129
|
-
options: {
|
|
130
|
-
/** If true, replace the entire item instead of merging (default: false) */
|
|
131
|
-
replace?: boolean;
|
|
132
|
-
/** If true, re-render even if not visible (default: false) */
|
|
133
|
-
forceRender?: boolean;
|
|
134
|
-
} = {},
|
|
135
|
-
): boolean {
|
|
136
|
-
const { replace = false } = options;
|
|
137
|
-
const items = this.getItems();
|
|
138
|
-
|
|
139
|
-
// Find the item by ID (handle sparse arrays from pagination)
|
|
140
|
-
// Check all possible ID fields since items may have different ID structures
|
|
141
|
-
const index = items.findIndex((item: any) => {
|
|
142
|
-
if (!item) return false;
|
|
143
|
-
// Check all possible ID fields - user_id is important for profile/contact items
|
|
144
|
-
const idsToCheck = [item.id, item._id, item.user_id].filter(
|
|
145
|
-
(v) => v !== undefined && v !== null,
|
|
146
|
-
);
|
|
147
|
-
return idsToCheck.some(
|
|
148
|
-
(itemId) => itemId === id || String(itemId) === String(id),
|
|
149
|
-
);
|
|
150
|
-
});
|
|
151
|
-
|
|
152
|
-
if (index === -1) {
|
|
153
|
-
console.warn(`[VList] updateItemById: item with id ${id} not found`);
|
|
154
|
-
return false;
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
const previousItem = items[index];
|
|
158
|
-
|
|
159
|
-
// Create updated item - either replace entirely or merge
|
|
160
|
-
let updatedItem: T;
|
|
161
|
-
if (replace) {
|
|
162
|
-
updatedItem = { ...data, id: previousItem.id ?? id } as T;
|
|
163
|
-
} else {
|
|
164
|
-
// Only merge properties that have actual values (not undefined)
|
|
165
|
-
// This prevents partial updates from overwriting existing data
|
|
166
|
-
updatedItem = { ...previousItem } as T;
|
|
167
|
-
for (const [key, value] of Object.entries(data)) {
|
|
168
|
-
if (value !== undefined) {
|
|
169
|
-
(updatedItem as any)[key] = value;
|
|
170
|
-
}
|
|
171
|
-
}
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
// Update in collection items array
|
|
175
|
-
if ((component as any).collection?.items) {
|
|
176
|
-
(component as any).collection.items[index] = updatedItem;
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
// Emit event for rendering feature to handle DOM update
|
|
180
|
-
component.emit?.("item:update-request", {
|
|
181
|
-
index,
|
|
182
|
-
item: updatedItem,
|
|
183
|
-
previousItem,
|
|
184
|
-
});
|
|
185
|
-
|
|
186
|
-
return true;
|
|
187
|
-
},
|
|
188
|
-
|
|
189
|
-
/**
|
|
190
|
-
* Remove item at a specific index
|
|
191
|
-
* Removes the item from the collection, updates totalItems, and triggers re-render
|
|
192
|
-
* @param index - The index of the item to remove
|
|
193
|
-
* @returns true if item was found and removed, false otherwise
|
|
194
|
-
*/
|
|
195
|
-
removeItem(index: number): boolean {
|
|
196
|
-
const items = this.getItems();
|
|
197
|
-
const collection = (component as any).collection;
|
|
198
|
-
|
|
199
|
-
if (index < 0 || index >= items.length) {
|
|
200
|
-
console.warn(`[VList] removeItem: index ${index} out of bounds`);
|
|
201
|
-
return false;
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
const removedItem = items[index];
|
|
205
|
-
|
|
206
|
-
// Remove from collection items array (component.collection.items is the actual array)
|
|
207
|
-
if ((component as any).collection?.items) {
|
|
208
|
-
(component as any).collection.items.splice(index, 1);
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
// IMPORTANT: Emit item:remove-request FIRST so rendering feature rebuilds collectionItems
|
|
212
|
-
// BEFORE setTotalItems triggers a render via viewport:total-items-changed
|
|
213
|
-
component.emit?.("item:remove-request", {
|
|
214
|
-
index,
|
|
215
|
-
item: removedItem,
|
|
216
|
-
});
|
|
217
|
-
|
|
218
|
-
// Update totalItems - this will trigger viewport:total-items-changed which calls renderItems
|
|
219
|
-
// By now, collectionItems has been rebuilt by the item:remove-request handler
|
|
220
|
-
if (collection?.getTotalItems && collection?.setTotalItems) {
|
|
221
|
-
const currentTotal = collection.getTotalItems();
|
|
222
|
-
collection.setTotalItems(Math.max(0, currentTotal - 1));
|
|
223
|
-
} else if (component.viewport?.collection?.setTotalItems) {
|
|
224
|
-
// Fallback to viewport.collection if available
|
|
225
|
-
const currentTotal = component.viewport.collection.getTotalItems();
|
|
226
|
-
component.viewport.collection.setTotalItems(
|
|
227
|
-
Math.max(0, currentTotal - 1),
|
|
228
|
-
);
|
|
229
|
-
} else {
|
|
230
|
-
// Last resort: update component.totalItems directly
|
|
231
|
-
if (component.totalItems !== undefined) {
|
|
232
|
-
component.totalItems = Math.max(0, component.totalItems - 1);
|
|
233
|
-
}
|
|
234
|
-
}
|
|
235
|
-
|
|
236
|
-
return true;
|
|
237
|
-
},
|
|
238
|
-
|
|
239
|
-
/**
|
|
240
|
-
* Remove item by ID
|
|
241
|
-
* Finds the item in the collection by its ID and removes it
|
|
242
|
-
* Updates totalItems and triggers re-render of visible items
|
|
243
|
-
* Optionally tracks as pending removal to filter from future fetches
|
|
244
|
-
* @param id - The item ID to find and remove
|
|
245
|
-
* @param options - Remove options (trackPending, pendingTimeout)
|
|
246
|
-
* @returns true if item was found and removed, false otherwise
|
|
247
|
-
*/
|
|
248
|
-
removeItemById(
|
|
249
|
-
id: string | number,
|
|
250
|
-
options: RemoveItemOptions = {},
|
|
251
|
-
): boolean {
|
|
252
|
-
const { trackPending = true, pendingTimeout = 5000 } = options;
|
|
253
|
-
|
|
254
|
-
if (id === undefined || id === null) {
|
|
255
|
-
console.warn(`[VList] removeItemById: invalid id`);
|
|
256
|
-
return false;
|
|
257
|
-
}
|
|
258
|
-
|
|
259
|
-
// Add to pending removals to filter from future server responses
|
|
260
|
-
if (trackPending) {
|
|
261
|
-
pendingRemovals.add(id);
|
|
262
|
-
|
|
263
|
-
// Clear from pending removals after timeout (server should have updated by then)
|
|
264
|
-
setTimeout(() => {
|
|
265
|
-
pendingRemovals.delete(id);
|
|
266
|
-
}, pendingTimeout);
|
|
267
|
-
}
|
|
268
|
-
|
|
269
|
-
const items = this.getItems();
|
|
270
|
-
|
|
271
|
-
// Find the item by ID (handle sparse arrays from pagination)
|
|
272
|
-
// Check all possible ID fields since items may have different ID structures
|
|
273
|
-
const index = items.findIndex((item: any) => {
|
|
274
|
-
if (!item) return false;
|
|
275
|
-
// Check all possible ID fields
|
|
276
|
-
const idsToCheck = [item.id, item._id, item.user_id].filter(
|
|
277
|
-
(v) => v !== undefined && v !== null,
|
|
278
|
-
);
|
|
279
|
-
return idsToCheck.some(
|
|
280
|
-
(itemId) => itemId === id || String(itemId) === String(id),
|
|
281
|
-
);
|
|
282
|
-
});
|
|
283
|
-
|
|
284
|
-
if (index === -1) {
|
|
285
|
-
return false;
|
|
286
|
-
}
|
|
287
|
-
|
|
288
|
-
return this.removeItem(index);
|
|
289
|
-
},
|
|
290
|
-
|
|
291
|
-
/**
|
|
292
|
-
* Check if an item ID is pending removal
|
|
293
|
-
* @param id - The item ID to check
|
|
294
|
-
* @returns true if the item is pending removal
|
|
295
|
-
*/
|
|
296
|
-
isPendingRemoval(id: string | number): boolean {
|
|
297
|
-
return pendingRemovals.has(id);
|
|
298
|
-
},
|
|
299
|
-
|
|
300
|
-
/**
|
|
301
|
-
* Get all pending removal IDs
|
|
302
|
-
* @returns Set of pending removal IDs
|
|
303
|
-
*/
|
|
304
|
-
getPendingRemovals(): Set<string | number> {
|
|
305
|
-
return new Set(pendingRemovals);
|
|
306
|
-
},
|
|
307
|
-
|
|
308
|
-
/**
|
|
309
|
-
* Clear a specific pending removal
|
|
310
|
-
* @param id - The item ID to clear from pending removals
|
|
311
|
-
*/
|
|
312
|
-
clearPendingRemoval(id: string | number): void {
|
|
313
|
-
pendingRemovals.delete(id);
|
|
314
|
-
},
|
|
315
|
-
|
|
316
|
-
/**
|
|
317
|
-
* Clear all pending removals
|
|
318
|
-
*/
|
|
319
|
-
clearAllPendingRemovals(): void {
|
|
320
|
-
pendingRemovals.clear();
|
|
321
|
-
},
|
|
322
|
-
|
|
323
|
-
/**
|
|
324
|
-
* Filter items array to exclude pending removals
|
|
325
|
-
* Utility method for use in collection adapters
|
|
326
|
-
* @param items - Array of items to filter
|
|
327
|
-
* @returns Filtered array without pending removal items
|
|
328
|
-
*/
|
|
329
|
-
filterPendingRemovals<I extends { id?: any; _id?: any }>(
|
|
330
|
-
items: I[],
|
|
331
|
-
): I[] {
|
|
332
|
-
if (pendingRemovals.size === 0) return items;
|
|
333
|
-
|
|
334
|
-
return items.filter((item) => {
|
|
335
|
-
const id = item._id || item.id;
|
|
336
|
-
return !pendingRemovals.has(id);
|
|
337
|
-
});
|
|
338
|
-
},
|
|
339
|
-
|
|
340
|
-
// Loading operations
|
|
341
|
-
async loadRange(
|
|
342
|
-
page: number,
|
|
343
|
-
limit: number,
|
|
344
|
-
strategy: string = "page",
|
|
345
|
-
alignment?: string,
|
|
346
|
-
) {
|
|
347
|
-
isLoading = true;
|
|
348
|
-
|
|
349
|
-
if (component.viewport?.collection) {
|
|
350
|
-
const offset = strategy === "page" ? (page - 1) * limit : page;
|
|
351
|
-
try {
|
|
352
|
-
await component.viewport.collection.loadRange(offset, limit);
|
|
353
|
-
component.emit?.("load", { page, limit, strategy });
|
|
354
|
-
|
|
355
|
-
// Scroll to the loaded range if alignment is specified
|
|
356
|
-
if (alignment) {
|
|
357
|
-
const index = offset;
|
|
358
|
-
this.scrollToIndex(index, alignment);
|
|
359
|
-
}
|
|
360
|
-
} catch (error) {
|
|
361
|
-
component.emit?.("error", { error });
|
|
362
|
-
throw error;
|
|
363
|
-
} finally {
|
|
364
|
-
isLoading = false;
|
|
365
|
-
}
|
|
366
|
-
}
|
|
367
|
-
},
|
|
368
|
-
|
|
369
|
-
// Data loading
|
|
370
|
-
loadNext: async function () {
|
|
371
|
-
console.log(`[VList] loadNext()`);
|
|
372
|
-
|
|
373
|
-
if (component.viewport?.collection) {
|
|
374
|
-
const totalItems = component.viewport.collection.getTotalItems();
|
|
375
|
-
const loadedRanges = component.viewport.collection.getLoadedRanges();
|
|
376
|
-
|
|
377
|
-
// Find the next unloaded range
|
|
378
|
-
let nextOffset = 0;
|
|
379
|
-
for (const rangeId of loadedRanges) {
|
|
380
|
-
const rangeEnd = (rangeId + 1) * (config.pagination?.limit || 20);
|
|
381
|
-
if (rangeEnd > nextOffset) {
|
|
382
|
-
nextOffset = rangeEnd;
|
|
383
|
-
}
|
|
384
|
-
}
|
|
385
|
-
|
|
386
|
-
if (nextOffset < totalItems) {
|
|
387
|
-
await component.viewport.collection.loadRange(
|
|
388
|
-
nextOffset,
|
|
389
|
-
config.pagination?.limit || 20,
|
|
390
|
-
);
|
|
391
|
-
}
|
|
392
|
-
}
|
|
393
|
-
|
|
394
|
-
return Promise.resolve();
|
|
395
|
-
},
|
|
396
|
-
|
|
397
|
-
isLoading(): boolean {
|
|
398
|
-
return isLoading;
|
|
399
|
-
},
|
|
400
|
-
|
|
401
|
-
hasNext(): boolean {
|
|
402
|
-
if (component.viewport?.collection) {
|
|
403
|
-
const total = component.viewport.collection.getTotalItems();
|
|
404
|
-
const items = component.viewport.collection.getItems();
|
|
405
|
-
return items.length < total;
|
|
406
|
-
}
|
|
407
|
-
return false;
|
|
408
|
-
},
|
|
409
|
-
|
|
410
|
-
// Note: Selection operations are handled by the withSelection feature
|
|
411
|
-
// These are kept for backward compatibility but delegate to selection feature if available
|
|
412
|
-
getSelectedIds(): (string | number)[] {
|
|
413
|
-
if (typeof component.getSelectedIndices === "function") {
|
|
414
|
-
// Use selection feature if available
|
|
415
|
-
const items = this.getItems();
|
|
416
|
-
const indices = component.getSelectedIndices();
|
|
417
|
-
return indices.map((idx: number) => (items[idx] as any)?.id || idx);
|
|
418
|
-
}
|
|
419
|
-
// Fallback to local tracking
|
|
420
|
-
return Array.from(selectedIds);
|
|
421
|
-
},
|
|
422
|
-
|
|
423
|
-
selectItem(id: string | number) {
|
|
424
|
-
if (typeof component.selectItems === "function") {
|
|
425
|
-
// Use selection feature if available
|
|
426
|
-
const items = this.getItems();
|
|
427
|
-
const index = items.findIndex((item: any) => item.id === id);
|
|
428
|
-
if (index >= 0) {
|
|
429
|
-
component.selectItems([index]);
|
|
430
|
-
}
|
|
431
|
-
} else {
|
|
432
|
-
// Fallback to local tracking
|
|
433
|
-
selectedIds.add(id);
|
|
434
|
-
component.emit?.("selection:change", {
|
|
435
|
-
selected: this.getSelectedIds(),
|
|
436
|
-
});
|
|
437
|
-
}
|
|
438
|
-
},
|
|
439
|
-
|
|
440
|
-
deselectItem(id: string | number) {
|
|
441
|
-
if (typeof component.deselectItems === "function") {
|
|
442
|
-
// Use selection feature if available
|
|
443
|
-
const items = this.getItems();
|
|
444
|
-
const index = items.findIndex((item: any) => item.id === id);
|
|
445
|
-
if (index >= 0) {
|
|
446
|
-
component.deselectItems([index]);
|
|
447
|
-
}
|
|
448
|
-
} else {
|
|
449
|
-
// Fallback to local tracking
|
|
450
|
-
selectedIds.delete(id);
|
|
451
|
-
component.emit?.("selection:change", {
|
|
452
|
-
selected: this.getSelectedIds(),
|
|
453
|
-
});
|
|
454
|
-
}
|
|
455
|
-
},
|
|
456
|
-
|
|
457
|
-
clearSelection() {
|
|
458
|
-
if (typeof component.clearSelection === "function") {
|
|
459
|
-
// Use selection feature if available
|
|
460
|
-
component.clearSelection();
|
|
461
|
-
} else {
|
|
462
|
-
// Fallback to local tracking
|
|
463
|
-
selectedIds.clear();
|
|
464
|
-
component.emit?.("selection:change", { selected: [] });
|
|
465
|
-
}
|
|
466
|
-
},
|
|
467
|
-
|
|
468
|
-
// Scrolling methods
|
|
469
|
-
scrollToIndex: (
|
|
470
|
-
index: number,
|
|
471
|
-
alignment: "start" | "center" | "end" = "start",
|
|
472
|
-
animate?: boolean,
|
|
473
|
-
) => {
|
|
474
|
-
if (component.viewport) {
|
|
475
|
-
component.viewport.scrollToIndex(index, alignment);
|
|
476
|
-
}
|
|
477
|
-
return Promise.resolve();
|
|
478
|
-
},
|
|
479
|
-
|
|
480
|
-
/**
|
|
481
|
-
* Scroll to index and select item after data loads
|
|
482
|
-
* Useful for runtime scroll+select when VList is already created
|
|
483
|
-
*/
|
|
484
|
-
scrollToIndexAndSelect: async function (
|
|
485
|
-
index: number,
|
|
486
|
-
selectId: string | number,
|
|
487
|
-
alignment: "start" | "center" | "end" = "start",
|
|
488
|
-
) {
|
|
489
|
-
if (component.viewport) {
|
|
490
|
-
component.viewport.scrollToIndex(index, alignment);
|
|
491
|
-
}
|
|
492
|
-
|
|
493
|
-
// Listen for range load to complete, then select
|
|
494
|
-
const onRangeLoaded = () => {
|
|
495
|
-
component.off?.("viewport:range-loaded", onRangeLoaded);
|
|
496
|
-
requestAnimationFrame(() => {
|
|
497
|
-
if (component.selectById) {
|
|
498
|
-
component.selectById(selectId);
|
|
499
|
-
}
|
|
500
|
-
});
|
|
501
|
-
};
|
|
502
|
-
|
|
503
|
-
component.on?.("viewport:range-loaded", onRangeLoaded);
|
|
504
|
-
|
|
505
|
-
// Fallback timeout in case event doesn't fire (data already loaded)
|
|
506
|
-
setTimeout(() => {
|
|
507
|
-
component.off?.("viewport:range-loaded", onRangeLoaded);
|
|
508
|
-
if (component.selectById) {
|
|
509
|
-
component.selectById(selectId);
|
|
510
|
-
}
|
|
511
|
-
}, 300);
|
|
512
|
-
|
|
513
|
-
return Promise.resolve();
|
|
514
|
-
},
|
|
515
|
-
|
|
516
|
-
scrollToItem: async function (
|
|
517
|
-
itemId: string,
|
|
518
|
-
alignment: "start" | "center" | "end" = "start",
|
|
519
|
-
animate?: boolean,
|
|
520
|
-
) {
|
|
521
|
-
const items = this.getItems();
|
|
522
|
-
const index = items.findIndex(
|
|
523
|
-
(item: any) => String(item.id) === String(itemId),
|
|
524
|
-
);
|
|
525
|
-
|
|
526
|
-
if (index >= 0) {
|
|
527
|
-
return this.scrollToIndex(index, alignment, animate);
|
|
528
|
-
}
|
|
529
|
-
|
|
530
|
-
console.warn(`Item ${itemId} not found`);
|
|
531
|
-
return Promise.resolve();
|
|
532
|
-
},
|
|
533
|
-
|
|
534
|
-
scrollToTop: function () {
|
|
535
|
-
return this.scrollToIndex(0, "start");
|
|
536
|
-
},
|
|
537
|
-
|
|
538
|
-
scrollToBottom: function () {
|
|
539
|
-
const totalItems = this.getItemCount();
|
|
540
|
-
if (totalItems > 0) {
|
|
541
|
-
return this.scrollToIndex(totalItems - 1, "end");
|
|
542
|
-
}
|
|
543
|
-
return Promise.resolve();
|
|
544
|
-
},
|
|
545
|
-
|
|
546
|
-
scrollToPage: async function (
|
|
547
|
-
pageNum: number,
|
|
548
|
-
alignment: "start" | "center" | "end" = "start",
|
|
549
|
-
animate?: boolean,
|
|
550
|
-
) {
|
|
551
|
-
// console.log(`[VList] scrollToPage(${pageNum})`);
|
|
552
|
-
|
|
553
|
-
// Get limit from config (rangeSize) or default
|
|
554
|
-
const limit = config.pagination?.limit || 20;
|
|
555
|
-
|
|
556
|
-
// Check if we're in cursor mode
|
|
557
|
-
if (config.pagination?.strategy === "cursor") {
|
|
558
|
-
// In cursor mode, we need to handle sequential loading
|
|
559
|
-
const collection = (component.viewport as any)?.collection;
|
|
560
|
-
if (collection) {
|
|
561
|
-
const loadedRanges = collection.getLoadedRanges();
|
|
562
|
-
const highestLoadedPage = loadedRanges.size;
|
|
563
|
-
|
|
564
|
-
if (pageNum > highestLoadedPage + 1) {
|
|
565
|
-
console.warn(
|
|
566
|
-
`[VList] Cannot jump to page ${pageNum} in cursor mode. ` +
|
|
567
|
-
`Loading pages sequentially from ${
|
|
568
|
-
highestLoadedPage + 1
|
|
569
|
-
} to ${pageNum}`,
|
|
570
|
-
);
|
|
571
|
-
}
|
|
572
|
-
}
|
|
573
|
-
}
|
|
574
|
-
|
|
575
|
-
// Use viewport's scrollToPage if available
|
|
576
|
-
if ((component.viewport as any)?.scrollToPage) {
|
|
577
|
-
(component.viewport as any).scrollToPage(pageNum, limit, alignment);
|
|
578
|
-
} else {
|
|
579
|
-
// Fallback to scrollToIndex
|
|
580
|
-
const targetIndex = (pageNum - 1) * limit;
|
|
581
|
-
await this.scrollToIndex(targetIndex, alignment);
|
|
582
|
-
}
|
|
583
|
-
},
|
|
584
|
-
|
|
585
|
-
getScrollPosition: () => {
|
|
586
|
-
return component.viewport?.getScrollPosition() || 0;
|
|
587
|
-
},
|
|
588
|
-
|
|
589
|
-
getCurrentCursor: () => {
|
|
590
|
-
const collection = (component.viewport as any)?.collection;
|
|
591
|
-
return collection?.getCurrentCursor?.() || null;
|
|
592
|
-
},
|
|
593
|
-
|
|
594
|
-
setScrollAnimation(enabled: boolean) {
|
|
595
|
-
// Store animation preference (would be used by viewport scrolling feature)
|
|
596
|
-
component.scrollAnimation = enabled;
|
|
597
|
-
},
|
|
598
|
-
|
|
599
|
-
// State
|
|
600
|
-
getState() {
|
|
601
|
-
return {
|
|
602
|
-
items: this.getItems(),
|
|
603
|
-
totalItems: component.viewport?.collection
|
|
604
|
-
? component.viewport.collection.getTotalItems()
|
|
605
|
-
: component.totalItems || 0,
|
|
606
|
-
visibleRange: component.viewport?.getVisibleRange() || {
|
|
607
|
-
start: 0,
|
|
608
|
-
end: 0,
|
|
609
|
-
},
|
|
610
|
-
scrollPosition: component.viewport?.getScrollPosition() || 0,
|
|
611
|
-
selectedIds: this.getSelectedIds(),
|
|
612
|
-
isLoading: this.isLoading(),
|
|
613
|
-
};
|
|
614
|
-
},
|
|
615
|
-
|
|
616
|
-
// Lifecycle
|
|
617
|
-
destroy() {
|
|
618
|
-
if (component.viewport?.destroy) {
|
|
619
|
-
component.viewport.destroy();
|
|
620
|
-
}
|
|
621
|
-
selectedIds.clear();
|
|
622
|
-
component.emit?.("destroyed");
|
|
623
|
-
},
|
|
624
|
-
};
|
|
625
|
-
};
|
|
626
|
-
};
|