vlist 1.9.1 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (182) hide show
  1. package/README.github.md +104 -97
  2. package/README.md +46 -33
  3. package/dist/constants.d.ts +11 -6
  4. package/dist/constants.js +83 -0
  5. package/dist/core/create.d.ts +10 -0
  6. package/dist/core/create.js +740 -0
  7. package/dist/core/dom.d.ts +8 -0
  8. package/dist/core/dom.js +47 -0
  9. package/dist/core/hooks.d.ts +16 -0
  10. package/dist/core/hooks.js +67 -0
  11. package/dist/core/index.d.ts +17 -0
  12. package/dist/core/index.js +13 -0
  13. package/dist/core/pipeline.d.ts +51 -0
  14. package/dist/core/pipeline.js +307 -0
  15. package/dist/core/pool.d.ts +9 -0
  16. package/dist/core/pool.js +42 -0
  17. package/dist/core/scroll.d.ts +32 -0
  18. package/dist/core/scroll.js +137 -0
  19. package/dist/core/sizes.d.ts +8 -0
  20. package/dist/core/sizes.js +6 -0
  21. package/dist/core/state.d.ts +47 -0
  22. package/dist/core/state.js +56 -0
  23. package/dist/core/types.d.ts +187 -0
  24. package/dist/core/types.js +7 -0
  25. package/dist/{builder → core}/velocity.d.ts +1 -1
  26. package/dist/core/velocity.js +33 -0
  27. package/dist/events/emitter.js +60 -0
  28. package/dist/events/index.js +6 -0
  29. package/dist/index.d.ts +28 -19
  30. package/dist/index.js +28 -1
  31. package/dist/internals.d.ts +11 -7
  32. package/dist/internals.js +60 -1
  33. package/dist/plugins/a11y/index.d.ts +2 -0
  34. package/dist/plugins/a11y/index.js +1 -0
  35. package/dist/plugins/a11y/plugin.d.ts +13 -0
  36. package/dist/plugins/a11y/plugin.js +259 -0
  37. package/dist/{features → plugins}/async/index.d.ts +1 -1
  38. package/dist/plugins/async/index.js +12 -0
  39. package/dist/{features → plugins}/async/manager.d.ts +5 -1
  40. package/dist/plugins/async/manager.js +568 -0
  41. package/dist/plugins/async/placeholder.js +154 -0
  42. package/dist/plugins/async/plugin.d.ts +48 -0
  43. package/dist/plugins/async/plugin.js +311 -0
  44. package/dist/plugins/async/sparse.js +540 -0
  45. package/dist/plugins/autosize/index.d.ts +5 -0
  46. package/dist/plugins/autosize/index.js +4 -0
  47. package/dist/plugins/autosize/plugin.d.ts +19 -0
  48. package/dist/plugins/autosize/plugin.js +185 -0
  49. package/dist/plugins/grid/index.d.ts +7 -0
  50. package/dist/plugins/grid/index.js +5 -0
  51. package/dist/plugins/grid/layout.js +275 -0
  52. package/dist/plugins/grid/plugin.d.ts +23 -0
  53. package/dist/plugins/grid/plugin.js +347 -0
  54. package/dist/plugins/grid/renderer.js +525 -0
  55. package/dist/plugins/grid/types.js +11 -0
  56. package/dist/plugins/groups/async-bridge.js +246 -0
  57. package/dist/{features → plugins}/groups/index.d.ts +1 -1
  58. package/dist/plugins/groups/index.js +13 -0
  59. package/dist/plugins/groups/layout.js +294 -0
  60. package/dist/plugins/groups/plugin.d.ts +22 -0
  61. package/dist/plugins/groups/plugin.js +571 -0
  62. package/dist/plugins/groups/sticky.js +255 -0
  63. package/dist/plugins/groups/types.js +12 -0
  64. package/dist/plugins/masonry/index.d.ts +8 -0
  65. package/dist/plugins/masonry/index.js +6 -0
  66. package/dist/plugins/masonry/layout.js +261 -0
  67. package/dist/plugins/masonry/plugin.d.ts +32 -0
  68. package/dist/plugins/masonry/plugin.js +381 -0
  69. package/dist/plugins/masonry/renderer.js +354 -0
  70. package/dist/plugins/masonry/types.js +9 -0
  71. package/dist/plugins/page/index.d.ts +5 -0
  72. package/dist/plugins/page/index.js +5 -0
  73. package/dist/plugins/page/plugin.d.ts +21 -0
  74. package/dist/plugins/page/plugin.js +166 -0
  75. package/dist/plugins/scale/index.d.ts +5 -0
  76. package/dist/plugins/scale/index.js +4 -0
  77. package/dist/plugins/scale/plugin.d.ts +24 -0
  78. package/dist/plugins/scale/plugin.js +507 -0
  79. package/dist/plugins/scrollbar/controller.js +574 -0
  80. package/dist/plugins/scrollbar/index.d.ts +7 -0
  81. package/dist/plugins/scrollbar/index.js +6 -0
  82. package/dist/plugins/scrollbar/plugin.d.ts +20 -0
  83. package/dist/plugins/scrollbar/plugin.js +93 -0
  84. package/dist/plugins/scrollbar/scrollbar.js +556 -0
  85. package/dist/plugins/selection/index.d.ts +6 -0
  86. package/dist/plugins/selection/index.js +7 -0
  87. package/dist/plugins/selection/plugin.d.ts +16 -0
  88. package/dist/plugins/selection/plugin.js +601 -0
  89. package/dist/{features → plugins}/selection/state.d.ts +8 -0
  90. package/dist/plugins/selection/state.js +332 -0
  91. package/dist/plugins/snapshots/index.d.ts +5 -0
  92. package/dist/plugins/snapshots/index.js +5 -0
  93. package/dist/plugins/snapshots/plugin.d.ts +17 -0
  94. package/dist/plugins/snapshots/plugin.js +301 -0
  95. package/dist/plugins/sortable/index.d.ts +6 -0
  96. package/dist/plugins/sortable/index.js +6 -0
  97. package/dist/plugins/sortable/plugin.d.ts +34 -0
  98. package/dist/plugins/sortable/plugin.js +753 -0
  99. package/dist/plugins/table/header.js +501 -0
  100. package/dist/{features → plugins}/table/index.d.ts +1 -1
  101. package/dist/plugins/table/index.js +12 -0
  102. package/dist/plugins/table/layout.js +211 -0
  103. package/dist/plugins/table/plugin.d.ts +20 -0
  104. package/dist/plugins/table/plugin.js +391 -0
  105. package/dist/plugins/table/renderer.js +625 -0
  106. package/dist/plugins/table/types.js +12 -0
  107. package/dist/plugins/transition/index.d.ts +5 -0
  108. package/dist/plugins/transition/index.js +5 -0
  109. package/dist/plugins/transition/plugin.d.ts +22 -0
  110. package/dist/plugins/transition/plugin.js +405 -0
  111. package/dist/rendering/aria.js +23 -0
  112. package/dist/rendering/index.js +18 -0
  113. package/dist/rendering/measured.js +98 -0
  114. package/dist/rendering/renderer.js +586 -0
  115. package/dist/rendering/scale.js +267 -0
  116. package/dist/rendering/scroll.js +71 -0
  117. package/dist/rendering/sizes.js +193 -0
  118. package/dist/rendering/sort.js +65 -0
  119. package/dist/rendering/viewport.js +268 -0
  120. package/dist/size.json +1 -1
  121. package/dist/types.js +5 -0
  122. package/dist/utils/padding.d.ts +2 -4
  123. package/dist/utils/padding.js +49 -0
  124. package/dist/utils/stats.js +124 -0
  125. package/dist/vlist-grid.css +1 -1
  126. package/dist/vlist-masonry.css +1 -1
  127. package/dist/vlist-table.css +1 -1
  128. package/dist/vlist.css +1 -1
  129. package/package.json +9 -4
  130. package/dist/builder/a11y.d.ts +0 -16
  131. package/dist/builder/api.d.ts +0 -21
  132. package/dist/builder/context.d.ts +0 -36
  133. package/dist/builder/core.d.ts +0 -16
  134. package/dist/builder/data.d.ts +0 -71
  135. package/dist/builder/dom.d.ts +0 -15
  136. package/dist/builder/index.d.ts +0 -25
  137. package/dist/builder/materialize.d.ts +0 -166
  138. package/dist/builder/pool.d.ts +0 -10
  139. package/dist/builder/range.d.ts +0 -10
  140. package/dist/builder/scroll.d.ts +0 -24
  141. package/dist/builder/types.d.ts +0 -512
  142. package/dist/features/async/feature.d.ts +0 -72
  143. package/dist/features/autosize/feature.d.ts +0 -34
  144. package/dist/features/autosize/index.d.ts +0 -2
  145. package/dist/features/grid/feature.d.ts +0 -48
  146. package/dist/features/grid/index.d.ts +0 -9
  147. package/dist/features/groups/feature.d.ts +0 -75
  148. package/dist/features/masonry/feature.d.ts +0 -45
  149. package/dist/features/masonry/index.d.ts +0 -9
  150. package/dist/features/page/feature.d.ts +0 -109
  151. package/dist/features/page/index.d.ts +0 -9
  152. package/dist/features/scale/feature.d.ts +0 -42
  153. package/dist/features/scale/index.d.ts +0 -10
  154. package/dist/features/scrollbar/feature.d.ts +0 -81
  155. package/dist/features/scrollbar/index.d.ts +0 -8
  156. package/dist/features/selection/feature.d.ts +0 -91
  157. package/dist/features/selection/index.d.ts +0 -7
  158. package/dist/features/snapshots/feature.d.ts +0 -79
  159. package/dist/features/snapshots/index.d.ts +0 -9
  160. package/dist/features/sortable/feature.d.ts +0 -101
  161. package/dist/features/sortable/index.d.ts +0 -6
  162. package/dist/features/table/feature.d.ts +0 -67
  163. package/dist/features/transition/feature.d.ts +0 -30
  164. package/dist/features/transition/index.d.ts +0 -9
  165. /package/dist/{features → plugins}/async/placeholder.d.ts +0 -0
  166. /package/dist/{features → plugins}/async/sparse.d.ts +0 -0
  167. /package/dist/{features → plugins}/grid/layout.d.ts +0 -0
  168. /package/dist/{features → plugins}/grid/renderer.d.ts +0 -0
  169. /package/dist/{features → plugins}/grid/types.d.ts +0 -0
  170. /package/dist/{features → plugins}/groups/async-bridge.d.ts +0 -0
  171. /package/dist/{features → plugins}/groups/layout.d.ts +0 -0
  172. /package/dist/{features → plugins}/groups/sticky.d.ts +0 -0
  173. /package/dist/{features → plugins}/groups/types.d.ts +0 -0
  174. /package/dist/{features → plugins}/masonry/layout.d.ts +0 -0
  175. /package/dist/{features → plugins}/masonry/renderer.d.ts +0 -0
  176. /package/dist/{features → plugins}/masonry/types.d.ts +0 -0
  177. /package/dist/{features → plugins}/scrollbar/controller.d.ts +0 -0
  178. /package/dist/{features → plugins}/scrollbar/scrollbar.d.ts +0 -0
  179. /package/dist/{features → plugins}/table/header.d.ts +0 -0
  180. /package/dist/{features → plugins}/table/layout.d.ts +0 -0
  181. /package/dist/{features → plugins}/table/renderer.d.ts +0 -0
  182. /package/dist/{features → plugins}/table/types.d.ts +0 -0
