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.
Files changed (117) hide show
  1. package/AI.md +28 -230
  2. package/CLAUDE.md +882 -0
  3. package/build.js +253 -24
  4. package/package.json +14 -4
  5. package/scripts/debug/vlist-selection.ts +121 -0
  6. package/src/components/index.ts +5 -41
  7. package/src/components/{list → vlist}/config.ts +66 -95
  8. package/src/components/vlist/constants.ts +23 -0
  9. package/src/components/vlist/features/api.ts +626 -0
  10. package/src/components/vlist/features/index.ts +10 -0
  11. package/src/components/vlist/features/selection.ts +436 -0
  12. package/src/components/vlist/features/viewport.ts +59 -0
  13. package/src/components/vlist/index.ts +17 -0
  14. package/src/components/{list → vlist}/types.ts +242 -32
  15. package/src/components/vlist/vlist.ts +92 -0
  16. package/src/core/compose/features/gestures/index.ts +227 -0
  17. package/src/core/compose/features/gestures/longpress.ts +383 -0
  18. package/src/core/compose/features/gestures/pan.ts +424 -0
  19. package/src/core/compose/features/gestures/pinch.ts +475 -0
  20. package/src/core/compose/features/gestures/rotate.ts +485 -0
  21. package/src/core/compose/features/gestures/swipe.ts +492 -0
  22. package/src/core/compose/features/gestures/tap.ts +334 -0
  23. package/src/core/compose/features/index.ts +2 -38
  24. package/src/core/compose/index.ts +13 -29
  25. package/src/core/gestures/index.ts +31 -0
  26. package/src/core/gestures/longpress.ts +68 -0
  27. package/src/core/gestures/manager.ts +418 -0
  28. package/src/core/gestures/pan.ts +48 -0
  29. package/src/core/gestures/pinch.ts +58 -0
  30. package/src/core/gestures/rotate.ts +58 -0
  31. package/src/core/gestures/swipe.ts +66 -0
  32. package/src/core/gestures/tap.ts +45 -0
  33. package/src/core/gestures/types.ts +387 -0
  34. package/src/core/gestures/utils.ts +128 -0
  35. package/src/core/index.ts +27 -151
  36. package/src/core/layout/schema.ts +153 -72
  37. package/src/core/layout/types.ts +5 -2
  38. package/src/core/viewport/constants.ts +145 -0
  39. package/src/core/viewport/features/base.ts +73 -0
  40. package/src/core/viewport/features/collection.ts +1182 -0
  41. package/src/core/viewport/features/events.ts +130 -0
  42. package/src/core/viewport/features/index.ts +20 -0
  43. package/src/core/{list-manager/features/viewport → viewport/features}/item-size.ts +31 -34
  44. package/src/core/{list-manager/features/viewport → viewport/features}/loading.ts +4 -4
  45. package/src/core/viewport/features/momentum.ts +269 -0
  46. package/src/core/viewport/features/placeholders.ts +335 -0
  47. package/src/core/viewport/features/rendering.ts +962 -0
  48. package/src/core/viewport/features/scrollbar.ts +434 -0
  49. package/src/core/viewport/features/scrolling.ts +634 -0
  50. package/src/core/viewport/features/utils.ts +94 -0
  51. package/src/core/viewport/features/virtual.ts +525 -0
  52. package/src/core/viewport/index.ts +31 -0
  53. package/src/core/viewport/types.ts +133 -0
  54. package/src/core/viewport/utils/speed-tracker.ts +79 -0
  55. package/src/core/viewport/viewport.ts +265 -0
  56. package/src/index.ts +0 -7
  57. package/src/styles/components/_vlist.scss +352 -0
  58. package/src/styles/index.scss +1 -1
  59. package/test/components/vlist-selection.test.ts +240 -0
  60. package/test/components/vlist.test.ts +63 -0
  61. package/test/core/collection/adapter.test.ts +161 -0
  62. package/bun.lock +0 -792
  63. package/src/components/list/api.ts +0 -314
  64. package/src/components/list/constants.ts +0 -56
  65. package/src/components/list/features/api.ts +0 -428
  66. package/src/components/list/features/index.ts +0 -31
  67. package/src/components/list/features/list-manager.ts +0 -502
  68. package/src/components/list/index.ts +0 -39
  69. package/src/components/list/list.ts +0 -234
  70. package/src/core/collection/base-collection.ts +0 -100
  71. package/src/core/collection/collection-composer.ts +0 -178
  72. package/src/core/collection/collection.ts +0 -745
  73. package/src/core/collection/constants.ts +0 -172
  74. package/src/core/collection/events.ts +0 -428
  75. package/src/core/collection/features/api/loading.ts +0 -279
  76. package/src/core/collection/features/operations/data-operations.ts +0 -147
  77. package/src/core/collection/index.ts +0 -104
  78. package/src/core/collection/state.ts +0 -497
  79. package/src/core/collection/types.ts +0 -404
  80. package/src/core/compose/features/collection.ts +0 -119
  81. package/src/core/compose/features/selection.ts +0 -213
  82. package/src/core/compose/features/styling.ts +0 -108
  83. package/src/core/list-manager/api.ts +0 -599
  84. package/src/core/list-manager/config.ts +0 -593
  85. package/src/core/list-manager/constants.ts +0 -268
  86. package/src/core/list-manager/features/api.ts +0 -58
  87. package/src/core/list-manager/features/collection/collection.ts +0 -705
  88. package/src/core/list-manager/features/collection/index.ts +0 -17
  89. package/src/core/list-manager/features/viewport/constants.ts +0 -42
  90. package/src/core/list-manager/features/viewport/index.ts +0 -16
  91. package/src/core/list-manager/features/viewport/placeholders.ts +0 -281
  92. package/src/core/list-manager/features/viewport/rendering.ts +0 -575
  93. package/src/core/list-manager/features/viewport/scrollbar.ts +0 -495
  94. package/src/core/list-manager/features/viewport/scrolling.ts +0 -795
  95. package/src/core/list-manager/features/viewport/template.ts +0 -220
  96. package/src/core/list-manager/features/viewport/viewport.ts +0 -654
  97. package/src/core/list-manager/features/viewport/virtual.ts +0 -309
  98. package/src/core/list-manager/index.ts +0 -279
  99. package/src/core/list-manager/list-manager.ts +0 -206
  100. package/src/core/list-manager/types.ts +0 -439
  101. package/src/core/list-manager/utils/calculations.ts +0 -290
  102. package/src/core/list-manager/utils/range-calculator.ts +0 -349
  103. package/src/core/list-manager/utils/speed-tracker.ts +0 -273
  104. package/src/styles/components/_list.scss +0 -244
  105. package/src/types/mtrl.d.ts +0 -6
  106. package/test/components/list.test.ts +0 -256
  107. package/test/core/collection/failed-ranges.test.ts +0 -270
  108. package/test/core/compose/features.test.ts +0 -183
  109. package/test/core/list-manager/features/collection.test.ts +0 -704
  110. package/test/core/list-manager/features/viewport.test.ts +0 -698
  111. package/test/core/list-manager/list-manager.test.ts +0 -593
  112. package/test/core/list-manager/utils/calculations.test.ts +0 -433
  113. package/test/core/list-manager/utils/range-calculator.test.ts +0 -569
  114. package/test/core/list-manager/utils/speed-tracker.test.ts +0 -530
  115. package/tsconfig.build.json +0 -23
  116. /package/src/components/{list → vlist}/features.ts +0 -0
  117. /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";