@svgrid/grid 2.6.21 → 2.6.23
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/CHANGELOG.md +62 -0
- package/README.md +22 -0
- package/dist/GridMenus.svelte +201 -18
- package/dist/SvAutoComplete.svelte +4 -0
- package/dist/SvComboBox.svelte +5 -1
- package/dist/SvDateRangeInput.svelte +25 -3
- package/dist/SvDropDownList.svelte +4 -0
- package/dist/SvForm.svelte +8 -8
- package/dist/SvGrid.controller.svelte.d.ts +61 -9
- package/dist/SvGrid.controller.svelte.js +363 -123
- package/dist/SvGrid.css +222 -1
- package/dist/SvGrid.helpers.d.ts +55 -0
- package/dist/SvGrid.helpers.js +83 -0
- package/dist/SvGrid.svelte +255 -56
- package/dist/SvGrid.types.d.ts +146 -1
- package/dist/SvGridCellEditor.svelte +99 -17
- package/dist/SvGridSelect.svelte +6 -2
- package/dist/SvGridSelect.svelte.d.ts +1 -1
- package/dist/SvListBox.svelte +8 -0
- package/dist/SvMultiSelect.svelte +6 -2
- package/dist/SvMultiSelect.svelte.d.ts +1 -1
- package/dist/SvNumberInput.svelte +4 -0
- package/dist/SvRichCell.svelte +125 -0
- package/dist/SvRichCell.svelte.d.ts +15 -0
- package/dist/SvTextArea.svelte +5 -1
- package/dist/SvTreeSelect.svelte +6 -2
- package/dist/SvTreeSelect.svelte.d.ts +1 -1
- package/dist/builtin-editors.d.ts +2 -2
- package/dist/builtin-editors.js +19 -3
- package/dist/cdn/GridMenus-BzWXQjv3.js +600 -0
- package/dist/cdn/GridMenus-xr_rHx8F.js +601 -0
- package/dist/cdn/SvDateRangeInput-CaOuMs8O.js +198 -0
- package/dist/cdn/SvDateRangeInput-DMLKmGEc.js +199 -0
- package/dist/cdn/SvDateTimePicker-CCbDZNZB.js +816 -0
- package/dist/cdn/SvDateTimePicker-sonaH0oh.js +812 -0
- package/dist/cdn/SvGridCellEditor-BAPSC7EL.js +550 -0
- package/dist/cdn/SvGridCellEditor-BwzmUWtL.js +549 -0
- package/dist/cdn/column-resize-DsfNXMom.js +102 -0
- package/dist/cdn/date-format-BNii4zeD.js +1393 -0
- package/dist/cdn/date-format-BnnHlqGw.js +1395 -0
- package/dist/cdn/{editor-registry-CiI0xlKg.js → editor-registry-BhgE3S84.js} +51 -25
- package/dist/cdn/row-resize-BRcimkUT.js +95 -0
- package/dist/cdn/{src-DBel9wRZ.js → src-DGo7IHug.js} +10017 -9488
- package/dist/cdn/{src-BYq-qyrp.js → src-skQN5eqh.js} +7829 -7300
- package/dist/cdn/svgrid.js +14 -10
- package/dist/cdn/svgrid.svelte-external.js +14 -10
- package/dist/cell-values.d.ts +16 -0
- package/dist/cell-values.js +28 -0
- package/dist/clipboard.d.ts +22 -0
- package/dist/clipboard.js +405 -27
- package/dist/column-groups.js +1 -1
- package/dist/column-resize.d.ts +46 -0
- package/dist/column-resize.js +205 -0
- package/dist/columns.d.ts +0 -3
- package/dist/columns.js +0 -57
- package/dist/core.d.ts +19 -4
- package/dist/core.js +460 -119
- package/dist/editing.js +12 -7
- package/dist/editors/cell-editors.d.ts +11 -0
- package/dist/editors/cell-editors.js +18 -1
- package/dist/filtering/excel-filters.js +28 -0
- package/dist/grid-messages.d.ts +15 -0
- package/dist/grid-messages.js +15 -0
- package/dist/group-display.d.ts +1 -1
- package/dist/index.d.ts +6 -1
- package/dist/index.js +10 -0
- package/dist/list-option.d.ts +6 -0
- package/dist/menus.js +41 -8
- package/dist/row-resize.d.ts +11 -0
- package/dist/row-resize.js +7 -1
- package/dist/sanitize-html.d.ts +37 -0
- package/dist/sanitize-html.js +234 -0
- package/dist/selection-bar-view.svelte.d.ts +25 -0
- package/dist/selection-bar-view.svelte.js +13 -0
- package/dist/selection.d.ts +2 -1
- package/dist/selection.js +133 -8
- package/dist/spreadsheet.d.ts +1 -1
- package/dist/spreadsheet.js +1 -1
- package/package.json +1 -1
- package/src/GridMenus.svelte +201 -18
- package/src/SvAutoComplete.svelte +4 -0
- package/src/SvComboBox.svelte +5 -1
- package/src/SvDateRangeInput.svelte +25 -3
- package/src/SvDropDownList.svelte +4 -0
- package/src/SvForm.svelte +8 -8
- package/src/SvGrid.controller.svelte.ts +404 -133
- package/src/SvGrid.css +222 -1
- package/src/SvGrid.helpers.ts +87 -0
- package/src/SvGrid.svelte +255 -56
- package/src/SvGrid.types.ts +153 -1
- package/src/SvGridCellEditor.svelte +99 -17
- package/src/SvGridSelect.svelte +6 -2
- package/src/SvListBox.svelte +8 -0
- package/src/SvMultiSelect.svelte +6 -2
- package/src/SvNumberInput.svelte +4 -0
- package/src/SvRichCell.svelte +125 -0
- package/src/SvTextArea.svelte +5 -1
- package/src/SvTreeSelect.svelte +6 -2
- package/src/builtin-editors.test.ts +18 -1
- package/src/builtin-editors.ts +19 -3
- package/src/cell-values.ts +30 -0
- package/src/clipboard.test.ts +93 -20
- package/src/clipboard.ts +433 -33
- package/src/column-groups.ts +1 -1
- package/src/column-resize.test.ts +381 -0
- package/src/column-resize.ts +227 -0
- package/src/columns.test.ts +0 -103
- package/src/columns.ts +0 -58
- package/src/core.aggregate.test.ts +134 -0
- package/src/core.filter.test.ts +156 -0
- package/src/core.grouping.test.ts +146 -0
- package/src/core.row-shape.test.ts +119 -0
- package/src/core.rowmodel-cache.test.ts +121 -0
- package/src/core.sort.test.ts +293 -0
- package/src/core.ts +516 -119
- package/src/date-string-column.test.ts +87 -0
- package/src/editing.test.ts +25 -0
- package/src/editing.ts +12 -9
- package/src/editor-block-prop.test.ts +132 -0
- package/src/editors/cell-editors.ts +27 -1
- package/src/filtering/excel-filters.ts +30 -0
- package/src/filtering/normalize-fast-path.test.ts +104 -0
- package/src/grid-messages.ts +34 -0
- package/src/group-display.ts +1 -1
- package/src/index.ts +25 -1
- package/src/list-option.ts +6 -0
- package/src/menus.test.ts +43 -0
- package/src/menus.ts +38 -8
- package/src/move-cells.test.ts +850 -0
- package/src/number-editor-literal.test.ts +84 -0
- package/src/number-editor.grid.test.ts +160 -0
- package/src/resize-props.test.ts +361 -0
- package/src/row-resize.test.ts +31 -0
- package/src/row-resize.ts +21 -3
- package/src/sanitize-html.test.ts +182 -0
- package/src/sanitize-html.ts +235 -0
- package/src/selection-bar-view.svelte.ts +36 -0
- package/src/selection.test.ts +157 -0
- package/src/selection.ts +135 -8
- package/src/spreadsheet.ts +1 -1
- package/src/svgrid.filter-menu-listbox.svelte.test.ts +56 -10
- package/src/svgrid.in-suggest-snapshot.svelte.test.ts +91 -0
- package/src/svgrid.menu-scroll-close.test.ts +204 -0
- package/src/svgrid.selection-bar-seam.test.ts +185 -0
- package/src/svgrid.upsell-license.test.ts +117 -0
- package/dist/cdn/GridMenus-BfTAKn84.js +0 -488
- package/dist/cdn/GridMenus-C3bJd7w8.js +0 -489
- package/dist/cdn/SvDateTimePicker-CHnUkWcH.js +0 -2206
- package/dist/cdn/SvDateTimePicker-CRQH_U7E.js +0 -2201
- package/dist/cdn/SvGridCellEditor-BGUCzuPB.js +0 -523
- package/dist/cdn/SvGridCellEditor-G8IMWIws.js +0 -522
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `cellDataType: 'dateString'` columns, which hold an ISO date STRING
|
|
3
|
+
* ('2026-06-27') rather than a Date or a timestamp.
|
|
4
|
+
*
|
|
5
|
+
* Two bugs lived here, both reported from the same demo:
|
|
6
|
+
*
|
|
7
|
+
* 1. Committing an edit ran through `parseEditorValue('date', ...)`, which
|
|
8
|
+
* does `new Date(v).toISOString()` - so '2026-12-25' was stored as
|
|
9
|
+
* '2026-12-25T01:00:00.000Z'. The cell then showed a timestamp next to
|
|
10
|
+
* neighbours showing plain dates. `dateString` resolves to
|
|
11
|
+
* `editorType: 'date'`, so nothing downstream knew the difference.
|
|
12
|
+
*
|
|
13
|
+
* 2. That conversion goes through UTC. A local midnight either side of the
|
|
14
|
+
* meridian lands on the previous or next day, so the stored CALENDAR DATE
|
|
15
|
+
* depended on where the user was sitting.
|
|
16
|
+
*/
|
|
17
|
+
import { describe, expect, it } from 'vitest'
|
|
18
|
+
import { parseEditorValue } from './editors/cell-editors'
|
|
19
|
+
import { toIsoDateLocal, usesRichDateFilter } from './SvGrid.helpers'
|
|
20
|
+
|
|
21
|
+
describe('parseEditorValue with dateOnly', () => {
|
|
22
|
+
it('keeps an ISO date exactly as picked', () => {
|
|
23
|
+
expect(parseEditorValue('date', '2026-12-25', { dateOnly: true })).toBe('2026-12-25')
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
it('trims a datetime down to its calendar date', () => {
|
|
27
|
+
expect(parseEditorValue('date', '2026-12-25T09:30', { dateOnly: true })).toBe('2026-12-25')
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
it('never sends the value through UTC', () => {
|
|
31
|
+
// The bug: a date whose UTC form is the day before. Whatever the runner's
|
|
32
|
+
// zone, the day the user picked has to be the day that is stored.
|
|
33
|
+
for (const raw of ['2026-01-01', '2026-12-31', '2026-06-15']) {
|
|
34
|
+
expect(parseEditorValue('date', raw, { dateOnly: true })).toBe(raw)
|
|
35
|
+
}
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
it('uses local parts when the input is not already ISO', () => {
|
|
39
|
+
const d = new Date(2026, 11, 25, 23, 30) // local 25 Dec, late evening
|
|
40
|
+
const out = parseEditorValue('date', d.toString(), { dateOnly: true })
|
|
41
|
+
expect(out).toBe('2026-12-25')
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
it('returns null for something that is not a date at all', () => {
|
|
45
|
+
expect(parseEditorValue('date', 'not a date', { dateOnly: true })).toBeNull()
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
it('leaves the normal date editor alone - it still stores a timestamp', () => {
|
|
49
|
+
// `cellDataType: 'date'` columns hold Date values and DO want the ISO
|
|
50
|
+
// timestamp. Only dateString opts out.
|
|
51
|
+
const out = parseEditorValue('date', '2026-12-25')
|
|
52
|
+
expect(String(out)).toMatch(/^2026-12-2[45]T/)
|
|
53
|
+
})
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
describe('toIsoDateLocal', () => {
|
|
57
|
+
it('formats from local parts, not UTC', () => {
|
|
58
|
+
expect(toIsoDateLocal(new Date(2026, 0, 1, 0, 30))).toBe('2026-01-01')
|
|
59
|
+
expect(toIsoDateLocal(new Date(2026, 11, 31, 23, 30))).toBe('2026-12-31')
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
it('is empty for no date', () => {
|
|
63
|
+
expect(toIsoDateLocal(null)).toBe('')
|
|
64
|
+
expect(toIsoDateLocal(undefined)).toBe('')
|
|
65
|
+
expect(toIsoDateLocal(new Date('nonsense'))).toBe('')
|
|
66
|
+
})
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
describe('usesRichDateFilter', () => {
|
|
70
|
+
it('is on for the date-ish editors', () => {
|
|
71
|
+
for (const t of ['date', 'datetime', 'time'] as const) {
|
|
72
|
+
expect(usesRichDateFilter(t), t).toBe(true)
|
|
73
|
+
}
|
|
74
|
+
})
|
|
75
|
+
|
|
76
|
+
it('honours the -native opt-out, same as the cell editor', () => {
|
|
77
|
+
for (const t of ['date-native', 'datetime-native', 'time-native'] as const) {
|
|
78
|
+
expect(usesRichDateFilter(t), t).toBe(false)
|
|
79
|
+
}
|
|
80
|
+
})
|
|
81
|
+
|
|
82
|
+
it('is off for everything else', () => {
|
|
83
|
+
for (const t of ['text', 'number', 'list', 'checkbox', undefined] as const) {
|
|
84
|
+
expect(usesRichDateFilter(t as never), String(t)).toBe(false)
|
|
85
|
+
}
|
|
86
|
+
})
|
|
87
|
+
})
|
package/src/editing.test.ts
CHANGED
|
@@ -925,6 +925,31 @@ describe('clipboard fallbacks (non-paste branches only)', () => {
|
|
|
925
925
|
expect(ctx.internalData[1]).toMatchObject({ a: 'Z', b: 'W' })
|
|
926
926
|
})
|
|
927
927
|
|
|
928
|
+
it('pastes into the selected row when getRowId does not return the array index', () => {
|
|
929
|
+
// Regression: the row -> data-slot lookup was `Number(row.id)`, which is
|
|
930
|
+
// the array index only under the DEFAULT getRowId. With 1-based ids
|
|
931
|
+
// (`getRowId={(t) => String(t.id)}`) every paste landed one row BELOW the
|
|
932
|
+
// selection; with non-numeric ids it parsed to NaN and wrote nothing.
|
|
933
|
+
Object.defineProperty(navigator, 'clipboard', {
|
|
934
|
+
value: undefined,
|
|
935
|
+
configurable: true,
|
|
936
|
+
})
|
|
937
|
+
const { ctx, ed } = editingFor({
|
|
938
|
+
columns: [{ id: 'a', field: 'a', editorType: 'text', editable: true }],
|
|
939
|
+
data: [{ a: 'x0' }, { a: 'x1' }, { a: 'x2' }],
|
|
940
|
+
})
|
|
941
|
+
ctx._rows.forEach((r: any, i: number) => { r.id = String(i + 1) })
|
|
942
|
+
ctx.selectionRange = {
|
|
943
|
+
anchor: { rowIndex: 1, colIndex: 0 },
|
|
944
|
+
focus: { rowIndex: 1, colIndex: 0 },
|
|
945
|
+
}
|
|
946
|
+
ed.onGridPaste({
|
|
947
|
+
clipboardData: { getData: () => 'PASTED' },
|
|
948
|
+
preventDefault: vi.fn(),
|
|
949
|
+
} as unknown as ClipboardEvent)
|
|
950
|
+
expect(ctx.internalData.map((d: any) => d.a)).toEqual(['x0', 'PASTED', 'x2'])
|
|
951
|
+
})
|
|
952
|
+
|
|
928
953
|
it('onGridPaste bails while a cell is being edited (native editor paste wins)', () => {
|
|
929
954
|
Object.defineProperty(navigator, 'clipboard', {
|
|
930
955
|
value: undefined, // fallback path (Firefox / insecure) - would otherwise hijack
|
package/src/editing.ts
CHANGED
|
@@ -17,6 +17,7 @@ import {
|
|
|
17
17
|
toValueArray,
|
|
18
18
|
} from "./SvGrid.helpers";
|
|
19
19
|
import {
|
|
20
|
+
createDataIndexLookup,
|
|
20
21
|
getColumnBaseValue,
|
|
21
22
|
isGroupRow,
|
|
22
23
|
} from "./cell-values";
|
|
@@ -156,6 +157,8 @@ export function createEditing<
|
|
|
156
157
|
ctx.editingCell.value,
|
|
157
158
|
{
|
|
158
159
|
multiple: column?.columnDef.editorMultiple === true,
|
|
160
|
+
// A dateString column holds 'YYYY-MM-DD', not a timestamp.
|
|
161
|
+
dateOnly: column?.columnDef.cellDataType === "dateString",
|
|
159
162
|
},
|
|
160
163
|
);
|
|
161
164
|
let oldValue: unknown = undefined;
|
|
@@ -416,18 +419,15 @@ export function createEditing<
|
|
|
416
419
|
: Math.max(...lines.map((l) => l.split("\t").length));
|
|
417
420
|
|
|
418
421
|
const next = ctx.internalData.slice() as Array<TData>;
|
|
422
|
+
const dataIndexOf = createDataIndexLookup(next);
|
|
419
423
|
for (let i = 0; i < rowSpan; i += 1) {
|
|
420
424
|
const targetRowIndex = startRow + i;
|
|
421
425
|
const row = ctx.allRows[targetRowIndex];
|
|
422
426
|
if (!row || isGroupRow(row)) continue;
|
|
423
|
-
// Map the visible row back to its
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
dataIndex < 0 ||
|
|
428
|
-
dataIndex >= next.length
|
|
429
|
-
)
|
|
430
|
-
continue;
|
|
427
|
+
// Map the visible row back to its slot in the data array by identity -
|
|
428
|
+
// `row.id` is only the index under the default getRowId.
|
|
429
|
+
const dataIndex = dataIndexOf(row);
|
|
430
|
+
if (dataIndex < 0) continue;
|
|
431
431
|
const originalRow = next[dataIndex];
|
|
432
432
|
if (!originalRow) continue;
|
|
433
433
|
const sourceCells = fillRange
|
|
@@ -443,7 +443,9 @@ export function createEditing<
|
|
|
443
443
|
const editorType = (column.columnDef.editorType ??
|
|
444
444
|
"text") as CellEditorType;
|
|
445
445
|
const raw = fillRange ? lines[0]! : sourceCells?.[j] ?? "";
|
|
446
|
-
updated[column.columnDef.field] = parseEditorValue(editorType, raw
|
|
446
|
+
updated[column.columnDef.field] = parseEditorValue(editorType, raw, {
|
|
447
|
+
dateOnly: column.columnDef.cellDataType === "dateString",
|
|
448
|
+
});
|
|
447
449
|
}
|
|
448
450
|
next[dataIndex] = updated as TData;
|
|
449
451
|
}
|
|
@@ -530,6 +532,7 @@ export function createEditing<
|
|
|
530
532
|
const editorType = (column.columnDef.editorType ?? "text") as CellEditorType;
|
|
531
533
|
const parsed = parseEditorValue(editorType, fr.draft[column.id], {
|
|
532
534
|
multiple: column.columnDef.editorMultiple === true,
|
|
535
|
+
dateOnly: column.columnDef.cellDataType === "dateString",
|
|
533
536
|
});
|
|
534
537
|
const oldValue = (row.original as Record<string, unknown>)[field];
|
|
535
538
|
const parser = (column.columnDef as { valueParser?: (p: unknown) => unknown }).valueParser;
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `block` is declared once, in `SvEditorProps`, and its doc comment states the
|
|
3
|
+
* contract: "in a form grid a row of inputs each stopping at a different width
|
|
4
|
+
* reads as broken."
|
|
5
|
+
*
|
|
6
|
+
* Nine editors accepted the prop by type and did nothing with it. `<SvForm>`
|
|
7
|
+
* mounts five of them, so the Edit-fields drawer came out with a full-width
|
|
8
|
+
* text box above a 200px select above a 240px date picker - the exact thing the
|
|
9
|
+
* doc comment warns about, shipped.
|
|
10
|
+
*
|
|
11
|
+
* A type that promises something no code honours is worse than no type: it
|
|
12
|
+
* reads as done. This walks the source instead of trusting the declaration, so
|
|
13
|
+
* the next editor added to the kit either honours `block` or is listed here as
|
|
14
|
+
* a deliberate exemption.
|
|
15
|
+
*/
|
|
16
|
+
import { describe, expect, it } from 'vitest'
|
|
17
|
+
import { readFileSync, readdirSync } from 'node:fs'
|
|
18
|
+
import { join } from 'node:path'
|
|
19
|
+
|
|
20
|
+
const SRC = join(import.meta.dirname, '.')
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Controls whose width is their content, where stretching to 100% would be
|
|
24
|
+
* wrong rather than merely unusual: a checkbox is a 16px box with a label
|
|
25
|
+
* beside it, a rating is five stars, a switch is a switch. Widening them puts
|
|
26
|
+
* the control at one end of an empty row.
|
|
27
|
+
*/
|
|
28
|
+
const INTRINSIC = new Set([
|
|
29
|
+
'SvCheckBox',
|
|
30
|
+
'SvSwitchButton',
|
|
31
|
+
'SvToggleButton',
|
|
32
|
+
'SvButtonGroup',
|
|
33
|
+
'SvRadioGroup',
|
|
34
|
+
'SvRating',
|
|
35
|
+
'SvOtpInput',
|
|
36
|
+
'SvFileUpload',
|
|
37
|
+
'SvSlider',
|
|
38
|
+
'SvColorInput',
|
|
39
|
+
])
|
|
40
|
+
|
|
41
|
+
function editorsDeclaringBlock(): string[] {
|
|
42
|
+
return readdirSync(SRC)
|
|
43
|
+
.filter((f) => f.startsWith('Sv') && f.endsWith('.svelte'))
|
|
44
|
+
.filter((f) => {
|
|
45
|
+
const src = readFileSync(join(SRC, f), 'utf8')
|
|
46
|
+
if (!src.includes('SvEditorProps')) return false
|
|
47
|
+
// A `Pick<SvEditorProps, ...>` that leaves 'block' out has opted out at
|
|
48
|
+
// the type level, which is a legitimate (and checkable) choice.
|
|
49
|
+
const pick = /Pick<\s*SvEditorProps,\s*([^>]+)>/.exec(src)
|
|
50
|
+
if (pick) return (pick[1] ?? '').includes("'block'")
|
|
51
|
+
return true
|
|
52
|
+
})
|
|
53
|
+
.map((f) => f.replace('.svelte', ''))
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
describe('the block prop is honoured wherever it is declared', () => {
|
|
57
|
+
const declaring = editorsDeclaringBlock().filter((n) => !INTRINSIC.has(n))
|
|
58
|
+
|
|
59
|
+
it('finds the editor kit (guards against the walker silently matching nothing)', () => {
|
|
60
|
+
expect(declaring.length).toBeGreaterThan(10)
|
|
61
|
+
expect(declaring).toContain('SvDropDownList')
|
|
62
|
+
expect(declaring).toContain('SvDateTimePicker')
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
it.each(declaring)('%s reads the prop and applies it to a width', (name) => {
|
|
66
|
+
const src = readFileSync(join(SRC, `${name}.svelte`), 'utf8')
|
|
67
|
+
|
|
68
|
+
// Declared but never destructured is the failure mode that shipped: the
|
|
69
|
+
// component compiles, the consumer passes `block`, nothing happens.
|
|
70
|
+
expect(src, `${name} never destructures block`).toMatch(/\bblock = false\b|\bblock,/)
|
|
71
|
+
|
|
72
|
+
// And destructured but never used is the same bug one step later.
|
|
73
|
+
const applied =
|
|
74
|
+
/class:is-block=\{block\}/.test(src) ||
|
|
75
|
+
/class:sv-[a-z]+--block=\{block\}/.test(src) ||
|
|
76
|
+
/\{block\}/.test(src)
|
|
77
|
+
expect(applied, `${name} destructures block but never applies it`).toBe(true)
|
|
78
|
+
})
|
|
79
|
+
})
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Every `<SvXxx ... />` in a Svelte source, with its props.
|
|
83
|
+
*
|
|
84
|
+
* Not a regex: props routinely contain `>`, in arrow functions like
|
|
85
|
+
* `onChange={(m) => o.onChange(m)}`. A `[^>]*?` walker stops at the first one
|
|
86
|
+
* and silently skips the tag - which here quietly excused SvComboBox,
|
|
87
|
+
* SvMaskedInput and SvPhoneInput from the check, three of the controls this
|
|
88
|
+
* test exists to cover. So brace depth is tracked instead.
|
|
89
|
+
*/
|
|
90
|
+
function selfClosingTags(src: string): Array<{ name: string; props: string }> {
|
|
91
|
+
const out: Array<{ name: string; props: string }> = []
|
|
92
|
+
const open = /<(Sv[A-Za-z]+)(?=[\s/>])/g
|
|
93
|
+
let m: RegExpExecArray | null
|
|
94
|
+
while ((m = open.exec(src))) {
|
|
95
|
+
let i = m.index + m[0].length
|
|
96
|
+
let depth = 0
|
|
97
|
+
for (; i < src.length; i++) {
|
|
98
|
+
const c = src[i]
|
|
99
|
+
if (c === '{') depth++
|
|
100
|
+
else if (c === '}') depth--
|
|
101
|
+
else if (depth === 0 && c === '/' && src[i + 1] === '>') {
|
|
102
|
+
out.push({ name: m[1]!, props: src.slice(m.index + m[0].length, i) })
|
|
103
|
+
break
|
|
104
|
+
} else if (depth === 0 && c === '>') break // an open tag, not self-closing
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
return out
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
describe('<SvForm> passes block to every control it mounts', () => {
|
|
111
|
+
// The components above can honour block perfectly and the form still look
|
|
112
|
+
// ragged, because the form is what decides to pass it. That is precisely how
|
|
113
|
+
// this bug survived: SvNumberInput, SvPasswordInput, SvMaskedInput and
|
|
114
|
+
// SvPhoneInput were passed `block` and the rest were not.
|
|
115
|
+
const form = readFileSync(join(SRC, 'SvForm.svelte'), 'utf8')
|
|
116
|
+
const honours = new Set(editorsDeclaringBlock())
|
|
117
|
+
const controls = selfClosingTags(form).filter(
|
|
118
|
+
(c) => honours.has(c.name) && !INTRINSIC.has(c.name),
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
it('finds the control snippet', () => {
|
|
122
|
+
// Every control that CAN fill its row must be seen by this walker. The
|
|
123
|
+
// three `=>`-carrying tags above are the reason this number is asserted.
|
|
124
|
+
expect(controls.length).toBeGreaterThanOrEqual(11)
|
|
125
|
+
for (const expected of ['SvDropDownList', 'SvComboBox', 'SvMaskedInput', 'SvPhoneInput'])
|
|
126
|
+
expect(controls.map((c) => c.name)).toContain(expected)
|
|
127
|
+
})
|
|
128
|
+
|
|
129
|
+
it.each(controls.map((c) => [c.name, c.props]))('%s is mounted block', (name, props) => {
|
|
130
|
+
expect(props, `<${name}> in SvForm is missing block`).toMatch(/(^|\s)block(\s|$|=)/)
|
|
131
|
+
})
|
|
132
|
+
})
|
|
@@ -35,6 +35,17 @@ export type CellEditorOption = { value: string | number; label: string; color?:
|
|
|
35
35
|
export type ParseEditorValueOptions = {
|
|
36
36
|
/** When true, list/chips return an array; otherwise a scalar. */
|
|
37
37
|
multiple?: boolean
|
|
38
|
+
/**
|
|
39
|
+
* Keep a date as the plain `YYYY-MM-DD` string the user picked, instead of a
|
|
40
|
+
* full ISO timestamp. Set for `cellDataType: 'dateString'` columns, which
|
|
41
|
+
* exist precisely to hold ISO date strings.
|
|
42
|
+
*
|
|
43
|
+
* It is not only about shape. `new Date('2026-12-25').toISOString()` sends
|
|
44
|
+
* the value through UTC, so a local midnight becomes the previous or next
|
|
45
|
+
* day either side of the meridian - the stored DATE changes depending on
|
|
46
|
+
* where the user is sitting. Slicing the picked date avoids that entirely.
|
|
47
|
+
*/
|
|
48
|
+
dateOnly?: boolean
|
|
38
49
|
}
|
|
39
50
|
|
|
40
51
|
/** Normalize the loose `editorOptions` ColumnDef prop into a uniform shape.
|
|
@@ -83,7 +94,22 @@ export function parseEditorValue(
|
|
|
83
94
|
return s || '#000000'
|
|
84
95
|
}
|
|
85
96
|
if (type === 'date') {
|
|
86
|
-
const
|
|
97
|
+
const raw = String(value ?? '')
|
|
98
|
+
if (opts?.dateOnly) {
|
|
99
|
+
// Take the calendar date the editor produced as-is. It already arrives
|
|
100
|
+
// as `YYYY-MM-DD` (or that prefix on a datetime), so there is nothing to
|
|
101
|
+
// convert - and converting is exactly what shifts the day across a
|
|
102
|
+
// timezone boundary.
|
|
103
|
+
const match = /^(\d{4}-\d{2}-\d{2})/.exec(raw.trim())
|
|
104
|
+
if (match) return match[1]
|
|
105
|
+
const parsed = new Date(raw)
|
|
106
|
+
if (Number.isNaN(parsed.getTime())) return null
|
|
107
|
+
// A non-ISO input (a locale string) still has to become a date string.
|
|
108
|
+
// Use the LOCAL parts, so the day is the one the user saw.
|
|
109
|
+
const pad = (n: number) => String(n).padStart(2, '0')
|
|
110
|
+
return `${parsed.getFullYear()}-${pad(parsed.getMonth() + 1)}-${pad(parsed.getDate())}`
|
|
111
|
+
}
|
|
112
|
+
const date = new Date(raw)
|
|
87
113
|
return Number.isNaN(date.getTime()) ? null : date.toISOString()
|
|
88
114
|
}
|
|
89
115
|
if (type === 'datetime') {
|
|
@@ -49,12 +49,42 @@ export function normalizeForFilter(
|
|
|
49
49
|
locale?: string | ReadonlyArray<string>,
|
|
50
50
|
): string {
|
|
51
51
|
if (!s) return ''
|
|
52
|
+
|
|
53
|
+
// Fast path for pure-ASCII input, which is the overwhelming majority of what
|
|
54
|
+
// a grid filters. Both of the expensive steps are provably no-ops there:
|
|
55
|
+
// ASCII is already NFD-normalised, and the combining-marks block this strips
|
|
56
|
+
// (U+0300..U+036F) contains no ASCII. So lowercasing alone is the same
|
|
57
|
+
// answer, and `normalize-fast-path.test.ts` checks that exhaustively over
|
|
58
|
+
// every ASCII code point rather than taking the argument on trust.
|
|
59
|
+
//
|
|
60
|
+
// Worth the branch: this runs once per cell per filter - 100,000 times for a
|
|
61
|
+
// one-column filter over a 100k grid - and a CPU profile of a browser filter
|
|
62
|
+
// put more time in this function than in any other.
|
|
63
|
+
//
|
|
64
|
+
// Note the locale still routes through toLocaleLowerCase even on the fast
|
|
65
|
+
// path: 'I'.toLocaleLowerCase('tr') is 'ı', so ASCII does NOT imply the
|
|
66
|
+
// locale can be ignored.
|
|
67
|
+
if (isAsciiOnly(s)) {
|
|
68
|
+
return locale ? s.toLocaleLowerCase(locale as string | string[]) : s.toLowerCase()
|
|
69
|
+
}
|
|
70
|
+
|
|
52
71
|
const stripped = s.normalize('NFD').replace(DIACRITIC_RE, '')
|
|
53
72
|
return locale
|
|
54
73
|
? stripped.toLocaleLowerCase(locale as string | string[])
|
|
55
74
|
: stripped.toLowerCase()
|
|
56
75
|
}
|
|
57
76
|
|
|
77
|
+
/**
|
|
78
|
+
* True when every code unit is ASCII. A plain char-code scan, which is far
|
|
79
|
+
* cheaper than the `normalize()` + regex pass it lets us skip.
|
|
80
|
+
*/
|
|
81
|
+
function isAsciiOnly(s: string): boolean {
|
|
82
|
+
for (let i = 0; i < s.length; i++) {
|
|
83
|
+
if (s.charCodeAt(i) > 127) return false
|
|
84
|
+
}
|
|
85
|
+
return true
|
|
86
|
+
}
|
|
87
|
+
|
|
58
88
|
/**
|
|
59
89
|
* Delimiter used to serialise the `in` / `notIn` value list into the single
|
|
60
90
|
* `value` string that the filter model carries.
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Equivalence tests for `normalizeForFilter`'s ASCII fast path.
|
|
3
|
+
*
|
|
4
|
+
* The function NFD-decomposes, strips combining marks, then lowercases. It runs
|
|
5
|
+
* once per cell per filter - 100,000 times for a one-column filter on a 100k
|
|
6
|
+
* grid - and a CPU profile of a browser filter put 15.5% of the whole operation
|
|
7
|
+
* inside it, more than any other single function.
|
|
8
|
+
*
|
|
9
|
+
* For a pure-ASCII string the first two steps are provably no-ops: ASCII is
|
|
10
|
+
* already in NFD, and the combining-marks block (U+0300..U+036F) contains no
|
|
11
|
+
* ASCII. So the fast path returns `lowercase(s)` directly.
|
|
12
|
+
*
|
|
13
|
+
* "Provably" is doing a lot of work in that sentence, so the first test checks
|
|
14
|
+
* it exhaustively over every ASCII code point rather than trusting the argument.
|
|
15
|
+
*/
|
|
16
|
+
import { describe, expect, it } from 'vitest'
|
|
17
|
+
import { normalizeForFilter } from './excel-filters'
|
|
18
|
+
|
|
19
|
+
/** The original implementation, verbatim, as the oracle. */
|
|
20
|
+
const DIACRITIC_RE = /[̀-ͯ]/g
|
|
21
|
+
function reference(s: string, locale?: string | ReadonlyArray<string>): string {
|
|
22
|
+
if (!s) return ''
|
|
23
|
+
const stripped = s.normalize('NFD').replace(DIACRITIC_RE, '')
|
|
24
|
+
return locale
|
|
25
|
+
? stripped.toLocaleLowerCase(locale as string | string[])
|
|
26
|
+
: stripped.toLowerCase()
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
describe('normalizeForFilter - ASCII fast path equivalence', () => {
|
|
30
|
+
it('matches on every single ASCII code point', () => {
|
|
31
|
+
for (let code = 0; code < 128; code++) {
|
|
32
|
+
const ch = String.fromCharCode(code)
|
|
33
|
+
expect(normalizeForFilter(ch), `code point ${code}`).toBe(reference(ch))
|
|
34
|
+
}
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
it('matches on every ASCII code point under a Turkish locale', () => {
|
|
38
|
+
// Turkish is the locale that makes ASCII lowercasing locale-dependent:
|
|
39
|
+
// 'I'.toLocaleLowerCase('tr') is 'ı', not 'i'. The fast path must still
|
|
40
|
+
// route through toLocaleLowerCase rather than assuming ASCII is safe.
|
|
41
|
+
for (let code = 0; code < 128; code++) {
|
|
42
|
+
const ch = String.fromCharCode(code)
|
|
43
|
+
expect(normalizeForFilter(ch, 'tr'), `code point ${code}`).toBe(reference(ch, 'tr'))
|
|
44
|
+
}
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
const CASES: Array<[string, string]> = [
|
|
48
|
+
['empty', ''],
|
|
49
|
+
['plain ascii', 'Hello World'],
|
|
50
|
+
['ascii with digits and punctuation', 'ABC-123_x.y,z!'],
|
|
51
|
+
['precomposed accents', 'café CAFÉ Café'],
|
|
52
|
+
['decomposed accents', 'café'],
|
|
53
|
+
['mixed ascii and accents', 'naive naïve NAÏVE'],
|
|
54
|
+
['german sharp s', 'straße STRASSE'],
|
|
55
|
+
['greek', 'ΣΊΣΥΦΟΣ σίσυφος'],
|
|
56
|
+
['cyrillic', 'Привет МИР'],
|
|
57
|
+
['cjk', '日本語テキスト'],
|
|
58
|
+
['emoji', 'grid 🚀 test'],
|
|
59
|
+
['combining marks alone', '̀́̂'],
|
|
60
|
+
['ascii then combining mark', 'áb'],
|
|
61
|
+
['turkish dotted capital', 'İstanbul ISTANBUL'],
|
|
62
|
+
['ligature', 'file flow'],
|
|
63
|
+
['fullwidth', 'ABC'],
|
|
64
|
+
['nbsp and control chars', 'a b\tc\nd'],
|
|
65
|
+
['long ascii', 'x'.repeat(500)],
|
|
66
|
+
['long with one non-ascii at the end', 'x'.repeat(500) + 'é'],
|
|
67
|
+
]
|
|
68
|
+
|
|
69
|
+
for (const [name, input] of CASES) {
|
|
70
|
+
it(`matches: ${name}`, () => {
|
|
71
|
+
expect(normalizeForFilter(input)).toBe(reference(input))
|
|
72
|
+
})
|
|
73
|
+
it(`matches with locale: ${name}`, () => {
|
|
74
|
+
expect(normalizeForFilter(input, 'tr')).toBe(reference(input, 'tr'))
|
|
75
|
+
expect(normalizeForFilter(input, 'de')).toBe(reference(input, 'de'))
|
|
76
|
+
})
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
it('matches across randomised mixed-script strings', () => {
|
|
80
|
+
let seed = 7
|
|
81
|
+
const rand = () => {
|
|
82
|
+
seed = (seed * 1103515245 + 12345) & 0x7fffffff
|
|
83
|
+
return seed / 0x7fffffff
|
|
84
|
+
}
|
|
85
|
+
const pools = [
|
|
86
|
+
'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789 -_.',
|
|
87
|
+
'áéíóúàèìòùâêîôûäëïöüñçÁÉÍÓÚÑÇ',
|
|
88
|
+
'̀́̂̃̈',
|
|
89
|
+
'ΑΒΓΔαβγδ',
|
|
90
|
+
'абвгдАБВГД',
|
|
91
|
+
'日本語中文한국어',
|
|
92
|
+
]
|
|
93
|
+
for (let trial = 0; trial < 300; trial++) {
|
|
94
|
+
let s = ''
|
|
95
|
+
const len = 1 + Math.floor(rand() * 20)
|
|
96
|
+
for (let i = 0; i < len; i++) {
|
|
97
|
+
const pool = pools[Math.floor(rand() * pools.length)]!
|
|
98
|
+
s += pool[Math.floor(rand() * pool.length)]
|
|
99
|
+
}
|
|
100
|
+
expect(normalizeForFilter(s), s).toBe(reference(s))
|
|
101
|
+
expect(normalizeForFilter(s, 'tr'), s).toBe(reference(s, 'tr'))
|
|
102
|
+
}
|
|
103
|
+
})
|
|
104
|
+
})
|
package/src/grid-messages.ts
CHANGED
|
@@ -78,10 +78,27 @@ export type GridMessages = {
|
|
|
78
78
|
menuRemoveRow: string
|
|
79
79
|
menuRemoveColumn: string
|
|
80
80
|
menuEditComment: string
|
|
81
|
+
// Selection bar
|
|
82
|
+
selectionBarLabel: string
|
|
83
|
+
selectionBarCount: string
|
|
84
|
+
selectionBarClear: string
|
|
85
|
+
selectionBarMore: string
|
|
86
|
+
selectionBarSelectAll: string
|
|
87
|
+
selectionBarEditFields: string
|
|
88
|
+
// Bulk edit dialog
|
|
89
|
+
bulkEditTitle: string
|
|
90
|
+
bulkEditLead: string
|
|
91
|
+
bulkEditMixed: string
|
|
92
|
+
bulkEditApply: string
|
|
93
|
+
bulkEditCancel: string
|
|
94
|
+
bulkEditNoFields: string
|
|
81
95
|
chartRange: string
|
|
82
96
|
// Upsell notes (enterprise views not installed)
|
|
83
97
|
pivotUpsellTitle: string
|
|
84
98
|
pivotUpsellBody: string
|
|
99
|
+
/** Shared licensing line under every Enterprise upsell note. */
|
|
100
|
+
upsellLicense: string
|
|
101
|
+
upsellLicenseLink: string
|
|
85
102
|
}
|
|
86
103
|
|
|
87
104
|
/** English defaults - the literal strings the grid shipped before localization. */
|
|
@@ -124,6 +141,20 @@ export const defaultGridMessages: GridMessages = {
|
|
|
124
141
|
menuRemoveRow: 'Remove row',
|
|
125
142
|
menuRemoveColumn: 'Remove column',
|
|
126
143
|
menuEditComment: 'Edit comment',
|
|
144
|
+
selectionBarLabel: 'Selection actions',
|
|
145
|
+
/** Label beside the count CHIP, which carries the number itself. */
|
|
146
|
+
selectionBarCount: 'selected',
|
|
147
|
+
selectionBarClear: 'Clear selection',
|
|
148
|
+
selectionBarMore: 'More actions',
|
|
149
|
+
selectionBarSelectAll: 'Select all',
|
|
150
|
+
selectionBarEditFields: 'Edit fields',
|
|
151
|
+
bulkEditTitle: 'Edit fields',
|
|
152
|
+
bulkEditLead:
|
|
153
|
+
'Changes apply to all {count} selected items. Fields you leave alone keep their existing values.',
|
|
154
|
+
bulkEditMixed: 'Multiple values',
|
|
155
|
+
bulkEditApply: 'Apply to {count} items',
|
|
156
|
+
bulkEditCancel: 'Cancel',
|
|
157
|
+
bulkEditNoFields: 'No editable fields on this grid.',
|
|
127
158
|
opContains: 'Contains',
|
|
128
159
|
opNotContains: 'Not contains',
|
|
129
160
|
opEquals: 'Equals',
|
|
@@ -143,6 +174,9 @@ export const defaultGridMessages: GridMessages = {
|
|
|
143
174
|
chartRange: 'Chart selected range',
|
|
144
175
|
pivotUpsellTitle: 'Pivot mode',
|
|
145
176
|
pivotUpsellBody: 'Pivot mode is an Enterprise feature. Install @svgrid/enterprise and call enablePivot() to use it.',
|
|
177
|
+
upsellLicense:
|
|
178
|
+
'A license key is required for production use. Without one the feature still works, but the grid renders an unlicensed watermark.',
|
|
179
|
+
upsellLicenseLink: 'View licensing',
|
|
146
180
|
}
|
|
147
181
|
|
|
148
182
|
/**
|
package/src/group-display.ts
CHANGED
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
* they are trivially unit-testable and carry no dependency on the grid runtime.
|
|
15
15
|
*/
|
|
16
16
|
|
|
17
|
-
/**
|
|
17
|
+
/** Grouping display variants. */
|
|
18
18
|
export type GroupDisplayType = 'groupRows' | 'singleColumn' | 'multipleColumns'
|
|
19
19
|
|
|
20
20
|
export type GroupFooterOptions<R> = {
|
package/src/index.ts
CHANGED
|
@@ -80,6 +80,15 @@ export { default as SvGrid } from './SvGrid.svelte'
|
|
|
80
80
|
// through this seam (see board-view.svelte.ts). Mirrors the scheduler-view seam.
|
|
81
81
|
export type { BoardConfig, BoardLane, BoardCardMoveEvent, BoardCardCommitEvent, BoardLayout, BoardSubtask, BoardComment, BoardMenuContext, BoardLaneMenuContext, BoardDrawerConfig } from './SvGrid.types'
|
|
82
82
|
export { registerBoardView, getBoardView, hasBoardView } from './board-view.svelte'
|
|
83
|
+
// Bulk-action bar over the row selection (`selectionBar`), and the right-click
|
|
84
|
+
// menu items - both are prop-shaped config a consumer has to be able to name
|
|
85
|
+
// when they lift the array into its own module.
|
|
86
|
+
export type {
|
|
87
|
+
SelectionBarAction,
|
|
88
|
+
SelectionBarTarget,
|
|
89
|
+
ContextMenuItem,
|
|
90
|
+
ContextMenuTarget,
|
|
91
|
+
} from './SvGrid.types'
|
|
83
92
|
// Built-in AI helpers (free): natural-language filter, smart-fill, summarize,
|
|
84
93
|
// classify, anomaly detection, chart-this and export planning. They ship no
|
|
85
94
|
// model client and no-op until you register one with `setAIProvider(...)`, so
|
|
@@ -111,6 +120,8 @@ export type {
|
|
|
111
120
|
SchedulerOccurrenceChangeEvent,
|
|
112
121
|
} from './SvGrid.types'
|
|
113
122
|
export { registerSchedulerView, getSchedulerView, hasSchedulerView } from './scheduler-view.svelte'
|
|
123
|
+
// Bulk-action bar over the row selection. Prop + types free, renderer Pro.
|
|
124
|
+
export { registerSelectionBarView, getSelectionBarView, hasSelectionBarView } from './selection-bar-view.svelte'
|
|
114
125
|
// Chart view. The `chart` prop + its config live in the free grid AND so does the
|
|
115
126
|
// renderer (SvGridChartView wraps the free SvChart) - so unlike board/scheduler
|
|
116
127
|
// it works out of the box. This seam only lets a host OVERRIDE the built-in view.
|
|
@@ -254,6 +265,7 @@ export {
|
|
|
254
265
|
type CellEditorRegistration,
|
|
255
266
|
} from './editor-registry'
|
|
256
267
|
export { registerBuiltinEditors } from './builtin-editors'
|
|
268
|
+
export { sanitizeHtml, renderMarkdown, htmlToText } from './sanitize-html'
|
|
257
269
|
// UI kit - shared field chrome (label + hint + error + direction wrapper)
|
|
258
270
|
export { default as SvField } from './SvField.svelte'
|
|
259
271
|
// UI kit - headless cores (state + prop-getters you render yourself, like createSvGrid)
|
|
@@ -441,6 +453,7 @@ export { default as SvAvatarGroup } from './SvAvatarGroup.svelte'
|
|
|
441
453
|
export { default as SvCommand, type CommandItem } from './SvCommand.svelte'
|
|
442
454
|
export { fuzzyScore, fuzzyMatch } from './fuzzy'
|
|
443
455
|
export { default as SvRichText, type RichTextTool } from './SvRichText.svelte'
|
|
456
|
+
export { default as SvRichCell } from './SvRichCell.svelte'
|
|
444
457
|
export { default as SvCarousel } from './SvCarousel.svelte'
|
|
445
458
|
export { default as SvTour, type TourStep } from './SvTour.svelte'
|
|
446
459
|
export { default as SvPagination } from './SvPagination.svelte'
|
|
@@ -588,7 +601,13 @@ export {
|
|
|
588
601
|
type CellBorderSpec,
|
|
589
602
|
type BorderSpec,
|
|
590
603
|
} from './spreadsheet'
|
|
604
|
+
// The two resize actions, exported as a matched pair. `<SvGrid>` drives both
|
|
605
|
+
// through the `rowResize` / `columnResize` props - both default OFF, both are
|
|
606
|
+
// turned on by setting them true, and the grid `import()`s the module itself so
|
|
607
|
+
// a grid that leaves them off carries neither. Import them directly only for a
|
|
608
|
+
// headless table, which renders its own rows and headers.
|
|
591
609
|
export { rowResize, type RowResizeOptions } from './row-resize'
|
|
610
|
+
export { columnResize, type ColumnResizeOptions } from './column-resize'
|
|
592
611
|
export { rowDropZone, type RowDragEndEvent, type RowDropZoneOptions } from './row-drag'
|
|
593
612
|
export { resolveColumnTypes, inferCellDataType, type CellDataType } from './column-types'
|
|
594
613
|
export {
|
|
@@ -690,7 +709,12 @@ export { createVirtualizer } from './virtualization/virtualizer'
|
|
|
690
709
|
export { createSvelteVirtualizer } from './virtualization/svelte-virtualizer.svelte'
|
|
691
710
|
export { createColumnVirtualizer } from './virtualization/column-virtualizer'
|
|
692
711
|
export type { VirtualItem, VirtualizerOptions, VirtualizerState } from './virtualization/types'
|
|
693
|
-
export type {
|
|
712
|
+
export type {
|
|
713
|
+
SvGridApi,
|
|
714
|
+
SvGridFilterOperator,
|
|
715
|
+
SvGridViewState,
|
|
716
|
+
SvGridWrapperProps,
|
|
717
|
+
} from './svgrid-wrapper.types'
|
|
694
718
|
export {
|
|
695
719
|
parseEditorValue,
|
|
696
720
|
normalizeEditorOptions,
|
package/src/list-option.ts
CHANGED
|
@@ -7,6 +7,12 @@ export type ListOption = {
|
|
|
7
7
|
group?: string
|
|
8
8
|
/** Optional color swatch (any CSS color) shown before the label. */
|
|
9
9
|
color?: string
|
|
10
|
+
/**
|
|
11
|
+
* Optional muted text pinned to the trailing edge of the row - a count, a
|
|
12
|
+
* shortcut, a unit. Not part of the label, so it does not affect matching
|
|
13
|
+
* or the accessible name.
|
|
14
|
+
*/
|
|
15
|
+
hint?: string
|
|
10
16
|
}
|
|
11
17
|
|
|
12
18
|
/**
|