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.
- package/README.github.md +104 -97
- package/README.md +46 -33
- package/dist/constants.d.ts +11 -6
- package/dist/constants.js +83 -0
- package/dist/core/create.d.ts +10 -0
- package/dist/core/create.js +740 -0
- package/dist/core/dom.d.ts +8 -0
- package/dist/core/dom.js +47 -0
- package/dist/core/hooks.d.ts +16 -0
- package/dist/core/hooks.js +67 -0
- package/dist/core/index.d.ts +17 -0
- package/dist/core/index.js +13 -0
- package/dist/core/pipeline.d.ts +51 -0
- package/dist/core/pipeline.js +307 -0
- package/dist/core/pool.d.ts +9 -0
- package/dist/core/pool.js +42 -0
- package/dist/core/scroll.d.ts +32 -0
- package/dist/core/scroll.js +137 -0
- package/dist/core/sizes.d.ts +8 -0
- package/dist/core/sizes.js +6 -0
- package/dist/core/state.d.ts +47 -0
- package/dist/core/state.js +56 -0
- package/dist/core/types.d.ts +187 -0
- package/dist/core/types.js +7 -0
- package/dist/{builder → core}/velocity.d.ts +1 -1
- package/dist/core/velocity.js +33 -0
- package/dist/events/emitter.js +60 -0
- package/dist/events/index.js +6 -0
- package/dist/index.d.ts +28 -19
- package/dist/index.js +28 -1
- package/dist/internals.d.ts +11 -7
- package/dist/internals.js +60 -1
- package/dist/plugins/a11y/index.d.ts +2 -0
- package/dist/plugins/a11y/index.js +1 -0
- package/dist/plugins/a11y/plugin.d.ts +13 -0
- package/dist/plugins/a11y/plugin.js +259 -0
- package/dist/{features → plugins}/async/index.d.ts +1 -1
- package/dist/plugins/async/index.js +12 -0
- package/dist/{features → plugins}/async/manager.d.ts +5 -1
- package/dist/plugins/async/manager.js +568 -0
- package/dist/plugins/async/placeholder.js +154 -0
- package/dist/plugins/async/plugin.d.ts +48 -0
- package/dist/plugins/async/plugin.js +311 -0
- package/dist/plugins/async/sparse.js +540 -0
- package/dist/plugins/autosize/index.d.ts +5 -0
- package/dist/plugins/autosize/index.js +4 -0
- package/dist/plugins/autosize/plugin.d.ts +19 -0
- package/dist/plugins/autosize/plugin.js +185 -0
- package/dist/plugins/grid/index.d.ts +7 -0
- package/dist/plugins/grid/index.js +5 -0
- package/dist/plugins/grid/layout.js +275 -0
- package/dist/plugins/grid/plugin.d.ts +23 -0
- package/dist/plugins/grid/plugin.js +347 -0
- package/dist/plugins/grid/renderer.js +525 -0
- package/dist/plugins/grid/types.js +11 -0
- package/dist/plugins/groups/async-bridge.js +246 -0
- package/dist/{features → plugins}/groups/index.d.ts +1 -1
- package/dist/plugins/groups/index.js +13 -0
- package/dist/plugins/groups/layout.js +294 -0
- package/dist/plugins/groups/plugin.d.ts +22 -0
- package/dist/plugins/groups/plugin.js +571 -0
- package/dist/plugins/groups/sticky.js +255 -0
- package/dist/plugins/groups/types.js +12 -0
- package/dist/plugins/masonry/index.d.ts +8 -0
- package/dist/plugins/masonry/index.js +6 -0
- package/dist/plugins/masonry/layout.js +261 -0
- package/dist/plugins/masonry/plugin.d.ts +32 -0
- package/dist/plugins/masonry/plugin.js +381 -0
- package/dist/plugins/masonry/renderer.js +354 -0
- package/dist/plugins/masonry/types.js +9 -0
- package/dist/plugins/page/index.d.ts +5 -0
- package/dist/plugins/page/index.js +5 -0
- package/dist/plugins/page/plugin.d.ts +21 -0
- package/dist/plugins/page/plugin.js +166 -0
- package/dist/plugins/scale/index.d.ts +5 -0
- package/dist/plugins/scale/index.js +4 -0
- package/dist/plugins/scale/plugin.d.ts +24 -0
- package/dist/plugins/scale/plugin.js +507 -0
- package/dist/plugins/scrollbar/controller.js +574 -0
- package/dist/plugins/scrollbar/index.d.ts +7 -0
- package/dist/plugins/scrollbar/index.js +6 -0
- package/dist/plugins/scrollbar/plugin.d.ts +20 -0
- package/dist/plugins/scrollbar/plugin.js +93 -0
- package/dist/plugins/scrollbar/scrollbar.js +556 -0
- package/dist/plugins/selection/index.d.ts +6 -0
- package/dist/plugins/selection/index.js +7 -0
- package/dist/plugins/selection/plugin.d.ts +16 -0
- package/dist/plugins/selection/plugin.js +601 -0
- package/dist/{features → plugins}/selection/state.d.ts +8 -0
- package/dist/plugins/selection/state.js +332 -0
- package/dist/plugins/snapshots/index.d.ts +5 -0
- package/dist/plugins/snapshots/index.js +5 -0
- package/dist/plugins/snapshots/plugin.d.ts +17 -0
- package/dist/plugins/snapshots/plugin.js +301 -0
- package/dist/plugins/sortable/index.d.ts +6 -0
- package/dist/plugins/sortable/index.js +6 -0
- package/dist/plugins/sortable/plugin.d.ts +34 -0
- package/dist/plugins/sortable/plugin.js +753 -0
- package/dist/plugins/table/header.js +501 -0
- package/dist/{features → plugins}/table/index.d.ts +1 -1
- package/dist/plugins/table/index.js +12 -0
- package/dist/plugins/table/layout.js +211 -0
- package/dist/plugins/table/plugin.d.ts +20 -0
- package/dist/plugins/table/plugin.js +391 -0
- package/dist/plugins/table/renderer.js +625 -0
- package/dist/plugins/table/types.js +12 -0
- package/dist/plugins/transition/index.d.ts +5 -0
- package/dist/plugins/transition/index.js +5 -0
- package/dist/plugins/transition/plugin.d.ts +22 -0
- package/dist/plugins/transition/plugin.js +405 -0
- package/dist/rendering/aria.js +23 -0
- package/dist/rendering/index.js +18 -0
- package/dist/rendering/measured.js +98 -0
- package/dist/rendering/renderer.js +586 -0
- package/dist/rendering/scale.js +267 -0
- package/dist/rendering/scroll.js +71 -0
- package/dist/rendering/sizes.js +193 -0
- package/dist/rendering/sort.js +65 -0
- package/dist/rendering/viewport.js +268 -0
- package/dist/size.json +1 -1
- package/dist/types.js +5 -0
- package/dist/utils/padding.d.ts +2 -4
- package/dist/utils/padding.js +49 -0
- package/dist/utils/stats.js +124 -0
- package/dist/vlist-grid.css +1 -1
- package/dist/vlist-masonry.css +1 -1
- package/dist/vlist-table.css +1 -1
- package/dist/vlist.css +1 -1
- package/package.json +9 -4
- package/dist/builder/a11y.d.ts +0 -16
- package/dist/builder/api.d.ts +0 -21
- package/dist/builder/context.d.ts +0 -36
- package/dist/builder/core.d.ts +0 -16
- package/dist/builder/data.d.ts +0 -71
- package/dist/builder/dom.d.ts +0 -15
- package/dist/builder/index.d.ts +0 -25
- package/dist/builder/materialize.d.ts +0 -166
- package/dist/builder/pool.d.ts +0 -10
- package/dist/builder/range.d.ts +0 -10
- package/dist/builder/scroll.d.ts +0 -24
- package/dist/builder/types.d.ts +0 -512
- package/dist/features/async/feature.d.ts +0 -72
- package/dist/features/autosize/feature.d.ts +0 -34
- package/dist/features/autosize/index.d.ts +0 -2
- package/dist/features/grid/feature.d.ts +0 -48
- package/dist/features/grid/index.d.ts +0 -9
- package/dist/features/groups/feature.d.ts +0 -75
- package/dist/features/masonry/feature.d.ts +0 -45
- package/dist/features/masonry/index.d.ts +0 -9
- package/dist/features/page/feature.d.ts +0 -109
- package/dist/features/page/index.d.ts +0 -9
- package/dist/features/scale/feature.d.ts +0 -42
- package/dist/features/scale/index.d.ts +0 -10
- package/dist/features/scrollbar/feature.d.ts +0 -81
- package/dist/features/scrollbar/index.d.ts +0 -8
- package/dist/features/selection/feature.d.ts +0 -91
- package/dist/features/selection/index.d.ts +0 -7
- package/dist/features/snapshots/feature.d.ts +0 -79
- package/dist/features/snapshots/index.d.ts +0 -9
- package/dist/features/sortable/feature.d.ts +0 -101
- package/dist/features/sortable/index.d.ts +0 -6
- package/dist/features/table/feature.d.ts +0 -67
- package/dist/features/transition/feature.d.ts +0 -30
- package/dist/features/transition/index.d.ts +0 -9
- /package/dist/{features → plugins}/async/placeholder.d.ts +0 -0
- /package/dist/{features → plugins}/async/sparse.d.ts +0 -0
- /package/dist/{features → plugins}/grid/layout.d.ts +0 -0
- /package/dist/{features → plugins}/grid/renderer.d.ts +0 -0
- /package/dist/{features → plugins}/grid/types.d.ts +0 -0
- /package/dist/{features → plugins}/groups/async-bridge.d.ts +0 -0
- /package/dist/{features → plugins}/groups/layout.d.ts +0 -0
- /package/dist/{features → plugins}/groups/sticky.d.ts +0 -0
- /package/dist/{features → plugins}/groups/types.d.ts +0 -0
- /package/dist/{features → plugins}/masonry/layout.d.ts +0 -0
- /package/dist/{features → plugins}/masonry/renderer.d.ts +0 -0
- /package/dist/{features → plugins}/masonry/types.d.ts +0 -0
- /package/dist/{features → plugins}/scrollbar/controller.d.ts +0 -0
- /package/dist/{features → plugins}/scrollbar/scrollbar.d.ts +0 -0
- /package/dist/{features → plugins}/table/header.d.ts +0 -0
- /package/dist/{features → plugins}/table/layout.d.ts +0 -0
- /package/dist/{features → plugins}/table/renderer.d.ts +0 -0
- /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
|
+
}
|