@svgrid/grid 1.0.2 → 1.1.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 +1 -1
- package/README.md +137 -39
- package/dist/GridFooter.svelte +164 -0
- package/dist/GridFooter.svelte.d.ts +29 -0
- package/dist/GridMenus.svelte +570 -0
- package/dist/GridMenus.svelte.d.ts +31 -0
- package/dist/SvGrid.controller.svelte.d.ts +429 -0
- package/dist/SvGrid.controller.svelte.js +1732 -0
- package/dist/SvGrid.css +1709 -0
- package/dist/SvGrid.helpers.d.ts +35 -0
- package/dist/SvGrid.helpers.js +160 -0
- package/dist/SvGrid.svelte +344 -7043
- package/dist/SvGrid.svelte.d.ts +4 -357
- package/dist/SvGrid.types.d.ts +436 -0
- package/dist/SvGrid.types.js +1 -0
- package/dist/SvGridChart.svelte +1060 -23
- package/dist/SvGridChart.svelte.d.ts +17 -0
- package/dist/build-api.d.ts +5 -0
- package/dist/build-api.js +527 -0
- package/dist/cell-render.d.ts +15 -0
- package/dist/cell-render.js +246 -0
- package/dist/cell-values.d.ts +28 -0
- package/dist/cell-values.js +89 -0
- package/dist/chart.d.ts +370 -3
- package/dist/chart.js +1135 -42
- package/dist/clipboard.d.ts +15 -0
- package/dist/clipboard.js +356 -0
- package/dist/columns.d.ts +30 -0
- package/dist/columns.js +277 -0
- package/dist/core.d.ts +1 -1
- package/dist/css.d.ts +3 -0
- package/dist/editing.d.ts +24 -0
- package/dist/editing.js +343 -0
- package/dist/editors/cell-editors.d.ts +1 -1
- package/dist/facet-buckets.d.ts +13 -0
- package/dist/facet-buckets.js +54 -0
- package/dist/features.d.ts +5 -0
- package/dist/features.js +30 -0
- package/dist/filter-operators.d.ts +16 -0
- package/dist/filter-operators.js +69 -0
- package/dist/hyperformula-adapter.d.ts +82 -0
- package/dist/hyperformula-adapter.js +73 -0
- package/dist/index.d.ts +5 -2
- package/dist/index.js +5 -2
- package/dist/keyboard-handlers.d.ts +7 -0
- package/dist/keyboard-handlers.js +197 -0
- package/dist/menus.d.ts +40 -0
- package/dist/menus.js +389 -0
- package/dist/named-views.d.ts +27 -0
- package/dist/named-views.js +39 -0
- package/dist/row-resize.d.ts +43 -0
- package/dist/row-resize.js +158 -0
- package/dist/scroll-sync.d.ts +9 -0
- package/dist/scroll-sync.js +86 -0
- package/dist/selection.d.ts +26 -0
- package/dist/selection.js +387 -0
- package/dist/spreadsheet.d.ts +80 -0
- package/dist/spreadsheet.js +194 -0
- package/dist/summaries.d.ts +12 -0
- package/dist/summaries.js +65 -0
- package/dist/svgrid-wrapper.types.d.ts +12 -1
- package/dist/svgrid.comments-autocomplete.test.d.ts +1 -0
- package/dist/svgrid.comments-autocomplete.test.js +96 -0
- package/dist/svgrid.context-menu.test.d.ts +1 -0
- package/dist/svgrid.context-menu.test.js +102 -0
- package/dist/svgrid.new-features.wrapper.test.js +30 -4
- package/dist/svgrid.wrapper.test.js +27 -1
- package/dist/virtualization/column-virtualizer.d.ts +2 -0
- package/dist/virtualization/scroll-scaling.d.ts +28 -0
- package/dist/virtualization/scroll-scaling.js +64 -0
- package/dist/virtualization/scroll-scaling.test.d.ts +1 -0
- package/dist/virtualization/scroll-scaling.test.js +86 -0
- package/dist/virtualization/svelte-virtualizer.svelte.d.ts +2 -0
- package/dist/virtualization/svelte-virtualizer.svelte.js +2 -0
- package/dist/virtualization/virtualizer.d.ts +7 -0
- package/dist/virtualization/virtualizer.js +30 -0
- package/package.json +1 -1
- package/src/GridFooter.svelte +164 -0
- package/src/GridMenus.svelte +570 -0
- package/src/SvGrid.controller.svelte.ts +2195 -0
- package/src/SvGrid.css +1747 -0
- package/src/SvGrid.helpers.test.ts +415 -0
- package/src/SvGrid.helpers.ts +185 -0
- package/src/SvGrid.svelte +348 -7043
- package/src/SvGrid.types.ts +456 -0
- package/src/SvGridChart.svelte +1060 -23
- package/src/build-api.coverage.test.ts +532 -0
- package/src/build-api.ts +663 -0
- package/src/cell-render.test.ts +451 -0
- package/src/cell-render.ts +426 -0
- package/src/cell-values.ts +114 -0
- package/src/chart-export.test.ts +370 -0
- package/src/chart.coverage.test.ts +814 -0
- package/src/chart.ts +1352 -47
- package/src/clipboard.test.ts +731 -0
- package/src/clipboard.ts +524 -0
- package/src/collaboration.coverage.test.ts +220 -0
- package/src/columns.test.ts +702 -0
- package/src/columns.ts +419 -0
- package/src/core.ts +8 -0
- package/src/css.d.ts +3 -0
- package/src/editing.test.ts +837 -0
- package/src/editing.ts +513 -0
- package/src/editors/cell-editors.coverage.test.ts +156 -0
- package/src/editors/cell-editors.ts +1 -0
- package/src/facet-buckets.test.ts +353 -0
- package/src/facet-buckets.ts +67 -0
- package/src/features.ts +128 -0
- package/src/filter-operators.test.ts +174 -0
- package/src/filter-operators.ts +87 -0
- package/src/hyperformula-adapter.test.ts +256 -0
- package/src/hyperformula-adapter.ts +124 -0
- package/src/index.ts +37 -0
- package/src/keyboard-handlers.coverage.test.ts +560 -0
- package/src/keyboard-handlers.ts +353 -0
- package/src/keyboard.ts +97 -97
- package/src/menus.test.ts +620 -0
- package/src/menus.ts +554 -0
- package/src/named-views.coverage.test.ts +210 -0
- package/src/named-views.ts +48 -0
- package/src/row-resize.test.ts +369 -0
- package/src/row-resize.ts +171 -0
- package/src/scroll-sync.test.ts +330 -0
- package/src/scroll-sync.ts +216 -0
- package/src/selection.test.ts +722 -0
- package/src/selection.ts +545 -0
- package/src/server-data-source.coverage.test.ts +180 -0
- package/src/spreadsheet.test.ts +445 -0
- package/src/spreadsheet.ts +246 -0
- package/src/summaries.ts +204 -0
- package/src/sv-grid-scrollbar.ts +13 -1
- package/src/svgrid-wrapper.types.ts +12 -1
- package/src/svgrid.behavior.test.ts +22 -0
- package/src/svgrid.comments-autocomplete.test.ts +112 -0
- package/src/svgrid.context-menu.test.ts +126 -0
- package/src/svgrid.interaction.test.ts +30 -0
- package/src/svgrid.new-features.wrapper.test.ts +65 -4
- package/src/svgrid.wrapper.test.ts +27 -1
- package/src/test-setup.ts +9 -6
- package/src/virtualization/scroll-scaling.test.ts +148 -0
- package/src/virtualization/scroll-scaling.ts +121 -0
- package/src/virtualization/svelte-virtualizer.svelte.ts +2 -0
- package/src/virtualization/virtualizer.ts +26 -0
package/dist/css.d.ts
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { type Column, type Row, type RowData, type TableFeatures } from "./index";
|
|
2
|
+
import "./sv-grid-scrollbar";
|
|
3
|
+
type HistoryStep = {
|
|
4
|
+
rowId: string;
|
|
5
|
+
columnId: string;
|
|
6
|
+
field: string;
|
|
7
|
+
before: unknown;
|
|
8
|
+
after: unknown;
|
|
9
|
+
};
|
|
10
|
+
export declare function createEditing<TFeatures extends TableFeatures = TableFeatures, TData extends RowData = RowData>(ctx: any): {
|
|
11
|
+
isCellEditable: (column: Column<TData>, row?: Row<TData>) => boolean;
|
|
12
|
+
isCellEditableAt: (rowIndex: number, colIndex: number) => boolean;
|
|
13
|
+
getRowColumnValue: (row: Row<TData>, columnId: string) => unknown;
|
|
14
|
+
getCellDisplayValue: (rowId: string, columnId: string, baseValue: unknown) => any;
|
|
15
|
+
startEditingWithChar: (rowIndex: number, colIndex: number, char: string) => boolean;
|
|
16
|
+
saveEditingCell: () => void;
|
|
17
|
+
applyHistoryStep: (step: HistoryStep, direction: "undo" | "redo") => void;
|
|
18
|
+
updateEditingCellValue: (value: string) => void;
|
|
19
|
+
onEditorKeyDown: (event: KeyboardEvent) => void;
|
|
20
|
+
focusOnMount: (node: HTMLInputElement | HTMLTextAreaElement) => void;
|
|
21
|
+
onCellDoubleClick: (rowIndex: number, colIndex: number) => void;
|
|
22
|
+
pasteFromClipboard: () => Promise<void>;
|
|
23
|
+
};
|
|
24
|
+
export {};
|
package/dist/editing.js
ADDED
|
@@ -0,0 +1,343 @@
|
|
|
1
|
+
// editing handlers extracted from the controller. Imperative event handlers
|
|
2
|
+
// reading/writing controller state via the `ctx` handle; the reactive core
|
|
3
|
+
// ($state/$derived/$effect) stays in the controller.
|
|
4
|
+
import { parseEditorValue, } from "./index";
|
|
5
|
+
import "./sv-grid-scrollbar";
|
|
6
|
+
import { getCellKey, toValueArray, } from "./SvGrid.helpers";
|
|
7
|
+
import { getColumnBaseValue, isGroupRow, } from "./cell-values";
|
|
8
|
+
export function createEditing(ctx) {
|
|
9
|
+
/**
|
|
10
|
+
* Resolve `columnDef.editable` for one row × column pair.
|
|
11
|
+
*
|
|
12
|
+
* - `undefined` / `true` → editable
|
|
13
|
+
* - `false` → not editable (column-level lockdown, fastest path)
|
|
14
|
+
* - function → call it with the cell's context and honour the
|
|
15
|
+
* boolean it returns (cell-level lockdown - used for things like
|
|
16
|
+
* "this field is owned by the server, this one isn't, by row")
|
|
17
|
+
*
|
|
18
|
+
* Called from every editing entry point: double-click, type-to-edit,
|
|
19
|
+
* fill-handle drag, Delete-clear, clipboard paste. A central helper
|
|
20
|
+
* keeps the cell-level callback signature consistent and saves a
|
|
21
|
+
* lookup of CellContext at each site.
|
|
22
|
+
*/
|
|
23
|
+
function isCellEditable(column, row) {
|
|
24
|
+
const editable = column.columnDef.editable;
|
|
25
|
+
if (editable === false)
|
|
26
|
+
return false;
|
|
27
|
+
if (typeof editable !== "function")
|
|
28
|
+
return true;
|
|
29
|
+
if (!row)
|
|
30
|
+
return true;
|
|
31
|
+
const cellCtx = {
|
|
32
|
+
cell: {
|
|
33
|
+
id: `${row.id}_${column.id}`,
|
|
34
|
+
row,
|
|
35
|
+
column,
|
|
36
|
+
getValue: () => getColumnBaseValue(row, column),
|
|
37
|
+
getContext: () => cellCtx,
|
|
38
|
+
},
|
|
39
|
+
row,
|
|
40
|
+
column,
|
|
41
|
+
table: ctx.grid,
|
|
42
|
+
getValue: () => getColumnBaseValue(row, column),
|
|
43
|
+
};
|
|
44
|
+
try {
|
|
45
|
+
return editable(cellCtx) !== false;
|
|
46
|
+
}
|
|
47
|
+
catch {
|
|
48
|
+
// A throwing predicate shouldn't crash the grid - log and treat
|
|
49
|
+
// as not-editable so any error biases toward safety.
|
|
50
|
+
return false;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
/** Variant that takes raw row + column indices for the rare callers
|
|
54
|
+
* (fill-handle, paste, Delete) that already work in index-space. */
|
|
55
|
+
function isCellEditableAt(rowIndex, colIndex) {
|
|
56
|
+
const row = ctx.allRows[rowIndex];
|
|
57
|
+
const column = ctx.allColumns[colIndex];
|
|
58
|
+
if (!column)
|
|
59
|
+
return false;
|
|
60
|
+
return isCellEditable(column, row);
|
|
61
|
+
}
|
|
62
|
+
function getRowColumnValue(row, columnId) {
|
|
63
|
+
const column = ctx.allColumns.find((entry) => entry.id === columnId);
|
|
64
|
+
return column
|
|
65
|
+
? getColumnBaseValue(row, column)
|
|
66
|
+
: row.getCellValueByColumnId(columnId);
|
|
67
|
+
}
|
|
68
|
+
function getCellDisplayValue(rowId, columnId, baseValue) {
|
|
69
|
+
const key = getCellKey(rowId, columnId);
|
|
70
|
+
return key in ctx.editedCellValues ? ctx.editedCellValues[key] : baseValue;
|
|
71
|
+
}
|
|
72
|
+
/** Start editing seeded with a typed character (Excel-style type-to-edit). */
|
|
73
|
+
function startEditingWithChar(rowIndex, colIndex, char) {
|
|
74
|
+
if (!ctx.editingEnabled)
|
|
75
|
+
return false;
|
|
76
|
+
const row = ctx.allRows[rowIndex];
|
|
77
|
+
const column = ctx.allColumns[colIndex];
|
|
78
|
+
if (!row || !column || isGroupRow(row))
|
|
79
|
+
return false;
|
|
80
|
+
if (!isCellEditable(column, row))
|
|
81
|
+
return false;
|
|
82
|
+
const editorType = (column.columnDef.editorType ??
|
|
83
|
+
"text");
|
|
84
|
+
if (editorType === "checkbox" ||
|
|
85
|
+
editorType === "date" ||
|
|
86
|
+
editorType === "datetime" ||
|
|
87
|
+
editorType === "list" ||
|
|
88
|
+
editorType === "chips") {
|
|
89
|
+
return false;
|
|
90
|
+
}
|
|
91
|
+
if (editorType === "number" && !/[0-9.+\-]/.test(char))
|
|
92
|
+
return false;
|
|
93
|
+
ctx.editorSelectAll = false;
|
|
94
|
+
ctx.editingCell = {
|
|
95
|
+
rowId: row.id,
|
|
96
|
+
columnId: column.id,
|
|
97
|
+
editorType,
|
|
98
|
+
value: char,
|
|
99
|
+
};
|
|
100
|
+
ctx.setActiveCell(rowIndex, colIndex);
|
|
101
|
+
ctx.setSelection(rowIndex, colIndex);
|
|
102
|
+
return true;
|
|
103
|
+
}
|
|
104
|
+
function saveEditingCell() {
|
|
105
|
+
if (!ctx.editingCell)
|
|
106
|
+
return;
|
|
107
|
+
const row = ctx.allRows.find((entry) => entry.id === ctx.editingCell?.rowId);
|
|
108
|
+
const column = ctx.allColumns.find((entry) => entry.id === ctx.editingCell?.columnId);
|
|
109
|
+
const parsedValue = parseEditorValue(ctx.editingCell.editorType, ctx.editingCell.value, {
|
|
110
|
+
multiple: column?.columnDef.editorMultiple === true,
|
|
111
|
+
});
|
|
112
|
+
let oldValue = undefined;
|
|
113
|
+
if (row?.original && column?.columnDef.field) {
|
|
114
|
+
oldValue = row.original[column.columnDef.field];
|
|
115
|
+
row.original[column.columnDef.field] =
|
|
116
|
+
parsedValue;
|
|
117
|
+
}
|
|
118
|
+
const key = getCellKey(ctx.editingCell.rowId, ctx.editingCell.columnId);
|
|
119
|
+
ctx.editedCellValues = {
|
|
120
|
+
...ctx.editedCellValues,
|
|
121
|
+
[key]: parsedValue,
|
|
122
|
+
};
|
|
123
|
+
ctx.grid.store.setState((prev) => ({ ...prev }));
|
|
124
|
+
// Record into the history at the current pointer. Any forward
|
|
125
|
+
// history (steps the user could have redone) is truncated - this
|
|
126
|
+
// is the standard "edit invalidates redo" rule.
|
|
127
|
+
if (oldValue !== parsedValue && row?.original && column?.columnDef.field) {
|
|
128
|
+
const step = {
|
|
129
|
+
rowId: ctx.editingCell.rowId,
|
|
130
|
+
columnId: ctx.editingCell.columnId,
|
|
131
|
+
field: column.columnDef.field,
|
|
132
|
+
before: oldValue,
|
|
133
|
+
after: parsedValue,
|
|
134
|
+
};
|
|
135
|
+
const truncated = ctx.history.slice(0, ctx.historyPtr + 1);
|
|
136
|
+
truncated.push(step);
|
|
137
|
+
// Cap the buffer at UNDO_LIMIT; drop the OLDEST entries so the
|
|
138
|
+
// pointer stays valid relative to the newest steps.
|
|
139
|
+
if (truncated.length > ctx.UNDO_LIMIT) {
|
|
140
|
+
const drop = truncated.length - ctx.UNDO_LIMIT;
|
|
141
|
+
ctx.history = truncated.slice(drop);
|
|
142
|
+
ctx.historyPtr = ctx.history.length - 1;
|
|
143
|
+
}
|
|
144
|
+
else {
|
|
145
|
+
ctx.history = truncated;
|
|
146
|
+
ctx.historyPtr = ctx.history.length - 1;
|
|
147
|
+
}
|
|
148
|
+
ctx.historyVersion += 1;
|
|
149
|
+
}
|
|
150
|
+
// Notify the consumer AFTER the row has been updated so any callback-
|
|
151
|
+
// driven recompute (cascade totals, server save, undo stack) sees the
|
|
152
|
+
// post-write state. `rowIndex` matches the position in `props.data`.
|
|
153
|
+
if (ctx.props.onCellValueChange && row?.original && column) {
|
|
154
|
+
const rowIndex = ctx.internalData.indexOf(row.original);
|
|
155
|
+
ctx.props.onCellValueChange({
|
|
156
|
+
rowIndex,
|
|
157
|
+
columnId: column.id,
|
|
158
|
+
oldValue,
|
|
159
|
+
newValue: parsedValue,
|
|
160
|
+
row: row.original,
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
ctx.editingCell = null;
|
|
164
|
+
}
|
|
165
|
+
/** Apply an undo / redo step directly to the underlying row, bypassing
|
|
166
|
+
* the editor pipeline so we don't accidentally re-push to the stack. */
|
|
167
|
+
function applyHistoryStep(step, direction) {
|
|
168
|
+
const row = ctx.allRows.find((r) => r.id === step.rowId);
|
|
169
|
+
const col = ctx.allColumns.find((c) => c.id === step.columnId);
|
|
170
|
+
if (!row?.original || !col)
|
|
171
|
+
return;
|
|
172
|
+
const value = direction === 'undo' ? step.before : step.after;
|
|
173
|
+
row.original[step.field] = value;
|
|
174
|
+
const key = getCellKey(step.rowId, step.columnId);
|
|
175
|
+
ctx.editedCellValues = { ...ctx.editedCellValues, [key]: value };
|
|
176
|
+
ctx.grid.store.setState((prev) => ({ ...prev }));
|
|
177
|
+
if (ctx.props.onCellValueChange) {
|
|
178
|
+
const rowIndex = ctx.internalData.indexOf(row.original);
|
|
179
|
+
ctx.props.onCellValueChange({
|
|
180
|
+
rowIndex,
|
|
181
|
+
columnId: step.columnId,
|
|
182
|
+
oldValue: direction === 'undo' ? step.after : step.before,
|
|
183
|
+
newValue: value,
|
|
184
|
+
row: row.original,
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
function updateEditingCellValue(value) {
|
|
189
|
+
ctx.editingCell = ctx.editingCell ? { ...ctx.editingCell, value } : ctx.editingCell;
|
|
190
|
+
}
|
|
191
|
+
function onEditorKeyDown(event) {
|
|
192
|
+
event.stopPropagation();
|
|
193
|
+
if (event.key === "Enter") {
|
|
194
|
+
event.preventDefault();
|
|
195
|
+
saveEditingCell();
|
|
196
|
+
ctx.gridRootEl?.focus({ preventScroll: true });
|
|
197
|
+
}
|
|
198
|
+
else if (event.key === "Escape") {
|
|
199
|
+
event.preventDefault();
|
|
200
|
+
ctx.editingCell = null;
|
|
201
|
+
ctx.gridRootEl?.focus({ preventScroll: true });
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
function focusOnMount(node) {
|
|
205
|
+
const selectAll = ctx.editorSelectAll;
|
|
206
|
+
requestAnimationFrame(() => {
|
|
207
|
+
node.focus({ preventScroll: true });
|
|
208
|
+
try {
|
|
209
|
+
if (selectAll) {
|
|
210
|
+
node.select();
|
|
211
|
+
}
|
|
212
|
+
else {
|
|
213
|
+
const end = node.value.length;
|
|
214
|
+
node.setSelectionRange(end, end);
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
catch {
|
|
218
|
+
/* date/number inputs may not support text selection */
|
|
219
|
+
}
|
|
220
|
+
});
|
|
221
|
+
}
|
|
222
|
+
function onCellDoubleClick(rowIndex, colIndex) {
|
|
223
|
+
if (!ctx.editingEnabled)
|
|
224
|
+
return;
|
|
225
|
+
const row = ctx.allRows[rowIndex];
|
|
226
|
+
const column = ctx.allColumns[colIndex];
|
|
227
|
+
if (!row || !column)
|
|
228
|
+
return;
|
|
229
|
+
if (isGroupRow(row))
|
|
230
|
+
return;
|
|
231
|
+
if (!isCellEditable(column, row))
|
|
232
|
+
return;
|
|
233
|
+
if (ctx.editingCell?.rowId === row.id && ctx.editingCell?.columnId === column.id)
|
|
234
|
+
return;
|
|
235
|
+
const editorType = (column.columnDef.editorType ??
|
|
236
|
+
"text");
|
|
237
|
+
ctx.editorSelectAll = true;
|
|
238
|
+
let initialValue = getCellDisplayValue(row.id, column.id, row.getCellValueByColumnId(column.id));
|
|
239
|
+
if (editorType === "list" || editorType === "chips") {
|
|
240
|
+
if (column.columnDef.editorMultiple) {
|
|
241
|
+
// Seed with an array so the editor can mutate slot-by-slot.
|
|
242
|
+
initialValue = toValueArray(initialValue);
|
|
243
|
+
}
|
|
244
|
+
else if (Array.isArray(initialValue)) {
|
|
245
|
+
initialValue = initialValue[0] ?? "";
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
ctx.editingCell = {
|
|
249
|
+
rowId: row.id,
|
|
250
|
+
columnId: column.id,
|
|
251
|
+
editorType,
|
|
252
|
+
value: initialValue,
|
|
253
|
+
};
|
|
254
|
+
ctx.setActiveCell(rowIndex, colIndex);
|
|
255
|
+
ctx.setSelection(rowIndex, colIndex);
|
|
256
|
+
}
|
|
257
|
+
async function pasteFromClipboard() {
|
|
258
|
+
if (!navigator.clipboard?.readText)
|
|
259
|
+
return;
|
|
260
|
+
let text;
|
|
261
|
+
try {
|
|
262
|
+
text = await navigator.clipboard.readText();
|
|
263
|
+
}
|
|
264
|
+
catch {
|
|
265
|
+
return;
|
|
266
|
+
}
|
|
267
|
+
const anchor = ctx.selectionRange.anchor ?? ctx.grid.getState().activeCell;
|
|
268
|
+
if (!anchor)
|
|
269
|
+
return;
|
|
270
|
+
const focus = ctx.selectionRange.focus ?? anchor;
|
|
271
|
+
const startRow = Math.min(anchor.rowIndex, focus.rowIndex);
|
|
272
|
+
const startCol = Math.min(anchor.colIndex, focus.colIndex);
|
|
273
|
+
const endRow = Math.max(anchor.rowIndex, focus.rowIndex);
|
|
274
|
+
const endCol = Math.max(anchor.colIndex, focus.colIndex);
|
|
275
|
+
const lines = text.replace(/\r\n/g, "\n").split("\n");
|
|
276
|
+
if (lines.length > 0 && lines[lines.length - 1] === "")
|
|
277
|
+
lines.pop();
|
|
278
|
+
if (!lines.length)
|
|
279
|
+
return;
|
|
280
|
+
// Excel/Sheets behaviour: when the clipboard holds a single cell and
|
|
281
|
+
// the selection is a range, fill the entire range with that value.
|
|
282
|
+
// For larger clipboards (multi-row/col TSV), paste at the top-left
|
|
283
|
+
// corner and let the clipboard determine the span (the original
|
|
284
|
+
// behaviour).
|
|
285
|
+
const clipboardIsSingleCell = lines.length === 1 && (lines[0]?.split("\t").length ?? 0) === 1;
|
|
286
|
+
const selectionIsRange = startRow !== endRow || startCol !== endCol;
|
|
287
|
+
const fillRange = clipboardIsSingleCell && selectionIsRange;
|
|
288
|
+
const rowSpan = fillRange ? endRow - startRow + 1 : lines.length;
|
|
289
|
+
const colSpan = fillRange
|
|
290
|
+
? endCol - startCol + 1
|
|
291
|
+
: Math.max(...lines.map((l) => l.split("\t").length));
|
|
292
|
+
const next = ctx.internalData.slice();
|
|
293
|
+
for (let i = 0; i < rowSpan; i += 1) {
|
|
294
|
+
const targetRowIndex = startRow + i;
|
|
295
|
+
const row = ctx.allRows[targetRowIndex];
|
|
296
|
+
if (!row || isGroupRow(row))
|
|
297
|
+
continue;
|
|
298
|
+
// Map the visible row back to its index in the data array via row.id (default = String(index)).
|
|
299
|
+
const dataIndex = Number(row.id);
|
|
300
|
+
if (!Number.isInteger(dataIndex) ||
|
|
301
|
+
dataIndex < 0 ||
|
|
302
|
+
dataIndex >= next.length)
|
|
303
|
+
continue;
|
|
304
|
+
const originalRow = next[dataIndex];
|
|
305
|
+
if (!originalRow)
|
|
306
|
+
continue;
|
|
307
|
+
const sourceCells = fillRange
|
|
308
|
+
? null
|
|
309
|
+
: lines[i]?.split("\t") ?? [];
|
|
310
|
+
const updated = {
|
|
311
|
+
...originalRow,
|
|
312
|
+
};
|
|
313
|
+
for (let j = 0; j < colSpan; j += 1) {
|
|
314
|
+
const column = ctx.allColumns[startCol + j];
|
|
315
|
+
if (!column?.columnDef.field)
|
|
316
|
+
continue;
|
|
317
|
+
if (!isCellEditableAt(targetRowIndex, startCol + j))
|
|
318
|
+
continue;
|
|
319
|
+
const editorType = (column.columnDef.editorType ??
|
|
320
|
+
"text");
|
|
321
|
+
const raw = fillRange ? lines[0] : sourceCells?.[j] ?? "";
|
|
322
|
+
updated[column.columnDef.field] = parseEditorValue(editorType, raw);
|
|
323
|
+
}
|
|
324
|
+
next[dataIndex] = updated;
|
|
325
|
+
}
|
|
326
|
+
ctx.internalData = next;
|
|
327
|
+
ctx.grid.store.setState((prev) => ({ ...prev }));
|
|
328
|
+
}
|
|
329
|
+
return {
|
|
330
|
+
isCellEditable,
|
|
331
|
+
isCellEditableAt,
|
|
332
|
+
getRowColumnValue,
|
|
333
|
+
getCellDisplayValue,
|
|
334
|
+
startEditingWithChar,
|
|
335
|
+
saveEditingCell,
|
|
336
|
+
applyHistoryStep,
|
|
337
|
+
updateEditingCellValue,
|
|
338
|
+
onEditorKeyDown,
|
|
339
|
+
focusOnMount,
|
|
340
|
+
onCellDoubleClick,
|
|
341
|
+
pasteFromClipboard,
|
|
342
|
+
};
|
|
343
|
+
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export type CellEditorType = 'text' | 'number' | 'date' | 'datetime' | 'time' | 'password' | 'checkbox' | 'list' | 'chips' | 'select' | 'rich-select' | 'textarea' | 'color' | 'rating';
|
|
1
|
+
export type CellEditorType = 'text' | 'number' | 'date' | 'datetime' | 'time' | 'password' | 'checkbox' | 'list' | 'chips' | 'select' | 'rich-select' | 'autocomplete' | 'textarea' | 'color' | 'rating';
|
|
2
2
|
/** Normalized option used by list/chips editors. The optional `color`
|
|
3
3
|
* paints the chip - both in the in-cell readonly chips render and in
|
|
4
4
|
* the dropdown trigger when `renderChipsInTrigger` is on. Any CSS
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { Column, RowData } from "./index";
|
|
2
|
+
export type FacetBucket = {
|
|
3
|
+
label: string;
|
|
4
|
+
numericMin: number;
|
|
5
|
+
numericMax: number;
|
|
6
|
+
isLast: boolean;
|
|
7
|
+
isDate: boolean;
|
|
8
|
+
};
|
|
9
|
+
export declare function isBucketableColumn<TData extends RowData>(column: Column<TData>): {
|
|
10
|
+
isDate: boolean;
|
|
11
|
+
} | null;
|
|
12
|
+
export declare function buildBuckets<TData extends RowData>(column: Column<TData>, isDate: boolean, data: ReadonlyArray<TData>, getAccessor: (row: TData, column: Column<TData>) => unknown): Array<FacetBucket> | null;
|
|
13
|
+
export declare function isInBucket(num: number, bucket: FacetBucket): boolean;
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { rawToNumber, formatFacetDate, formatFacetNumber } from "./SvGrid.helpers";
|
|
2
|
+
const FACET_BUCKET_THRESHOLD = 30;
|
|
3
|
+
const FACET_BUCKET_COUNT = 10;
|
|
4
|
+
export function isBucketableColumn(column) {
|
|
5
|
+
const editorType = column.columnDef.editorType;
|
|
6
|
+
if (editorType === "number")
|
|
7
|
+
return { isDate: false };
|
|
8
|
+
if (editorType === "date" || editorType === "datetime")
|
|
9
|
+
return { isDate: true };
|
|
10
|
+
return null;
|
|
11
|
+
}
|
|
12
|
+
export function buildBuckets(column, isDate, data, getAccessor) {
|
|
13
|
+
let min = Number.POSITIVE_INFINITY;
|
|
14
|
+
let max = Number.NEGATIVE_INFINITY;
|
|
15
|
+
const distinct = new Set();
|
|
16
|
+
for (const rowData of data) {
|
|
17
|
+
const num = rawToNumber(getAccessor(rowData, column), isDate);
|
|
18
|
+
if (!Number.isFinite(num))
|
|
19
|
+
continue;
|
|
20
|
+
distinct.add(num);
|
|
21
|
+
if (num < min)
|
|
22
|
+
min = num;
|
|
23
|
+
if (num > max)
|
|
24
|
+
max = num;
|
|
25
|
+
}
|
|
26
|
+
if (distinct.size <= FACET_BUCKET_THRESHOLD)
|
|
27
|
+
return null;
|
|
28
|
+
if (!Number.isFinite(min) || !Number.isFinite(max) || min === max)
|
|
29
|
+
return null;
|
|
30
|
+
const span = max - min;
|
|
31
|
+
const buckets = [];
|
|
32
|
+
for (let i = 0; i < FACET_BUCKET_COUNT; i += 1) {
|
|
33
|
+
const lo = min + (span * i) / FACET_BUCKET_COUNT;
|
|
34
|
+
const hi = min + (span * (i + 1)) / FACET_BUCKET_COUNT;
|
|
35
|
+
const label = isDate
|
|
36
|
+
? `${formatFacetDate(lo)} – ${formatFacetDate(hi)}`
|
|
37
|
+
: `${formatFacetNumber(lo)} – ${formatFacetNumber(hi)}`;
|
|
38
|
+
buckets.push({
|
|
39
|
+
label,
|
|
40
|
+
numericMin: lo,
|
|
41
|
+
numericMax: hi,
|
|
42
|
+
isLast: i === FACET_BUCKET_COUNT - 1,
|
|
43
|
+
isDate,
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
return buckets;
|
|
47
|
+
}
|
|
48
|
+
export function isInBucket(num, bucket) {
|
|
49
|
+
if (!Number.isFinite(num))
|
|
50
|
+
return false;
|
|
51
|
+
if (num < bucket.numericMin)
|
|
52
|
+
return false;
|
|
53
|
+
return bucket.isLast ? num <= bucket.numericMax : num < bucket.numericMax;
|
|
54
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import { type RowData, type TableFeatures } from "./index";
|
|
2
|
+
import "./sv-grid-scrollbar";
|
|
3
|
+
export declare function createFeatures<TFeatures extends TableFeatures = TableFeatures, TData extends RowData = RowData>(ctx: any): {
|
|
4
|
+
resolveEffectiveFeatures: () => TableFeatures;
|
|
5
|
+
};
|
package/dist/features.js
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
// features handlers extracted from the controller. Imperative event handlers
|
|
2
|
+
// reading/writing controller state via the `ctx` handle; the reactive core
|
|
3
|
+
// ($state/$derived/$effect) stays in the controller.
|
|
4
|
+
import { tableFeatures, rowSortingFeature, columnFilteringFeature, columnGroupingFeature, } from "./index";
|
|
5
|
+
import "./sv-grid-scrollbar";
|
|
6
|
+
export function createFeatures(ctx) {
|
|
7
|
+
// Merge the consumer's `features` set with whatever the boolean shortcuts
|
|
8
|
+
// imply. A shortcut set to `true` injects its feature; set to `false` it
|
|
9
|
+
// removes it (so `sortable={false}` wins even if `rowSortingFeature` was
|
|
10
|
+
// passed in `features`); left undefined it defers to `features`.
|
|
11
|
+
function resolveEffectiveFeatures() {
|
|
12
|
+
const merged = { ...(ctx.props.features ?? {}) };
|
|
13
|
+
if (ctx.props.sortable === true)
|
|
14
|
+
merged.rowSortingFeature = rowSortingFeature;
|
|
15
|
+
else if (ctx.props.sortable === false)
|
|
16
|
+
delete merged.rowSortingFeature;
|
|
17
|
+
if (ctx.props.filterable === true)
|
|
18
|
+
merged.columnFilteringFeature = columnFilteringFeature;
|
|
19
|
+
else if (ctx.props.filterable === false)
|
|
20
|
+
delete merged.columnFilteringFeature;
|
|
21
|
+
if (ctx.props.groupable === true)
|
|
22
|
+
merged.columnGroupingFeature = columnGroupingFeature;
|
|
23
|
+
else if (ctx.props.groupable === false)
|
|
24
|
+
delete merged.columnGroupingFeature;
|
|
25
|
+
return tableFeatures(merged);
|
|
26
|
+
}
|
|
27
|
+
return {
|
|
28
|
+
resolveEffectiveFeatures,
|
|
29
|
+
};
|
|
30
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { Column, RowData } from "./index";
|
|
2
|
+
import type { FilterOperator, FilterOption } from "./SvGrid.types";
|
|
3
|
+
export declare const filterOperatorOptions: Array<FilterOption>;
|
|
4
|
+
/** Which operators make sense for each column editor type. */
|
|
5
|
+
export declare const TEXT_OPERATORS: Array<FilterOperator>;
|
|
6
|
+
export declare const NUMBER_OPERATORS: Array<FilterOperator>;
|
|
7
|
+
export declare const DATE_OPERATORS: Array<FilterOperator>;
|
|
8
|
+
export declare const CHECKBOX_OPERATORS: Array<FilterOperator>;
|
|
9
|
+
export declare const fallbackOperatorOption: FilterOption;
|
|
10
|
+
export declare function operatorOption(value: FilterOperator): FilterOption;
|
|
11
|
+
/** Returns the operators that make sense for the given column's data type. */
|
|
12
|
+
export declare function operatorsForColumn<TData extends RowData>(column: Column<TData> | undefined): Array<FilterOption>;
|
|
13
|
+
/** Default operator for a column (first one valid for its type). */
|
|
14
|
+
export declare function defaultOperatorFor<TData extends RowData>(column: Column<TData> | undefined): FilterOperator;
|
|
15
|
+
/** Date columns get friendlier labels for "less / greater than". */
|
|
16
|
+
export declare function operatorLabelFor<TData extends RowData>(option: FilterOption, column: Column<TData> | undefined): string;
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
export const filterOperatorOptions = [
|
|
2
|
+
{ value: "contains", label: "Contains", iconName: "op-contains" },
|
|
3
|
+
{ value: "equals", label: "Equals", iconName: "op-equals" },
|
|
4
|
+
{ value: "startsWith", label: "Starts with", iconName: "op-startsWith" },
|
|
5
|
+
{ value: "greaterThan", label: "Greater than", iconName: "op-greaterThan" },
|
|
6
|
+
{ value: "lessThan", label: "Less than", iconName: "op-lessThan" },
|
|
7
|
+
{ value: "between", label: "Between", iconName: "op-between" },
|
|
8
|
+
{ value: "isBlank", label: "Is blank", iconName: "op-isBlank" },
|
|
9
|
+
];
|
|
10
|
+
/** Which operators make sense for each column editor type. */
|
|
11
|
+
export const TEXT_OPERATORS = [
|
|
12
|
+
"contains",
|
|
13
|
+
"equals",
|
|
14
|
+
"startsWith",
|
|
15
|
+
"isBlank",
|
|
16
|
+
];
|
|
17
|
+
export const NUMBER_OPERATORS = [
|
|
18
|
+
"equals",
|
|
19
|
+
"greaterThan",
|
|
20
|
+
"lessThan",
|
|
21
|
+
"between",
|
|
22
|
+
"isBlank",
|
|
23
|
+
];
|
|
24
|
+
export const DATE_OPERATORS = [
|
|
25
|
+
"equals",
|
|
26
|
+
"lessThan",
|
|
27
|
+
"greaterThan",
|
|
28
|
+
"between",
|
|
29
|
+
"isBlank",
|
|
30
|
+
];
|
|
31
|
+
export const CHECKBOX_OPERATORS = ["equals", "isBlank"];
|
|
32
|
+
export const fallbackOperatorOption = {
|
|
33
|
+
value: "contains",
|
|
34
|
+
label: "Contains",
|
|
35
|
+
iconName: "op-contains",
|
|
36
|
+
};
|
|
37
|
+
export function operatorOption(value) {
|
|
38
|
+
return (filterOperatorOptions.find((option) => option.value === value) ??
|
|
39
|
+
fallbackOperatorOption);
|
|
40
|
+
}
|
|
41
|
+
/** Returns the operators that make sense for the given column's data type. */
|
|
42
|
+
export function operatorsForColumn(column) {
|
|
43
|
+
const editorType = column?.columnDef.editorType ?? "text";
|
|
44
|
+
const ids = editorType === "number"
|
|
45
|
+
? NUMBER_OPERATORS
|
|
46
|
+
: editorType === "date" || editorType === "datetime"
|
|
47
|
+
? DATE_OPERATORS
|
|
48
|
+
: editorType === "checkbox"
|
|
49
|
+
? CHECKBOX_OPERATORS
|
|
50
|
+
: TEXT_OPERATORS;
|
|
51
|
+
return ids
|
|
52
|
+
.map((id) => filterOperatorOptions.find((option) => option.value === id))
|
|
53
|
+
.filter((option) => Boolean(option));
|
|
54
|
+
}
|
|
55
|
+
/** Default operator for a column (first one valid for its type). */
|
|
56
|
+
export function defaultOperatorFor(column) {
|
|
57
|
+
return operatorsForColumn(column)[0]?.value ?? "contains";
|
|
58
|
+
}
|
|
59
|
+
/** Date columns get friendlier labels for "less / greater than". */
|
|
60
|
+
export function operatorLabelFor(option, column) {
|
|
61
|
+
const editorType = column?.columnDef.editorType;
|
|
62
|
+
if (editorType === "date" || editorType === "datetime") {
|
|
63
|
+
if (option.value === "lessThan")
|
|
64
|
+
return "Before";
|
|
65
|
+
if (option.value === "greaterThan")
|
|
66
|
+
return "After";
|
|
67
|
+
}
|
|
68
|
+
return option.label;
|
|
69
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* HyperFormula adapter — Excel-class formula evaluation for SvGrid.
|
|
3
|
+
*
|
|
4
|
+
* `hyperformula` is an OPTIONAL peer dependency. Consumers who want
|
|
5
|
+
* full Excel-compatible formulas install it themselves:
|
|
6
|
+
*
|
|
7
|
+
* ```bash
|
|
8
|
+
* pnpm add hyperformula
|
|
9
|
+
* ```
|
|
10
|
+
*
|
|
11
|
+
* The adapter wraps an HF instance around the grid's flat-row data:
|
|
12
|
+
*
|
|
13
|
+
* - Treats each `data[]` row as a spreadsheet row
|
|
14
|
+
* - Each registered field maps to a spreadsheet column
|
|
15
|
+
* - Values that start with `=` are evaluated by HF
|
|
16
|
+
* - The adapter returns the computed rows ready to render
|
|
17
|
+
* - On edit, call `update(rowIdx, field, value)` and the adapter
|
|
18
|
+
* re-evaluates only the cells HF tells it are dirty
|
|
19
|
+
*
|
|
20
|
+
* Why not bundle HF: it's ~1MB minified and 99% of grids never need it.
|
|
21
|
+
* Keeping it peer-optional means tiny default bundle + heavy-formula
|
|
22
|
+
* users get the ~400 functions (VLOOKUP, INDIRECT, IFS, XLOOKUP, full
|
|
23
|
+
* date/time/financial libraries) on demand.
|
|
24
|
+
*/
|
|
25
|
+
/** Minimal HF type surface this adapter uses. Mirrored here (rather
|
|
26
|
+
* than importing) so consumers without `hyperformula` installed still
|
|
27
|
+
* type-check. */
|
|
28
|
+
export type HyperFormulaInstance = {
|
|
29
|
+
setCellContents(cell: {
|
|
30
|
+
sheet: number;
|
|
31
|
+
row: number;
|
|
32
|
+
col: number;
|
|
33
|
+
}, contents: unknown): Array<{
|
|
34
|
+
address: {
|
|
35
|
+
sheet: number;
|
|
36
|
+
row: number;
|
|
37
|
+
col: number;
|
|
38
|
+
};
|
|
39
|
+
newValue: unknown;
|
|
40
|
+
}>;
|
|
41
|
+
getCellValue(cell: {
|
|
42
|
+
sheet: number;
|
|
43
|
+
row: number;
|
|
44
|
+
col: number;
|
|
45
|
+
}): unknown;
|
|
46
|
+
destroy(): void;
|
|
47
|
+
rebuildAndRecalculate(): void;
|
|
48
|
+
};
|
|
49
|
+
export type HyperFormulaSheetConfig<T extends Record<string, unknown>> = {
|
|
50
|
+
/** The HF instance the consumer constructed (so they pick the
|
|
51
|
+
* license key + options). */
|
|
52
|
+
hyperformula: HyperFormulaInstance;
|
|
53
|
+
/** Sheet index inside the HF instance. Default 0. */
|
|
54
|
+
sheetId?: number;
|
|
55
|
+
/** Source rows. Cell values starting with `=` are evaluated. */
|
|
56
|
+
rows: T[];
|
|
57
|
+
/** Ordered list of fields to expose to the formula engine. The
|
|
58
|
+
* order determines the HF column index, which is how A1-style
|
|
59
|
+
* references resolve. */
|
|
60
|
+
fields: ReadonlyArray<keyof T & string>;
|
|
61
|
+
};
|
|
62
|
+
export type HyperFormulaSheet<T extends Record<string, unknown>> = {
|
|
63
|
+
/** Rows with every `=...` formula replaced by its evaluated value.
|
|
64
|
+
* Use this in a custom cell renderer to DISPLAY the result. */
|
|
65
|
+
computed: T[];
|
|
66
|
+
/** Rows with formulas kept as their raw `=SUM(...)` strings.
|
|
67
|
+
* Pass this as `<SvGrid data={...}>` so the inline editor lets the
|
|
68
|
+
* user edit the formula text itself, not the evaluated value. */
|
|
69
|
+
raw: T[];
|
|
70
|
+
/** Call when a cell edit lands. Returns the new computed + raw
|
|
71
|
+
* snapshots so the consumer can reassign both. */
|
|
72
|
+
update(rowIndex: number, field: keyof T & string, value: unknown): {
|
|
73
|
+
computed: T[];
|
|
74
|
+
raw: T[];
|
|
75
|
+
};
|
|
76
|
+
/** Tear the adapter + the HF instance down (releases memory). */
|
|
77
|
+
destroy(): void;
|
|
78
|
+
};
|
|
79
|
+
/** Build a live spreadsheet adapter around a HyperFormula instance.
|
|
80
|
+
* Returns computed rows + an `update()` hook to push edits back into
|
|
81
|
+
* the engine. */
|
|
82
|
+
export declare function createHyperFormulaSheet<T extends Record<string, unknown>>(config: HyperFormulaSheetConfig<T>): HyperFormulaSheet<T>;
|