mtrl-addons 0.1.2 → 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 +253 -24
- package/package.json +14 -4
- package/scripts/debug/vlist-selection.ts +121 -0
- package/src/components/index.ts +5 -41
- package/src/components/{list → vlist}/config.ts +66 -95
- package/src/components/vlist/constants.ts +23 -0
- package/src/components/vlist/features/api.ts +626 -0
- package/src/components/vlist/features/index.ts +10 -0
- package/src/components/vlist/features/selection.ts +436 -0
- package/src/components/vlist/features/viewport.ts +59 -0
- package/src/components/vlist/index.ts +17 -0
- package/src/components/{list → vlist}/types.ts +242 -32
- package/src/components/vlist/vlist.ts +92 -0
- package/src/core/compose/features/gestures/index.ts +227 -0
- package/src/core/compose/features/gestures/longpress.ts +383 -0
- package/src/core/compose/features/gestures/pan.ts +424 -0
- package/src/core/compose/features/gestures/pinch.ts +475 -0
- package/src/core/compose/features/gestures/rotate.ts +485 -0
- package/src/core/compose/features/gestures/swipe.ts +492 -0
- package/src/core/compose/features/gestures/tap.ts +334 -0
- package/src/core/compose/features/index.ts +2 -38
- package/src/core/compose/index.ts +13 -29
- package/src/core/gestures/index.ts +31 -0
- package/src/core/gestures/longpress.ts +68 -0
- package/src/core/gestures/manager.ts +418 -0
- package/src/core/gestures/pan.ts +48 -0
- package/src/core/gestures/pinch.ts +58 -0
- package/src/core/gestures/rotate.ts +58 -0
- package/src/core/gestures/swipe.ts +66 -0
- package/src/core/gestures/tap.ts +45 -0
- package/src/core/gestures/types.ts +387 -0
- package/src/core/gestures/utils.ts +128 -0
- package/src/core/index.ts +27 -151
- package/src/core/layout/schema.ts +153 -72
- package/src/core/layout/types.ts +5 -2
- package/src/core/viewport/constants.ts +145 -0
- package/src/core/viewport/features/base.ts +73 -0
- package/src/core/viewport/features/collection.ts +1182 -0
- package/src/core/viewport/features/events.ts +130 -0
- package/src/core/viewport/features/index.ts +20 -0
- package/src/core/{list-manager/features/viewport → viewport/features}/item-size.ts +31 -34
- package/src/core/{list-manager/features/viewport → viewport/features}/loading.ts +4 -4
- package/src/core/viewport/features/momentum.ts +269 -0
- package/src/core/viewport/features/placeholders.ts +335 -0
- package/src/core/viewport/features/rendering.ts +962 -0
- package/src/core/viewport/features/scrollbar.ts +434 -0
- package/src/core/viewport/features/scrolling.ts +634 -0
- package/src/core/viewport/features/utils.ts +94 -0
- package/src/core/viewport/features/virtual.ts +525 -0
- package/src/core/viewport/index.ts +31 -0
- package/src/core/viewport/types.ts +133 -0
- package/src/core/viewport/utils/speed-tracker.ts +79 -0
- package/src/core/viewport/viewport.ts +265 -0
- package/src/index.ts +0 -7
- package/src/styles/components/_vlist.scss +352 -0
- package/src/styles/index.scss +1 -1
- package/test/components/vlist-selection.test.ts +240 -0
- package/test/components/vlist.test.ts +63 -0
- package/test/core/collection/adapter.test.ts +161 -0
- package/bun.lock +0 -792
- package/src/components/list/api.ts +0 -314
- package/src/components/list/constants.ts +0 -56
- package/src/components/list/features/api.ts +0 -428
- package/src/components/list/features/index.ts +0 -31
- package/src/components/list/features/list-manager.ts +0 -502
- package/src/components/list/index.ts +0 -39
- package/src/components/list/list.ts +0 -234
- package/src/core/collection/base-collection.ts +0 -100
- package/src/core/collection/collection-composer.ts +0 -178
- package/src/core/collection/collection.ts +0 -745
- package/src/core/collection/constants.ts +0 -172
- package/src/core/collection/events.ts +0 -428
- package/src/core/collection/features/api/loading.ts +0 -279
- package/src/core/collection/features/operations/data-operations.ts +0 -147
- package/src/core/collection/index.ts +0 -104
- package/src/core/collection/state.ts +0 -497
- package/src/core/collection/types.ts +0 -404
- package/src/core/compose/features/collection.ts +0 -119
- package/src/core/compose/features/selection.ts +0 -213
- package/src/core/compose/features/styling.ts +0 -108
- package/src/core/list-manager/api.ts +0 -599
- package/src/core/list-manager/config.ts +0 -593
- package/src/core/list-manager/constants.ts +0 -268
- package/src/core/list-manager/features/api.ts +0 -58
- package/src/core/list-manager/features/collection/collection.ts +0 -705
- package/src/core/list-manager/features/collection/index.ts +0 -17
- package/src/core/list-manager/features/viewport/constants.ts +0 -42
- package/src/core/list-manager/features/viewport/index.ts +0 -16
- package/src/core/list-manager/features/viewport/placeholders.ts +0 -281
- package/src/core/list-manager/features/viewport/rendering.ts +0 -575
- package/src/core/list-manager/features/viewport/scrollbar.ts +0 -495
- package/src/core/list-manager/features/viewport/scrolling.ts +0 -795
- package/src/core/list-manager/features/viewport/template.ts +0 -220
- package/src/core/list-manager/features/viewport/viewport.ts +0 -654
- package/src/core/list-manager/features/viewport/virtual.ts +0 -309
- package/src/core/list-manager/index.ts +0 -279
- package/src/core/list-manager/list-manager.ts +0 -206
- package/src/core/list-manager/types.ts +0 -439
- package/src/core/list-manager/utils/calculations.ts +0 -290
- package/src/core/list-manager/utils/range-calculator.ts +0 -349
- package/src/core/list-manager/utils/speed-tracker.ts +0 -273
- package/src/styles/components/_list.scss +0 -244
- package/src/types/mtrl.d.ts +0 -6
- package/test/components/list.test.ts +0 -256
- package/test/core/collection/failed-ranges.test.ts +0 -270
- package/test/core/compose/features.test.ts +0 -183
- package/test/core/list-manager/features/collection.test.ts +0 -704
- package/test/core/list-manager/features/viewport.test.ts +0 -698
- package/test/core/list-manager/list-manager.test.ts +0 -593
- package/test/core/list-manager/utils/calculations.test.ts +0 -433
- package/test/core/list-manager/utils/range-calculator.test.ts +0 -569
- package/test/core/list-manager/utils/speed-tracker.test.ts +0 -530
- package/tsconfig.build.json +0 -23
- /package/src/components/{list → vlist}/features.ts +0 -0
- /package/src/core/{compose → viewport}/features/performance.ts +0 -0
|
@@ -0,0 +1,436 @@
|
|
|
1
|
+
// src/components/vlist/features/selection.ts
|
|
2
|
+
|
|
3
|
+
import type { VListConfig, VListComponent, VListItem } from "../types";
|
|
4
|
+
import { VLIST_CLASSES } from "../constants";
|
|
5
|
+
import { PREFIX, addClass, removeClass } from "mtrl";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Selection state interface
|
|
9
|
+
*/
|
|
10
|
+
interface SelectionState {
|
|
11
|
+
selectedIds: Set<string | number>;
|
|
12
|
+
mode: "none" | "single" | "multiple";
|
|
13
|
+
lastSelectedIndex?: number;
|
|
14
|
+
}
|
|
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
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Adds selection management capabilities to VList component
|
|
28
|
+
*/
|
|
29
|
+
export const withSelection = <T extends VListItem = VListItem>(
|
|
30
|
+
config: VListConfig<T>,
|
|
31
|
+
) => {
|
|
32
|
+
return (component: VListComponent<T>): VListComponent<T> => {
|
|
33
|
+
if (!config.selection?.enabled || config.selection?.mode === "none") {
|
|
34
|
+
return component;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const state: SelectionState = {
|
|
38
|
+
selectedIds: new Set(),
|
|
39
|
+
mode: config.selection?.mode || "single",
|
|
40
|
+
lastSelectedIndex: undefined,
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
const requireModifiers = config.selection?.requireModifiers ?? false;
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Apply selection class to rendered elements matching selected IDs
|
|
47
|
+
*/
|
|
48
|
+
const applySelectionToElements = () => {
|
|
49
|
+
const container = component.element?.querySelector(
|
|
50
|
+
`.${PREFIX}-viewport-items`,
|
|
51
|
+
);
|
|
52
|
+
if (!container) return;
|
|
53
|
+
|
|
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
|
+
});
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Handle item click for selection
|
|
76
|
+
*/
|
|
77
|
+
const handleItemClick = (e: MouseEvent) => {
|
|
78
|
+
const viewportItem = (e.target as HTMLElement).closest(
|
|
79
|
+
`.${PREFIX}-viewport-item[data-index]`,
|
|
80
|
+
) as HTMLElement;
|
|
81
|
+
if (!viewportItem) return;
|
|
82
|
+
|
|
83
|
+
const index = parseInt(viewportItem.dataset.index || "-1");
|
|
84
|
+
if (index < 0) return;
|
|
85
|
+
|
|
86
|
+
const enhancedComponent = component as any;
|
|
87
|
+
const items = enhancedComponent.getItems?.();
|
|
88
|
+
const item = items?.[index];
|
|
89
|
+
if (!item) return;
|
|
90
|
+
|
|
91
|
+
const itemId = getItemId(item);
|
|
92
|
+
if (itemId === undefined) return;
|
|
93
|
+
|
|
94
|
+
const wasSelected = state.selectedIds.has(itemId);
|
|
95
|
+
|
|
96
|
+
if (state.mode === "single") {
|
|
97
|
+
state.selectedIds.clear();
|
|
98
|
+
if (!wasSelected) {
|
|
99
|
+
state.selectedIds.add(itemId);
|
|
100
|
+
state.lastSelectedIndex = index;
|
|
101
|
+
} else {
|
|
102
|
+
state.lastSelectedIndex = undefined;
|
|
103
|
+
}
|
|
104
|
+
} else if (state.mode === "multiple") {
|
|
105
|
+
if (e.shiftKey && state.lastSelectedIndex !== undefined) {
|
|
106
|
+
const start = Math.min(state.lastSelectedIndex, index);
|
|
107
|
+
const end = Math.max(state.lastSelectedIndex, index);
|
|
108
|
+
|
|
109
|
+
if (!e.ctrlKey && !e.metaKey) {
|
|
110
|
+
state.selectedIds.clear();
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
for (let i = start; i <= end; i++) {
|
|
114
|
+
const rangeItem = items?.[i];
|
|
115
|
+
const rangeItemId = getItemId(rangeItem);
|
|
116
|
+
if (rangeItemId !== undefined) {
|
|
117
|
+
state.selectedIds.add(rangeItemId);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
} else if (e.ctrlKey || e.metaKey) {
|
|
121
|
+
if (wasSelected) {
|
|
122
|
+
state.selectedIds.delete(itemId);
|
|
123
|
+
} else {
|
|
124
|
+
state.selectedIds.add(itemId);
|
|
125
|
+
}
|
|
126
|
+
state.lastSelectedIndex = index;
|
|
127
|
+
} else {
|
|
128
|
+
if (requireModifiers) {
|
|
129
|
+
state.selectedIds.clear();
|
|
130
|
+
state.selectedIds.add(itemId);
|
|
131
|
+
} else {
|
|
132
|
+
if (wasSelected) {
|
|
133
|
+
state.selectedIds.delete(itemId);
|
|
134
|
+
} else {
|
|
135
|
+
state.selectedIds.add(itemId);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
state.lastSelectedIndex = index;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
applySelectionToElements();
|
|
143
|
+
emitSelectionChange();
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Emit selection change event
|
|
148
|
+
*/
|
|
149
|
+
const emitSelectionChange = () => {
|
|
150
|
+
const enhancedComponent = component as any;
|
|
151
|
+
const items = enhancedComponent.getItems?.() || [];
|
|
152
|
+
|
|
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) => {
|
|
160
|
+
const id = getItemId(item);
|
|
161
|
+
if (id !== undefined && state.selectedIds.has(id)) {
|
|
162
|
+
acc.push(idx);
|
|
163
|
+
}
|
|
164
|
+
return acc;
|
|
165
|
+
},
|
|
166
|
+
[] as number[],
|
|
167
|
+
);
|
|
168
|
+
|
|
169
|
+
component.emit?.("selection:change", { selectedItems, selectedIndices });
|
|
170
|
+
|
|
171
|
+
if (config.selection?.onSelectionChange) {
|
|
172
|
+
config.selection.onSelectionChange(selectedItems, selectedIndices);
|
|
173
|
+
}
|
|
174
|
+
};
|
|
175
|
+
|
|
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);
|
|
183
|
+
}
|
|
184
|
+
|
|
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
|
+
},
|
|
212
|
+
);
|
|
213
|
+
};
|
|
214
|
+
|
|
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
|
+
});
|
|
233
|
+
}
|
|
234
|
+
},
|
|
235
|
+
);
|
|
236
|
+
|
|
237
|
+
// Cleanup on destroy
|
|
238
|
+
const originalDestroy = component.destroy;
|
|
239
|
+
component.destroy = () => {
|
|
240
|
+
component.element?.removeEventListener("click", handleItemClick, true);
|
|
241
|
+
originalDestroy?.();
|
|
242
|
+
};
|
|
243
|
+
|
|
244
|
+
// Enhanced component with selection API
|
|
245
|
+
return {
|
|
246
|
+
...component,
|
|
247
|
+
|
|
248
|
+
selectItems(indices: number[]) {
|
|
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
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
indices.forEach((index) => {
|
|
261
|
+
const itemId = getItemId(items[index]);
|
|
262
|
+
if (itemId !== undefined) {
|
|
263
|
+
state.selectedIds.add(itemId);
|
|
264
|
+
}
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
if (indices.length > 0) {
|
|
268
|
+
state.lastSelectedIndex = indices[indices.length - 1];
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
applySelectionToElements();
|
|
272
|
+
emitSelectionChange();
|
|
273
|
+
},
|
|
274
|
+
|
|
275
|
+
deselectItems(indices: number[]) {
|
|
276
|
+
const items = (this as any).getItems?.();
|
|
277
|
+
if (!items) return;
|
|
278
|
+
|
|
279
|
+
indices.forEach((index) => {
|
|
280
|
+
const itemId = getItemId(items[index]);
|
|
281
|
+
if (itemId !== undefined) {
|
|
282
|
+
state.selectedIds.delete(itemId);
|
|
283
|
+
}
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
applySelectionToElements();
|
|
287
|
+
emitSelectionChange();
|
|
288
|
+
},
|
|
289
|
+
|
|
290
|
+
clearSelection() {
|
|
291
|
+
state.selectedIds.clear();
|
|
292
|
+
state.lastSelectedIndex = undefined;
|
|
293
|
+
applySelectionToElements();
|
|
294
|
+
emitSelectionChange();
|
|
295
|
+
},
|
|
296
|
+
|
|
297
|
+
getSelectedItems(): T[] {
|
|
298
|
+
const items = (this as any).getItems?.() || [];
|
|
299
|
+
return items.filter((item: any) => {
|
|
300
|
+
const id = getItemId(item);
|
|
301
|
+
return id !== undefined && state.selectedIds.has(id);
|
|
302
|
+
});
|
|
303
|
+
},
|
|
304
|
+
|
|
305
|
+
getSelectedIndices(): number[] {
|
|
306
|
+
const items = (this as any).getItems?.() || [];
|
|
307
|
+
return items.reduce((acc: number[], item: any, index: number) => {
|
|
308
|
+
const id = getItemId(item);
|
|
309
|
+
if (id !== undefined && state.selectedIds.has(id)) {
|
|
310
|
+
acc.push(index);
|
|
311
|
+
}
|
|
312
|
+
return acc;
|
|
313
|
+
}, [] as number[]);
|
|
314
|
+
},
|
|
315
|
+
|
|
316
|
+
isSelected(index: number): boolean {
|
|
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) {
|
|
346
|
+
return false;
|
|
347
|
+
}
|
|
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
|
+
});
|
|
386
|
+
},
|
|
387
|
+
|
|
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
|
+
};
|
|
433
|
+
};
|
|
434
|
+
};
|
|
435
|
+
|
|
436
|
+
export default withSelection;
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
// src/components/vlist/features/viewport.ts
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Viewport feature for VList
|
|
5
|
+
* Integrates the core viewport functionality with VList component
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { VListConfig, VListItem } from "../types";
|
|
9
|
+
import { createViewport } from "../../../core/viewport";
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Adds viewport functionality to VList
|
|
13
|
+
*/
|
|
14
|
+
export const withViewport = <T extends VListItem = VListItem>(
|
|
15
|
+
config: VListConfig<T>,
|
|
16
|
+
) => {
|
|
17
|
+
return (component: any) => {
|
|
18
|
+
// Set initial items if provided
|
|
19
|
+
if (config.items) {
|
|
20
|
+
component.items = config.items;
|
|
21
|
+
component.totalItems = config.items.length;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// Set template if provided
|
|
25
|
+
if (config.template) {
|
|
26
|
+
component.template = config.template;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// Ensure the element has both vlist and viewport classes
|
|
30
|
+
const viewportConfig = {
|
|
31
|
+
...config,
|
|
32
|
+
className: "mtrl-viewport", // This will be added by viewport base feature
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
// Pass VList config directly to viewport
|
|
36
|
+
const viewportEnhanced = createViewport(viewportConfig as any)(component);
|
|
37
|
+
|
|
38
|
+
// Handle parent element if provided
|
|
39
|
+
if (config.parent || config.container) {
|
|
40
|
+
const container = config.parent || config.container;
|
|
41
|
+
const element =
|
|
42
|
+
typeof container === "string"
|
|
43
|
+
? document.querySelector(container)
|
|
44
|
+
: container;
|
|
45
|
+
|
|
46
|
+
if (element && viewportEnhanced.element) {
|
|
47
|
+
element.appendChild(viewportEnhanced.element);
|
|
48
|
+
|
|
49
|
+
// Ensure viewport is initialized after DOM attachment
|
|
50
|
+
if (viewportEnhanced.viewport && viewportEnhanced.viewport.initialize) {
|
|
51
|
+
// console.log("📋 [VList] Initializing viewport after DOM attachment");
|
|
52
|
+
viewportEnhanced.viewport.initialize();
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return viewportEnhanced;
|
|
58
|
+
};
|
|
59
|
+
};
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* VList Component - Virtual List with direct viewport integration
|
|
3
|
+
*
|
|
4
|
+
* A high-performance virtual list component that uses the viewport
|
|
5
|
+
* feature directly without the list-manager layer.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
export { createVList } from "./vlist";
|
|
9
|
+
export type {
|
|
10
|
+
RemoveItemOptions,
|
|
11
|
+
VListConfig,
|
|
12
|
+
VListComponent,
|
|
13
|
+
VListItem,
|
|
14
|
+
VListAPI,
|
|
15
|
+
VListState,
|
|
16
|
+
VListEvents,
|
|
17
|
+
} from "./types";
|