@svgrid/grid 1.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/LICENSE +33 -0
- package/README.md +39 -0
- package/dist/FlexRender.svelte +96 -0
- package/dist/FlexRender.svelte.d.ts +49 -0
- package/dist/SvGrid.svelte +8742 -0
- package/dist/SvGrid.svelte.d.ts +381 -0
- package/dist/SvGridChart.svelte +653 -0
- package/dist/SvGridChart.svelte.d.ts +25 -0
- package/dist/SvGridChart.test.d.ts +1 -0
- package/dist/SvGridChart.test.js +62 -0
- package/dist/SvGridDropdown.svelte +675 -0
- package/dist/SvGridDropdown.svelte.d.ts +40 -0
- package/dist/a11y.contract.test.d.ts +1 -0
- package/dist/a11y.contract.test.js +39 -0
- package/dist/a11y.d.ts +38 -0
- package/dist/a11y.js +37 -0
- package/dist/a11y.test.d.ts +1 -0
- package/dist/a11y.test.js +44 -0
- package/dist/cell-formatting.d.ts +17 -0
- package/dist/cell-formatting.js +141 -0
- package/dist/cell-formatting.test.d.ts +1 -0
- package/dist/cell-formatting.test.js +234 -0
- package/dist/chart-export.d.ts +14 -0
- package/dist/chart-export.js +112 -0
- package/dist/chart.d.ts +226 -0
- package/dist/chart.js +732 -0
- package/dist/chart.test.d.ts +1 -0
- package/dist/chart.test.js +289 -0
- package/dist/collaboration.d.ts +74 -0
- package/dist/collaboration.js +98 -0
- package/dist/collaboration.test.d.ts +1 -0
- package/dist/collaboration.test.js +65 -0
- package/dist/conditional-formatting.d.ts +91 -0
- package/dist/conditional-formatting.js +170 -0
- package/dist/conditional-formatting.test.d.ts +1 -0
- package/dist/conditional-formatting.test.js +87 -0
- package/dist/core.coverage.test.d.ts +1 -0
- package/dist/core.coverage.test.js +186 -0
- package/dist/core.d.ts +370 -0
- package/dist/core.js +568 -0
- package/dist/core.performance.test.d.ts +1 -0
- package/dist/core.performance.test.js +29 -0
- package/dist/createGrid.svelte.d.ts +6 -0
- package/dist/createGrid.svelte.js +17 -0
- package/dist/createGrid.test.d.ts +1 -0
- package/dist/createGrid.test.js +9 -0
- package/dist/createGridState.svelte.d.ts +3 -0
- package/dist/createGridState.svelte.js +13 -0
- package/dist/editors/cell-editors.d.ts +23 -0
- package/dist/editors/cell-editors.js +97 -0
- package/dist/editors/cell-editors.test.d.ts +1 -0
- package/dist/editors/cell-editors.test.js +75 -0
- package/dist/fill-patterns.d.ts +30 -0
- package/dist/fill-patterns.js +207 -0
- package/dist/fill-patterns.test.d.ts +1 -0
- package/dist/fill-patterns.test.js +81 -0
- package/dist/filtering/excel-filters.d.ts +26 -0
- package/dist/filtering/excel-filters.js +64 -0
- package/dist/filtering/excel-filters.test.d.ts +1 -0
- package/dist/filtering/excel-filters.test.js +116 -0
- package/dist/filtering/locale-filter.test.d.ts +13 -0
- package/dist/filtering/locale-filter.test.js +189 -0
- package/dist/flex-render.d.ts +1 -0
- package/dist/flex-render.js +2 -0
- package/dist/flex-render.test.d.ts +1 -0
- package/dist/flex-render.test.js +142 -0
- package/dist/group-aggregate.test.d.ts +1 -0
- package/dist/group-aggregate.test.js +32 -0
- package/dist/index.d.ts +29 -0
- package/dist/index.js +27 -0
- package/dist/keyboard.d.ts +8 -0
- package/dist/keyboard.js +91 -0
- package/dist/keyboard.test.d.ts +1 -0
- package/dist/keyboard.test.js +27 -0
- package/dist/merge-objects.d.ts +9 -0
- package/dist/merge-objects.js +28 -0
- package/dist/named-views.d.ts +42 -0
- package/dist/named-views.js +80 -0
- package/dist/named-views.test.d.ts +1 -0
- package/dist/named-views.test.js +57 -0
- package/dist/new-features.test.d.ts +1 -0
- package/dist/new-features.test.js +183 -0
- package/dist/render-component.d.ts +13 -0
- package/dist/render-component.js +14 -0
- package/dist/render-component.test.d.ts +1 -0
- package/dist/render-component.test.js +41 -0
- package/dist/server-data-source.d.ts +76 -0
- package/dist/server-data-source.js +83 -0
- package/dist/server-data-source.test.d.ts +1 -0
- package/dist/server-data-source.test.js +81 -0
- package/dist/sparkline.d.ts +56 -0
- package/dist/sparkline.js +97 -0
- package/dist/sparkline.test.d.ts +1 -0
- package/dist/sparkline.test.js +52 -0
- package/dist/static-functions.d.ts +1 -0
- package/dist/static-functions.js +1 -0
- package/dist/subscribe.d.ts +9 -0
- package/dist/subscribe.js +31 -0
- package/dist/subscribe.test.d.ts +1 -0
- package/dist/subscribe.test.js +93 -0
- package/dist/sv-grid-scrollbar.d.ts +1 -0
- package/dist/sv-grid-scrollbar.js +310 -0
- package/dist/svgrid-wrapper.types.d.ts +341 -0
- package/dist/svgrid-wrapper.types.js +1 -0
- package/dist/svgrid.api-extensions.test.d.ts +14 -0
- package/dist/svgrid.api-extensions.test.js +334 -0
- package/dist/svgrid.api.test.d.ts +15 -0
- package/dist/svgrid.api.test.js +169 -0
- package/dist/svgrid.behavior.test.d.ts +15 -0
- package/dist/svgrid.behavior.test.js +605 -0
- package/dist/svgrid.column-reorder.test.d.ts +15 -0
- package/dist/svgrid.column-reorder.test.js +224 -0
- package/dist/svgrid.features.test.d.ts +1 -0
- package/dist/svgrid.features.test.js +129 -0
- package/dist/svgrid.interaction.test.d.ts +9 -0
- package/dist/svgrid.interaction.test.js +318 -0
- package/dist/svgrid.locale-filtering.test.d.ts +15 -0
- package/dist/svgrid.locale-filtering.test.js +261 -0
- package/dist/svgrid.new-features.wrapper.test.d.ts +1 -0
- package/dist/svgrid.new-features.wrapper.test.js +136 -0
- package/dist/svgrid.row-pinning.test.d.ts +14 -0
- package/dist/svgrid.row-pinning.test.js +250 -0
- package/dist/svgrid.set-filter.test.d.ts +17 -0
- package/dist/svgrid.set-filter.test.js +268 -0
- package/dist/svgrid.wrapper.test.d.ts +1 -0
- package/dist/svgrid.wrapper.test.js +33 -0
- package/dist/test-setup.d.ts +1 -0
- package/dist/test-setup.js +29 -0
- package/dist/transaction.test.d.ts +1 -0
- package/dist/transaction.test.js +89 -0
- package/dist/virtualization/column-virtualizer.d.ts +20 -0
- package/dist/virtualization/column-virtualizer.js +19 -0
- package/dist/virtualization/column-virtualizer.test.d.ts +1 -0
- package/dist/virtualization/column-virtualizer.test.js +25 -0
- package/dist/virtualization/svelte-virtualizer.svelte.d.ts +11 -0
- package/dist/virtualization/svelte-virtualizer.svelte.js +20 -0
- package/dist/virtualization/types.d.ts +28 -0
- package/dist/virtualization/types.js +1 -0
- package/dist/virtualization/virtualizer.d.ts +13 -0
- package/dist/virtualization/virtualizer.js +232 -0
- package/dist/virtualization/virtualizer.test.d.ts +1 -0
- package/dist/virtualization/virtualizer.test.js +41 -0
- package/package.json +69 -0
- package/src/FlexRender.svelte +96 -0
- package/src/SvGrid.svelte +8742 -0
- package/src/SvGridChart.svelte +653 -0
- package/src/SvGridChart.test.ts +69 -0
- package/src/SvGridDropdown.svelte +675 -0
- package/src/a11y.contract.test.ts +49 -0
- package/src/a11y.test.ts +59 -0
- package/src/a11y.ts +59 -0
- package/src/cell-formatting.test.ts +273 -0
- package/src/cell-formatting.ts +169 -0
- package/src/chart-export.ts +144 -0
- package/src/chart.test.ts +311 -0
- package/src/chart.ts +934 -0
- package/src/collaboration.test.ts +74 -0
- package/src/collaboration.ts +161 -0
- package/src/conditional-formatting.test.ts +107 -0
- package/src/conditional-formatting.ts +239 -0
- package/src/core.coverage.test.ts +230 -0
- package/src/core.performance.test.ts +30 -0
- package/src/core.ts +991 -0
- package/src/createGrid.svelte.ts +42 -0
- package/src/createGrid.test.ts +10 -0
- package/src/createGridState.svelte.ts +17 -0
- package/src/editors/cell-editors.test.ts +80 -0
- package/src/editors/cell-editors.ts +127 -0
- package/src/fill-patterns.test.ts +93 -0
- package/src/fill-patterns.ts +229 -0
- package/src/filtering/excel-filters.test.ts +131 -0
- package/src/filtering/excel-filters.ts +101 -0
- package/src/filtering/locale-filter.test.ts +220 -0
- package/src/flex-render.test.ts +143 -0
- package/src/flex-render.ts +3 -0
- package/src/group-aggregate.test.ts +48 -0
- package/src/index.ts +159 -0
- package/src/keyboard.test.ts +59 -0
- package/src/keyboard.ts +97 -0
- package/src/merge-objects.ts +48 -0
- package/src/named-views.test.ts +66 -0
- package/src/named-views.ts +120 -0
- package/src/new-features.test.ts +217 -0
- package/src/render-component.test.ts +51 -0
- package/src/render-component.ts +28 -0
- package/src/server-data-source.test.ts +88 -0
- package/src/server-data-source.ts +163 -0
- package/src/sparkline.test.ts +59 -0
- package/src/sparkline.ts +164 -0
- package/src/static-functions.ts +11 -0
- package/src/subscribe.test.ts +103 -0
- package/src/subscribe.ts +38 -0
- package/src/sv-grid-scrollbar.ts +347 -0
- package/src/svgrid-wrapper.types.ts +382 -0
- package/src/svgrid.api-extensions.test.ts +362 -0
- package/src/svgrid.api.test.ts +192 -0
- package/src/svgrid.behavior.test.ts +657 -0
- package/src/svgrid.column-reorder.test.ts +234 -0
- package/src/svgrid.features.test.ts +157 -0
- package/src/svgrid.interaction.test.ts +355 -0
- package/src/svgrid.locale-filtering.test.ts +259 -0
- package/src/svgrid.new-features.wrapper.test.ts +164 -0
- package/src/svgrid.row-pinning.test.ts +266 -0
- package/src/svgrid.set-filter.test.ts +270 -0
- package/src/svgrid.wrapper.test.ts +35 -0
- package/src/test-setup.ts +37 -0
- package/src/transaction.test.ts +100 -0
- package/src/virtualization/column-virtualizer.test.ts +27 -0
- package/src/virtualization/column-virtualizer.ts +30 -0
- package/src/virtualization/svelte-virtualizer.svelte.ts +24 -0
- package/src/virtualization/types.ts +30 -0
- package/src/virtualization/virtualizer.test.ts +47 -0
- package/src/virtualization/virtualizer.ts +270 -0
package/dist/core.js
ADDED
|
@@ -0,0 +1,568 @@
|
|
|
1
|
+
/** Apply a group aggregator over a bucket's leaf rows for one column. */
|
|
2
|
+
export function applyGroupAggregate(agg, columnId, rows) {
|
|
3
|
+
const raw = rows.map((r) => r.getCellValueByColumnId(columnId));
|
|
4
|
+
if (typeof agg === 'function') {
|
|
5
|
+
const nums = raw.map((v) => Number(v)).filter((n) => Number.isFinite(n));
|
|
6
|
+
return agg(nums, rows.map((r) => r.original));
|
|
7
|
+
}
|
|
8
|
+
if (agg === 'count')
|
|
9
|
+
return rows.length;
|
|
10
|
+
if (agg === 'countDistinct')
|
|
11
|
+
return new Set(raw.map((v) => String(v ?? ''))).size;
|
|
12
|
+
if (agg === 'first')
|
|
13
|
+
return raw[0];
|
|
14
|
+
const nums = raw.map((v) => Number(v)).filter((n) => Number.isFinite(n));
|
|
15
|
+
if (!nums.length)
|
|
16
|
+
return undefined;
|
|
17
|
+
switch (agg) {
|
|
18
|
+
case 'sum':
|
|
19
|
+
return nums.reduce((a, b) => a + b, 0);
|
|
20
|
+
case 'avg':
|
|
21
|
+
return nums.reduce((a, b) => a + b, 0) / nums.length;
|
|
22
|
+
case 'min':
|
|
23
|
+
return Math.min(...nums);
|
|
24
|
+
case 'max':
|
|
25
|
+
return Math.max(...nums);
|
|
26
|
+
case 'extent':
|
|
27
|
+
return `${Math.min(...nums)} – ${Math.max(...nums)}`;
|
|
28
|
+
default:
|
|
29
|
+
return undefined;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
function createStore(initial) {
|
|
33
|
+
let value = initial;
|
|
34
|
+
const listeners = new Set();
|
|
35
|
+
return {
|
|
36
|
+
get state() {
|
|
37
|
+
return value;
|
|
38
|
+
},
|
|
39
|
+
setState(updater) {
|
|
40
|
+
value = updater(value);
|
|
41
|
+
listeners.forEach((listener) => listener());
|
|
42
|
+
},
|
|
43
|
+
subscribe(listener) {
|
|
44
|
+
listeners.add(listener);
|
|
45
|
+
return () => listeners.delete(listener);
|
|
46
|
+
},
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
export const rowSortingFeature = { key: 'rowSortingFeature' };
|
|
50
|
+
export const columnFilteringFeature = { key: 'columnFilteringFeature' };
|
|
51
|
+
export const rowPaginationFeature = { key: 'rowPaginationFeature' };
|
|
52
|
+
export const columnGroupingFeature = { key: 'columnGroupingFeature' };
|
|
53
|
+
export const rowSelectionFeature = { key: 'rowSelectionFeature' };
|
|
54
|
+
export const rowExpandingFeature = { key: 'rowExpandingFeature' };
|
|
55
|
+
export function tableFeatures(features) {
|
|
56
|
+
return features;
|
|
57
|
+
}
|
|
58
|
+
export const sortFns = {
|
|
59
|
+
auto: (a, b) => String(a).localeCompare(String(b)),
|
|
60
|
+
number: (a, b) => Number(a ?? 0) - Number(b ?? 0),
|
|
61
|
+
date: (a, b) => {
|
|
62
|
+
const aa = new Date(a).getTime();
|
|
63
|
+
const bb = new Date(b).getTime();
|
|
64
|
+
return aa - bb;
|
|
65
|
+
},
|
|
66
|
+
};
|
|
67
|
+
export const filterFns = {
|
|
68
|
+
includesString: (value, query) => String(value).toLowerCase().includes(query.toLowerCase()),
|
|
69
|
+
equals: (value, query) => value === query,
|
|
70
|
+
};
|
|
71
|
+
export function createCoreRowModel() {
|
|
72
|
+
return ({ rows }) => rows;
|
|
73
|
+
}
|
|
74
|
+
export function createFilteredRowModel() {
|
|
75
|
+
return ({ table, rows }) => {
|
|
76
|
+
const filters = table.getState().columnFilters ?? [];
|
|
77
|
+
if (!filters.length)
|
|
78
|
+
return rows;
|
|
79
|
+
return rows.filter((row) => {
|
|
80
|
+
return filters.every((filter) => {
|
|
81
|
+
const cellValue = row
|
|
82
|
+
.getAllCells()
|
|
83
|
+
.find((cell) => cell.column.id === filter.id)
|
|
84
|
+
?.getValue();
|
|
85
|
+
const filterFn = filter.fn ? filterFns[filter.fn] : filterFns.includesString;
|
|
86
|
+
return filterFn(cellValue, filter.value);
|
|
87
|
+
});
|
|
88
|
+
});
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
export function createPaginatedRowModel() {
|
|
92
|
+
return ({ table, rows }) => {
|
|
93
|
+
const pagination = table.getState().pagination ?? { pageIndex: 0, pageSize: rows.length || 10 };
|
|
94
|
+
const start = pagination.pageIndex * pagination.pageSize;
|
|
95
|
+
return rows.slice(start, start + pagination.pageSize);
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
export function createGroupedRowModel() {
|
|
99
|
+
return ({ table, rows }) => {
|
|
100
|
+
const grouping = table.getState().grouping ?? [];
|
|
101
|
+
if (!grouping.length)
|
|
102
|
+
return rows;
|
|
103
|
+
const columns = table.getAllColumns();
|
|
104
|
+
// Recursively bucket rows by each grouping column in turn. At every level a
|
|
105
|
+
// group row is built that stands in for its children - a non-group column
|
|
106
|
+
// resolves to the value shared by every leaf row, or to undefined when the
|
|
107
|
+
// leaves disagree.
|
|
108
|
+
function buildGroups(input, levelIndex, depth, idPrefix) {
|
|
109
|
+
if (levelIndex >= grouping.length) {
|
|
110
|
+
// Leaves: actual data rows, with their nesting depth recorded.
|
|
111
|
+
return input.map((row) => ({ ...row, depth }));
|
|
112
|
+
}
|
|
113
|
+
const groupKey = grouping[levelIndex];
|
|
114
|
+
if (!groupKey)
|
|
115
|
+
return input;
|
|
116
|
+
const buckets = new Map();
|
|
117
|
+
for (const row of input) {
|
|
118
|
+
const value = row.getCellValueByColumnId(groupKey);
|
|
119
|
+
const key = String(value ?? '');
|
|
120
|
+
const list = buckets.get(key) ?? [];
|
|
121
|
+
list.push(row);
|
|
122
|
+
buckets.set(key, list);
|
|
123
|
+
}
|
|
124
|
+
const groupRows = [];
|
|
125
|
+
let index = 0;
|
|
126
|
+
buckets.forEach((children, key) => {
|
|
127
|
+
const id = `${idPrefix}_${groupKey}_${key}`;
|
|
128
|
+
const subRows = buildGroups(children, levelIndex + 1, depth + 1, id);
|
|
129
|
+
const isDeepest = levelIndex + 1 >= grouping.length;
|
|
130
|
+
const leafCount = isDeepest
|
|
131
|
+
? subRows.length
|
|
132
|
+
: subRows.reduce((sum, sub) => sum + (sub.leafCount ?? 0), 0);
|
|
133
|
+
const resolveColumnValue = (columnId) => {
|
|
134
|
+
if (columnId === groupKey)
|
|
135
|
+
return key;
|
|
136
|
+
let resolved;
|
|
137
|
+
let hasResolved = false;
|
|
138
|
+
for (const child of children) {
|
|
139
|
+
const childValue = child.getCellValueByColumnId(columnId);
|
|
140
|
+
if (!hasResolved) {
|
|
141
|
+
resolved = childValue;
|
|
142
|
+
hasResolved = true;
|
|
143
|
+
}
|
|
144
|
+
else if (childValue !== resolved) {
|
|
145
|
+
return undefined;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
return resolved;
|
|
149
|
+
};
|
|
150
|
+
const groupOriginal = {};
|
|
151
|
+
columns.forEach((column) => {
|
|
152
|
+
const field = column.columnDef.field;
|
|
153
|
+
if (!field)
|
|
154
|
+
return;
|
|
155
|
+
const agg = column.columnDef.aggregate;
|
|
156
|
+
groupOriginal[field] = agg
|
|
157
|
+
? applyGroupAggregate(agg, column.id, children)
|
|
158
|
+
: resolveColumnValue(column.id);
|
|
159
|
+
});
|
|
160
|
+
const groupRow = {
|
|
161
|
+
id,
|
|
162
|
+
index: index++,
|
|
163
|
+
original: groupOriginal,
|
|
164
|
+
depth,
|
|
165
|
+
subRows,
|
|
166
|
+
leafCount,
|
|
167
|
+
getCanExpand: () => true,
|
|
168
|
+
getIsExpanded: () => Boolean((table.getState().expanded ?? {})[id]),
|
|
169
|
+
toggleExpanded: () => {
|
|
170
|
+
table.setExpanded((prev) => ({ ...prev, [id]: !prev[id] }));
|
|
171
|
+
},
|
|
172
|
+
getIsSelected: () => Boolean((table.getState().rowSelection ?? {})[id]),
|
|
173
|
+
toggleSelected: () => {
|
|
174
|
+
table.setRowSelection((prev) => ({ ...prev, [id]: !prev[id] }));
|
|
175
|
+
},
|
|
176
|
+
getAllCells: () => [],
|
|
177
|
+
// Prefer the precomputed group value (which carries aggregates)
|
|
178
|
+
// and fall back to the shared-value resolver for columns without
|
|
179
|
+
// a field.
|
|
180
|
+
getCellValueByColumnId: (columnId) => {
|
|
181
|
+
const col = columns.find((c) => c.id === columnId);
|
|
182
|
+
const field = col?.columnDef.field;
|
|
183
|
+
if (field && field in groupOriginal)
|
|
184
|
+
return groupOriginal[field];
|
|
185
|
+
return resolveColumnValue(columnId);
|
|
186
|
+
},
|
|
187
|
+
};
|
|
188
|
+
groupRows.push(groupRow);
|
|
189
|
+
});
|
|
190
|
+
return groupRows;
|
|
191
|
+
}
|
|
192
|
+
return buildGroups(rows, 0, 0, 'group');
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
export function createExpandedRowModel() {
|
|
196
|
+
return ({ table, rows }) => {
|
|
197
|
+
const expanded = table.getState().expanded ?? {};
|
|
198
|
+
const flattened = [];
|
|
199
|
+
const visit = (row) => {
|
|
200
|
+
flattened.push(row);
|
|
201
|
+
if (row.subRows?.length && expanded[row.id]) {
|
|
202
|
+
for (const sub of row.subRows)
|
|
203
|
+
visit(sub);
|
|
204
|
+
}
|
|
205
|
+
};
|
|
206
|
+
for (const row of rows)
|
|
207
|
+
visit(row);
|
|
208
|
+
return flattened;
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
export function createSortedRowModel(localSortFns = sortFns) {
|
|
212
|
+
return ({ table, rows }) => {
|
|
213
|
+
const sorting = table.getState().sorting ?? [];
|
|
214
|
+
if (!sorting.length)
|
|
215
|
+
return rows;
|
|
216
|
+
const sorted = [...rows].sort((a, b) => {
|
|
217
|
+
for (const clause of sorting) {
|
|
218
|
+
const column = table.getAllColumns().find((col) => col.id === clause.id);
|
|
219
|
+
if (!column)
|
|
220
|
+
continue;
|
|
221
|
+
const editorType = column.columnDef.editorType;
|
|
222
|
+
const comparator = editorType === 'number'
|
|
223
|
+
? localSortFns.number
|
|
224
|
+
: editorType === 'date' || editorType === 'datetime'
|
|
225
|
+
? localSortFns.date
|
|
226
|
+
: localSortFns.auto;
|
|
227
|
+
const result = comparator(a.getCellValueByColumnId(column.id), b.getCellValueByColumnId(column.id));
|
|
228
|
+
if (result !== 0)
|
|
229
|
+
return clause.desc ? -result : result;
|
|
230
|
+
}
|
|
231
|
+
return 0;
|
|
232
|
+
});
|
|
233
|
+
return sorted;
|
|
234
|
+
};
|
|
235
|
+
}
|
|
236
|
+
export function createSvGridCore(options) {
|
|
237
|
+
const internalState = {
|
|
238
|
+
sorting: [],
|
|
239
|
+
columnFilters: [],
|
|
240
|
+
pagination: { pageIndex: 0, pageSize: options.data.length || 10 },
|
|
241
|
+
grouping: [],
|
|
242
|
+
expanded: {},
|
|
243
|
+
rowSelection: {},
|
|
244
|
+
activeCell: { rowIndex: 0, colIndex: 0, cellId: null },
|
|
245
|
+
...(options.state ?? {}),
|
|
246
|
+
};
|
|
247
|
+
const store = createStore(internalState);
|
|
248
|
+
const optionsStore = createStore(options);
|
|
249
|
+
let cachedColumnsInput = null;
|
|
250
|
+
let cachedColumns = [];
|
|
251
|
+
let cachedHeaderGroups = [];
|
|
252
|
+
let cachedBaseRowsInput = null;
|
|
253
|
+
let cachedBaseRowsColumns = null;
|
|
254
|
+
let cachedBaseRows = [];
|
|
255
|
+
let cachedRowModel = null;
|
|
256
|
+
let cachedRowModelBaseRows = null;
|
|
257
|
+
let cachedPipeline = options._rowModels;
|
|
258
|
+
let cachedSlices = null;
|
|
259
|
+
const grid = {
|
|
260
|
+
store,
|
|
261
|
+
optionsStore,
|
|
262
|
+
get state() {
|
|
263
|
+
return store.state;
|
|
264
|
+
},
|
|
265
|
+
getState() {
|
|
266
|
+
return store.state;
|
|
267
|
+
},
|
|
268
|
+
setOptions(updater) {
|
|
269
|
+
optionsStore.setState((prev) => typeof updater === 'function' ? updater(prev) : updater);
|
|
270
|
+
},
|
|
271
|
+
setColumnFilters(updater) {
|
|
272
|
+
store.setState((prev) => ({
|
|
273
|
+
...prev,
|
|
274
|
+
columnFilters: typeof updater === 'function' ? updater(prev.columnFilters ?? []) : updater,
|
|
275
|
+
}));
|
|
276
|
+
options.onColumnFiltersChange?.(updater);
|
|
277
|
+
},
|
|
278
|
+
setPagination(updater) {
|
|
279
|
+
store.setState((prev) => ({
|
|
280
|
+
...prev,
|
|
281
|
+
pagination: typeof updater === 'function'
|
|
282
|
+
? updater(prev.pagination ?? { pageIndex: 0, pageSize: 10 })
|
|
283
|
+
: updater,
|
|
284
|
+
}));
|
|
285
|
+
options.onPaginationChange?.(updater);
|
|
286
|
+
},
|
|
287
|
+
setGrouping(updater) {
|
|
288
|
+
store.setState((prev) => ({
|
|
289
|
+
...prev,
|
|
290
|
+
grouping: typeof updater === 'function' ? updater(prev.grouping ?? []) : updater,
|
|
291
|
+
}));
|
|
292
|
+
options.onGroupingChange?.(updater);
|
|
293
|
+
},
|
|
294
|
+
setExpanded(updater) {
|
|
295
|
+
store.setState((prev) => ({
|
|
296
|
+
...prev,
|
|
297
|
+
expanded: typeof updater === 'function' ? updater(prev.expanded ?? {}) : updater,
|
|
298
|
+
}));
|
|
299
|
+
options.onExpandedChange?.(updater);
|
|
300
|
+
},
|
|
301
|
+
setRowSelection(updater) {
|
|
302
|
+
store.setState((prev) => ({
|
|
303
|
+
...prev,
|
|
304
|
+
rowSelection: typeof updater === 'function' ? updater(prev.rowSelection ?? {}) : updater,
|
|
305
|
+
}));
|
|
306
|
+
options.onRowSelectionChange?.(updater);
|
|
307
|
+
},
|
|
308
|
+
setActiveCell(updater) {
|
|
309
|
+
store.setState((prev) => {
|
|
310
|
+
const previous = prev.activeCell ?? {
|
|
311
|
+
rowIndex: 0,
|
|
312
|
+
colIndex: 0,
|
|
313
|
+
cellId: null,
|
|
314
|
+
};
|
|
315
|
+
const nextActive = typeof updater === 'function' ? updater(previous) : updater;
|
|
316
|
+
return {
|
|
317
|
+
...prev,
|
|
318
|
+
activeCell: nextActive,
|
|
319
|
+
};
|
|
320
|
+
});
|
|
321
|
+
options.onActiveCellChange?.(updater);
|
|
322
|
+
},
|
|
323
|
+
moveActiveCell(next) {
|
|
324
|
+
const rows = grid.getRowModel().rows;
|
|
325
|
+
const columns = grid.getAllColumns();
|
|
326
|
+
const maxRow = Math.max(rows.length - 1, 0);
|
|
327
|
+
const maxCol = Math.max(columns.length - 1, 0);
|
|
328
|
+
const current = grid.getState().activeCell ?? {
|
|
329
|
+
rowIndex: 0,
|
|
330
|
+
colIndex: 0,
|
|
331
|
+
cellId: null,
|
|
332
|
+
};
|
|
333
|
+
const rowIndex = Math.min(Math.max(current.rowIndex + (next.rowDelta ?? 0), 0), maxRow);
|
|
334
|
+
const colIndex = Math.min(Math.max(current.colIndex + (next.colDelta ?? 0), 0), maxCol);
|
|
335
|
+
const columnId = columns[colIndex]?.id ?? 'col_0';
|
|
336
|
+
grid.setActiveCell({
|
|
337
|
+
rowIndex,
|
|
338
|
+
colIndex,
|
|
339
|
+
cellId: `${rowIndex}_${columnId}`,
|
|
340
|
+
});
|
|
341
|
+
},
|
|
342
|
+
getAllColumns() {
|
|
343
|
+
// Cache hit: referentially identical columns array.
|
|
344
|
+
if (cachedColumnsInput === options.columns && cachedColumns.length) {
|
|
345
|
+
return cachedColumns;
|
|
346
|
+
}
|
|
347
|
+
// Soft cache hit: consumers commonly recreate the columns array
|
|
348
|
+
// inline on every render (e.g. `columns={[...]}`). If the new
|
|
349
|
+
// array has the same length AND each entry has the same `field` /
|
|
350
|
+
// `id` / `header` (the visibility-affecting structure of a
|
|
351
|
+
// column), trust the previous build. Mutable inner fields like
|
|
352
|
+
// `cell` and `editorOptions` are still picked up on the next real
|
|
353
|
+
// render that bumps an actual data dep - they're read at cell-
|
|
354
|
+
// render time, not at this top-level cache.
|
|
355
|
+
if (cachedColumnsInput &&
|
|
356
|
+
options.columns.length === cachedColumnsInput.length &&
|
|
357
|
+
cachedColumns.length === options.columns.length &&
|
|
358
|
+
options.columns.every((c, i) => {
|
|
359
|
+
const prev = cachedColumnsInput[i];
|
|
360
|
+
return (c.field === prev.field &&
|
|
361
|
+
c.id === prev.id &&
|
|
362
|
+
c.header === prev.header &&
|
|
363
|
+
c.editorType === prev.editorType);
|
|
364
|
+
})) {
|
|
365
|
+
// Update the stored input reference so the strict check hits
|
|
366
|
+
// next time, but reuse the built column model.
|
|
367
|
+
cachedColumnsInput = options.columns;
|
|
368
|
+
return cachedColumns;
|
|
369
|
+
}
|
|
370
|
+
cachedColumnsInput = options.columns;
|
|
371
|
+
cachedHeaderGroups = [];
|
|
372
|
+
const build = (defs, depth, parentId) => {
|
|
373
|
+
const leaves = [];
|
|
374
|
+
defs.forEach((columnDef, index) => {
|
|
375
|
+
const id = columnDef.id ?? columnDef.field ?? `${parentId ?? 'col'}_${depth}_${index}`;
|
|
376
|
+
if (columnDef.columns?.length) {
|
|
377
|
+
leaves.push(...build(columnDef.columns, depth + 1, id));
|
|
378
|
+
return;
|
|
379
|
+
}
|
|
380
|
+
leaves.push({
|
|
381
|
+
id,
|
|
382
|
+
depth,
|
|
383
|
+
parentId,
|
|
384
|
+
columnDef,
|
|
385
|
+
getCanSort: () => Boolean(options._features.rowSortingFeature) &&
|
|
386
|
+
columnDef.sortable !== false,
|
|
387
|
+
getCanFilter: () => Boolean(options._features.columnFilteringFeature) &&
|
|
388
|
+
columnDef.filterable !== false,
|
|
389
|
+
getIsSorted: () => {
|
|
390
|
+
const entry = store.state.sorting?.find((s) => s.id === id);
|
|
391
|
+
if (!entry)
|
|
392
|
+
return false;
|
|
393
|
+
return entry.desc ? 'desc' : 'asc';
|
|
394
|
+
},
|
|
395
|
+
getToggleSortingHandler: () => () => {
|
|
396
|
+
const clauses = store.state.sorting ?? [];
|
|
397
|
+
const current = clauses.find((s) => s.id === id);
|
|
398
|
+
const nextClause = !current
|
|
399
|
+
? [...clauses, { id, desc: false }]
|
|
400
|
+
: current.desc
|
|
401
|
+
? clauses.filter((s) => s.id !== id)
|
|
402
|
+
: clauses.map((s) => (s.id === id ? { ...s, desc: true } : s));
|
|
403
|
+
store.setState((prev) => ({ ...prev, sorting: nextClause }));
|
|
404
|
+
options.onSortingChange?.(nextClause);
|
|
405
|
+
},
|
|
406
|
+
});
|
|
407
|
+
});
|
|
408
|
+
return leaves;
|
|
409
|
+
};
|
|
410
|
+
cachedColumns = build(options.columns, 0);
|
|
411
|
+
return cachedColumns;
|
|
412
|
+
},
|
|
413
|
+
getHeaderGroups() {
|
|
414
|
+
if (cachedHeaderGroups.length)
|
|
415
|
+
return cachedHeaderGroups;
|
|
416
|
+
const headers = grid.getAllColumns().map((column) => {
|
|
417
|
+
const header = {
|
|
418
|
+
id: column.id,
|
|
419
|
+
isPlaceholder: false,
|
|
420
|
+
colSpan: 1,
|
|
421
|
+
column,
|
|
422
|
+
getContext: () => ({ header, column, table: grid }),
|
|
423
|
+
};
|
|
424
|
+
return header;
|
|
425
|
+
});
|
|
426
|
+
cachedHeaderGroups = [{ id: 'header_group_0', headers }];
|
|
427
|
+
return cachedHeaderGroups;
|
|
428
|
+
},
|
|
429
|
+
getFooterGroups() {
|
|
430
|
+
return grid.getHeaderGroups();
|
|
431
|
+
},
|
|
432
|
+
getRowModel() {
|
|
433
|
+
const columns = grid.getAllColumns();
|
|
434
|
+
if (cachedBaseRowsInput !== options.data || cachedBaseRowsColumns !== columns) {
|
|
435
|
+
cachedBaseRowsInput = options.data;
|
|
436
|
+
cachedBaseRowsColumns = columns;
|
|
437
|
+
// O(1) column-id → index lookup so getCellValueByColumnId doesn't do
|
|
438
|
+
// a linear `findIndex` on every cell read (was O(rows × cells × cols)).
|
|
439
|
+
const columnIndexById = new Map();
|
|
440
|
+
for (let i = 0; i < columns.length; i++)
|
|
441
|
+
columnIndexById.set(columns[i].id, i);
|
|
442
|
+
const columnCount = columns.length;
|
|
443
|
+
cachedBaseRows = new Array(options.data.length);
|
|
444
|
+
const getRowId = options.getRowId;
|
|
445
|
+
for (let index = 0; index < options.data.length; index++) {
|
|
446
|
+
const original = options.data[index];
|
|
447
|
+
const id = getRowId ? getRowId(original, index) : String(index);
|
|
448
|
+
// `values` and `cells` are computed lazily - for a 100k-row grid
|
|
449
|
+
// with only ~20 visible rows we don't want to materialise every
|
|
450
|
+
// row's full value array or cell objects up front.
|
|
451
|
+
let cachedValues = null;
|
|
452
|
+
let cachedCells = null;
|
|
453
|
+
function computeValues() {
|
|
454
|
+
const values = new Array(columnCount);
|
|
455
|
+
for (let i = 0; i < columnCount; i++) {
|
|
456
|
+
const column = columns[i];
|
|
457
|
+
if (column.columnDef.accessorFn) {
|
|
458
|
+
values[i] = column.columnDef.accessorFn(original);
|
|
459
|
+
}
|
|
460
|
+
else if (column.columnDef.field) {
|
|
461
|
+
values[i] = original[column.columnDef.field];
|
|
462
|
+
}
|
|
463
|
+
else {
|
|
464
|
+
values[i] = undefined;
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
return values;
|
|
468
|
+
}
|
|
469
|
+
const row = {
|
|
470
|
+
id,
|
|
471
|
+
index,
|
|
472
|
+
original,
|
|
473
|
+
depth: 0,
|
|
474
|
+
getCanExpand: () => false,
|
|
475
|
+
getIsExpanded: () => Boolean((store.state.expanded ?? {})[id]),
|
|
476
|
+
toggleExpanded: () => {
|
|
477
|
+
grid.setExpanded((prev) => ({ ...prev, [id]: !prev[id] }));
|
|
478
|
+
},
|
|
479
|
+
getIsSelected: () => Boolean((store.state.rowSelection ?? {})[id]),
|
|
480
|
+
toggleSelected: () => {
|
|
481
|
+
grid.setRowSelection((prev) => ({ ...prev, [id]: !prev[id] }));
|
|
482
|
+
},
|
|
483
|
+
getAllCells: () => {
|
|
484
|
+
if (cachedCells)
|
|
485
|
+
return cachedCells;
|
|
486
|
+
const built = new Array(columnCount);
|
|
487
|
+
for (let i = 0; i < columnCount; i++) {
|
|
488
|
+
const column = columns[i];
|
|
489
|
+
const colIndex = i;
|
|
490
|
+
const cell = {
|
|
491
|
+
id: `${id}_${column.id}`,
|
|
492
|
+
row,
|
|
493
|
+
column,
|
|
494
|
+
getValue: () => {
|
|
495
|
+
if (!cachedValues)
|
|
496
|
+
cachedValues = computeValues();
|
|
497
|
+
return cachedValues[colIndex];
|
|
498
|
+
},
|
|
499
|
+
getContext: () => ({
|
|
500
|
+
cell,
|
|
501
|
+
row,
|
|
502
|
+
column,
|
|
503
|
+
table: grid,
|
|
504
|
+
getValue: () => cell.getValue(),
|
|
505
|
+
}),
|
|
506
|
+
};
|
|
507
|
+
built[i] = cell;
|
|
508
|
+
}
|
|
509
|
+
cachedCells = built;
|
|
510
|
+
return built;
|
|
511
|
+
},
|
|
512
|
+
getCellValueByColumnId: (columnId) => {
|
|
513
|
+
const idx = columnIndexById.get(columnId);
|
|
514
|
+
if (idx === undefined)
|
|
515
|
+
return undefined;
|
|
516
|
+
if (!cachedValues)
|
|
517
|
+
cachedValues = computeValues();
|
|
518
|
+
return cachedValues[idx];
|
|
519
|
+
},
|
|
520
|
+
};
|
|
521
|
+
cachedBaseRows[index] = row;
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
const currentSlices = {
|
|
525
|
+
sorting: store.state.sorting,
|
|
526
|
+
columnFilters: store.state.columnFilters,
|
|
527
|
+
pagination: store.state.pagination,
|
|
528
|
+
grouping: store.state.grouping,
|
|
529
|
+
expanded: store.state.expanded,
|
|
530
|
+
rowSelection: store.state.rowSelection,
|
|
531
|
+
};
|
|
532
|
+
if (cachedRowModel &&
|
|
533
|
+
cachedRowModelBaseRows === cachedBaseRows &&
|
|
534
|
+
cachedPipeline === options._rowModels &&
|
|
535
|
+
cachedSlices?.sorting === currentSlices.sorting &&
|
|
536
|
+
cachedSlices?.columnFilters === currentSlices.columnFilters &&
|
|
537
|
+
cachedSlices?.pagination === currentSlices.pagination &&
|
|
538
|
+
cachedSlices?.grouping === currentSlices.grouping &&
|
|
539
|
+
cachedSlices?.expanded === currentSlices.expanded &&
|
|
540
|
+
cachedSlices?.rowSelection === currentSlices.rowSelection) {
|
|
541
|
+
return cachedRowModel;
|
|
542
|
+
}
|
|
543
|
+
let rows = cachedBaseRows;
|
|
544
|
+
const pipeline = options._rowModels ?? {};
|
|
545
|
+
const ordered = [
|
|
546
|
+
pipeline.coreRowModel,
|
|
547
|
+
pipeline.filteredRowModel,
|
|
548
|
+
pipeline.sortedRowModel,
|
|
549
|
+
pipeline.groupedRowModel,
|
|
550
|
+
pipeline.expandedRowModel,
|
|
551
|
+
pipeline.paginatedRowModel,
|
|
552
|
+
];
|
|
553
|
+
ordered.forEach((fn) => {
|
|
554
|
+
if (fn)
|
|
555
|
+
rows = fn({ table: grid, rows });
|
|
556
|
+
});
|
|
557
|
+
cachedPipeline = options._rowModels;
|
|
558
|
+
cachedSlices = currentSlices;
|
|
559
|
+
cachedRowModelBaseRows = cachedBaseRows;
|
|
560
|
+
cachedRowModel = { rows };
|
|
561
|
+
return cachedRowModel;
|
|
562
|
+
},
|
|
563
|
+
};
|
|
564
|
+
return grid;
|
|
565
|
+
}
|
|
566
|
+
export function isFunction(value) {
|
|
567
|
+
return typeof value === 'function';
|
|
568
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { createCoreRowModel, createSortedRowModel, createSvGrid, sortFns, tableFeatures } from './index';
|
|
3
|
+
describe('core performance smoke', () => {
|
|
4
|
+
it('handles large synthetic shape with bounded row model call', () => {
|
|
5
|
+
const columns = Array.from({ length: 50 }, (_, index) => ({
|
|
6
|
+
field: `c${index}`,
|
|
7
|
+
editorType: 'number',
|
|
8
|
+
}));
|
|
9
|
+
const data = Array.from({ length: 5000 }, (_, rowIndex) => {
|
|
10
|
+
const row = {};
|
|
11
|
+
for (let col = 0; col < 50; col += 1)
|
|
12
|
+
row[`c${col}`] = rowIndex + col;
|
|
13
|
+
return row;
|
|
14
|
+
});
|
|
15
|
+
const grid = createSvGrid({
|
|
16
|
+
_features: tableFeatures({}),
|
|
17
|
+
_rowModels: {
|
|
18
|
+
coreRowModel: createCoreRowModel(),
|
|
19
|
+
sortedRowModel: createSortedRowModel(sortFns),
|
|
20
|
+
},
|
|
21
|
+
columns,
|
|
22
|
+
data,
|
|
23
|
+
state: { sorting: [{ id: 'c0', desc: true }, { id: 'c1', desc: false }] },
|
|
24
|
+
});
|
|
25
|
+
const rows = grid.getRowModel().rows;
|
|
26
|
+
expect(rows.length).toBe(5000);
|
|
27
|
+
expect(rows[0]?.getCellValueByColumnId('c0')).toBe(4999);
|
|
28
|
+
});
|
|
29
|
+
});
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { RowData, SvGrid, SvGridOptions, TableFeatures } from './core';
|
|
2
|
+
export type SvelteGrid<TFeatures extends TableFeatures, TData extends RowData, TSelected = {}> = SvGrid<TData> & {
|
|
3
|
+
readonly state: Readonly<TSelected>;
|
|
4
|
+
};
|
|
5
|
+
export declare function createSvGrid<TFeatures extends TableFeatures, TData extends RowData, TSelected = {}>(gridOptions: SvGridOptions<TFeatures, TData>, selector?: (state: Record<string, any>) => TSelected): SvelteGrid<TFeatures, TData, TSelected>;
|
|
6
|
+
export declare const createGrid: typeof createSvGrid;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { createSvGridCore } from './core';
|
|
2
|
+
export function createSvGrid(gridOptions, selector = () => ({})) {
|
|
3
|
+
const grid = createSvGridCore(gridOptions);
|
|
4
|
+
let selected = $state(selector(grid.store.state));
|
|
5
|
+
grid.store.subscribe(() => {
|
|
6
|
+
selected = selector(grid.store.state);
|
|
7
|
+
});
|
|
8
|
+
Object.defineProperty(grid, 'state', {
|
|
9
|
+
get() {
|
|
10
|
+
return selected;
|
|
11
|
+
},
|
|
12
|
+
configurable: true,
|
|
13
|
+
enumerable: true,
|
|
14
|
+
});
|
|
15
|
+
return grid;
|
|
16
|
+
}
|
|
17
|
+
export const createGrid = createSvGrid;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { createGridState } from './index';
|
|
3
|
+
describe('createGrid', () => {
|
|
4
|
+
it('createGridState updates controlled values', () => {
|
|
5
|
+
const [sorting, setSorting] = createGridState([{ id: 'age', desc: false }]);
|
|
6
|
+
setSorting((prev) => prev.map((entry) => ({ ...entry, desc: true })));
|
|
7
|
+
expect(sorting()[0]?.desc).toBe(true);
|
|
8
|
+
});
|
|
9
|
+
});
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export function createGridState(initialValue) {
|
|
2
|
+
let value = $state(initialValue);
|
|
3
|
+
return [
|
|
4
|
+
() => value,
|
|
5
|
+
(updater) => {
|
|
6
|
+
if (updater instanceof Function)
|
|
7
|
+
value = updater(value);
|
|
8
|
+
else
|
|
9
|
+
value = updater;
|
|
10
|
+
},
|
|
11
|
+
];
|
|
12
|
+
}
|
|
13
|
+
export const createSvGridState = createGridState;
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
export type CellEditorType = 'text' | 'number' | 'date' | 'datetime' | 'time' | 'password' | 'checkbox' | 'list' | 'chips' | 'select' | 'rich-select' | 'textarea' | 'color' | 'rating';
|
|
2
|
+
/** Normalized option used by list/chips editors. The optional `color`
|
|
3
|
+
* paints the chip - both in the in-cell readonly chips render and in
|
|
4
|
+
* the dropdown trigger when `renderChipsInTrigger` is on. Any CSS
|
|
5
|
+
* color works (hex, rgb, hsl, oklch, named); the chip uses a soft
|
|
6
|
+
* tint built via `color-mix` so it sits well on both themes. */
|
|
7
|
+
export type CellEditorOption = {
|
|
8
|
+
value: string | number;
|
|
9
|
+
label: string;
|
|
10
|
+
color?: string;
|
|
11
|
+
};
|
|
12
|
+
export type ParseEditorValueOptions = {
|
|
13
|
+
/** When true, list/chips return an array; otherwise a scalar. */
|
|
14
|
+
multiple?: boolean;
|
|
15
|
+
};
|
|
16
|
+
/** Normalize the loose `editorOptions` ColumnDef prop into a uniform shape.
|
|
17
|
+
* Dynamic (function) editorOptions are resolved before reaching here. */
|
|
18
|
+
export declare function normalizeEditorOptions(options: ReadonlyArray<string | number | {
|
|
19
|
+
value: string | number;
|
|
20
|
+
label?: string;
|
|
21
|
+
color?: string;
|
|
22
|
+
}> | undefined): CellEditorOption[];
|
|
23
|
+
export declare function parseEditorValue(type: CellEditorType, value: unknown, opts?: ParseEditorValueOptions): any;
|