@svgrid/grid 2.2.28 → 2.2.29
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/dist/GridMenus.svelte +148 -100
- package/dist/SvGrid.controller.svelte.d.ts +14 -1
- package/dist/SvGrid.controller.svelte.js +112 -28
- package/dist/SvGrid.css +15 -34
- package/dist/SvGrid.svelte +49 -21
- package/dist/SvGrid.types.d.ts +20 -5
- package/dist/SvListBox.svelte +42 -3
- package/dist/SvListBox.svelte.d.ts +10 -1
- package/dist/cdn/GridMenus-Da_p7SKS.js +486 -0
- package/dist/cdn/GridMenus-Ds3OpzZT.js +490 -0
- package/dist/cdn/{src-BxNEslEo.js → src-BLJfdyL0.js} +4014 -3869
- package/dist/cdn/{src-C4yKQZ5t.js → src-DYDCiC0D.js} +7036 -6891
- package/dist/cdn/svgrid.js +8 -8
- package/dist/cdn/svgrid.svelte-external.js +8 -8
- package/dist/column-groups.js +8 -10
- package/dist/column-id.d.ts +9 -0
- package/dist/column-id.js +25 -0
- package/dist/core.js +2 -1
- package/dist/createListbox.svelte.d.ts +15 -2
- package/dist/createListbox.svelte.js +27 -6
- package/dist/editing.d.ts +1 -0
- package/dist/editing.js +45 -10
- package/dist/filtering/excel-filters.d.ts +30 -10
- package/dist/filtering/excel-filters.js +96 -15
- package/dist/index.d.ts +2 -2
- package/dist/index.js +2 -2
- package/dist/menus.d.ts +2 -0
- package/dist/menus.js +25 -0
- package/dist/row-resize.js +40 -0
- package/dist/selection.js +1 -1
- package/package.json +1 -1
- package/src/GridMenus.svelte +148 -100
- package/src/SvGrid.controller.svelte.ts +123 -27
- package/src/SvGrid.css +15 -34
- package/src/SvGrid.svelte +49 -21
- package/src/SvGrid.types.ts +20 -5
- package/src/SvListBox.svelte +42 -3
- package/src/column-groups.test.ts +48 -0
- package/src/column-groups.ts +8 -10
- package/src/column-id.ts +30 -0
- package/src/core.ts +2 -1
- package/src/createListbox.svelte.ts +39 -7
- package/src/editing.test.ts +90 -0
- package/src/editing.ts +46 -12
- package/src/filtering/excel-filters.test.ts +44 -1
- package/src/filtering/excel-filters.ts +91 -14
- package/src/index.ts +2 -0
- package/src/menus.test.ts +40 -4
- package/src/menus.ts +27 -0
- package/src/row-resize.test.ts +100 -0
- package/src/row-resize.ts +36 -0
- package/src/selection.ts +1 -1
- package/src/svgrid.api-extensions.test.ts +8 -3
- package/src/svgrid.cell-dom-ids.test.ts +96 -0
- package/src/svgrid.conditional-stat-scope.test.ts +111 -0
- package/src/svgrid.filter-menu-listbox.svelte.test.ts +308 -0
- package/src/svgrid.filter-menu-scroll.test.ts +3 -1
- package/src/svgrid.group-header-ids.test.ts +121 -0
- package/dist/cdn/GridMenus-BbzEvTYO.js +0 -421
- package/dist/cdn/GridMenus-E220X2kM.js +0 -417
package/dist/editing.js
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
// ($state/$derived/$effect) stays in the controller.
|
|
4
4
|
import { parseEditorValue, } from "./index";
|
|
5
5
|
import "./sv-grid-scrollbar";
|
|
6
|
+
import { getNextActiveCell } from "./keyboard";
|
|
6
7
|
import { getCellKey, toValueArray, } from "./SvGrid.helpers";
|
|
7
8
|
import { getColumnBaseValue, isGroupRow, } from "./cell-values";
|
|
8
9
|
export function createEditing(ctx) {
|
|
@@ -96,6 +97,7 @@ export function createEditing(ctx) {
|
|
|
96
97
|
columnId: column.id,
|
|
97
98
|
editorType,
|
|
98
99
|
value: char,
|
|
100
|
+
rowRef: row.original,
|
|
99
101
|
};
|
|
100
102
|
ctx.setActiveCell(rowIndex, colIndex);
|
|
101
103
|
ctx.setSelection(rowIndex, colIndex);
|
|
@@ -104,15 +106,22 @@ export function createEditing(ctx) {
|
|
|
104
106
|
function saveEditingCell() {
|
|
105
107
|
if (!ctx.editingCell)
|
|
106
108
|
return;
|
|
107
|
-
const
|
|
109
|
+
const editing = ctx.editingCell;
|
|
110
|
+
// Resolve the row this edit belongs to. `allRows` is only the current page,
|
|
111
|
+
// so fall back to the full filtered model, then to the data object captured
|
|
112
|
+
// when the edit started. Without that last hop an edit is silently dropped
|
|
113
|
+
// when a filter typed mid-edit hides the row from both models (#49).
|
|
114
|
+
const row = ctx.allRows.find((entry) => entry.id === editing.rowId) ??
|
|
115
|
+
ctx.allRowsBeforePagination.find((entry) => entry.id === editing.rowId);
|
|
116
|
+
const rowData = (row?.original ?? editing.rowRef);
|
|
108
117
|
const column = ctx.allColumns.find((entry) => entry.id === ctx.editingCell?.columnId);
|
|
109
118
|
const parsedValue = parseEditorValue(ctx.editingCell.editorType, ctx.editingCell.value, {
|
|
110
119
|
multiple: column?.columnDef.editorMultiple === true,
|
|
111
120
|
});
|
|
112
121
|
let oldValue = undefined;
|
|
113
122
|
let finalValue = parsedValue;
|
|
114
|
-
if (
|
|
115
|
-
oldValue =
|
|
123
|
+
if (rowData && column?.columnDef.field) {
|
|
124
|
+
oldValue = rowData[column.columnDef.field];
|
|
116
125
|
// Per-column valueParser refines the type-coerced value before storing.
|
|
117
126
|
const parser = column.columnDef.valueParser;
|
|
118
127
|
if (parser) {
|
|
@@ -120,12 +129,11 @@ export function createEditing(ctx) {
|
|
|
120
129
|
newValue: parsedValue,
|
|
121
130
|
oldValue,
|
|
122
131
|
rawInput: ctx.editingCell.value,
|
|
123
|
-
data:
|
|
132
|
+
data: rowData,
|
|
124
133
|
columnId: ctx.editingCell.columnId,
|
|
125
134
|
});
|
|
126
135
|
}
|
|
127
|
-
|
|
128
|
-
finalValue;
|
|
136
|
+
rowData[column.columnDef.field] = finalValue;
|
|
129
137
|
}
|
|
130
138
|
const key = getCellKey(ctx.editingCell.rowId, ctx.editingCell.columnId);
|
|
131
139
|
ctx.editedCellValues = {
|
|
@@ -136,7 +144,7 @@ export function createEditing(ctx) {
|
|
|
136
144
|
// Record into the history at the current pointer. Any forward
|
|
137
145
|
// history (steps the user could have redone) is truncated - this
|
|
138
146
|
// is the standard "edit invalidates redo" rule.
|
|
139
|
-
if (oldValue !== finalValue &&
|
|
147
|
+
if (oldValue !== finalValue && rowData && column?.columnDef.field) {
|
|
140
148
|
const step = {
|
|
141
149
|
rowId: ctx.editingCell.rowId,
|
|
142
150
|
columnId: ctx.editingCell.columnId,
|
|
@@ -162,14 +170,14 @@ export function createEditing(ctx) {
|
|
|
162
170
|
// Notify the consumer AFTER the row has been updated so any callback-
|
|
163
171
|
// driven recompute (cascade totals, server save, undo stack) sees the
|
|
164
172
|
// post-write state. `rowIndex` matches the position in `props.data`.
|
|
165
|
-
if (ctx.props.onCellValueChange &&
|
|
166
|
-
const rowIndex = ctx.internalData.indexOf(
|
|
173
|
+
if (ctx.props.onCellValueChange && rowData && column) {
|
|
174
|
+
const rowIndex = ctx.internalData.indexOf(rowData);
|
|
167
175
|
ctx.props.onCellValueChange({
|
|
168
176
|
rowIndex,
|
|
169
177
|
columnId: column.id,
|
|
170
178
|
oldValue,
|
|
171
179
|
newValue: finalValue,
|
|
172
|
-
row:
|
|
180
|
+
row: rowData,
|
|
173
181
|
});
|
|
174
182
|
}
|
|
175
183
|
ctx.editingCell = null;
|
|
@@ -200,6 +208,27 @@ export function createEditing(ctx) {
|
|
|
200
208
|
function updateEditingCellValue(value) {
|
|
201
209
|
ctx.editingCell = ctx.editingCell ? { ...ctx.editingCell, value } : ctx.editingCell;
|
|
202
210
|
}
|
|
211
|
+
/**
|
|
212
|
+
* Commit the open editor and move the active cell one column along, wrapping
|
|
213
|
+
* at row boundaries the way Excel does. Editors `stopPropagation()` every key
|
|
214
|
+
* so the grid's own Tab handler never sees this one - without the explicit
|
|
215
|
+
* move, Tab committed the edit and then let the browser walk focus out of the
|
|
216
|
+
* grid entirely (#48). Shared by every editor type, including the textarea.
|
|
217
|
+
*/
|
|
218
|
+
function commitAndMoveByTab(shiftKey) {
|
|
219
|
+
const active = ctx.activeCell;
|
|
220
|
+
saveEditingCell();
|
|
221
|
+
ctx.gridRootEl?.focus({ preventScroll: true });
|
|
222
|
+
if (!active)
|
|
223
|
+
return;
|
|
224
|
+
const next = getNextActiveCell(active, shiftKey ? "tabPrev" : "tabNext", {
|
|
225
|
+
maxRow: ctx.allRows.length - 1,
|
|
226
|
+
maxCol: ctx.allColumns.length - 1,
|
|
227
|
+
});
|
|
228
|
+
ctx.setActiveCell(next.rowIndex, next.colIndex);
|
|
229
|
+
ctx.setSelection(next.rowIndex, next.colIndex);
|
|
230
|
+
ctx.scrollActiveCellIntoView(next.rowIndex, next.colIndex);
|
|
231
|
+
}
|
|
203
232
|
function onEditorKeyDown(event) {
|
|
204
233
|
event.stopPropagation();
|
|
205
234
|
if (event.key === "Enter") {
|
|
@@ -207,6 +236,10 @@ export function createEditing(ctx) {
|
|
|
207
236
|
saveEditingCell();
|
|
208
237
|
ctx.gridRootEl?.focus({ preventScroll: true });
|
|
209
238
|
}
|
|
239
|
+
else if (event.key === "Tab") {
|
|
240
|
+
event.preventDefault();
|
|
241
|
+
commitAndMoveByTab(event.shiftKey);
|
|
242
|
+
}
|
|
210
243
|
else if (event.key === "Escape") {
|
|
211
244
|
event.preventDefault();
|
|
212
245
|
ctx.editingCell = null;
|
|
@@ -269,6 +302,7 @@ export function createEditing(ctx) {
|
|
|
269
302
|
columnId: column.id,
|
|
270
303
|
editorType,
|
|
271
304
|
value: initialValue,
|
|
305
|
+
rowRef: row.original,
|
|
272
306
|
};
|
|
273
307
|
ctx.setActiveCell(rowIndex, colIndex);
|
|
274
308
|
ctx.setSelection(rowIndex, colIndex);
|
|
@@ -528,6 +562,7 @@ export function createEditing(ctx) {
|
|
|
528
562
|
applyHistoryStep,
|
|
529
563
|
updateEditingCellValue,
|
|
530
564
|
onEditorKeyDown,
|
|
565
|
+
commitAndMoveByTab,
|
|
531
566
|
focusOnMount,
|
|
532
567
|
onCellDoubleClick,
|
|
533
568
|
pasteFromClipboard,
|
|
@@ -25,19 +25,39 @@ export type ExcelFilterOptions = {
|
|
|
25
25
|
export declare function normalizeForFilter(s: string, locale?: string | ReadonlyArray<string>): string;
|
|
26
26
|
/**
|
|
27
27
|
* Delimiter used to serialise the `in` / `notIn` value list into the single
|
|
28
|
-
* `value` string that the filter model carries.
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
-
*
|
|
28
|
+
* `value` string that the filter model carries.
|
|
29
|
+
*
|
|
30
|
+
* This has to be single-line safe: the tool panel and the filter menu show the
|
|
31
|
+
* serialised list in a plain `<input type="text">`, and the HTML input value
|
|
32
|
+
* sanitiser STRIPS newlines - so a newline-joined list came back out of the DOM
|
|
33
|
+
* as one run-together token. `splitInTokens` accepts newlines too, so values
|
|
34
|
+
* serialised by older builds still parse.
|
|
32
35
|
*/
|
|
33
|
-
export declare const IN_TOKEN_SEP = "
|
|
36
|
+
export declare const IN_TOKEN_SEP = ", ";
|
|
34
37
|
/**
|
|
35
|
-
* Split a serialised `in` / `notIn` value into its individual tokens.
|
|
36
|
-
*
|
|
37
|
-
*
|
|
38
|
-
*
|
|
38
|
+
* Split a serialised `in` / `notIn` value into its individual tokens.
|
|
39
|
+
*
|
|
40
|
+
* The grammar is CSV-shaped: comma OR newline separates, and a token may be
|
|
41
|
+
* double-quoted to carry a separator literally (`""` escapes a quote inside
|
|
42
|
+
* one). Quoting is what makes a comma separator safe - facet values commonly
|
|
43
|
+
* contain commas of their own, because numeric bucket labels are built with
|
|
44
|
+
* `toLocaleString` ("1,234 - 5,678") and date labels with a short month
|
|
45
|
+
* ("Aug 17, 2026"). A quote only opens a token when it is the token's first
|
|
46
|
+
* non-space character, so an unquoted `5" pipe` stays literal.
|
|
39
47
|
*/
|
|
40
48
|
export declare function splitInTokens(value: unknown): string[];
|
|
41
|
-
/**
|
|
49
|
+
/**
|
|
50
|
+
* Serialise a list of tokens back into the single `in` / `notIn` value,
|
|
51
|
+
* quoting any token that carries a separator or a quote of its own.
|
|
52
|
+
*/
|
|
42
53
|
export declare function joinInTokens(tokens: ReadonlyArray<string>): string;
|
|
54
|
+
/**
|
|
55
|
+
* The token still being typed at the end of an `in` / `notIn` value - the text
|
|
56
|
+
* after the last separator. `'AAPL, MS'` -> `'MS'`; a value ending in a
|
|
57
|
+
* separator (or empty) has no trailing token.
|
|
58
|
+
*
|
|
59
|
+
* Inputs that hold the whole token list use this to drive the value-suggestion
|
|
60
|
+
* dropdown: the trailing fragment is the search query, not a committed token.
|
|
61
|
+
*/
|
|
62
|
+
export declare function trailingInToken(value: unknown): string;
|
|
43
63
|
export declare function applyExcelFilter(cellValue: unknown, filter: ExcelFilter, options?: ExcelFilterOptions): boolean;
|
|
@@ -15,27 +15,108 @@ export function normalizeForFilter(s, locale) {
|
|
|
15
15
|
}
|
|
16
16
|
/**
|
|
17
17
|
* Delimiter used to serialise the `in` / `notIn` value list into the single
|
|
18
|
-
* `value` string that the filter model carries.
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
18
|
+
* `value` string that the filter model carries.
|
|
19
|
+
*
|
|
20
|
+
* This has to be single-line safe: the tool panel and the filter menu show the
|
|
21
|
+
* serialised list in a plain `<input type="text">`, and the HTML input value
|
|
22
|
+
* sanitiser STRIPS newlines - so a newline-joined list came back out of the DOM
|
|
23
|
+
* as one run-together token. `splitInTokens` accepts newlines too, so values
|
|
24
|
+
* serialised by older builds still parse.
|
|
22
25
|
*/
|
|
23
|
-
export const IN_TOKEN_SEP = '
|
|
26
|
+
export const IN_TOKEN_SEP = ', ';
|
|
24
27
|
/**
|
|
25
|
-
* Split a serialised `in` / `notIn` value into its individual tokens.
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
28
|
+
* Split a serialised `in` / `notIn` value into its individual tokens.
|
|
29
|
+
*
|
|
30
|
+
* The grammar is CSV-shaped: comma OR newline separates, and a token may be
|
|
31
|
+
* double-quoted to carry a separator literally (`""` escapes a quote inside
|
|
32
|
+
* one). Quoting is what makes a comma separator safe - facet values commonly
|
|
33
|
+
* contain commas of their own, because numeric bucket labels are built with
|
|
34
|
+
* `toLocaleString` ("1,234 - 5,678") and date labels with a short month
|
|
35
|
+
* ("Aug 17, 2026"). A quote only opens a token when it is the token's first
|
|
36
|
+
* non-space character, so an unquoted `5" pipe` stays literal.
|
|
29
37
|
*/
|
|
30
38
|
export function splitInTokens(value) {
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
.
|
|
34
|
-
|
|
39
|
+
const { tokens, trailing } = scanInTokens(value);
|
|
40
|
+
if (trailing)
|
|
41
|
+
tokens.push(trailing);
|
|
42
|
+
return tokens;
|
|
35
43
|
}
|
|
36
|
-
/**
|
|
44
|
+
/**
|
|
45
|
+
* Serialise a list of tokens back into the single `in` / `notIn` value,
|
|
46
|
+
* quoting any token that carries a separator or a quote of its own.
|
|
47
|
+
*/
|
|
37
48
|
export function joinInTokens(tokens) {
|
|
38
|
-
return tokens
|
|
49
|
+
return tokens
|
|
50
|
+
.map((t) => t.trim())
|
|
51
|
+
.filter((t) => t.length > 0)
|
|
52
|
+
.map((t) => (/["\n,]/.test(t) ? `"${t.replace(/"/g, '""')}"` : t))
|
|
53
|
+
.join(IN_TOKEN_SEP);
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* The token still being typed at the end of an `in` / `notIn` value - the text
|
|
57
|
+
* after the last separator. `'AAPL, MS'` -> `'MS'`; a value ending in a
|
|
58
|
+
* separator (or empty) has no trailing token.
|
|
59
|
+
*
|
|
60
|
+
* Inputs that hold the whole token list use this to drive the value-suggestion
|
|
61
|
+
* dropdown: the trailing fragment is the search query, not a committed token.
|
|
62
|
+
*/
|
|
63
|
+
export function trailingInToken(value) {
|
|
64
|
+
return scanInTokens(value).trailing;
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* One pass over a serialised `in` / `notIn` value, splitting it into the
|
|
68
|
+
* tokens that are terminated by a separator and the unterminated fragment at
|
|
69
|
+
* the end. Every token helper above is a view onto this, so they can never
|
|
70
|
+
* disagree about where a quoted token starts or ends.
|
|
71
|
+
*/
|
|
72
|
+
function scanInTokens(value) {
|
|
73
|
+
const raw = String(value ?? '');
|
|
74
|
+
const tokens = [];
|
|
75
|
+
let buf = '';
|
|
76
|
+
let quoted = false;
|
|
77
|
+
let wasQuoted = false;
|
|
78
|
+
let closed = false;
|
|
79
|
+
// A quoted token keeps its inner spacing; an unquoted one is trimmed.
|
|
80
|
+
const take = () => {
|
|
81
|
+
const t = wasQuoted ? buf : buf.trim();
|
|
82
|
+
buf = '';
|
|
83
|
+
wasQuoted = false;
|
|
84
|
+
closed = false;
|
|
85
|
+
return t;
|
|
86
|
+
};
|
|
87
|
+
for (let i = 0; i < raw.length; i += 1) {
|
|
88
|
+
const ch = raw[i];
|
|
89
|
+
if (quoted) {
|
|
90
|
+
if (ch !== '"') {
|
|
91
|
+
buf += ch;
|
|
92
|
+
continue;
|
|
93
|
+
}
|
|
94
|
+
// A doubled quote is an escaped literal quote, not the closing one.
|
|
95
|
+
if (raw[i + 1] === '"') {
|
|
96
|
+
buf += '"';
|
|
97
|
+
i += 1;
|
|
98
|
+
continue;
|
|
99
|
+
}
|
|
100
|
+
quoted = false;
|
|
101
|
+
closed = true;
|
|
102
|
+
continue;
|
|
103
|
+
}
|
|
104
|
+
if (ch === ',' || ch === '\n') {
|
|
105
|
+
const t = take();
|
|
106
|
+
if (t.length)
|
|
107
|
+
tokens.push(t);
|
|
108
|
+
continue;
|
|
109
|
+
}
|
|
110
|
+
// Only opens a quoted token at the token's start - `5" pipe` is literal.
|
|
111
|
+
if (ch === '"' && !closed && !buf.trim()) {
|
|
112
|
+
quoted = true;
|
|
113
|
+
wasQuoted = true;
|
|
114
|
+
buf = '';
|
|
115
|
+
continue;
|
|
116
|
+
}
|
|
117
|
+
buf += ch;
|
|
118
|
+
}
|
|
119
|
+
return { tokens, trailing: take() };
|
|
39
120
|
}
|
|
40
121
|
export function applyExcelFilter(cellValue, filter, options) {
|
|
41
122
|
const text = String(cellValue ?? '');
|
package/dist/index.d.ts
CHANGED
|
@@ -56,7 +56,7 @@ export { default as SvAspectRatio } from './SvAspectRatio.svelte';
|
|
|
56
56
|
export { registerCellEditor, getCellEditor, hasCellEditor, unregisterCellEditor, registeredCellEditorTypes, defaultEditorProps, resolveEditorProps, type CellEditorContext, type CellEditorRegistration, } from './editor-registry';
|
|
57
57
|
export { registerBuiltinEditors } from './builtin-editors';
|
|
58
58
|
export { default as SvField } from './SvField.svelte';
|
|
59
|
-
export { createListbox, toSelectedArray, type Listbox, type ListboxConfig, type ListboxValue, type ListboxRootProps, type OptionProps, } from './createListbox.svelte';
|
|
59
|
+
export { createListbox, toSelectedArray, toSelectedSet, type Listbox, type ListboxConfig, type ListboxValue, type ListboxRootProps, type OptionProps, } from './createListbox.svelte';
|
|
60
60
|
export { createCombobox, type Combobox, type ComboboxConfig, type ComboboxValue } from './createCombobox.svelte';
|
|
61
61
|
export { createDropdownList, type DropdownList, type DropdownListConfig, type DropdownValue } from './createDropdownList.svelte';
|
|
62
62
|
export { createPopoverSelect, type PopoverSelect, type PopoverSelectConfig } from './createPopoverSelect.svelte';
|
|
@@ -214,7 +214,7 @@ export { createColumnVirtualizer } from './virtualization/column-virtualizer';
|
|
|
214
214
|
export type { VirtualItem, VirtualizerOptions, VirtualizerState } from './virtualization/types';
|
|
215
215
|
export type { SvGridApi, SvGridFilterOperator, SvGridWrapperProps } from './svgrid-wrapper.types';
|
|
216
216
|
export { parseEditorValue, normalizeEditorOptions, type CellEditorType, type CellEditorOption, } from './editors/cell-editors';
|
|
217
|
-
export { applyExcelFilter, normalizeForFilter, splitInTokens, joinInTokens, type ExcelFilter, type ExcelFilterOperator, type ExcelFilterOptions, } from './filtering/excel-filters';
|
|
217
|
+
export { applyExcelFilter, normalizeForFilter, splitInTokens, joinInTokens, trailingInToken, type ExcelFilter, type ExcelFilterOperator, type ExcelFilterOptions, } from './filtering/excel-filters';
|
|
218
218
|
export { getGridCellDomId, getGridCellA11yProps, getGridHeaderA11yProps, getGridRootA11yProps, getGridRowA11yProps, type GridCellA11yInput, type GridColumnA11yInput, type GridSortDirection, } from './a11y';
|
|
219
219
|
/**
|
|
220
220
|
* Compatibility alias for teams migrating from `createTable`.
|
package/dist/index.js
CHANGED
|
@@ -76,7 +76,7 @@ export { registerBuiltinEditors } from './builtin-editors';
|
|
|
76
76
|
// UI kit - shared field chrome (label + hint + error + direction wrapper)
|
|
77
77
|
export { default as SvField } from './SvField.svelte';
|
|
78
78
|
// UI kit - headless cores (state + prop-getters you render yourself, like createSvGrid)
|
|
79
|
-
export { createListbox, toSelectedArray, } from './createListbox.svelte';
|
|
79
|
+
export { createListbox, toSelectedArray, toSelectedSet, } from './createListbox.svelte';
|
|
80
80
|
// Selection family cores
|
|
81
81
|
export { createCombobox } from './createCombobox.svelte';
|
|
82
82
|
export { createDropdownList } from './createDropdownList.svelte';
|
|
@@ -249,7 +249,7 @@ export { createVirtualizer } from './virtualization/virtualizer';
|
|
|
249
249
|
export { createSvelteVirtualizer } from './virtualization/svelte-virtualizer.svelte';
|
|
250
250
|
export { createColumnVirtualizer } from './virtualization/column-virtualizer';
|
|
251
251
|
export { parseEditorValue, normalizeEditorOptions, } from './editors/cell-editors';
|
|
252
|
-
export { applyExcelFilter, normalizeForFilter, splitInTokens, joinInTokens, } from './filtering/excel-filters';
|
|
252
|
+
export { applyExcelFilter, normalizeForFilter, splitInTokens, joinInTokens, trailingInToken, } from './filtering/excel-filters';
|
|
253
253
|
export { getGridCellDomId, getGridCellA11yProps, getGridHeaderA11yProps, getGridRootA11yProps, getGridRowA11yProps, } from './a11y';
|
|
254
254
|
/**
|
|
255
255
|
* Compatibility alias for teams migrating from `createTable`.
|
package/dist/menus.d.ts
CHANGED
|
@@ -24,6 +24,8 @@ export declare function createMenus<TFeatures extends TableFeatures = TableFeatu
|
|
|
24
24
|
clearGroupingFromMenu: (columnId: string) => void;
|
|
25
25
|
isFacetChecked: (columnId: string, value: string) => any;
|
|
26
26
|
toggleFacetValue: (columnId: string, value: string) => void;
|
|
27
|
+
setFacetSelection: (columnId: string, next: ReadonlySet<string>) => void;
|
|
28
|
+
setFilterTokens: (columnId: string, tokens: Iterable<string>) => void;
|
|
27
29
|
isAllFacetsChecked: (columnId: string) => boolean;
|
|
28
30
|
toggleAllFacets: (columnId: string) => void;
|
|
29
31
|
clearColumnFilter: (columnId: string) => void;
|
package/dist/menus.js
CHANGED
|
@@ -73,6 +73,15 @@ export function createMenus(ctx) {
|
|
|
73
73
|
else
|
|
74
74
|
addFilterToken(columnId, token);
|
|
75
75
|
}
|
|
76
|
+
/**
|
|
77
|
+
* Replace a column's `in` / `notIn` list wholesale. The suggestions dropdown
|
|
78
|
+
* hands back the full token set after a click, which keeps tokens the user
|
|
79
|
+
* typed by hand (or that the search box is currently hiding) intact.
|
|
80
|
+
*/
|
|
81
|
+
function setFilterTokens(columnId, tokens) {
|
|
82
|
+
const list = [...tokens].map((t) => t.trim()).filter(Boolean);
|
|
83
|
+
updateFilterRow(columnId, joinInTokens(list));
|
|
84
|
+
}
|
|
76
85
|
function toggleCheckboxWithKeyboard(event, toggle) {
|
|
77
86
|
if (event.key !== "Enter" && event.key !== " ")
|
|
78
87
|
return;
|
|
@@ -226,6 +235,20 @@ export function createMenus(ctx) {
|
|
|
226
235
|
ctx.valueFilters = { ...ctx.valueFilters, [columnId]: next };
|
|
227
236
|
}
|
|
228
237
|
}
|
|
238
|
+
/**
|
|
239
|
+
* Replace a column's checked-facet set. "Everything checked" is stored as an
|
|
240
|
+
* ABSENT entry (so an unfiltered high-cardinality column never holds a set of
|
|
241
|
+
* every value), which is why the full-size case deletes instead of writing.
|
|
242
|
+
*/
|
|
243
|
+
function setFacetSelection(columnId, next) {
|
|
244
|
+
if (next.size >= ctx.columnMenuFacetValues.length) {
|
|
245
|
+
const copy = { ...ctx.valueFilters };
|
|
246
|
+
delete copy[columnId];
|
|
247
|
+
ctx.valueFilters = copy;
|
|
248
|
+
return;
|
|
249
|
+
}
|
|
250
|
+
ctx.valueFilters = { ...ctx.valueFilters, [columnId]: new Set(next) };
|
|
251
|
+
}
|
|
229
252
|
function isAllFacetsChecked(columnId) {
|
|
230
253
|
const selected = ctx.valueFilters[columnId];
|
|
231
254
|
return !selected || selected.size >= ctx.columnMenuFacetValues.length;
|
|
@@ -464,6 +487,8 @@ export function createMenus(ctx) {
|
|
|
464
487
|
clearGroupingFromMenu,
|
|
465
488
|
isFacetChecked,
|
|
466
489
|
toggleFacetValue,
|
|
490
|
+
setFacetSelection,
|
|
491
|
+
setFilterTokens,
|
|
467
492
|
isAllFacetsChecked,
|
|
468
493
|
toggleAllFacets,
|
|
469
494
|
clearColumnFilter,
|
package/dist/row-resize.js
CHANGED
|
@@ -60,6 +60,41 @@ export function rowResize(node, opts) {
|
|
|
60
60
|
window.removeEventListener('pointerup', onPointerUp);
|
|
61
61
|
document.body.style.cursor = '';
|
|
62
62
|
}
|
|
63
|
+
/**
|
|
64
|
+
* Keyboard resize (#79). The strip is `role="separator"` acting as a
|
|
65
|
+
* splitter, so it has to be reachable by Tab and respond to arrow keys.
|
|
66
|
+
* Up/Down shrink/grow the row, Shift gives a fine 1px step - the same
|
|
67
|
+
* contract the column resize handle uses (Left/Right, Shift = 1px).
|
|
68
|
+
*/
|
|
69
|
+
function onKeyDown(e) {
|
|
70
|
+
if (current.disabled)
|
|
71
|
+
return;
|
|
72
|
+
const t = e.target;
|
|
73
|
+
if (!t?.classList.contains(STRIP_CLASS))
|
|
74
|
+
return;
|
|
75
|
+
const step = e.shiftKey ? 1 : 10;
|
|
76
|
+
let delta = 0;
|
|
77
|
+
if (e.key === 'ArrowUp')
|
|
78
|
+
delta = -step;
|
|
79
|
+
else if (e.key === 'ArrowDown')
|
|
80
|
+
delta = step;
|
|
81
|
+
else
|
|
82
|
+
return;
|
|
83
|
+
const tr = t.closest('tr.sv-grid-row');
|
|
84
|
+
if (!tr)
|
|
85
|
+
return;
|
|
86
|
+
const rowIndex = rowIndexOf(tr);
|
|
87
|
+
if (!Number.isFinite(rowIndex))
|
|
88
|
+
return;
|
|
89
|
+
e.preventDefault();
|
|
90
|
+
e.stopPropagation();
|
|
91
|
+
const min = current.min ?? 20;
|
|
92
|
+
const max = current.max ?? 320;
|
|
93
|
+
const next = Math.round(Math.max(min, Math.min(max, tr.getBoundingClientRect().height + delta)));
|
|
94
|
+
tr.style.height = `${next}px`;
|
|
95
|
+
current.onResizeMove?.(rowIndex, next);
|
|
96
|
+
current.onResize(rowIndex, next);
|
|
97
|
+
}
|
|
63
98
|
function onPointerDown(e) {
|
|
64
99
|
if (current.disabled)
|
|
65
100
|
return;
|
|
@@ -122,6 +157,9 @@ export function rowResize(node, opts) {
|
|
|
122
157
|
strip.setAttribute('role', 'separator');
|
|
123
158
|
strip.setAttribute('aria-orientation', 'horizontal');
|
|
124
159
|
strip.setAttribute('aria-label', 'Resize row');
|
|
160
|
+
// A separator used as an interactive splitter has to be focusable so
|
|
161
|
+
// keyboard-only users can reach it and drive `onKeyDown` (#79).
|
|
162
|
+
strip.tabIndex = 0;
|
|
125
163
|
// Inline the geometry + pointer-events so the strip works even
|
|
126
164
|
// before SvGrid.css is parsed; the hover tint still comes from
|
|
127
165
|
// the stylesheet. We keep the strip fully inside the gutter
|
|
@@ -139,6 +177,7 @@ export function rowResize(node, opts) {
|
|
|
139
177
|
node.querySelectorAll(`.${STRIP_CLASS}`).forEach((el) => el.remove());
|
|
140
178
|
}
|
|
141
179
|
node.addEventListener('pointerdown', onPointerDown, { capture: true });
|
|
180
|
+
node.addEventListener('keydown', onKeyDown, { capture: true });
|
|
142
181
|
const observer = new MutationObserver(() => decorate());
|
|
143
182
|
observer.observe(node, { childList: true, subtree: true });
|
|
144
183
|
decorate();
|
|
@@ -152,6 +191,7 @@ export function rowResize(node, opts) {
|
|
|
152
191
|
},
|
|
153
192
|
destroy() {
|
|
154
193
|
node.removeEventListener('pointerdown', onPointerDown, { capture: true });
|
|
194
|
+
node.removeEventListener('keydown', onKeyDown, { capture: true });
|
|
155
195
|
window.removeEventListener('pointermove', onPointerMove);
|
|
156
196
|
window.removeEventListener('pointerup', onPointerUp);
|
|
157
197
|
observer.disconnect();
|
package/dist/selection.js
CHANGED
|
@@ -30,7 +30,7 @@ export function createSelection(ctx) {
|
|
|
30
30
|
ctx.grid.setActiveCell({
|
|
31
31
|
rowIndex,
|
|
32
32
|
colIndex,
|
|
33
|
-
cellId: getGridCellDomId(
|
|
33
|
+
cellId: getGridCellDomId(ctx.gridDomId, rowIndex, colIndex),
|
|
34
34
|
});
|
|
35
35
|
// Notify consumers (toolbars, ribbons) so they stay synced without
|
|
36
36
|
// having to listen on the DOM. Fired on EVERY active-cell move -
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@svgrid/grid",
|
|
3
|
-
"version": "2.2.
|
|
3
|
+
"version": "2.2.29",
|
|
4
4
|
"description": "Svelte 5-native data grid and data table. Headless-first engine with virtual scrolling for 100k+ rows, Excel-style filtering, inline editing, grouping, tree data, pivot, and server-side data. Drop-in <SvGrid> component. TypeScript, SSR-ready.",
|
|
5
5
|
"author": "jQWidgets <boikom@jqwidgets.com>",
|
|
6
6
|
"license": "MIT",
|