@@ -0,0 +1,211 @@
1
+ /**
2
+ * vlist/table - Layout
3
+ * Manages column widths, offsets, and resize operations.
4
+ *
5
+ * Column width resolution strategy:
6
+ * 1. Columns with explicit `width` get their requested width (clamped to min/max)
7
+ * 2. Remaining container space is distributed equally among columns without `width`
8
+ * 3. If all columns have explicit widths and total < container, no stretching occurs
9
+ * 4. If total column width > container, the table scrolls horizontally
10
+ *
11
+ * All offset calculations are O(n) where n = number of columns (typically small).
12
+ * Resize operations recalculate offsets for columns after the resized one.
13
+ */
14
+ // =============================================================================
15
+ // Defaults
16
+ // =============================================================================
17
+ const MIN_COLUMN_WIDTH = 50;
18
+ const MAX_COLUMN_WIDTH = Infinity;
19
+ // =============================================================================
20
+ // Factory
21
+ // =============================================================================
22
+ /**
23
+ * Create a TableLayout instance.
24
+ *
25
+ * @param columnDefs - Column definitions from config
26
+ * @param globalMinWidth - Default min column width (from TableConfig.minColumnWidth)
27
+ * @param globalMaxWidth - Default max column width (from TableConfig.maxColumnWidth)
28
+ * @param globalResizable - Default resizable flag (from TableConfig.resizable)
29
+ * @returns TableLayout with column resolution and resize capabilities
30
+ */
31
+ export const createTableLayout = (columnDefs, globalMinWidth = MIN_COLUMN_WIDTH, globalMaxWidth = MAX_COLUMN_WIDTH, globalResizable = true) => {
32
+ let defs = columnDefs;
33
+ let resolved = [];
34
+ let totalWidth = 0;
35
+ // =========================================================================
36
+ // Column Resolution
37
+ // =========================================================================
38
+ /**
39
+ * Build resolved columns from definitions.
40
+ * Does NOT compute widths yet — call resolve(containerWidth) for that.
41
+ */
42
+ const buildResolved = () => {
43
+ resolved = defs.map((def, index) => ({
44
+ def,
45
+ index,
46
+ width: 0,
47
+ minWidth: clampPositive(def.minWidth ?? globalMinWidth, 1),
48
+ maxWidth: def.maxWidth ?? globalMaxWidth,
49
+ resizable: def.resizable ?? globalResizable,
50
+ offset: 0,
51
+ }));
52
+ };
53
+ /**
54
+ * Resolve column widths given the available container width.
55
+ *
56
+ * Strategy:
57
+ * - Columns with explicit `width` are assigned that width (clamped)
58
+ * - Remaining space goes to columns without explicit `width`
59
+ * - If no columns lack a width, total is just the sum of assigned widths
60
+ * - Widths are always clamped to [minWidth, maxWidth]
61
+ */
62
+ const resolve = (containerWidth) => {
63
+ if (resolved.length === 0) {
64
+ totalWidth = 0;
65
+ return;
66
+ }
67
+ let usedWidth = 0;
68
+ let flexCount = 0;
69
+ // First pass: assign explicit widths, count flex columns
70
+ for (let i = 0; i < resolved.length; i++) {
71
+ const col = resolved[i];
72
+ const def = col.def;
73
+ if (def.width !== undefined) {
74
+ col.width = clamp(def.width, col.minWidth, col.maxWidth);
75
+ usedWidth += col.width;
76
+ }
77
+ else {
78
+ flexCount++;
79
+ }
80
+ }
81
+ // Second pass: distribute remaining space to flex columns
82
+ if (flexCount) {
83
+ const remaining = Math.max(0, containerWidth - usedWidth);
84
+ const flexWidth = remaining / flexCount;
85
+ for (let i = 0; i < resolved.length; i++) {
86
+ const col = resolved[i];
87
+ if (col.def.width === undefined) {
88
+ col.width = clamp(flexWidth, col.minWidth, col.maxWidth);
89
+ }
90
+ }
91
+ }
92
+ // Third pass: compute cumulative offsets and total width
93
+ recalculateOffsets();
94
+ };
95
+ /**
96
+ * Recalculate offsets from column widths.
97
+ * Called after resolve() and after any resize operation.
98
+ */
99
+ const recalculateOffsets = () => {
100
+ let offset = 0;
101
+ for (let i = 0; i < resolved.length; i++) {
102
+ const col = resolved[i];
103
+ col.offset = offset;
104
+ offset += col.width;
105
+ }
106
+ totalWidth = offset;
107
+ };
108
+ // =========================================================================
109
+ // Resize
110
+ // =========================================================================
111
+ /**
112
+ * Resize a column to a new width.
113
+ * Clamps to the column's min/max bounds.
114
+ * Recalculates offsets for all subsequent columns.
115
+ *
116
+ * @returns The actual new width after clamping
117
+ */
118
+ const resizeColumn = (columnIndex, newWidth) => {
119
+ if (columnIndex < 0 || columnIndex >= resolved.length)
120
+ return 0;
121
+ const col = resolved[columnIndex];
122
+ if (!col.resizable)
123
+ return col.width;
124
+ const clamped = clamp(newWidth, col.minWidth, col.maxWidth);
125
+ col.width = clamped;
126
+ // Recalculate offsets from this column onward
127
+ recalculateOffsets();
128
+ return clamped;
129
+ };
130
+ // =========================================================================
131
+ // Accessors
132
+ // =========================================================================
133
+ const getColumn = (index) => {
134
+ return resolved[index];
135
+ };
136
+ const getColumnAtX = (x) => {
137
+ // Binary search for the column containing x
138
+ if (resolved.length === 0)
139
+ return undefined;
140
+ if (x < 0)
141
+ return resolved[0];
142
+ if (x >= totalWidth)
143
+ return resolved[resolved.length - 1];
144
+ let lo = 0;
145
+ let hi = resolved.length - 1;
146
+ while (lo < hi) {
147
+ const mid = (lo + hi) >>> 1;
148
+ const col = resolved[mid];
149
+ if (x < col.offset) {
150
+ hi = mid - 1;
151
+ }
152
+ else if (x >= col.offset + col.width) {
153
+ lo = mid + 1;
154
+ }
155
+ else {
156
+ return col;
157
+ }
158
+ }
159
+ return resolved[lo];
160
+ };
161
+ const getColumnOffset = (columnIndex) => {
162
+ if (columnIndex < 0 || columnIndex >= resolved.length)
163
+ return 0;
164
+ return resolved[columnIndex].offset;
165
+ };
166
+ const getColumnWidth = (columnIndex) => {
167
+ if (columnIndex < 0 || columnIndex >= resolved.length)
168
+ return 0;
169
+ return resolved[columnIndex].width;
170
+ };
171
+ // =========================================================================
172
+ // Update
173
+ // =========================================================================
174
+ /**
175
+ * Replace column definitions and rebuild.
176
+ * The caller must call resolve(containerWidth) after this to compute widths.
177
+ */
178
+ const updateColumns = (columns) => {
179
+ defs = columns;
180
+ buildResolved();
181
+ };
182
+ // =========================================================================
183
+ // Initialize
184
+ // =========================================================================
185
+ buildResolved();
186
+ // =========================================================================
187
+ // Return
188
+ // =========================================================================
189
+ return {
190
+ get columns() {
191
+ return resolved;
192
+ },
193
+ get totalWidth() {
194
+ return totalWidth;
195
+ },
196
+ resolve,
197
+ updateColumns,
198
+ resizeColumn,
199
+ getColumn,
200
+ getColumnAtX,
201
+ getColumnOffset,
202
+ getColumnWidth,
203
+ };
204
+ };
205
+ // =============================================================================
206
+ // Helpers
207
+ // =============================================================================
208
+ /** Clamp a value to [min, max] */
209
+ const clamp = (value, min, max) => Math.min(max, Math.max(min, value));
210
+ /** Ensure a positive value (at least `floor`) */
211
+ const clampPositive = (value, floor) => Math.max(floor, value);
@@ -0,0 +1,20 @@
1
+ /**
2
+ * vlist v2 — Table Plugin
3
+ *
4
+ * Switches from list layout to a data table with columns, resizable headers,
5
+ * sticky header row, and cell-based rendering.
6
+ *
7
+ * Priority 10 — runs before selection (50) so layout is ready.
8
+ *
9
+ * Restrictions:
10
+ * - Cannot be combined with grid or masonry plugins
11
+ * - Cannot be combined with horizontal orientation
12
+ * - Cannot be combined with reverse mode
13
+ */
14
+ import type { VListItem } from "../../types";
15
+ import type { VListPlugin } from "../../core/types";
16
+ import type { TableConfig } from "./types";
17
+ export interface TablePluginConfig<T extends VListItem = VListItem> extends TableConfig<T> {
18
+ }
19
+ export declare function table<T extends VListItem = VListItem>(config: TablePluginConfig<T>): VListPlugin<T>;
20
+ //# sourceMappingURL=plugin.d.ts.map
@@ -0,0 +1,391 @@
1
+ /**
2
+ * vlist v2 — Table Plugin
3
+ *
4
+ * Switches from list layout to a data table with columns, resizable headers,
5
+ * sticky header row, and cell-based rendering.
6
+ *
7
+ * Priority 10 — runs before selection (50) so layout is ready.
8
+ *
9
+ * Restrictions:
10
+ * - Cannot be combined with grid or masonry plugins
11
+ * - Cannot be combined with horizontal orientation
12
+ * - Cannot be combined with reverse mode
13
+ */
14
+ import { createTableLayout } from "./layout";
15
+ import { createTableHeader } from "./header";
16
+ import { createTableRenderer } from "./renderer";
17
+ // =============================================================================
18
+ // Config
19
+ // =============================================================================
20
+ const EMPTY_ID_SET = new Set();
21
+ // =============================================================================
22
+ // Factory
23
+ // =============================================================================
24
+ export function table(config) {
25
+ if (!config.columns?.length) {
26
+ throw new Error("[vlist] table: columns must be a non-empty array");
27
+ }
28
+ if (config.rowHeight === undefined && config.estimatedRowHeight === undefined) {
29
+ throw new Error("[vlist] table: either rowHeight or estimatedRowHeight is required");
30
+ }
31
+ let tableLayout = null;
32
+ let tableHeader = null;
33
+ let tableRenderer = null;
34
+ let engineState;
35
+ let sizeCache;
36
+ let storedCtx = null;
37
+ let selectedIdsGetter = null;
38
+ let focusedIndexGetter = null;
39
+ let selectionResolved = false;
40
+ let lastScrollPosition = -1;
41
+ let lastContainerSize = -1;
42
+ let lastTotalItems = -1;
43
+ let forceNextRender = true;
44
+ let lastAriaRowCount = -1;
45
+ function resolveSelectionMethods() {
46
+ if (selectionResolved || !storedCtx)
47
+ return;
48
+ selectionResolved = true;
49
+ selectedIdsGetter = storedCtx.getMethod("_getSelectedIds") ?? null;
50
+ focusedIndexGetter = storedCtx.getMethod("_getFocusedIndex") ?? null;
51
+ }
52
+ // =========================================================================
53
+ // Render Functions
54
+ // =========================================================================
55
+ // Persistent per-render buffers — hoisted to avoid per-frame allocation
56
+ const rangeItems = [];
57
+ const range = { start: 0, end: 0 };
58
+ function tableRenderIfNeeded() {
59
+ if (engineState.destroyed || !storedCtx || !tableRenderer || !tableLayout)
60
+ return;
61
+ const scrollPos = engineState.scrollPosition;
62
+ const containerSize = engineState.containerSize;
63
+ const totalItems = engineState.totalItems;
64
+ if (!forceNextRender &&
65
+ scrollPos === lastScrollPosition &&
66
+ containerSize === lastContainerSize &&
67
+ totalItems === lastTotalItems) {
68
+ return;
69
+ }
70
+ lastScrollPosition = scrollPos;
71
+ lastContainerSize = containerSize;
72
+ lastTotalItems = totalItems;
73
+ forceNextRender = false;
74
+ if (containerSize <= 0 || totalItems === 0)
75
+ return;
76
+ // Update aria-rowcount when item count changes (+1 for header row)
77
+ const ariaRowCount = totalItems + 1;
78
+ if (ariaRowCount !== lastAriaRowCount) {
79
+ lastAriaRowCount = ariaRowCount;
80
+ storedCtx.dom.root.setAttribute("aria-rowcount", `${ariaRowCount}`);
81
+ }
82
+ // Calculate visible range
83
+ let visStart = sizeCache.indexAtOffset(scrollPos);
84
+ let visEnd = sizeCache.indexAtOffset(scrollPos + containerSize);
85
+ if (visEnd < totalItems - 1)
86
+ visEnd++;
87
+ visStart = Math.max(0, visStart);
88
+ visEnd = Math.min(totalItems - 1, visEnd);
89
+ const overscan = storedCtx.config.overscan;
90
+ const renderStart = Math.max(0, visStart - overscan);
91
+ const renderEnd = Math.min(totalItems - 1, visEnd + overscan);
92
+ if (renderStart === engineState.prevRangeStart &&
93
+ renderEnd === engineState.prevRangeEnd &&
94
+ !engineState.renderPending) {
95
+ return;
96
+ }
97
+ resolveSelectionMethods();
98
+ const selectedIds = selectedIdsGetter?.() ?? EMPTY_ID_SET;
99
+ const focusedIndex = focusedIndexGetter?.() ?? -1;
100
+ rangeItems.length = 0;
101
+ for (let i = renderStart; i <= renderEnd; i++) {
102
+ const item = storedCtx.getItem(i);
103
+ if (item)
104
+ rangeItems.push(item);
105
+ }
106
+ range.start = renderStart;
107
+ range.end = renderEnd;
108
+ tableRenderer.render(rangeItems, range, selectedIds, focusedIndex);
109
+ // Update engine state
110
+ engineState.prevRangeStart = renderStart;
111
+ engineState.prevRangeEnd = renderEnd;
112
+ engineState.renderPending = false;
113
+ const count = renderEnd - renderStart + 1;
114
+ engineState.visibleCount = Math.min(count, engineState.capacity);
115
+ engineState.startIndex = renderStart;
116
+ for (let i = 0; i < engineState.visibleCount; i++) {
117
+ const idx = renderStart + i;
118
+ engineState.visibleIndices[i] = idx;
119
+ engineState.visibleOffsets[i] = sizeCache.getOffset(idx);
120
+ engineState.visibleSizes[i] = sizeCache.getSize(idx);
121
+ }
122
+ // Update content size
123
+ storedCtx.dom.content.style.height = sizeCache.getTotalSize() + "px";
124
+ }
125
+ function tableForceRender() {
126
+ if (engineState.destroyed)
127
+ return;
128
+ engineState.prevRangeStart = -1;
129
+ engineState.prevRangeEnd = -1;
130
+ engineState.renderPending = true;
131
+ forceNextRender = true;
132
+ tableRenderIfNeeded();
133
+ }
134
+ // =========================================================================
135
+ // Plugin
136
+ // =========================================================================
137
+ return {
138
+ name: "table",
139
+ priority: 10,
140
+ conflicts: ["grid", "masonry"],
141
+ setup(ctx) {
142
+ storedCtx = ctx;
143
+ engineState = ctx.getState();
144
+ sizeCache = ctx.sizeCache;
145
+ const { dom, config: resolvedConfig, emitter } = ctx;
146
+ const { classPrefix } = resolvedConfig;
147
+ if (resolvedConfig.horizontal) {
148
+ throw new Error("[vlist] table: cannot be used with horizontal orientation");
149
+ }
150
+ if (resolvedConfig.reverse) {
151
+ throw new Error("[vlist] table: cannot be used with reverse mode");
152
+ }
153
+ // ── Resolve config ──────────────────────────────────────────
154
+ const resizable = config.resizable ?? true;
155
+ const minColumnWidth = config.minColumnWidth ?? 50;
156
+ const maxColumnWidth = config.maxColumnWidth ?? Infinity;
157
+ const rowBorders = config.rowBorders ?? true;
158
+ const rowHeight = config.rowHeight;
159
+ const headerHeight = config.headerHeight ??
160
+ (typeof rowHeight === "number" ? rowHeight : 40);
161
+ // ── Sort state ──────────────────────────────────────────────
162
+ let sortKey = config.sort?.key ?? null;
163
+ let sortDirection = config.sort?.direction ?? "asc";
164
+ // ── Create table layout ─────────────────────────────────────
165
+ tableLayout = createTableLayout(config.columns, minColumnWidth, maxColumnWidth, resizable);
166
+ // ── Set row height ──────────────────────────────────────────
167
+ if (typeof rowHeight === "function" || typeof rowHeight === "number") {
168
+ ctx.setSizeConfig(rowHeight);
169
+ }
170
+ ctx.rebuildSizeCache();
171
+ // ── CSS classes ─────────────────────────────────────────────
172
+ dom.root.classList.add(`${classPrefix}--table`);
173
+ if (rowBorders)
174
+ dom.root.classList.add(`${classPrefix}--table-row-borders`);
175
+ if (config.columnBorders)
176
+ dom.root.classList.add(`${classPrefix}--table-col-borders`);
177
+ // ── ARIA ────────────────────────────────────────────────────
178
+ if (resolvedConfig.interactive) {
179
+ dom.root.setAttribute("tabindex", "0");
180
+ }
181
+ dom.root.setAttribute("role", "grid");
182
+ dom.root.setAttribute("aria-colcount", `${config.columns.length}`);
183
+ dom.viewport.setAttribute("role", "none");
184
+ dom.content.setAttribute("role", "rowgroup");
185
+ // ── Resolve initial column widths ───────────────────────────
186
+ const containerWidth = engineState.crossSize;
187
+ tableLayout.resolve(containerWidth);
188
+ // ── Content width ───────────────────────────────────────────
189
+ const updateContentWidth = () => {
190
+ if (!tableLayout)
191
+ return;
192
+ dom.content.style.minWidth = `${tableLayout.totalWidth}px`;
193
+ };
194
+ // ── Column resize handler ───────────────────────────────────
195
+ const onColumnResize = (columnIndex, newWidth) => {
196
+ if (!tableLayout)
197
+ return;
198
+ const col = tableLayout.getColumn(columnIndex);
199
+ if (!col)
200
+ return;
201
+ const previousWidth = col.width;
202
+ const actualWidth = tableLayout.resizeColumn(columnIndex, newWidth);
203
+ tableHeader?.update(tableLayout);
204
+ tableRenderer?.updateColumnLayout(tableLayout);
205
+ updateContentWidth();
206
+ emitter.emit("column:resize", {
207
+ key: col.def.key,
208
+ index: columnIndex,
209
+ previousWidth,
210
+ width: actualWidth,
211
+ });
212
+ };
213
+ // ── Column sort handler ─────────────────────────────────────
214
+ const onColumnSort = (event) => {
215
+ sortKey = event.direction === null ? null : event.key;
216
+ sortDirection = event.direction ?? "asc";
217
+ tableHeader?.updateSort(sortKey, sortDirection);
218
+ emitter.emit("column:sort", event);
219
+ };
220
+ // ── Create table header ─────────────────────────────────────
221
+ tableHeader = createTableHeader(dom.root, headerHeight, classPrefix, onColumnResize, onColumnSort);
222
+ tableHeader.rebuild(tableLayout);
223
+ if (sortKey)
224
+ tableHeader.updateSort(sortKey, sortDirection);
225
+ updateContentWidth();
226
+ // ── Create table renderer ───────────────────────────────────
227
+ tableRenderer = createTableRenderer(dom.content, () => sizeCache, tableLayout, config.columns, classPrefix, classPrefix, () => engineState.totalItems, resolvedConfig.striped || undefined);
228
+ // ── Wire render pipeline ────────────────────────────────────
229
+ ctx.setRenderFn(tableRenderIfNeeded, tableForceRender);
230
+ // ── Header scroll sync ──────────────────────────────────────
231
+ const headerWithSync = tableHeader;
232
+ let lastSyncedScrollLeft = -1;
233
+ const syncHeaderScroll = () => {
234
+ const scrollLeft = dom.viewport.scrollLeft;
235
+ if (scrollLeft !== lastSyncedScrollLeft) {
236
+ lastSyncedScrollLeft = scrollLeft;
237
+ headerWithSync?.syncScroll?.(scrollLeft);
238
+ }
239
+ };
240
+ dom.viewport.addEventListener("scroll", syncHeaderScroll, { passive: true });
241
+ // ── Public methods ──────────────────────────────────────────
242
+ ctx.registerMethod("updateColumns", (columns) => {
243
+ if (!tableLayout || !tableHeader)
244
+ return;
245
+ tableLayout.updateColumns(columns);
246
+ tableLayout.resolve(engineState.crossSize);
247
+ tableHeader.rebuild(tableLayout);
248
+ if (sortKey)
249
+ tableHeader.updateSort(sortKey, sortDirection);
250
+ updateContentWidth();
251
+ tableRenderer?.updateColumnLayout(tableLayout);
252
+ tableRenderer?.clear();
253
+ ctx.forceRender();
254
+ });
255
+ ctx.registerMethod("resizeColumn", (keyOrIndex, width) => {
256
+ if (!tableLayout)
257
+ return;
258
+ let columnIndex;
259
+ if (typeof keyOrIndex === "string") {
260
+ const cols = tableLayout.columns;
261
+ columnIndex = -1;
262
+ for (let i = 0; i < cols.length; i++) {
263
+ if (cols[i].def.key === keyOrIndex) {
264
+ columnIndex = i;
265
+ break;
266
+ }
267
+ }
268
+ if (columnIndex === -1)
269
+ return;
270
+ }
271
+ else {
272
+ columnIndex = keyOrIndex;
273
+ }
274
+ onColumnResize(columnIndex, width);
275
+ });
276
+ ctx.registerMethod("getColumnWidths", () => {
277
+ if (!tableLayout)
278
+ return {};
279
+ const result = {};
280
+ const cols = tableLayout.columns;
281
+ for (let i = 0; i < cols.length; i++) {
282
+ result[cols[i].def.key] = cols[i].width;
283
+ }
284
+ return result;
285
+ });
286
+ ctx.registerMethod("setSort", (key, direction) => {
287
+ sortKey = key;
288
+ sortDirection = direction ?? "asc";
289
+ tableHeader?.updateSort(sortKey, sortDirection);
290
+ });
291
+ ctx.registerMethod("getSort", () => {
292
+ return { key: sortKey, direction: sortDirection };
293
+ });
294
+ ctx.registerMethod("_getTableLayout", () => tableLayout);
295
+ ctx.registerMethod("_getTableHeaderHeight", () => headerHeight);
296
+ ctx.registerMethod("_updateRenderedItem", (index, item, isSelected, isFocused) => {
297
+ tableRenderer?.updateItem(index, item, isSelected, isFocused);
298
+ });
299
+ ctx.registerMethod("_replaceTableRenderer", (newRenderer) => {
300
+ tableRenderer = newRenderer;
301
+ });
302
+ ctx.registerMethod("_updateTableForGroups", (isHeaderFn, headerTemplate) => {
303
+ tableRenderer?.setGroupHeaderFn(isHeaderFn, headerTemplate);
304
+ });
305
+ // Replace item-class updater for selection integration
306
+ ctx.registerMethod("_updateItemClasses", (index, isSelected, isFocused) => {
307
+ tableRenderer?.updateItemClasses(index, isSelected, isFocused);
308
+ });
309
+ // ── Keyboard horizontal scroll ─────────────────────────────
310
+ ctx.registerKeydownHandler((event) => {
311
+ if (event.key !== "ArrowLeft" && event.key !== "ArrowRight")
312
+ return;
313
+ if (!tableLayout)
314
+ return;
315
+ const cols = tableLayout.columns;
316
+ if (cols.length === 0)
317
+ return;
318
+ const viewportWidth = dom.viewport.clientWidth;
319
+ if (tableLayout.totalWidth <= viewportWidth)
320
+ return;
321
+ const scrollLeft = dom.viewport.scrollLeft;
322
+ if (event.key === "ArrowRight") {
323
+ for (let i = 0; i < cols.length; i++) {
324
+ if (cols[i].offset > scrollLeft + 1) {
325
+ dom.viewport.scrollLeft = cols[i].offset;
326
+ event.preventDefault();
327
+ return;
328
+ }
329
+ }
330
+ }
331
+ else {
332
+ for (let i = cols.length - 1; i >= 0; i--) {
333
+ if (cols[i].offset < scrollLeft - 1) {
334
+ dom.viewport.scrollLeft = cols[i].offset;
335
+ event.preventDefault();
336
+ return;
337
+ }
338
+ }
339
+ if (scrollLeft > 0) {
340
+ dom.viewport.scrollLeft = 0;
341
+ event.preventDefault();
342
+ }
343
+ }
344
+ });
345
+ // ── Cleanup ─────────────────────────────────────────────────
346
+ ctx.registerDestroyHandler(() => {
347
+ dom.viewport.removeEventListener("scroll", syncHeaderScroll);
348
+ tableHeader?.destroy();
349
+ tableHeader = null;
350
+ tableRenderer?.destroy();
351
+ tableRenderer = null;
352
+ dom.content.style.minWidth = "";
353
+ dom.root.classList.remove(`${classPrefix}--table`);
354
+ dom.root.classList.remove(`${classPrefix}--table-row-borders`);
355
+ dom.root.classList.remove(`${classPrefix}--table-col-borders`);
356
+ dom.root.removeAttribute("role");
357
+ dom.root.removeAttribute("aria-colcount");
358
+ dom.root.removeAttribute("aria-rowcount");
359
+ dom.root.removeAttribute("tabindex");
360
+ dom.viewport.removeAttribute("role");
361
+ dom.content.removeAttribute("role");
362
+ });
363
+ },
364
+ hooks: {
365
+ onResize(_width, _height) {
366
+ if (!tableLayout || !storedCtx)
367
+ return;
368
+ const newCross = engineState.crossSize;
369
+ tableLayout.resolve(newCross);
370
+ tableHeader?.update(tableLayout);
371
+ tableRenderer?.updateColumnLayout(tableLayout);
372
+ if (tableLayout) {
373
+ storedCtx.dom.content.style.minWidth = `${tableLayout.totalWidth}px`;
374
+ }
375
+ },
376
+ onAfterScroll() {
377
+ if (!storedCtx || !tableHeader)
378
+ return;
379
+ const h = tableHeader;
380
+ h?.syncScroll?.(storedCtx.dom.viewport.scrollLeft);
381
+ },
382
+ },
383
+ destroy() {
384
+ tableHeader?.destroy();
385
+ tableHeader = null;
386
+ tableRenderer?.destroy();
387
+ tableRenderer = null;
388
+ storedCtx = null;
389
+ },
390
+ };
391
+ }