mtrl-addons 0.2.1 → 0.2.2
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/AI.md +28 -230
- package/CLAUDE.md +882 -0
- package/build.js +286 -110
- package/package.json +2 -1
- package/src/components/vlist/features/api.ts +316 -12
- package/src/components/vlist/features/selection.ts +248 -256
- package/src/components/vlist/features/viewport.ts +1 -7
- package/src/components/vlist/index.ts +1 -0
- package/src/components/vlist/types.ts +140 -8
- package/src/core/layout/schema.ts +81 -38
- package/src/core/viewport/constants.ts +7 -2
- package/src/core/viewport/features/collection.ts +376 -76
- package/src/core/viewport/features/item-size.ts +4 -4
- package/src/core/viewport/features/momentum.ts +11 -2
- package/src/core/viewport/features/rendering.ts +424 -30
- package/src/core/viewport/features/scrolling.ts +41 -25
- package/src/core/viewport/features/utils.ts +11 -5
- package/src/core/viewport/features/virtual.ts +169 -28
- package/src/core/viewport/types.ts +2 -2
- package/src/core/viewport/viewport.ts +29 -10
- package/src/styles/components/_vlist.scss +234 -213
|
@@ -13,101 +13,76 @@ interface SelectionState {
|
|
|
13
13
|
lastSelectedIndex?: number;
|
|
14
14
|
}
|
|
15
15
|
|
|
16
|
+
/**
|
|
17
|
+
* Get item ID from element or item data
|
|
18
|
+
*/
|
|
19
|
+
const getItemId = (item: any): string | number | undefined => {
|
|
20
|
+
if (item?.id !== undefined) return item.id;
|
|
21
|
+
if (item?._id !== undefined) return item._id;
|
|
22
|
+
if (typeof item === "string" || typeof item === "number") return item;
|
|
23
|
+
return undefined;
|
|
24
|
+
};
|
|
25
|
+
|
|
16
26
|
/**
|
|
17
27
|
* Adds selection management capabilities to VList component
|
|
18
|
-
* Works with viewport's virtual scrolling architecture
|
|
19
|
-
*
|
|
20
|
-
* @param config - VList configuration with selection options
|
|
21
|
-
* @returns Function that enhances a component with selection management
|
|
22
28
|
*/
|
|
23
29
|
export const withSelection = <T extends VListItem = VListItem>(
|
|
24
|
-
config: VListConfig<T
|
|
30
|
+
config: VListConfig<T>,
|
|
25
31
|
) => {
|
|
26
32
|
return (component: VListComponent<T>): VListComponent<T> => {
|
|
27
|
-
// Skip if selection is not enabled
|
|
28
33
|
if (!config.selection?.enabled || config.selection?.mode === "none") {
|
|
29
|
-
// console.log("🎯 [Selection] Skipped - not enabled or mode is none");
|
|
30
34
|
return component;
|
|
31
35
|
}
|
|
32
36
|
|
|
33
|
-
// console.log("🎯 [Selection] Initializing selection feature", {
|
|
34
|
-
// enabled: config.selection?.enabled,
|
|
35
|
-
// mode: config.selection?.mode,
|
|
36
|
-
// });
|
|
37
|
-
|
|
38
|
-
// Initialize selection state
|
|
39
37
|
const state: SelectionState = {
|
|
40
38
|
selectedIds: new Set(),
|
|
41
39
|
mode: config.selection?.mode || "single",
|
|
42
40
|
lastSelectedIndex: undefined,
|
|
43
41
|
};
|
|
44
42
|
|
|
45
|
-
// Get configuration options
|
|
46
43
|
const requireModifiers = config.selection?.requireModifiers ?? false;
|
|
47
44
|
|
|
48
|
-
// Add BEM modifier class to container element
|
|
49
|
-
const addContainerModifier = () => {
|
|
50
|
-
if (component.element) {
|
|
51
|
-
// Add selection mode modifier class following BEM convention
|
|
52
|
-
addClass(component.element, `vlist--selection`);
|
|
53
|
-
// Also add specific mode modifier
|
|
54
|
-
addClass(component.element, `vlist--selection-${state.mode}`);
|
|
55
|
-
}
|
|
56
|
-
};
|
|
57
|
-
|
|
58
|
-
// Defer initialization of pre-selected items until after setup
|
|
59
|
-
const initializePreselectedItems = () => {
|
|
60
|
-
if (config.selection?.selectedIndices && component.getItems) {
|
|
61
|
-
config.selection.selectedIndices.forEach((index) => {
|
|
62
|
-
const items = component.getItems();
|
|
63
|
-
const item = items?.[index];
|
|
64
|
-
if (item && (item as any).id !== undefined) {
|
|
65
|
-
state.selectedIds.add((item as any).id);
|
|
66
|
-
}
|
|
67
|
-
});
|
|
68
|
-
}
|
|
69
|
-
};
|
|
70
|
-
|
|
71
45
|
/**
|
|
72
|
-
* Apply selection class to
|
|
46
|
+
* Apply selection class to rendered elements matching selected IDs
|
|
73
47
|
*/
|
|
74
|
-
const
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
}
|
|
80
|
-
};
|
|
48
|
+
const applySelectionToElements = () => {
|
|
49
|
+
const container = component.element?.querySelector(
|
|
50
|
+
`.${PREFIX}-viewport-items`,
|
|
51
|
+
);
|
|
52
|
+
if (!container) return;
|
|
81
53
|
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
54
|
+
const items = container.querySelectorAll(
|
|
55
|
+
`.${PREFIX}-viewport-item[data-id]`,
|
|
56
|
+
);
|
|
57
|
+
|
|
58
|
+
items.forEach((el) => {
|
|
59
|
+
const element = el as HTMLElement;
|
|
60
|
+
const id = element.dataset.id;
|
|
61
|
+
if (id !== undefined) {
|
|
62
|
+
// Check both string and number versions of the ID
|
|
63
|
+
const isSelected =
|
|
64
|
+
state.selectedIds.has(id) || state.selectedIds.has(Number(id));
|
|
65
|
+
if (isSelected) {
|
|
66
|
+
addClass(element, VLIST_CLASSES.SELECTED);
|
|
67
|
+
} else {
|
|
68
|
+
removeClass(element, VLIST_CLASSES.SELECTED);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
});
|
|
89
72
|
};
|
|
90
73
|
|
|
91
74
|
/**
|
|
92
75
|
* Handle item click for selection
|
|
93
76
|
*/
|
|
94
77
|
const handleItemClick = (e: MouseEvent) => {
|
|
95
|
-
// console.log("🎯 [Selection] Click detected on:", e.target);
|
|
96
|
-
|
|
97
|
-
// Find the clicked viewport item element (wrapper)
|
|
98
78
|
const viewportItem = (e.target as HTMLElement).closest(
|
|
99
|
-
`.${PREFIX}-viewport-item[data-index]
|
|
79
|
+
`.${PREFIX}-viewport-item[data-index]`,
|
|
100
80
|
) as HTMLElement;
|
|
101
|
-
if (!viewportItem)
|
|
102
|
-
console.log("🎯 [Selection] No viewport item found for click");
|
|
103
|
-
return;
|
|
104
|
-
}
|
|
81
|
+
if (!viewportItem) return;
|
|
105
82
|
|
|
106
83
|
const index = parseInt(viewportItem.dataset.index || "-1");
|
|
107
|
-
// console.log(`🎯 [Selection] Clicked item index: ${index}`);
|
|
108
84
|
if (index < 0) return;
|
|
109
85
|
|
|
110
|
-
// Get the item data
|
|
111
86
|
const enhancedComponent = component as any;
|
|
112
87
|
const items = enhancedComponent.getItems?.();
|
|
113
88
|
const item = items?.[index];
|
|
@@ -116,25 +91,10 @@ export const withSelection = <T extends VListItem = VListItem>(
|
|
|
116
91
|
const itemId = getItemId(item);
|
|
117
92
|
if (itemId === undefined) return;
|
|
118
93
|
|
|
119
|
-
// Handle selection based on mode
|
|
120
94
|
const wasSelected = state.selectedIds.has(itemId);
|
|
121
95
|
|
|
122
|
-
// console.log("🎯 [Selection] Click detected:", {
|
|
123
|
-
// index,
|
|
124
|
-
// itemId,
|
|
125
|
-
// wasSelected,
|
|
126
|
-
// mode: state.mode,
|
|
127
|
-
// shiftKey: e.shiftKey,
|
|
128
|
-
// ctrlKey: e.ctrlKey,
|
|
129
|
-
// metaKey: e.metaKey,
|
|
130
|
-
// lastSelectedIndex: state.lastSelectedIndex,
|
|
131
|
-
// });
|
|
132
|
-
|
|
133
96
|
if (state.mode === "single") {
|
|
134
|
-
// Clear previous selection
|
|
135
97
|
state.selectedIds.clear();
|
|
136
|
-
|
|
137
|
-
// Toggle selection
|
|
138
98
|
if (!wasSelected) {
|
|
139
99
|
state.selectedIds.add(itemId);
|
|
140
100
|
state.lastSelectedIndex = index;
|
|
@@ -142,14 +102,11 @@ export const withSelection = <T extends VListItem = VListItem>(
|
|
|
142
102
|
state.lastSelectedIndex = undefined;
|
|
143
103
|
}
|
|
144
104
|
} else if (state.mode === "multiple") {
|
|
145
|
-
// Handle multi-select with keyboard modifiers
|
|
146
105
|
if (e.shiftKey && state.lastSelectedIndex !== undefined) {
|
|
147
|
-
// Range selection
|
|
148
106
|
const start = Math.min(state.lastSelectedIndex, index);
|
|
149
107
|
const end = Math.max(state.lastSelectedIndex, index);
|
|
150
108
|
|
|
151
109
|
if (!e.ctrlKey && !e.metaKey) {
|
|
152
|
-
// Clear existing selection if not holding ctrl/cmd
|
|
153
110
|
state.selectedIds.clear();
|
|
154
111
|
}
|
|
155
112
|
|
|
@@ -161,7 +118,6 @@ export const withSelection = <T extends VListItem = VListItem>(
|
|
|
161
118
|
}
|
|
162
119
|
}
|
|
163
120
|
} else if (e.ctrlKey || e.metaKey) {
|
|
164
|
-
// Toggle individual selection with Ctrl/Cmd
|
|
165
121
|
if (wasSelected) {
|
|
166
122
|
state.selectedIds.delete(itemId);
|
|
167
123
|
} else {
|
|
@@ -169,19 +125,10 @@ export const withSelection = <T extends VListItem = VListItem>(
|
|
|
169
125
|
}
|
|
170
126
|
state.lastSelectedIndex = index;
|
|
171
127
|
} else {
|
|
172
|
-
// Single click without modifiers
|
|
173
|
-
// console.log("🎯 [Selection] Single click without modifiers:", {
|
|
174
|
-
// requireModifiers,
|
|
175
|
-
// wasSelected,
|
|
176
|
-
// willToggle: !requireModifiers,
|
|
177
|
-
// });
|
|
178
|
-
|
|
179
128
|
if (requireModifiers) {
|
|
180
|
-
// If modifiers are required, single click selects only this item
|
|
181
129
|
state.selectedIds.clear();
|
|
182
130
|
state.selectedIds.add(itemId);
|
|
183
131
|
} else {
|
|
184
|
-
// If modifiers are NOT required, single click toggles selection
|
|
185
132
|
if (wasSelected) {
|
|
186
133
|
state.selectedIds.delete(itemId);
|
|
187
134
|
} else {
|
|
@@ -192,216 +139,163 @@ export const withSelection = <T extends VListItem = VListItem>(
|
|
|
192
139
|
}
|
|
193
140
|
}
|
|
194
141
|
|
|
195
|
-
|
|
196
|
-
|
|
142
|
+
applySelectionToElements();
|
|
143
|
+
emitSelectionChange();
|
|
144
|
+
};
|
|
197
145
|
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
146
|
+
/**
|
|
147
|
+
* Emit selection change event
|
|
148
|
+
*/
|
|
149
|
+
const emitSelectionChange = () => {
|
|
150
|
+
const enhancedComponent = component as any;
|
|
151
|
+
const items = enhancedComponent.getItems?.() || [];
|
|
204
152
|
|
|
205
|
-
const
|
|
206
|
-
|
|
153
|
+
const selectedItems = items.filter((item: any) => {
|
|
154
|
+
const id = getItemId(item);
|
|
155
|
+
return id !== undefined && state.selectedIds.has(id);
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
const selectedIndices = items.reduce(
|
|
159
|
+
(acc: number[], item: any, idx: number) => {
|
|
207
160
|
const id = getItemId(item);
|
|
208
161
|
if (id !== undefined && state.selectedIds.has(id)) {
|
|
209
162
|
acc.push(idx);
|
|
210
163
|
}
|
|
211
164
|
return acc;
|
|
212
|
-
},
|
|
165
|
+
},
|
|
166
|
+
[] as number[],
|
|
167
|
+
);
|
|
213
168
|
|
|
214
|
-
component.emit?.("selection:change", {
|
|
215
|
-
selectedItems,
|
|
216
|
-
selectedIndices,
|
|
217
|
-
});
|
|
169
|
+
component.emit?.("selection:change", { selectedItems, selectedIndices });
|
|
218
170
|
|
|
219
|
-
// Call the selection change callback if provided
|
|
220
171
|
if (config.selection?.onSelectionChange) {
|
|
221
172
|
config.selection.onSelectionChange(selectedItems, selectedIndices);
|
|
222
173
|
}
|
|
223
|
-
|
|
224
|
-
// Emit individual item selection event
|
|
225
|
-
component.emit?.("item:selection:change", {
|
|
226
|
-
item,
|
|
227
|
-
index,
|
|
228
|
-
isSelected: state.selectedIds.has(itemId),
|
|
229
|
-
});
|
|
230
174
|
};
|
|
231
175
|
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
if (!container) {
|
|
240
|
-
// console.warn("🎯 [Selection] No viewport items container found");
|
|
241
|
-
return;
|
|
176
|
+
// Setup: listen for renders and clicks
|
|
177
|
+
const setup = () => {
|
|
178
|
+
// Add mode class to container
|
|
179
|
+
if (component.element) {
|
|
180
|
+
addClass(component.element, "vlist--selection");
|
|
181
|
+
addClass(component.element, `vlist--selection-${state.mode}`);
|
|
182
|
+
component.element.addEventListener("click", handleItemClick, true);
|
|
242
183
|
}
|
|
243
184
|
|
|
244
|
-
|
|
245
|
-
|
|
185
|
+
// Apply selection after each render
|
|
186
|
+
(component as any).on?.(
|
|
187
|
+
"viewport:items-rendered",
|
|
188
|
+
applySelectionToElements,
|
|
189
|
+
);
|
|
190
|
+
(component as any).on?.("viewport:rendered", applySelectionToElements);
|
|
191
|
+
|
|
192
|
+
// Clean up selection when item is removed
|
|
193
|
+
(component as any).on?.(
|
|
194
|
+
"item:removed",
|
|
195
|
+
(data: { item: any; index: number }) => {
|
|
196
|
+
const itemId = getItemId(data.item);
|
|
197
|
+
if (itemId !== undefined && state.selectedIds.has(itemId)) {
|
|
198
|
+
state.selectedIds.delete(itemId);
|
|
199
|
+
// If the removed item was the last selected index, clear it
|
|
200
|
+
if (state.lastSelectedIndex === data.index) {
|
|
201
|
+
state.lastSelectedIndex = undefined;
|
|
202
|
+
} else if (
|
|
203
|
+
state.lastSelectedIndex !== undefined &&
|
|
204
|
+
state.lastSelectedIndex > data.index
|
|
205
|
+
) {
|
|
206
|
+
// Adjust lastSelectedIndex since items shifted down
|
|
207
|
+
state.lastSelectedIndex--;
|
|
208
|
+
}
|
|
209
|
+
emitSelectionChange();
|
|
210
|
+
}
|
|
211
|
+
},
|
|
246
212
|
);
|
|
247
|
-
// console.log(
|
|
248
|
-
// `🎯 [Selection] Updating ${viewportItems.length} visible elements`
|
|
249
|
-
// );
|
|
250
|
-
|
|
251
|
-
const enhancedComponent = component as any;
|
|
252
|
-
const items = enhancedComponent.getItems?.();
|
|
253
|
-
|
|
254
|
-
viewportItems.forEach((viewportItem) => {
|
|
255
|
-
const index = parseInt(
|
|
256
|
-
(viewportItem as HTMLElement).dataset.index || "-1"
|
|
257
|
-
);
|
|
258
|
-
if (index < 0) return;
|
|
259
|
-
|
|
260
|
-
const item = items?.[index];
|
|
261
|
-
if (!item) return;
|
|
262
|
-
|
|
263
|
-
const itemId = getItemId(item);
|
|
264
|
-
if (itemId === undefined) return;
|
|
265
|
-
|
|
266
|
-
const isSelected = state.selectedIds.has(itemId);
|
|
267
|
-
|
|
268
|
-
// Apply selection class to the viewport item itself
|
|
269
|
-
// The new layout system doesn't have a separate inner item
|
|
270
|
-
applySelectionClass(viewportItem as HTMLElement, isSelected);
|
|
271
|
-
});
|
|
272
213
|
};
|
|
273
214
|
|
|
274
|
-
//
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
// Using type assertion since viewport:rendered is not in ListEvents type
|
|
293
|
-
(component as any).on?.("viewport:rendered", () => {
|
|
294
|
-
updateVisibleElements();
|
|
295
|
-
});
|
|
296
|
-
|
|
297
|
-
// Add click listener to the viewport element
|
|
298
|
-
if (component.element) {
|
|
299
|
-
// Use capture phase to ensure we get the event
|
|
300
|
-
component.element.addEventListener("click", handleItemClick, true);
|
|
301
|
-
// console.log(
|
|
302
|
-
// "🎯 [Selection] Click handler attached to element (capture phase)"
|
|
303
|
-
// );
|
|
304
|
-
|
|
305
|
-
// Test if handler works
|
|
306
|
-
// setTimeout(() => {
|
|
307
|
-
// const testItem = component.element?.querySelector(
|
|
308
|
-
// `.${PREFIX}-viewport-item`
|
|
309
|
-
// );
|
|
310
|
-
// // console.log("🎯 [Selection] Test item found:", !!testItem);
|
|
311
|
-
// }, 500);
|
|
215
|
+
// Initialize after component is ready
|
|
216
|
+
setTimeout(setup, 0);
|
|
217
|
+
|
|
218
|
+
// Listen for initial load complete to auto-select item
|
|
219
|
+
// This must be in withSelection because selectById is defined here
|
|
220
|
+
(component as any).on?.(
|
|
221
|
+
"collection:initial-load-complete",
|
|
222
|
+
(data: { selectId: string | number }) => {
|
|
223
|
+
if (data?.selectId !== undefined) {
|
|
224
|
+
// Use requestAnimationFrame to ensure rendering is complete
|
|
225
|
+
requestAnimationFrame(() => {
|
|
226
|
+
if (state.mode === "single") {
|
|
227
|
+
state.selectedIds.clear();
|
|
228
|
+
}
|
|
229
|
+
state.selectedIds.add(data.selectId);
|
|
230
|
+
applySelectionToElements();
|
|
231
|
+
emitSelectionChange();
|
|
232
|
+
});
|
|
312
233
|
}
|
|
313
|
-
},
|
|
314
|
-
|
|
234
|
+
},
|
|
235
|
+
);
|
|
315
236
|
|
|
316
|
-
//
|
|
237
|
+
// Cleanup on destroy
|
|
317
238
|
const originalDestroy = component.destroy;
|
|
318
239
|
component.destroy = () => {
|
|
319
|
-
|
|
320
|
-
component.element.removeEventListener("click", handleItemClick, true);
|
|
321
|
-
}
|
|
240
|
+
component.element?.removeEventListener("click", handleItemClick, true);
|
|
322
241
|
originalDestroy?.();
|
|
323
242
|
};
|
|
324
243
|
|
|
325
|
-
//
|
|
326
|
-
|
|
244
|
+
// Enhanced component with selection API
|
|
245
|
+
return {
|
|
327
246
|
...component,
|
|
328
247
|
|
|
329
|
-
// Selection API methods
|
|
330
248
|
selectItems(indices: number[]) {
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
// In single mode, only select the first item
|
|
341
|
-
indices = [indices[0]];
|
|
249
|
+
const items = (this as any).getItems?.();
|
|
250
|
+
if (!items) return;
|
|
251
|
+
|
|
252
|
+
if (state.mode === "single") {
|
|
253
|
+
// In single mode, clear previous selection before adding new one
|
|
254
|
+
state.selectedIds.clear();
|
|
255
|
+
if (indices.length > 1) {
|
|
256
|
+
indices = [indices[0]];
|
|
257
|
+
}
|
|
342
258
|
}
|
|
343
259
|
|
|
344
260
|
indices.forEach((index) => {
|
|
345
|
-
const
|
|
346
|
-
const itemId = getItemId(item);
|
|
261
|
+
const itemId = getItemId(items[index]);
|
|
347
262
|
if (itemId !== undefined) {
|
|
348
263
|
state.selectedIds.add(itemId);
|
|
349
264
|
}
|
|
350
265
|
});
|
|
351
266
|
|
|
352
|
-
// Update lastSelectedIndex for shift+click range selection
|
|
353
267
|
if (indices.length > 0) {
|
|
354
268
|
state.lastSelectedIndex = indices[indices.length - 1];
|
|
355
269
|
}
|
|
356
270
|
|
|
357
|
-
|
|
358
|
-
(
|
|
359
|
-
selectedItems: (this as any).getSelectedItems(),
|
|
360
|
-
selectedIndices: (this as any).getSelectedIndices(),
|
|
361
|
-
});
|
|
271
|
+
applySelectionToElements();
|
|
272
|
+
emitSelectionChange();
|
|
362
273
|
},
|
|
363
274
|
|
|
364
275
|
deselectItems(indices: number[]) {
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
if (!getItemsFn) {
|
|
368
|
-
console.warn("🎯 [Selection] getItems not available yet");
|
|
369
|
-
return;
|
|
370
|
-
}
|
|
371
|
-
const items = getItemsFn();
|
|
276
|
+
const items = (this as any).getItems?.();
|
|
277
|
+
if (!items) return;
|
|
372
278
|
|
|
373
279
|
indices.forEach((index) => {
|
|
374
|
-
const
|
|
375
|
-
const itemId = getItemId(item);
|
|
280
|
+
const itemId = getItemId(items[index]);
|
|
376
281
|
if (itemId !== undefined) {
|
|
377
282
|
state.selectedIds.delete(itemId);
|
|
378
283
|
}
|
|
379
284
|
});
|
|
380
285
|
|
|
381
|
-
|
|
382
|
-
(
|
|
383
|
-
selectedItems: (this as any).getSelectedItems(),
|
|
384
|
-
selectedIndices: (this as any).getSelectedIndices(),
|
|
385
|
-
});
|
|
286
|
+
applySelectionToElements();
|
|
287
|
+
emitSelectionChange();
|
|
386
288
|
},
|
|
387
289
|
|
|
388
290
|
clearSelection() {
|
|
389
291
|
state.selectedIds.clear();
|
|
390
292
|
state.lastSelectedIndex = undefined;
|
|
391
|
-
|
|
392
|
-
(
|
|
393
|
-
selectedItems: [],
|
|
394
|
-
selectedIndices: [],
|
|
395
|
-
});
|
|
293
|
+
applySelectionToElements();
|
|
294
|
+
emitSelectionChange();
|
|
396
295
|
},
|
|
397
296
|
|
|
398
297
|
getSelectedItems(): T[] {
|
|
399
|
-
|
|
400
|
-
const getItemsFn = (this as any).getItems;
|
|
401
|
-
if (!getItemsFn) {
|
|
402
|
-
return [];
|
|
403
|
-
}
|
|
404
|
-
const items = getItemsFn() || [];
|
|
298
|
+
const items = (this as any).getItems?.() || [];
|
|
405
299
|
return items.filter((item: any) => {
|
|
406
300
|
const id = getItemId(item);
|
|
407
301
|
return id !== undefined && state.selectedIds.has(id);
|
|
@@ -409,12 +303,7 @@ export const withSelection = <T extends VListItem = VListItem>(
|
|
|
409
303
|
},
|
|
410
304
|
|
|
411
305
|
getSelectedIndices(): number[] {
|
|
412
|
-
|
|
413
|
-
const getItemsFn = (this as any).getItems;
|
|
414
|
-
if (!getItemsFn) {
|
|
415
|
-
return [];
|
|
416
|
-
}
|
|
417
|
-
const items = getItemsFn() || [];
|
|
306
|
+
const items = (this as any).getItems?.() || [];
|
|
418
307
|
return items.reduce((acc: number[], item: any, index: number) => {
|
|
419
308
|
const id = getItemId(item);
|
|
420
309
|
if (id !== undefined && state.selectedIds.has(id)) {
|
|
@@ -425,19 +314,122 @@ export const withSelection = <T extends VListItem = VListItem>(
|
|
|
425
314
|
},
|
|
426
315
|
|
|
427
316
|
isSelected(index: number): boolean {
|
|
428
|
-
|
|
429
|
-
const
|
|
430
|
-
|
|
317
|
+
const items = (this as any).getItems?.();
|
|
318
|
+
const itemId = getItemId(items?.[index]);
|
|
319
|
+
return itemId !== undefined && state.selectedIds.has(itemId);
|
|
320
|
+
},
|
|
321
|
+
|
|
322
|
+
selectById(id: string | number, silent: boolean = false): boolean {
|
|
323
|
+
if (id === undefined || id === null) return false;
|
|
324
|
+
|
|
325
|
+
if (state.mode === "single") {
|
|
326
|
+
state.selectedIds.clear();
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
state.selectedIds.add(id);
|
|
330
|
+
applySelectionToElements();
|
|
331
|
+
if (!silent) {
|
|
332
|
+
emitSelectionChange();
|
|
333
|
+
}
|
|
334
|
+
return true;
|
|
335
|
+
},
|
|
336
|
+
|
|
337
|
+
/**
|
|
338
|
+
* Select item at index, scrolling and waiting for data if needed
|
|
339
|
+
* Handles virtual scrolling by loading data before selecting
|
|
340
|
+
*/
|
|
341
|
+
async selectAtIndex(index: number): Promise<boolean> {
|
|
342
|
+
const enhancedComponent = component as any;
|
|
343
|
+
const totalItems = enhancedComponent.getItemCount?.() || 0;
|
|
344
|
+
|
|
345
|
+
if (index < 0 || index >= totalItems) {
|
|
431
346
|
return false;
|
|
432
347
|
}
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
348
|
+
|
|
349
|
+
// First scroll to the index (triggers data loading if needed)
|
|
350
|
+
if (enhancedComponent.viewport?.scrollToIndex) {
|
|
351
|
+
enhancedComponent.viewport.scrollToIndex(index);
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
// Try to select immediately - works if item is already loaded
|
|
355
|
+
const items = enhancedComponent.getItems?.() || [];
|
|
356
|
+
if (items[index]) {
|
|
357
|
+
this.selectItems([index]);
|
|
358
|
+
return true;
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
// Item not loaded yet - wait for data to load then select
|
|
362
|
+
return new Promise<boolean>((resolve) => {
|
|
363
|
+
let resolved = false;
|
|
364
|
+
|
|
365
|
+
const onRangeLoaded = () => {
|
|
366
|
+
if (resolved) return;
|
|
367
|
+
resolved = true;
|
|
368
|
+
component.off?.("viewport:range-loaded", onRangeLoaded);
|
|
369
|
+
requestAnimationFrame(() => {
|
|
370
|
+
this.selectItems([index]);
|
|
371
|
+
resolve(true);
|
|
372
|
+
});
|
|
373
|
+
};
|
|
374
|
+
|
|
375
|
+
component.on?.("viewport:range-loaded", onRangeLoaded);
|
|
376
|
+
|
|
377
|
+
// Fallback timeout in case event doesn't fire (data already loaded)
|
|
378
|
+
setTimeout(() => {
|
|
379
|
+
if (resolved) return;
|
|
380
|
+
resolved = true;
|
|
381
|
+
component.off?.("viewport:range-loaded", onRangeLoaded);
|
|
382
|
+
this.selectItems([index]);
|
|
383
|
+
resolve(true);
|
|
384
|
+
}, 300);
|
|
385
|
+
});
|
|
437
386
|
},
|
|
438
|
-
};
|
|
439
387
|
|
|
440
|
-
|
|
388
|
+
/**
|
|
389
|
+
* Select next item relative to current selection
|
|
390
|
+
* Handles virtual scrolling by loading data before selecting
|
|
391
|
+
*/
|
|
392
|
+
async selectNext(): Promise<boolean> {
|
|
393
|
+
const enhancedComponent = component as any;
|
|
394
|
+
const selectedIndices = this.getSelectedIndices();
|
|
395
|
+
|
|
396
|
+
if (selectedIndices.length === 0) {
|
|
397
|
+
// Select first item
|
|
398
|
+
return this.selectAtIndex(0);
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
const currentIndex = selectedIndices[0];
|
|
402
|
+
const nextIndex = currentIndex + 1;
|
|
403
|
+
const totalItems = enhancedComponent.getItemCount?.() || 0;
|
|
404
|
+
|
|
405
|
+
if (nextIndex < totalItems) {
|
|
406
|
+
return this.selectAtIndex(nextIndex);
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
return false;
|
|
410
|
+
},
|
|
411
|
+
|
|
412
|
+
/**
|
|
413
|
+
* Select previous item relative to current selection
|
|
414
|
+
* Handles virtual scrolling by loading data before selecting
|
|
415
|
+
*/
|
|
416
|
+
async selectPrevious(): Promise<boolean> {
|
|
417
|
+
const selectedIndices = this.getSelectedIndices();
|
|
418
|
+
|
|
419
|
+
if (selectedIndices.length === 0) {
|
|
420
|
+
return false;
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
const currentIndex = selectedIndices[0];
|
|
424
|
+
const prevIndex = currentIndex - 1;
|
|
425
|
+
|
|
426
|
+
if (prevIndex >= 0) {
|
|
427
|
+
return this.selectAtIndex(prevIndex);
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
return false;
|
|
431
|
+
},
|
|
432
|
+
};
|
|
441
433
|
};
|
|
442
434
|
};
|
|
443
435
|
|
|
@@ -12,15 +12,9 @@ import { createViewport } from "../../../core/viewport";
|
|
|
12
12
|
* Adds viewport functionality to VList
|
|
13
13
|
*/
|
|
14
14
|
export const withViewport = <T extends VListItem = VListItem>(
|
|
15
|
-
config: VListConfig<T
|
|
15
|
+
config: VListConfig<T>,
|
|
16
16
|
) => {
|
|
17
17
|
return (component: any) => {
|
|
18
|
-
// console.log("📋 [VList] Applying viewport feature", {
|
|
19
|
-
// hasElement: !!component.element,
|
|
20
|
-
// hasItems: !!config.items,
|
|
21
|
-
// itemCount: config.items?.length || 0,
|
|
22
|
-
// });
|
|
23
|
-
|
|
24
18
|
// Set initial items if provided
|
|
25
19
|
if (config.items) {
|
|
26
20
|
component.items = config.items;
|