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,501 @@
1
+ /**
2
+ * vlist/table - Header
3
+ * Manages the sticky header row that sits above the scrolling viewport.
4
+ *
5
+ * The header is a positioned DOM element inserted into the vlist root
6
+ * container (above the viewport, like the sticky group header). It contains
7
+ * one cell per column, each showing the column label. Resize handles are
8
+ * rendered at the right edge of each resizable column's header cell.
9
+ *
10
+ * Layout:
11
+ * .vlist (root, position: relative)
12
+ * ├── .vlist-table-header (position: absolute, top: 0, z-index: 5)
13
+ * │ ├── .vlist-table-header-cell [col 0]
14
+ * │ │ ├── .vlist-table-header-content (label)
15
+ * │ │ ├── .vlist-table-header-sort (sort indicator)
16
+ * │ │ └── .vlist-table-header-resize (drag handle)
17
+ * │ ├── .vlist-table-header-cell [col 1]
18
+ * │ │ └── ...
19
+ * │ └── ...
20
+ * └── .vlist-viewport (scrollable, top offset by headerHeight)
21
+ *
22
+ * Resize interaction:
23
+ * mousedown on handle → pointermove updates column width → pointerup commits
24
+ * During drag, a class is added to the root for cursor override.
25
+ *
26
+ * Sort interaction:
27
+ * click on a sortable header cell emits column:sort via the provided callback.
28
+ * The header renders a visual indicator (▲/▼) for the active sort column.
29
+ *
30
+ * Horizontal scroll sync:
31
+ * The header's scrollLeft is kept in sync with the viewport's scrollLeft
32
+ * via the `syncScroll` method, called from the feature's afterScroll hook.
33
+ */
34
+ // =============================================================================
35
+ // Constants
36
+ // =============================================================================
37
+ /** Minimum drag distance (px) before resize is committed */
38
+ const MIN_DRAG_DELTA = 1;
39
+ /** Keyboard resize step in pixels */
40
+ const RESIZE_STEP = 10;
41
+ /** Sort indicator characters */
42
+ const SORT_ASC = "\u25B2"; // ▲
43
+ const SORT_DESC = "\u25BC"; // ▼
44
+ // =============================================================================
45
+ // Factory
46
+ // =============================================================================
47
+ /**
48
+ * Create a TableHeader instance.
49
+ *
50
+ * @param root - The vlist root element (.vlist)
51
+ * @param headerHeight - Height of the header row in pixels
52
+ * @param classPrefix - CSS class prefix (default: 'vlist')
53
+ * @param onResize - Callback when a column is resized (receives column index and new width)
54
+ * @param onSort - Callback when a sortable header is clicked
55
+ * @param onClick - Callback when any header cell is clicked
56
+ * @returns TableHeader instance
57
+ */
58
+ export const createTableHeader = (root, headerHeight, classPrefix, onResize, onSort, onClick) => {
59
+ // =========================================================================
60
+ // DOM Setup
61
+ // =========================================================================
62
+ // Rowgroup wrapper — ensures the header row sits inside a proper
63
+ // ARIA rowgroup within the grid (dom.root has role="grid").
64
+ const rowgroup = document.createElement("div");
65
+ rowgroup.setAttribute("role", "rowgroup");
66
+ const element = document.createElement("div");
67
+ element.className = `${classPrefix}-table-header`;
68
+ element.setAttribute("role", "row");
69
+ element.setAttribute("aria-rowindex", "1");
70
+ // Only dynamic style — height comes from config
71
+ element.style.height = `${headerHeight}px`;
72
+ // Scroll container inside the header — this is what we scroll in sync
73
+ // with the viewport for horizontal scrolling.
74
+ const scrollContainer = document.createElement("div");
75
+ scrollContainer.className = `${classPrefix}-table-header-scroll`;
76
+ scrollContainer.setAttribute("role", "presentation");
77
+ element.appendChild(scrollContainer);
78
+ // Insert header rowgroup as first child of root (above viewport)
79
+ rowgroup.appendChild(element);
80
+ root.insertBefore(rowgroup, root.firstChild);
81
+ // Expose the header height as a CSS variable on the root so the custom
82
+ // scrollbar (if active) can offset its track below the header row.
83
+ // The viewport layout is handled entirely by CSS flex — no inline style needed.
84
+ root.style.setProperty('--vlist-table-header-height', `${headerHeight}px`);
85
+ // =========================================================================
86
+ // State
87
+ // =========================================================================
88
+ let cells = [];
89
+ let sortIndicators = [];
90
+ let isVisible = true;
91
+ let currentSortKey = null;
92
+ let currentSortDirection = "asc";
93
+ let currentLayout = null;
94
+ // Drag state
95
+ let isDragging = false;
96
+ let dragColumnIndex = -1;
97
+ let dragStartX = 0;
98
+ let dragStartWidth = 0;
99
+ // Keyboard focus state (roving tabindex)
100
+ let focusedCellIndex = 0;
101
+ // =========================================================================
102
+ // Cell Creation
103
+ // =========================================================================
104
+ /**
105
+ * Create a single header cell element for a resolved column.
106
+ */
107
+ const createCell = (col, colIndex) => {
108
+ const cell = document.createElement("div");
109
+ cell.className = `${classPrefix}-table-header-cell`;
110
+ cell.setAttribute("role", "columnheader");
111
+ cell.setAttribute("aria-colindex", String(colIndex + 1));
112
+ cell.setAttribute("tabindex", colIndex === 0 ? "0" : "-1");
113
+ cell.dataset.columnKey = col.def.key;
114
+ // Alignment modifier class (left is the default — no class needed)
115
+ const align = col.def.align;
116
+ if (align === "center") {
117
+ cell.classList.add(`${classPrefix}-table-header-cell--center`);
118
+ }
119
+ else if (align === "right") {
120
+ cell.classList.add(`${classPrefix}-table-header-cell--right`);
121
+ }
122
+ // Content wrapper
123
+ const content = document.createElement("div");
124
+ content.className = `${classPrefix}-table-header-content`;
125
+ // Render label
126
+ const label = col.def.header
127
+ ? col.def.header(col.def)
128
+ : col.def.label;
129
+ if (typeof label === "string") {
130
+ content.textContent = label;
131
+ }
132
+ else {
133
+ content.appendChild(label);
134
+ }
135
+ cell.appendChild(content);
136
+ // Sort indicator — only created for sortable columns (avoids unnecessary DOM nodes)
137
+ if (col.def.sortable) {
138
+ const sortIndicator = document.createElement("span");
139
+ sortIndicator.className = `${classPrefix}-table-header-sort`;
140
+ sortIndicator.setAttribute("aria-hidden", "true");
141
+ cell.appendChild(sortIndicator);
142
+ sortIndicators.push(sortIndicator);
143
+ cell.classList.add(`${classPrefix}-table-header-cell--sortable`);
144
+ }
145
+ else {
146
+ // Keep index alignment with columns array
147
+ sortIndicators.push(null);
148
+ }
149
+ // Resize handle (at right edge)
150
+ if (col.resizable) {
151
+ const handle = document.createElement("div");
152
+ handle.className = `${classPrefix}-table-header-resize`;
153
+ handle.dataset.resizeIndex = String(colIndex);
154
+ cell.appendChild(handle);
155
+ }
156
+ return cell;
157
+ };
158
+ // =========================================================================
159
+ // Build / Rebuild
160
+ // =========================================================================
161
+ /**
162
+ * Build all header cells from a layout.
163
+ */
164
+ const rebuild = (layout) => {
165
+ currentLayout = layout;
166
+ // Clear existing cells
167
+ scrollContainer.textContent = "";
168
+ cells = [];
169
+ sortIndicators = [];
170
+ const columns = layout.columns;
171
+ for (let i = 0; i < columns.length; i++) {
172
+ const col = columns[i];
173
+ const cell = createCell(col, i);
174
+ cells.push(cell);
175
+ scrollContainer.appendChild(cell);
176
+ }
177
+ // Apply widths and offsets
178
+ update(layout);
179
+ // Restore sort indicator if active
180
+ if (currentSortKey) {
181
+ updateSort(currentSortKey, currentSortDirection);
182
+ }
183
+ };
184
+ // =========================================================================
185
+ // Update (positions & widths)
186
+ // =========================================================================
187
+ /**
188
+ * Update header cell widths to match the layout.
189
+ * Called after layout.resolve() or after a resize.
190
+ */
191
+ const update = (layout) => {
192
+ currentLayout = layout;
193
+ const columns = layout.columns;
194
+ // Set scroll container to total column width
195
+ scrollContainer.style.width = `${layout.totalWidth}px`;
196
+ for (let i = 0; i < cells.length && i < columns.length; i++) {
197
+ const cell = cells[i];
198
+ const col = columns[i];
199
+ cell.style.width = `${col.width}px`;
200
+ }
201
+ };
202
+ // =========================================================================
203
+ // Sort Indicator
204
+ // =========================================================================
205
+ /**
206
+ * Update the sort indicator on header cells.
207
+ *
208
+ * @param key - Column key to show sort on, or null to clear
209
+ * @param direction - Sort direction
210
+ */
211
+ const updateSort = (key, direction) => {
212
+ currentSortKey = key;
213
+ currentSortDirection = direction;
214
+ if (!currentLayout)
215
+ return;
216
+ const columns = currentLayout.columns;
217
+ for (let i = 0; i < sortIndicators.length && i < columns.length; i++) {
218
+ const indicator = sortIndicators[i];
219
+ if (!indicator)
220
+ continue;
221
+ const col = columns[i];
222
+ if (col.def.key === key) {
223
+ indicator.textContent = direction === "asc" ? SORT_ASC : SORT_DESC;
224
+ indicator.style.opacity = "0.7";
225
+ cells[i].setAttribute("aria-sort", direction === "asc" ? "ascending" : "descending");
226
+ }
227
+ else {
228
+ indicator.textContent = "";
229
+ indicator.style.opacity = "0";
230
+ cells[i].removeAttribute("aria-sort");
231
+ }
232
+ }
233
+ };
234
+ // =========================================================================
235
+ // Horizontal Scroll Sync
236
+ // =========================================================================
237
+ /**
238
+ * Synchronize header scroll position with the viewport.
239
+ * Call this from the feature's scroll handler.
240
+ */
241
+ const syncScroll = (scrollLeft) => {
242
+ scrollContainer.style.transform = `translateX(${-scrollLeft}px)`;
243
+ };
244
+ // =========================================================================
245
+ // Resize Interaction (Pointer Events)
246
+ // =========================================================================
247
+ const onPointerDown = (e) => {
248
+ const target = e.target;
249
+ if (!target.dataset.resizeIndex)
250
+ return;
251
+ e.preventDefault();
252
+ e.stopPropagation();
253
+ dragColumnIndex = +target.dataset.resizeIndex;
254
+ if (!currentLayout)
255
+ return;
256
+ const col = currentLayout.getColumn(dragColumnIndex);
257
+ if (!col?.resizable)
258
+ return;
259
+ isDragging = true;
260
+ dragStartX = e.clientX;
261
+ dragStartWidth = col.width;
262
+ // Set cursor on the root to avoid flickering during drag
263
+ root.classList.add(`${classPrefix}--col-resizing`);
264
+ root.style.cursor = "col-resize";
265
+ // Highlight the active resize handle
266
+ target.classList.add(`${classPrefix}-table-header-resize--active`);
267
+ // Capture pointer for tracking outside the element
268
+ target.setPointerCapture(e.pointerId);
269
+ // Bind move and up handlers
270
+ target.addEventListener("pointermove", onPointerMove);
271
+ target.addEventListener("pointerup", onPointerUp);
272
+ target.addEventListener("pointercancel", onPointerUp);
273
+ };
274
+ const onPointerMove = (e) => {
275
+ if (!isDragging || !currentLayout)
276
+ return;
277
+ const delta = e.clientX - dragStartX;
278
+ if (Math.abs(delta) < MIN_DRAG_DELTA)
279
+ return;
280
+ const newWidth = Math.max(0, dragStartWidth + delta);
281
+ onResize(dragColumnIndex, newWidth);
282
+ };
283
+ const onPointerUp = (e) => {
284
+ if (!isDragging)
285
+ return;
286
+ const target = e.target;
287
+ // Remove move/up listeners
288
+ target.removeEventListener("pointermove", onPointerMove);
289
+ target.removeEventListener("pointerup", onPointerUp);
290
+ target.removeEventListener("pointercancel", onPointerUp);
291
+ // Release pointer capture
292
+ try {
293
+ target.releasePointerCapture(e.pointerId);
294
+ }
295
+ catch {
296
+ // Pointer may have been released already
297
+ }
298
+ // Reset drag handle highlight
299
+ target.classList.remove(`${classPrefix}-table-header-resize--active`);
300
+ // Reset cursor
301
+ root.classList.remove(`${classPrefix}--col-resizing`);
302
+ root.style.cursor = "";
303
+ isDragging = false;
304
+ dragColumnIndex = -1;
305
+ };
306
+ // =========================================================================
307
+ // Click Interaction (Sort + General)
308
+ // =========================================================================
309
+ const onCellClick = (e) => {
310
+ // Ignore clicks on resize handles
311
+ const target = e.target;
312
+ if (target.dataset.resizeIndex)
313
+ return;
314
+ if (isDragging)
315
+ return;
316
+ // Walk up to find the header cell
317
+ let cell = target;
318
+ while (cell && !cell.dataset.columnKey) {
319
+ cell = cell.parentElement;
320
+ // Don't walk outside the header
321
+ if (cell === element || cell === null)
322
+ return;
323
+ }
324
+ const key = cell.dataset.columnKey;
325
+ if (!currentLayout)
326
+ return;
327
+ // Find the column
328
+ const columns = currentLayout.columns;
329
+ let colIndex = -1;
330
+ for (let i = 0; i < columns.length; i++) {
331
+ if (columns[i].def.key === key) {
332
+ colIndex = i;
333
+ break;
334
+ }
335
+ }
336
+ if (colIndex === -1)
337
+ return;
338
+ const col = columns[colIndex];
339
+ // Emit general click
340
+ if (onClick) {
341
+ onClick({ key, index: colIndex, event: e });
342
+ }
343
+ // Emit sort event for sortable columns
344
+ if (col.def.sortable && onSort) {
345
+ let direction;
346
+ if (currentSortKey === key) {
347
+ // Cycle: asc → desc → null
348
+ direction = currentSortDirection === "asc" ? "desc" : null;
349
+ }
350
+ else {
351
+ direction = "asc";
352
+ }
353
+ onSort({ key, index: colIndex, direction });
354
+ }
355
+ };
356
+ // =========================================================================
357
+ // Keyboard Navigation (roving tabindex on header cells)
358
+ // =========================================================================
359
+ const moveFocusToCell = (index) => {
360
+ if (index < 0 || index >= cells.length)
361
+ return;
362
+ cells[focusedCellIndex]?.setAttribute("tabindex", "-1");
363
+ focusedCellIndex = index;
364
+ const cell = cells[focusedCellIndex];
365
+ cell.setAttribute("tabindex", "0");
366
+ cell.focus();
367
+ };
368
+ const onKeyDown = (e) => {
369
+ const key = e.key;
370
+ if (key === "ArrowRight") {
371
+ if (e.ctrlKey || e.metaKey) {
372
+ // Ctrl+Right: resize column wider
373
+ if (!currentLayout)
374
+ return;
375
+ const col = currentLayout.getColumn(focusedCellIndex);
376
+ if (col?.resizable) {
377
+ onResize(focusedCellIndex, col.width + RESIZE_STEP);
378
+ e.preventDefault();
379
+ }
380
+ }
381
+ else {
382
+ // Move focus to next header cell
383
+ if (focusedCellIndex < cells.length - 1) {
384
+ moveFocusToCell(focusedCellIndex + 1);
385
+ e.preventDefault();
386
+ }
387
+ }
388
+ e.stopPropagation();
389
+ return;
390
+ }
391
+ if (key === "ArrowLeft") {
392
+ if (e.ctrlKey || e.metaKey) {
393
+ // Ctrl+Left: resize column narrower
394
+ if (!currentLayout)
395
+ return;
396
+ const col = currentLayout.getColumn(focusedCellIndex);
397
+ if (col?.resizable) {
398
+ onResize(focusedCellIndex, col.width - RESIZE_STEP);
399
+ e.preventDefault();
400
+ }
401
+ }
402
+ else {
403
+ // Move focus to previous header cell
404
+ if (focusedCellIndex > 0) {
405
+ moveFocusToCell(focusedCellIndex - 1);
406
+ e.preventDefault();
407
+ }
408
+ }
409
+ e.stopPropagation();
410
+ return;
411
+ }
412
+ if (key === "Home") {
413
+ moveFocusToCell(0);
414
+ e.preventDefault();
415
+ e.stopPropagation();
416
+ return;
417
+ }
418
+ if (key === "End") {
419
+ moveFocusToCell(cells.length - 1);
420
+ e.preventDefault();
421
+ e.stopPropagation();
422
+ return;
423
+ }
424
+ if (key === "Enter" || key === " ") {
425
+ // Trigger sort on sortable columns
426
+ if (!currentLayout)
427
+ return;
428
+ const col = currentLayout.getColumn(focusedCellIndex);
429
+ if (col?.def.sortable && onSort) {
430
+ let direction;
431
+ if (currentSortKey === col.def.key) {
432
+ direction = currentSortDirection === "asc" ? "desc" : null;
433
+ }
434
+ else {
435
+ direction = "asc";
436
+ }
437
+ onSort({ key: col.def.key, index: focusedCellIndex, direction });
438
+ e.preventDefault();
439
+ }
440
+ e.stopPropagation();
441
+ return;
442
+ }
443
+ if (key === "ArrowDown") {
444
+ // Return focus to the grid body
445
+ root.focus();
446
+ e.preventDefault();
447
+ e.stopPropagation();
448
+ return;
449
+ }
450
+ };
451
+ // =========================================================================
452
+ // Event Binding
453
+ // =========================================================================
454
+ element.addEventListener("pointerdown", onPointerDown);
455
+ element.addEventListener("click", onCellClick);
456
+ element.addEventListener("keydown", onKeyDown);
457
+ // =========================================================================
458
+ // Visibility
459
+ // =========================================================================
460
+ const show = () => {
461
+ if (isVisible)
462
+ return;
463
+ isVisible = true;
464
+ element.style.display = "";
465
+ };
466
+ const hide = () => {
467
+ if (!isVisible)
468
+ return;
469
+ isVisible = false;
470
+ element.style.display = "none";
471
+ };
472
+ // =========================================================================
473
+ // Destroy
474
+ // =========================================================================
475
+ const destroy = () => {
476
+ element.removeEventListener("pointerdown", onPointerDown);
477
+ element.removeEventListener("click", onCellClick);
478
+ element.removeEventListener("keydown", onKeyDown);
479
+ // Clear the CSS variable set during setup
480
+ root.style.removeProperty('--vlist-table-header-height');
481
+ rowgroup.remove();
482
+ cells = [];
483
+ sortIndicators = [];
484
+ currentLayout = null;
485
+ isVisible = false;
486
+ };
487
+ // =========================================================================
488
+ // Return
489
+ // =========================================================================
490
+ return {
491
+ element,
492
+ update,
493
+ updateSort,
494
+ rebuild,
495
+ show,
496
+ hide,
497
+ destroy,
498
+ // Extra method exposed for scroll sync (not in interface — cast in feature)
499
+ syncScroll,
500
+ };
501
+ };
@@ -2,7 +2,7 @@
2
2
  * vlist - Table Domain
3
3
  * Data table layout with columns, resizable headers, and cell rendering
4
4
  */
5
- export { withTable, type TableFeatureConfig } from "./feature";
5
+ export { table, type TablePluginConfig } from "./plugin";
6
6
  export { createTableLayout } from "./layout";
7
7
  export { createTableHeader } from "./header";
8
8
  export { createTableRenderer, type TableRendererInstance } from "./renderer";
@@ -0,0 +1,12 @@
1
+ /**
2
+ * vlist - Table Domain
3
+ * Data table layout with columns, resizable headers, and cell rendering
4
+ */
5
+ // v2 Plugin
6
+ export { table } from "./plugin";
7
+ // Layout
8
+ export { createTableLayout } from "./layout";
9
+ // Header
10
+ export { createTableHeader } from "./header";
11
+ // Renderer
12
+ export { createTableRenderer } from "./renderer";