@svgrid/grid 2.2.16 → 2.2.17
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/GridFooter.svelte +12 -11
- package/dist/SvAnchor.svelte +65 -0
- package/dist/SvAnchor.svelte.d.ts +24 -0
- package/dist/SvAspectRatio.svelte +38 -0
- package/dist/SvAspectRatio.svelte.d.ts +16 -0
- package/dist/SvAutoComplete.svelte +52 -7
- package/dist/SvAutoComplete.svelte.d.ts +4 -1
- package/dist/SvBlockquote.svelte +43 -0
- package/dist/SvBlockquote.svelte.d.ts +16 -0
- package/dist/SvCalendar.svelte +33 -2
- package/dist/SvCalendar.svelte.d.ts +8 -1
- package/dist/SvCheckBox.svelte +6 -2
- package/dist/SvCode.svelte +52 -0
- package/dist/SvCode.svelte.d.ts +19 -0
- package/dist/SvCollapsible.svelte +75 -0
- package/dist/SvCollapsible.svelte.d.ts +27 -0
- package/dist/SvColorInput.svelte +11 -17
- package/dist/SvColorInput.svelte.d.ts +2 -0
- package/dist/SvComboBox.svelte +85 -8
- package/dist/SvComboBox.svelte.d.ts +14 -0
- package/dist/SvCountryInput.svelte +2 -2
- package/dist/SvCountryInput.svelte.d.ts +1 -1
- package/dist/SvDropDownList.svelte +154 -51
- package/dist/SvDropDownList.svelte.d.ts +17 -3
- package/dist/SvDurationInput.svelte +59 -41
- package/dist/SvDurationInput.svelte.d.ts +18 -0
- package/dist/SvField.svelte +203 -12
- package/dist/SvField.svelte.d.ts +45 -8
- package/dist/SvForm.svelte +195 -21
- package/dist/SvForm.svelte.d.ts +23 -0
- package/dist/SvGrid.controller.svelte.d.ts +12 -1
- package/dist/SvGrid.controller.svelte.js +87 -7
- package/dist/SvGrid.svelte +383 -181
- package/dist/SvGrid.types.d.ts +123 -2
- package/dist/SvGridChartView.svelte +70 -0
- package/dist/SvGridChartView.svelte.d.ts +11 -0
- package/dist/SvGridDropdown.svelte +9 -2
- package/dist/SvGridSelect.svelte +5 -2
- package/dist/SvGridSelect.svelte.d.ts +1 -1
- package/dist/SvGroup.svelte +41 -0
- package/dist/SvGroup.svelte.d.ts +27 -0
- package/dist/SvHoverCard.svelte +70 -0
- package/dist/SvHoverCard.svelte.d.ts +41 -0
- package/dist/SvKbd.svelte +51 -0
- package/dist/SvKbd.svelte.d.ts +18 -0
- package/dist/SvList.svelte +55 -0
- package/dist/SvList.svelte.d.ts +23 -0
- package/dist/SvListBox.svelte +166 -77
- package/dist/SvListBox.svelte.d.ts +17 -4
- package/dist/SvLoadingOverlay.svelte +53 -0
- package/dist/SvLoadingOverlay.svelte.d.ts +28 -0
- package/dist/SvMark.svelte +36 -0
- package/dist/SvMark.svelte.d.ts +14 -0
- package/dist/SvMaskedInput.svelte +50 -35
- package/dist/SvMaskedInput.svelte.d.ts +17 -2
- package/dist/SvMenuList.svelte +32 -3
- package/dist/SvMenubar.svelte +200 -0
- package/dist/SvMenubar.svelte.d.ts +18 -0
- package/dist/SvMultiSelect.svelte +7 -4
- package/dist/SvMultiSelect.svelte.d.ts +1 -1
- package/dist/SvNumberInput.svelte +79 -42
- package/dist/SvNumberInput.svelte.d.ts +25 -0
- package/dist/SvOtpInput.svelte +2 -2
- package/dist/SvPasswordInput.svelte +51 -31
- package/dist/SvPasswordInput.svelte.d.ts +17 -0
- package/dist/SvPhoneInput.svelte +39 -28
- package/dist/SvPhoneInput.svelte.d.ts +4 -0
- package/dist/SvPopconfirm.svelte +79 -0
- package/dist/SvPopconfirm.svelte.d.ts +42 -0
- package/dist/SvPopover.svelte +87 -29
- package/dist/SvPopover.svelte.d.ts +18 -4
- package/dist/SvRadioGroup.svelte +5 -1
- package/dist/SvResult.svelte +73 -0
- package/dist/SvResult.svelte.d.ts +28 -0
- package/dist/SvSegmented.svelte +114 -0
- package/dist/SvSegmented.svelte.d.ts +29 -0
- package/dist/SvSimpleGrid.svelte +41 -0
- package/dist/SvSimpleGrid.svelte.d.ts +25 -0
- package/dist/SvSlider.svelte +23 -10
- package/dist/SvSpinner.svelte +43 -0
- package/dist/SvSpinner.svelte.d.ts +20 -0
- package/dist/SvStack.svelte +37 -0
- package/dist/SvStack.svelte.d.ts +24 -0
- package/dist/SvSwitchButton.svelte +6 -1
- package/dist/SvTagsInput.svelte +9 -5
- package/dist/SvText.svelte +76 -0
- package/dist/SvText.svelte.d.ts +31 -0
- package/dist/SvTextInput.svelte +75 -43
- package/dist/SvTextInput.svelte.d.ts +28 -1
- package/dist/SvTimePicker.svelte +32 -2
- package/dist/SvTimePicker.svelte.d.ts +8 -9
- package/dist/SvTitle.svelte +57 -0
- package/dist/SvTitle.svelte.d.ts +24 -0
- package/dist/SvToaster.svelte +41 -6
- package/dist/SvToggleButton.svelte +4 -1
- package/dist/SvTooltip.svelte +68 -34
- package/dist/SvTooltip.svelte.d.ts +14 -2
- package/dist/SvTree.svelte +81 -21
- package/dist/SvTree.svelte.d.ts +3 -2
- package/dist/SvTreeSelect.svelte +5 -2
- package/dist/SvTreeSelect.svelte.d.ts +1 -1
- package/dist/SvVisuallyHidden.svelte +49 -0
- package/dist/SvVisuallyHidden.svelte.d.ts +19 -0
- package/dist/ai.d.ts +289 -0
- package/dist/ai.js +1013 -0
- package/dist/board-view.svelte.d.ts +22 -0
- package/dist/board-view.svelte.js +13 -0
- package/dist/build-api.js +36 -6
- package/dist/cdn/GridMenus-0AK9O7qG.js +420 -0
- package/dist/cdn/GridMenus-BjUoCXoD.js +416 -0
- package/dist/cdn/SvDateTimePicker-CYC7gRER.js +2176 -0
- package/dist/cdn/SvDateTimePicker-DtQKd4Ns.js +2198 -0
- package/dist/cdn/SvGridChart-BcLCYvw9.js +1195 -0
- package/dist/cdn/SvGridChart-BhnFdLd0.js +1203 -0
- package/dist/cdn/SvGridChartPanel-Cmx9DHAS.js +561 -0
- package/dist/cdn/SvGridChartPanel-DvjwxLvK.js +577 -0
- package/dist/cdn/SvGridChartView-B40DTJSX.js +54 -0
- package/dist/cdn/SvGridChartView-PwqI4s02.js +53 -0
- package/dist/cdn/SvGridDropdown-DjALc_4f.js +255 -0
- package/dist/cdn/SvGridDropdown-hpnHvOGt.js +254 -0
- package/dist/cdn/chart-T1usDN0o.js +1212 -0
- package/dist/cdn/disclose-version-DoD9AFD_.js +2958 -0
- package/dist/cdn/export-format-JT5pWVT8.js +561 -0
- package/dist/cdn/popover-P-9P2YYn.js +69 -0
- package/dist/cdn/rolldown-runtime-Dy4uBu1J.js +11 -0
- package/dist/cdn/src-BLhdLmoN.js +25707 -0
- package/dist/cdn/src-BwnUVj3T.js +25623 -0
- package/dist/cdn/svgrid.js +9 -32897
- package/dist/cdn/svgrid.svelte-external.js +9 -30052
- package/dist/cell-formatting.d.ts +2 -2
- package/dist/cell-render.js +4 -4
- package/dist/chart-view.svelte.d.ts +25 -0
- package/dist/chart-view.svelte.js +14 -0
- package/dist/createCombobox.svelte.d.ts +5 -0
- package/dist/createCombobox.svelte.js +21 -5
- package/dist/createDropdownList.svelte.d.ts +3 -0
- package/dist/createDropdownList.svelte.js +8 -2
- package/dist/createForm.svelte.d.ts +18 -0
- package/dist/createForm.svelte.js +51 -6
- package/dist/createMenu.svelte.js +27 -0
- package/dist/createPopoverSelect.svelte.d.ts +3 -0
- package/dist/createPopoverSelect.svelte.js +9 -1
- package/dist/createSlider.svelte.d.ts +8 -21
- package/dist/createSlider.svelte.js +34 -0
- package/dist/createTooltip.svelte.d.ts +12 -0
- package/dist/createTooltip.svelte.js +72 -9
- package/dist/editor-contract.d.ts +15 -0
- package/dist/export-provider.d.ts +27 -0
- package/dist/export-provider.js +9 -0
- package/dist/filter-operators.d.ts +6 -0
- package/dist/filter-operators.js +33 -0
- package/dist/form-field.d.ts +45 -2
- package/dist/grid-messages.d.ts +61 -0
- package/dist/grid-messages.js +72 -0
- package/dist/group-display.d.ts +62 -0
- package/dist/group-display.js +78 -0
- package/dist/index.d.ts +36 -4
- package/dist/index.js +43 -2
- package/dist/js-scroller.svelte.d.ts +35 -0
- package/dist/js-scroller.svelte.js +154 -0
- package/dist/list-option.d.ts +46 -0
- package/dist/list-option.js +53 -0
- package/dist/menus.js +25 -11
- package/dist/pivot-view.svelte.d.ts +72 -0
- package/dist/pivot-view.svelte.js +13 -0
- package/dist/positioning.d.ts +111 -0
- package/dist/positioning.js +191 -0
- package/dist/scheduler-model.d.ts +3 -0
- package/dist/scheduler-model.js +22 -5
- package/dist/svgrid-wrapper.types.d.ts +18 -0
- package/dist/toast-store.svelte.d.ts +54 -0
- package/dist/toast-store.svelte.js +65 -15
- package/package.json +9 -1
- package/src/GridFooter.svelte +12 -11
- package/src/SvAnchor.svelte +65 -0
- package/src/SvAspectRatio.svelte +38 -0
- package/src/SvAutoComplete.svelte +52 -7
- package/src/SvBlockquote.svelte +43 -0
- package/src/SvCalendar.svelte +33 -2
- package/src/SvCalendar.test.ts +15 -0
- package/src/SvCheckBox.svelte +6 -2
- package/src/SvCode.svelte +52 -0
- package/src/SvCollapsible.svelte +75 -0
- package/src/SvColorInput.svelte +11 -17
- package/src/SvComboBox.svelte +85 -8
- package/src/SvCountryInput.svelte +2 -2
- package/src/SvDropDownList.svelte +154 -51
- package/src/SvDurationInput.svelte +59 -41
- package/src/SvField.svelte +203 -12
- package/src/SvForm.svelte +195 -21
- package/src/SvForm.test.ts +112 -0
- package/src/SvGrid.controller.svelte.ts +88 -7
- package/src/SvGrid.svelte +383 -181
- package/src/SvGrid.types.ts +125 -1
- package/src/SvGridChartView.svelte +70 -0
- package/src/SvGridDropdown.svelte +9 -2
- package/src/SvGridSelect.svelte +5 -2
- package/src/SvGroup.svelte +41 -0
- package/src/SvHoverCard.svelte +70 -0
- package/src/SvKbd.svelte +51 -0
- package/src/SvList.svelte +55 -0
- package/src/SvListBox.svelte +166 -77
- package/src/SvLoadingOverlay.svelte +53 -0
- package/src/SvMark.svelte +36 -0
- package/src/SvMaskedInput.svelte +50 -35
- package/src/SvMenuList.svelte +32 -3
- package/src/SvMenubar.svelte +200 -0
- package/src/SvMultiSelect.svelte +7 -4
- package/src/SvNumberInput.svelte +79 -42
- package/src/SvOtpInput.svelte +2 -2
- package/src/SvPasswordInput.svelte +51 -31
- package/src/SvPhoneInput.svelte +39 -28
- package/src/SvPopconfirm.svelte +79 -0
- package/src/SvPopover.svelte +87 -29
- package/src/SvRadioGroup.svelte +5 -1
- package/src/SvResult.svelte +73 -0
- package/src/SvSegmented.svelte +114 -0
- package/src/SvSimpleGrid.svelte +41 -0
- package/src/SvSlider.svelte +23 -10
- package/src/SvSpinner.svelte +43 -0
- package/src/SvStack.svelte +37 -0
- package/src/SvSwitchButton.svelte +6 -1
- package/src/SvTagsInput.svelte +9 -5
- package/src/SvText.svelte +76 -0
- package/src/SvTextInput.svelte +75 -43
- package/src/SvTimePicker.svelte +32 -2
- package/src/SvTitle.svelte +57 -0
- package/src/SvToaster.svelte +41 -6
- package/src/SvToaster.test.ts +32 -0
- package/src/SvToggleButton.svelte +4 -1
- package/src/SvTooltip.svelte +68 -34
- package/src/SvTree.svelte +81 -21
- package/src/SvTreeSelect.svelte +5 -2
- package/src/SvVisuallyHidden.svelte +49 -0
- package/src/ai.test.ts +502 -0
- package/src/ai.ts +1391 -0
- package/src/board-view.svelte.ts +33 -0
- package/src/build-api.coverage.test.ts +101 -0
- package/src/build-api.ts +43 -18
- package/src/builtin-editors.grid.test.ts +1 -1
- package/src/cell-formatting.ts +5 -5
- package/src/cell-render.ts +4 -4
- package/src/chart-view.svelte.ts +36 -0
- package/src/createCombobox.svelte.ts +17 -5
- package/src/createDropdownList.svelte.ts +8 -2
- package/src/createForm.svelte.ts +59 -6
- package/src/createMenu.svelte.ts +23 -0
- package/src/createPopoverSelect.svelte.ts +8 -1
- package/src/createSlider.svelte.ts +20 -0
- package/src/createTooltip.svelte.ts +67 -9
- package/src/editor-contract.ts +16 -0
- package/src/export-provider.ts +40 -0
- package/src/filter-operators.ts +37 -0
- package/src/form-field.ts +42 -2
- package/src/grid-messages.test.ts +24 -0
- package/src/grid-messages.ts +137 -0
- package/src/group-display.test.ts +77 -0
- package/src/group-display.ts +117 -0
- package/src/i18n.grid.test.ts +73 -0
- package/src/index.ts +91 -2
- package/src/js-scroller.svelte.ts +173 -0
- package/src/list-option.test.ts +56 -0
- package/src/list-option.ts +81 -0
- package/src/menus.test.ts +32 -0
- package/src/menus.ts +25 -12
- package/src/pivot-view.svelte.ts +90 -0
- package/src/pivot-view.test.ts +38 -0
- package/src/pivot.grid.test.ts +102 -0
- package/src/positioning.test.ts +141 -0
- package/src/positioning.ts +266 -0
- package/src/scheduler-model.test.ts +16 -0
- package/src/scheduler-model.ts +24 -5
- package/src/svgrid-wrapper.types.ts +23 -0
- package/src/svgrid.charting.test.ts +8 -1
- package/src/svgrid.comments-autocomplete.test.ts +7 -2
- package/src/svgrid.context-menu.test.ts +8 -3
- package/src/toast-store.svelte.ts +98 -0
- package/src/toast-store.test.ts +62 -1
- package/src/ui-breadth.test.ts +138 -0
- package/src/ui-buttons.test.ts +39 -0
- package/src/ui-inputs.test.ts +2 -2
- package/src/ui-range.test.ts +9 -0
- package/src/ui-selection.test.ts +36 -0
- package/src/ui-tier1.test.ts +1 -1
- package/src/virtual.test.ts +20 -6
- package/dist/SvGridBoard.svelte +0 -2339
- package/dist/SvGridBoard.svelte.d.ts +0 -31
- package/src/SvGridBoard.svelte +0 -2339
- package/src/board.test.ts +0 -927
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Row-grouping display helpers. Two pure, framework-free transforms:
|
|
3
|
+
*
|
|
4
|
+
* - `insertGroupFooters` inserts a footer/total row after each group's children
|
|
5
|
+
* (and optionally a grand-total row at the end), carrying the group's already
|
|
6
|
+
* computed aggregate values. Mirrors the server model's footer shape
|
|
7
|
+
* (`server-group-model.ts`) client-side.
|
|
8
|
+
* - `buildAutoGroupColumns` decides, for a grouping display mode, which synthetic
|
|
9
|
+
* "auto group" columns to show and which grouped source columns to hide - the
|
|
10
|
+
* groundwork for `singleColumn` / `multipleColumns` display types (the current
|
|
11
|
+
* default `groupRows` renders a full-width banner and needs neither).
|
|
12
|
+
*
|
|
13
|
+
* Both are generic over the row / column shape (via small accessor callbacks) so
|
|
14
|
+
* they are trivially unit-testable and carry no dependency on the grid runtime.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
/** AG-Grid-style grouping display variants. */
|
|
18
|
+
export type GroupDisplayType = 'groupRows' | 'singleColumn' | 'multipleColumns'
|
|
19
|
+
|
|
20
|
+
export type GroupFooterOptions<R> = {
|
|
21
|
+
/** Depth of a row (group rows and their descendants). */
|
|
22
|
+
getDepth: (row: R) => number
|
|
23
|
+
/** True when a row is a group (banner) row. */
|
|
24
|
+
isGroup: (row: R) => boolean
|
|
25
|
+
/** Build a footer row from the group row it closes (carries its aggregates). */
|
|
26
|
+
makeFooter: (groupRow: R) => R
|
|
27
|
+
/** Insert a footer after each group's children. */
|
|
28
|
+
includeGroupFooter?: boolean
|
|
29
|
+
/** Append a grand-total footer at the very end. */
|
|
30
|
+
includeGrandTotalFooter?: boolean
|
|
31
|
+
/** Build the grand-total footer (over the whole set). Return null to skip. */
|
|
32
|
+
makeGrandTotal?: () => R | null
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Insert group footer rows after each group's descendants. A group opened at
|
|
37
|
+
* depth d closes when a later row appears at depth <= d (a sibling group or an
|
|
38
|
+
* ancestor-level row); its footer is emitted at that point. Order is otherwise
|
|
39
|
+
* preserved. No-op when neither footer flag is set.
|
|
40
|
+
*/
|
|
41
|
+
export function insertGroupFooters<R>(rows: ReadonlyArray<R>, opts: GroupFooterOptions<R>): R[] {
|
|
42
|
+
const { getDepth, isGroup, makeFooter, includeGroupFooter, includeGrandTotalFooter, makeGrandTotal } = opts
|
|
43
|
+
if (!includeGroupFooter && !includeGrandTotalFooter) return [...rows]
|
|
44
|
+
|
|
45
|
+
const out: R[] = []
|
|
46
|
+
const openGroups: R[] = []
|
|
47
|
+
|
|
48
|
+
const closeTo = (depth: number) => {
|
|
49
|
+
while (openGroups.length > 0) {
|
|
50
|
+
const top = openGroups[openGroups.length - 1]!
|
|
51
|
+
if (getDepth(top) < depth) break
|
|
52
|
+
openGroups.pop()
|
|
53
|
+
if (includeGroupFooter) out.push(makeFooter(top))
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
for (const row of rows) {
|
|
58
|
+
// Before adding this row, close any groups it ends (same-or-shallower depth).
|
|
59
|
+
closeTo(getDepth(row))
|
|
60
|
+
out.push(row)
|
|
61
|
+
if (isGroup(row)) openGroups.push(row)
|
|
62
|
+
}
|
|
63
|
+
// Close everything still open at the end.
|
|
64
|
+
closeTo(Number.NEGATIVE_INFINITY)
|
|
65
|
+
|
|
66
|
+
if (includeGrandTotalFooter && makeGrandTotal) {
|
|
67
|
+
const gt = makeGrandTotal()
|
|
68
|
+
if (gt != null) out.push(gt)
|
|
69
|
+
}
|
|
70
|
+
return out
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export type AutoGroupColumnSpec = {
|
|
74
|
+
/** Synthetic column id (e.g. `__autoGroup` or `__group_<field>`). */
|
|
75
|
+
id: string
|
|
76
|
+
/** The grouped field this auto column stands in for (null for the single
|
|
77
|
+
* combined auto-group column). */
|
|
78
|
+
field: string | null
|
|
79
|
+
/** Nesting level this column represents (0-based), for indentation. */
|
|
80
|
+
level: number
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export type AutoGroupResult = {
|
|
84
|
+
/** Synthetic auto-group column specs to prepend to the visible columns. */
|
|
85
|
+
autoColumns: AutoGroupColumnSpec[]
|
|
86
|
+
/** Ids of the grouped SOURCE columns to hide (rolled into the auto columns). */
|
|
87
|
+
hiddenSourceIds: Set<string>
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Decide the synthetic auto-group columns for a grouping display mode:
|
|
92
|
+
* - `groupRows`: none (the banner path handles display) - `{ [], {} }`.
|
|
93
|
+
* - `singleColumn`: one combined auto-group column; every grouped source column
|
|
94
|
+
* is hidden.
|
|
95
|
+
* - `multipleColumns`: one auto column per grouped field; the grouped source
|
|
96
|
+
* columns are hidden.
|
|
97
|
+
*/
|
|
98
|
+
export function buildAutoGroupColumns(
|
|
99
|
+
grouping: ReadonlyArray<string>,
|
|
100
|
+
displayType: GroupDisplayType,
|
|
101
|
+
): AutoGroupResult {
|
|
102
|
+
if (displayType === 'groupRows' || grouping.length === 0) {
|
|
103
|
+
return { autoColumns: [], hiddenSourceIds: new Set() }
|
|
104
|
+
}
|
|
105
|
+
const hiddenSourceIds = new Set(grouping)
|
|
106
|
+
if (displayType === 'singleColumn') {
|
|
107
|
+
return {
|
|
108
|
+
autoColumns: [{ id: '__autoGroup', field: null, level: 0 }],
|
|
109
|
+
hiddenSourceIds,
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
// multipleColumns
|
|
113
|
+
return {
|
|
114
|
+
autoColumns: grouping.map((field, level) => ({ id: `__group_${field}`, field, level })),
|
|
115
|
+
hiddenSourceIds,
|
|
116
|
+
}
|
|
117
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DOM: `localeText` translates the grid's chrome strings (empty state, pager,
|
|
3
|
+
* tool panel) while leaving everything else English.
|
|
4
|
+
*/
|
|
5
|
+
import { afterEach, describe, expect, it } from 'vitest'
|
|
6
|
+
import { mount, unmount } from 'svelte'
|
|
7
|
+
import SvGrid from './SvGrid.svelte'
|
|
8
|
+
import {
|
|
9
|
+
createCoreRowModel,
|
|
10
|
+
createFilteredRowModel,
|
|
11
|
+
createPaginatedRowModel,
|
|
12
|
+
createSortedRowModel,
|
|
13
|
+
sortFns,
|
|
14
|
+
tableFeatures,
|
|
15
|
+
rowSortingFeature,
|
|
16
|
+
} from './index'
|
|
17
|
+
import type { ColumnDef } from './index'
|
|
18
|
+
|
|
19
|
+
type Row = { id: number; name: string }
|
|
20
|
+
const features = tableFeatures({ rowSortingFeature })
|
|
21
|
+
const cols: ColumnDef<typeof features, Row>[] = [{ field: 'name', header: 'Name', width: 160 }]
|
|
22
|
+
|
|
23
|
+
const tick = () => new Promise<void>((r) => queueMicrotask(r))
|
|
24
|
+
|
|
25
|
+
function mountGrid(props: Record<string, unknown>) {
|
|
26
|
+
const target = document.createElement('div')
|
|
27
|
+
document.body.appendChild(target)
|
|
28
|
+
const app = mount(SvGrid, {
|
|
29
|
+
target,
|
|
30
|
+
props: {
|
|
31
|
+
columns: cols,
|
|
32
|
+
features,
|
|
33
|
+
_rowModels: {
|
|
34
|
+
coreRowModel: createCoreRowModel(),
|
|
35
|
+
filteredRowModel: createFilteredRowModel(),
|
|
36
|
+
sortedRowModel: createSortedRowModel(sortFns),
|
|
37
|
+
paginatedRowModel: createPaginatedRowModel(),
|
|
38
|
+
},
|
|
39
|
+
containerHeight: 240,
|
|
40
|
+
virtualization: false,
|
|
41
|
+
...props,
|
|
42
|
+
} as never,
|
|
43
|
+
})
|
|
44
|
+
return { target, destroy: () => { unmount(app); target.remove() } }
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
let cleanup: (() => void) | null = null
|
|
48
|
+
afterEach(() => { cleanup?.(); cleanup = null })
|
|
49
|
+
|
|
50
|
+
describe('localeText', () => {
|
|
51
|
+
it('translates the empty-state message', async () => {
|
|
52
|
+
const { target, destroy } = mountGrid({ data: [], localization: { text: { noRows: 'Aucune ligne a afficher' } } })
|
|
53
|
+
cleanup = destroy
|
|
54
|
+
await tick()
|
|
55
|
+
expect(target.textContent).toContain('Aucune ligne a afficher')
|
|
56
|
+
expect(target.textContent).not.toContain('No rows to display')
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
it('translates the pager while leaving unset strings English', async () => {
|
|
60
|
+
const data: Row[] = Array.from({ length: 12 }, (_, i) => ({ id: i, name: `n${i}` }))
|
|
61
|
+
const { target, destroy } = mountGrid({
|
|
62
|
+
data,
|
|
63
|
+
showPagination: true,
|
|
64
|
+
pageSize: 5,
|
|
65
|
+
localization: { text: { pageSize: 'Taille de page :', page: 'Page FR' } },
|
|
66
|
+
})
|
|
67
|
+
cleanup = destroy
|
|
68
|
+
await tick()
|
|
69
|
+
await tick()
|
|
70
|
+
expect(target.textContent).toContain('Taille de page :')
|
|
71
|
+
expect(target.textContent).toContain('Page FR')
|
|
72
|
+
})
|
|
73
|
+
})
|
package/src/index.ts
CHANGED
|
@@ -69,8 +69,24 @@ export { createGrid, createSvGrid, type SvelteGrid } from './createGrid.svelte'
|
|
|
69
69
|
export { createGridState, createSvGridState } from './createGridState.svelte'
|
|
70
70
|
export { subscribeGrid, subscribeSvGrid } from './subscribe'
|
|
71
71
|
export { default as SvGrid } from './SvGrid.svelte'
|
|
72
|
-
|
|
72
|
+
// Kanban board view. The `board` prop + its config types live in the free grid,
|
|
73
|
+
// but the RENDERER (SvGridBoard) ships in @svgrid/enterprise and registers itself
|
|
74
|
+
// through this seam (see board-view.svelte.ts). Mirrors the scheduler-view seam.
|
|
73
75
|
export type { BoardConfig, BoardLane, BoardCardMoveEvent, BoardCardCommitEvent, BoardLayout, BoardSubtask, BoardComment, BoardMenuContext, BoardLaneMenuContext, BoardDrawerConfig } from './SvGrid.types'
|
|
76
|
+
export { registerBoardView, getBoardView, hasBoardView } from './board-view.svelte'
|
|
77
|
+
// Built-in AI helpers (free): natural-language filter, smart-fill, summarize,
|
|
78
|
+
// classify, anomaly detection, chart-this and export planning. They ship no
|
|
79
|
+
// model client and no-op until you register one with `setAIProvider(...)`, so
|
|
80
|
+
// they tree-shake out of the base bundle unless you actually import them. The
|
|
81
|
+
// export-provider seam lets @svgrid/enterprise register its Excel/PDF engine so
|
|
82
|
+
// AI-planned exports can WRITE those formats.
|
|
83
|
+
export * from './ai'
|
|
84
|
+
export {
|
|
85
|
+
registerExportProvider,
|
|
86
|
+
getExportProvider,
|
|
87
|
+
type GridExportProvider,
|
|
88
|
+
type GridExportRunOptions,
|
|
89
|
+
} from './export-provider'
|
|
74
90
|
// Scheduler / calendar view. The `scheduler` prop + its config types live in
|
|
75
91
|
// the free grid, but the RENDERER ships in @svgrid/enterprise and registers
|
|
76
92
|
// itself through this seam (see scheduler-view.svelte.ts). Mirrors editor-registry.
|
|
@@ -89,6 +105,20 @@ export type {
|
|
|
89
105
|
SchedulerOccurrenceChangeEvent,
|
|
90
106
|
} from './SvGrid.types'
|
|
91
107
|
export { registerSchedulerView, getSchedulerView, hasSchedulerView } from './scheduler-view.svelte'
|
|
108
|
+
// Chart view. The `chart` prop + its config live in the free grid AND so does the
|
|
109
|
+
// renderer (SvGridChartView wraps the free SvChart) - so unlike board/scheduler
|
|
110
|
+
// it works out of the box. This seam only lets a host OVERRIDE the built-in view.
|
|
111
|
+
export type { ChartViewConfig } from './SvGrid.types'
|
|
112
|
+
export { registerChartView, getChartView, hasChartView } from './chart-view.svelte'
|
|
113
|
+
export {
|
|
114
|
+
registerPivotEngine,
|
|
115
|
+
getPivotEngine,
|
|
116
|
+
hasPivotEngine,
|
|
117
|
+
type PivotEngine,
|
|
118
|
+
type GridPivotConfig,
|
|
119
|
+
type GridPivotValue,
|
|
120
|
+
type GridPivotRow,
|
|
121
|
+
} from './pivot-view.svelte'
|
|
92
122
|
// Pure scheduler layout/model helpers (framework-free, like recurrence/date-core).
|
|
93
123
|
// The calendar RENDERER that consumes these ships in @svgrid/enterprise.
|
|
94
124
|
export {
|
|
@@ -147,6 +177,7 @@ export {
|
|
|
147
177
|
type SvEditorProps,
|
|
148
178
|
type EditorSize,
|
|
149
179
|
type EditorDir,
|
|
180
|
+
type EditorAction,
|
|
150
181
|
type EditorAriaState,
|
|
151
182
|
} from './editor-contract'
|
|
152
183
|
// UI kit - editor interaction surface (commit/cancel/close for grid-cell use)
|
|
@@ -183,7 +214,26 @@ export { createCarousel, type Carousel, type CarouselConfig } from './createCaro
|
|
|
183
214
|
// createOverlay for the focus-trap/scroll-lock/dismissal lifecycle.
|
|
184
215
|
export { createCommand, type Command, type CommandConfig } from './createCommand.svelte'
|
|
185
216
|
// Schema-driven form core (SvForm): values/errors/touched + validation + submit.
|
|
186
|
-
export { createForm, type Form, type FormConfig } from './createForm.svelte'
|
|
217
|
+
export { createForm, type Form, type FormConfig, type FormMessages } from './createForm.svelte'
|
|
218
|
+
export { default as SvSegmented, type SegmentedOption } from './SvSegmented.svelte'
|
|
219
|
+
export { default as SvSpinner } from './SvSpinner.svelte'
|
|
220
|
+
export { default as SvLoadingOverlay } from './SvLoadingOverlay.svelte'
|
|
221
|
+
export { default as SvResult } from './SvResult.svelte'
|
|
222
|
+
export { default as SvCollapsible } from './SvCollapsible.svelte'
|
|
223
|
+
export { default as SvStack } from './SvStack.svelte'
|
|
224
|
+
export { default as SvGroup } from './SvGroup.svelte'
|
|
225
|
+
export { default as SvSimpleGrid } from './SvSimpleGrid.svelte'
|
|
226
|
+
// UI kit - typography + display primitives (Pillar C2)
|
|
227
|
+
export { default as SvText } from './SvText.svelte'
|
|
228
|
+
export { default as SvTitle } from './SvTitle.svelte'
|
|
229
|
+
export { default as SvAnchor } from './SvAnchor.svelte'
|
|
230
|
+
export { default as SvBlockquote } from './SvBlockquote.svelte'
|
|
231
|
+
export { default as SvMark } from './SvMark.svelte'
|
|
232
|
+
export { default as SvList } from './SvList.svelte'
|
|
233
|
+
export { default as SvKbd } from './SvKbd.svelte'
|
|
234
|
+
export { default as SvCode } from './SvCode.svelte'
|
|
235
|
+
export { default as SvVisuallyHidden } from './SvVisuallyHidden.svelte'
|
|
236
|
+
export { default as SvAspectRatio } from './SvAspectRatio.svelte'
|
|
187
237
|
// UI kit - custom cell-editor registry (register any component as a grid editor)
|
|
188
238
|
export {
|
|
189
239
|
registerCellEditor,
|
|
@@ -395,6 +445,7 @@ export { default as SvContextMenu } from './SvContextMenu.svelte'
|
|
|
395
445
|
export {
|
|
396
446
|
toast,
|
|
397
447
|
dismissToast,
|
|
448
|
+
updateToast,
|
|
398
449
|
clearToasts,
|
|
399
450
|
pauseToast,
|
|
400
451
|
resumeToast,
|
|
@@ -403,9 +454,15 @@ export {
|
|
|
403
454
|
type ToastVariant,
|
|
404
455
|
type ToastOptions,
|
|
405
456
|
type ToastFn,
|
|
457
|
+
type ToastAction,
|
|
458
|
+
type UpdateToast,
|
|
459
|
+
type PromiseMessages,
|
|
406
460
|
} from './toast-store.svelte'
|
|
407
461
|
export { default as SvMenu, type MenuItem } from './SvMenu.svelte'
|
|
408
462
|
export { default as SvMenuList } from './SvMenuList.svelte'
|
|
463
|
+
export { default as SvMenubar, type MenubarMenu } from './SvMenubar.svelte'
|
|
464
|
+
export { default as SvHoverCard } from './SvHoverCard.svelte'
|
|
465
|
+
export { default as SvPopconfirm } from './SvPopconfirm.svelte'
|
|
409
466
|
export { default as SvNavPane, type NavItem, type NavSection, type NavModule } from './SvNavPane.svelte'
|
|
410
467
|
export { default as SvProgress } from './SvProgress.svelte'
|
|
411
468
|
export { default as SvCircularProgress } from './SvCircularProgress.svelte'
|
|
@@ -413,6 +470,18 @@ export { popIn } from './popover'
|
|
|
413
470
|
export { default as SvForm, type FormField, type FormFieldType, type FormSection, type FormEntry } from './SvForm.svelte'
|
|
414
471
|
export { rules, runRules, isEmptyValue, type Validator, type RuleOptions, type CompareOp } from './validators'
|
|
415
472
|
export { anchoredRect, portalToBody, PANEL_THEME_VARS, type AnchoredRect } from './popover'
|
|
473
|
+
export {
|
|
474
|
+
computePosition,
|
|
475
|
+
autoUpdate,
|
|
476
|
+
parsePlacement,
|
|
477
|
+
type Placement,
|
|
478
|
+
type Side,
|
|
479
|
+
type Align,
|
|
480
|
+
type Rect,
|
|
481
|
+
type Viewport,
|
|
482
|
+
type ComputePositionOptions,
|
|
483
|
+
type ComputePositionResult,
|
|
484
|
+
} from './positioning'
|
|
416
485
|
export {
|
|
417
486
|
toDate, startOfDay, startOfWeek, startOfMonth, addDays, addMonths, addYears, clampDate, isSameDay,
|
|
418
487
|
monthMatrix, weekdayOrder, isoWeek, decadeRange, withTime, snapMinute, type DateLike,
|
|
@@ -434,7 +503,14 @@ export { default as FlexRender } from './FlexRender.svelte'
|
|
|
434
503
|
export { renderComponent, renderSnippet } from './render-component'
|
|
435
504
|
export { default as SvGridChart } from './SvGridChart.svelte'
|
|
436
505
|
export { default as SvGridChartPanel } from './SvGridChartPanel.svelte'
|
|
506
|
+
// Preferred names for the standalone chart primitives. `SvChart` is a genuine
|
|
507
|
+
// standalone component (spec-driven, no grid needed), so it sits alongside its
|
|
508
|
+
// unprefixed siblings SvGauge / SvSparkline. The `SvGrid*` names remain exported
|
|
509
|
+
// as aliases so existing imports keep working.
|
|
510
|
+
export { default as SvChart } from './SvGridChart.svelte'
|
|
511
|
+
export { default as SvChartPanel } from './SvGridChartPanel.svelte'
|
|
437
512
|
export type { ChartingConfig, ChartAggregateRequest, ChartAggregateBucket } from './SvGrid.types'
|
|
513
|
+
export type { GridLocalization } from './SvGrid.types'
|
|
438
514
|
export {
|
|
439
515
|
buildChart,
|
|
440
516
|
formatChartValue,
|
|
@@ -585,6 +661,19 @@ export {
|
|
|
585
661
|
type RuleFormat,
|
|
586
662
|
type ResolvedCellFormat,
|
|
587
663
|
} from './conditional-formatting'
|
|
664
|
+
export {
|
|
665
|
+
defaultGridMessages,
|
|
666
|
+
resolveGridMessages,
|
|
667
|
+
type GridMessages,
|
|
668
|
+
} from './grid-messages'
|
|
669
|
+
export {
|
|
670
|
+
insertGroupFooters,
|
|
671
|
+
buildAutoGroupColumns,
|
|
672
|
+
type GroupDisplayType,
|
|
673
|
+
type GroupFooterOptions,
|
|
674
|
+
type AutoGroupColumnSpec,
|
|
675
|
+
type AutoGroupResult,
|
|
676
|
+
} from './group-display'
|
|
588
677
|
export { getKeyboardIntent, getNextActiveCell, type GridKeyboardIntent } from './keyboard'
|
|
589
678
|
export { createVirtualizer } from './virtualization/virtualizer'
|
|
590
679
|
export { createSvelteVirtualizer } from './virtualization/svelte-virtualizer.svelte'
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* createJsScroller - a JS-driven vertical scroller for the virtualized selection
|
|
3
|
+
* controls (SvListBox / SvDropDownList / SvTree).
|
|
4
|
+
*
|
|
5
|
+
* Native overflow scroll is composited off the main thread, so on a fast
|
|
6
|
+
* scrollbar-thumb drag the browser paints the new (empty) scroll position a
|
|
7
|
+
* frame before JS can re-window the virtualizer - the "blank flash while
|
|
8
|
+
* scrolling". This helper removes that async gap the way Smart's list does: the
|
|
9
|
+
* content does NOT use native scroll. A single `scrollOffset` state positions
|
|
10
|
+
* the rows (`translateY`) AND drives the virtualizer window in the same reactive
|
|
11
|
+
* flush, and a custom scrollbar + wheel/touch handlers set that offset directly.
|
|
12
|
+
*
|
|
13
|
+
* The component owns the markup (translated rows + the `sb`/`thumb` elements);
|
|
14
|
+
* this owns the offset, the geometry, and the input handlers.
|
|
15
|
+
*/
|
|
16
|
+
import { untrack } from 'svelte'
|
|
17
|
+
|
|
18
|
+
type ScrollerVirtualizer = {
|
|
19
|
+
setScrollOffset: (offset: number) => void
|
|
20
|
+
getOffsetForIndex: (index: number) => number
|
|
21
|
+
getSizeForIndex: (index: number) => number
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export type JsScrollerConfig = {
|
|
25
|
+
/** The shared virtualizer to drive. */
|
|
26
|
+
virtualizer: ScrollerVirtualizer
|
|
27
|
+
/** Full scrollable content height (px). */
|
|
28
|
+
totalSize: () => number
|
|
29
|
+
/** Visible viewport height (px). */
|
|
30
|
+
viewport: () => number
|
|
31
|
+
/** Row height used to convert line-mode wheel deltas to px. */
|
|
32
|
+
lineStep: () => number
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function createJsScroller(cfg: JsScrollerConfig) {
|
|
36
|
+
// The single source of truth for the scroll position.
|
|
37
|
+
let scrollOffset = $state(0)
|
|
38
|
+
const maxOffset = $derived(Math.max(0, cfg.totalSize() - cfg.viewport()))
|
|
39
|
+
|
|
40
|
+
// Push the offset into the virtualizer whenever it changes. `untrack` keeps
|
|
41
|
+
// the virtualizer's `version` write out of this effect's dependencies.
|
|
42
|
+
$effect(() => {
|
|
43
|
+
const o = scrollOffset
|
|
44
|
+
untrack(() => cfg.virtualizer.setScrollOffset(o))
|
|
45
|
+
})
|
|
46
|
+
// Re-clamp if the content shrinks under the current offset.
|
|
47
|
+
$effect(() => {
|
|
48
|
+
const m = maxOffset
|
|
49
|
+
if (untrack(() => scrollOffset) > m) scrollOffset = m
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
function scrollTo(offset: number) {
|
|
53
|
+
scrollOffset = Math.max(0, Math.min(maxOffset, offset))
|
|
54
|
+
}
|
|
55
|
+
function scrollByPx(delta: number) {
|
|
56
|
+
scrollTo(scrollOffset + delta)
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function onWheel(e: WheelEvent) {
|
|
60
|
+
if (maxOffset <= 0) return
|
|
61
|
+
// deltaMode: 0 = pixels, 1 = lines, 2 = pages.
|
|
62
|
+
const step =
|
|
63
|
+
e.deltaMode === 1 ? e.deltaY * cfg.lineStep() : e.deltaMode === 2 ? e.deltaY * cfg.viewport() : e.deltaY
|
|
64
|
+
const before = scrollOffset
|
|
65
|
+
scrollByPx(step)
|
|
66
|
+
// Only swallow the wheel when it moved us, so the page can scroll once the
|
|
67
|
+
// list is pinned at its top/bottom edge.
|
|
68
|
+
if (scrollOffset !== before) e.preventDefault()
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// --- Custom scrollbar geometry --------------------------------------------
|
|
72
|
+
const thumbH = $derived(
|
|
73
|
+
maxOffset > 0 && cfg.totalSize() > 0
|
|
74
|
+
? Math.max(24, Math.min(cfg.viewport(), (cfg.viewport() / cfg.totalSize()) * cfg.viewport()))
|
|
75
|
+
: 0,
|
|
76
|
+
)
|
|
77
|
+
const trackRange = $derived(Math.max(0, cfg.viewport() - thumbH))
|
|
78
|
+
const thumbTop = $derived(maxOffset > 0 ? (scrollOffset / maxOffset) * trackRange : 0)
|
|
79
|
+
|
|
80
|
+
let dragging = $state(false)
|
|
81
|
+
let dragStartY = 0
|
|
82
|
+
let dragStartOffset = 0
|
|
83
|
+
function onThumbDown(e: PointerEvent) {
|
|
84
|
+
e.stopPropagation()
|
|
85
|
+
e.preventDefault()
|
|
86
|
+
dragging = true
|
|
87
|
+
dragStartY = e.clientY
|
|
88
|
+
dragStartOffset = scrollOffset
|
|
89
|
+
;(e.currentTarget as HTMLElement).setPointerCapture(e.pointerId)
|
|
90
|
+
}
|
|
91
|
+
function onThumbMove(e: PointerEvent) {
|
|
92
|
+
if (!dragging) return
|
|
93
|
+
const dy = e.clientY - dragStartY
|
|
94
|
+
scrollTo(dragStartOffset + (trackRange > 0 ? (dy / trackRange) * maxOffset : 0))
|
|
95
|
+
}
|
|
96
|
+
function onThumbUp(e: PointerEvent) {
|
|
97
|
+
dragging = false
|
|
98
|
+
releaseCapture(e)
|
|
99
|
+
}
|
|
100
|
+
function onTrackDown(e: PointerEvent) {
|
|
101
|
+
const rect = (e.currentTarget as HTMLElement).getBoundingClientRect()
|
|
102
|
+
scrollByPx(e.clientY - rect.top < thumbTop ? -cfg.viewport() : cfg.viewport())
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// --- Touch: drag the content to pan ---------------------------------------
|
|
106
|
+
let touchActive = false
|
|
107
|
+
let touchStartY = 0
|
|
108
|
+
let touchStartOffset = 0
|
|
109
|
+
function onContentDown(e: PointerEvent) {
|
|
110
|
+
if (e.pointerType !== 'touch') return
|
|
111
|
+
touchActive = true
|
|
112
|
+
touchStartY = e.clientY
|
|
113
|
+
touchStartOffset = scrollOffset
|
|
114
|
+
;(e.currentTarget as HTMLElement).setPointerCapture(e.pointerId)
|
|
115
|
+
}
|
|
116
|
+
function onContentMove(e: PointerEvent) {
|
|
117
|
+
if (!touchActive) return
|
|
118
|
+
scrollTo(touchStartOffset - (e.clientY - touchStartY))
|
|
119
|
+
}
|
|
120
|
+
function onContentUp(e: PointerEvent) {
|
|
121
|
+
if (!touchActive) return
|
|
122
|
+
touchActive = false
|
|
123
|
+
releaseCapture(e)
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function releaseCapture(e: PointerEvent) {
|
|
127
|
+
try {
|
|
128
|
+
;(e.currentTarget as HTMLElement).releasePointerCapture(e.pointerId)
|
|
129
|
+
} catch {
|
|
130
|
+
/* capture may already be gone */
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/** Scroll the minimal amount to bring `[top, top+h]` fully into view. */
|
|
135
|
+
function ensureVisible(top: number, h: number) {
|
|
136
|
+
if (top < scrollOffset) scrollTo(top)
|
|
137
|
+
else if (top + h > scrollOffset + cfg.viewport()) scrollTo(top + h - cfg.viewport())
|
|
138
|
+
}
|
|
139
|
+
/** Bring a virtualizer row index into view (variable-height aware). */
|
|
140
|
+
function ensureIndexVisible(index: number) {
|
|
141
|
+
ensureVisible(cfg.virtualizer.getOffsetForIndex(index), cfg.virtualizer.getSizeForIndex(index))
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
return {
|
|
145
|
+
get scrollOffset() {
|
|
146
|
+
return scrollOffset
|
|
147
|
+
},
|
|
148
|
+
get maxOffset() {
|
|
149
|
+
return maxOffset
|
|
150
|
+
},
|
|
151
|
+
get thumbH() {
|
|
152
|
+
return thumbH
|
|
153
|
+
},
|
|
154
|
+
get thumbTop() {
|
|
155
|
+
return thumbTop
|
|
156
|
+
},
|
|
157
|
+
get dragging() {
|
|
158
|
+
return dragging
|
|
159
|
+
},
|
|
160
|
+
scrollTo,
|
|
161
|
+
scrollByPx,
|
|
162
|
+
ensureVisible,
|
|
163
|
+
ensureIndexVisible,
|
|
164
|
+
onWheel,
|
|
165
|
+
onThumbDown,
|
|
166
|
+
onThumbMove,
|
|
167
|
+
onThumbUp,
|
|
168
|
+
onTrackDown,
|
|
169
|
+
onContentDown,
|
|
170
|
+
onContentMove,
|
|
171
|
+
onContentUp,
|
|
172
|
+
}
|
|
173
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for the option-list helpers, focused on `flattenForVirtual` - the pure
|
|
3
|
+
* model that lets the selection controls window a GROUPED list and mix row
|
|
4
|
+
* heights, reusing the shared virtualizer.
|
|
5
|
+
*/
|
|
6
|
+
import { describe, expect, it } from 'vitest'
|
|
7
|
+
import { flattenForVirtual, normalizeOptions, type ListOption } from './list-option'
|
|
8
|
+
|
|
9
|
+
describe('flattenForVirtual', () => {
|
|
10
|
+
it('passes an ungrouped list through as option rows (uniform height)', () => {
|
|
11
|
+
const options = normalizeOptions([1, 2, 3])
|
|
12
|
+
const m = flattenForVirtual(options, { rowHeight: 32 })
|
|
13
|
+
expect(m.hasGroups).toBe(false)
|
|
14
|
+
expect(m.entries).toHaveLength(3)
|
|
15
|
+
expect(m.entries.every((e) => e.type === 'option')).toBe(true)
|
|
16
|
+
expect(m.entries.map((e) => e.size)).toEqual([32, 32, 32])
|
|
17
|
+
// Ungrouped: option index === flat index.
|
|
18
|
+
expect(m.optionFlatIndex).toEqual([0, 1, 2])
|
|
19
|
+
expect(m.sizeAt(1)).toBe(32)
|
|
20
|
+
})
|
|
21
|
+
|
|
22
|
+
it('interleaves group heading rows and keeps option->flat mapping', () => {
|
|
23
|
+
const options: ListOption[] = [
|
|
24
|
+
{ value: 'a', label: 'A', group: 'G1' },
|
|
25
|
+
{ value: 'b', label: 'B', group: 'G1' },
|
|
26
|
+
{ value: 'c', label: 'C', group: 'G2' },
|
|
27
|
+
]
|
|
28
|
+
const m = flattenForVirtual(options, { rowHeight: 30, groupHeaderHeight: 20 })
|
|
29
|
+
expect(m.hasGroups).toBe(true)
|
|
30
|
+
// [G1 header, a, b, G2 header, c]
|
|
31
|
+
expect(m.entries.map((e) => e.type)).toEqual(['group', 'option', 'option', 'group', 'option'])
|
|
32
|
+
expect(m.entries.map((e) => e.size)).toEqual([20, 30, 30, 20, 30])
|
|
33
|
+
// Option indices 0,1,2 land at flat rows 1,2,4.
|
|
34
|
+
expect(m.optionFlatIndex).toEqual([1, 2, 4])
|
|
35
|
+
expect(m.entries[0]).toMatchObject({ type: 'group', label: 'G1' })
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
it('supports a per-option variable height function', () => {
|
|
39
|
+
const options = normalizeOptions(['x', 'y', 'z'])
|
|
40
|
+
const m = flattenForVirtual(options, { rowHeight: (_opt, i) => 20 + i * 10 })
|
|
41
|
+
expect(m.entries.map((e) => e.size)).toEqual([20, 30, 40])
|
|
42
|
+
expect(m.sizeAt(2)).toBe(40)
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
it('clamps non-positive heights to at least 1px', () => {
|
|
46
|
+
const options = normalizeOptions(['x'])
|
|
47
|
+
const m = flattenForVirtual(options, { rowHeight: 0, groupHeaderHeight: 0 })
|
|
48
|
+
expect(m.sizeAt(0)).toBe(1)
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
it('is empty for no options', () => {
|
|
52
|
+
const m = flattenForVirtual([], { rowHeight: 32 })
|
|
53
|
+
expect(m.entries).toEqual([])
|
|
54
|
+
expect(m.optionFlatIndex).toEqual([])
|
|
55
|
+
})
|
|
56
|
+
})
|
package/src/list-option.ts
CHANGED
|
@@ -9,6 +9,25 @@ export type ListOption = {
|
|
|
9
9
|
color?: string
|
|
10
10
|
}
|
|
11
11
|
|
|
12
|
+
/**
|
|
13
|
+
* Coerce a raw options array into well-formed ListOptions. A primitive item
|
|
14
|
+
* (`1`, `'a'`) becomes `{ value, label: String(value) }`; an object missing a
|
|
15
|
+
* `label` or `value` gets the other filled in. This makes a caller who passes
|
|
16
|
+
* e.g. `[1, 2, 3]` or `['a', 'b']` render sensibly instead of crashing a keyed
|
|
17
|
+
* `#each` on `undefined`/duplicate `value`s.
|
|
18
|
+
*/
|
|
19
|
+
export function normalizeOptions(raw: ReadonlyArray<ListOption | string | number> | null | undefined): ListOption[] {
|
|
20
|
+
if (!raw) return []
|
|
21
|
+
return raw.map((o) => {
|
|
22
|
+
if (o != null && typeof o === 'object') {
|
|
23
|
+
const opt = o as Partial<ListOption>
|
|
24
|
+
const value = opt.value ?? opt.label ?? ''
|
|
25
|
+
return { ...(opt as ListOption), value, label: opt.label ?? String(value) }
|
|
26
|
+
}
|
|
27
|
+
return { value: o, label: String(o) }
|
|
28
|
+
})
|
|
29
|
+
}
|
|
30
|
+
|
|
12
31
|
/** Case-insensitive substring filter over option labels. */
|
|
13
32
|
export function filterOptions(options: ReadonlyArray<ListOption>, query: string): ListOption[] {
|
|
14
33
|
const q = query.trim().toLowerCase()
|
|
@@ -44,6 +63,68 @@ export function hasGroups(options: ReadonlyArray<ListOption>): boolean {
|
|
|
44
63
|
return options.some((o) => o.group != null)
|
|
45
64
|
}
|
|
46
65
|
|
|
66
|
+
/** Fixed px, or a per-option function - lets a virtualized list mix row heights. */
|
|
67
|
+
export type RowHeight = number | ((opt: IndexedOption, index: number) => number)
|
|
68
|
+
|
|
69
|
+
/** One row of the flattened virtualization model: a group heading or an option. */
|
|
70
|
+
export type VirtualListRow =
|
|
71
|
+
| { type: 'group'; label: string; size: number }
|
|
72
|
+
| { type: 'option'; opt: IndexedOption; size: number }
|
|
73
|
+
|
|
74
|
+
export type FlatVirtualModel = {
|
|
75
|
+
/** Group headings + options in render order, each with its px height. */
|
|
76
|
+
entries: VirtualListRow[]
|
|
77
|
+
/** Whether any group heading rows are present. */
|
|
78
|
+
hasGroups: boolean
|
|
79
|
+
/** Height (px) of the entry at flat index `i` - feeds the virtualizer's `estimateSize`. */
|
|
80
|
+
sizeAt: (i: number) => number
|
|
81
|
+
/** Maps an OPTION index (position in the source options array, as used by
|
|
82
|
+
* `optionProps(index)`) to its flat entry index - so the active option can
|
|
83
|
+
* be scrolled into view even with group rows interleaved. */
|
|
84
|
+
optionFlatIndex: number[]
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Flatten a (possibly grouped) option list into a single virtualization model:
|
|
89
|
+
* group headings become rows interleaved with their options, and each row
|
|
90
|
+
* carries its pixel height. This is what lets the selection controls window a
|
|
91
|
+
* GROUPED list (previously they fell back to rendering every node) and support
|
|
92
|
+
* variable row heights, while reusing the shared `createSvelteVirtualizer`.
|
|
93
|
+
*
|
|
94
|
+
* Pure - no DOM. Builds on `groupOptions`, which already tags each option with
|
|
95
|
+
* its flat `index` (the value `optionProps(index)` expects).
|
|
96
|
+
*/
|
|
97
|
+
export function flattenForVirtual(
|
|
98
|
+
options: ReadonlyArray<ListOption>,
|
|
99
|
+
opts: { rowHeight: RowHeight; groupHeaderHeight?: number },
|
|
100
|
+
): FlatVirtualModel {
|
|
101
|
+
const headerH = Math.max(1, opts.groupHeaderHeight ?? 28)
|
|
102
|
+
const rh = opts.rowHeight
|
|
103
|
+
const sizeOf = (o: IndexedOption) => Math.max(1, typeof rh === 'function' ? rh(o, o.index) : rh)
|
|
104
|
+
|
|
105
|
+
const entries: VirtualListRow[] = []
|
|
106
|
+
const optionFlatIndex = new Array<number>(options.length)
|
|
107
|
+
let anyGroups = false
|
|
108
|
+
|
|
109
|
+
for (const g of groupOptions(options)) {
|
|
110
|
+
if (g.group != null) {
|
|
111
|
+
anyGroups = true
|
|
112
|
+
entries.push({ type: 'group', label: g.group, size: headerH })
|
|
113
|
+
}
|
|
114
|
+
for (const opt of g.options) {
|
|
115
|
+
optionFlatIndex[opt.index] = entries.length
|
|
116
|
+
entries.push({ type: 'option', opt, size: sizeOf(opt) })
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
return {
|
|
121
|
+
entries,
|
|
122
|
+
hasGroups: anyGroups,
|
|
123
|
+
sizeAt: (i) => entries[i]?.size ?? 0,
|
|
124
|
+
optionFlatIndex,
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
47
128
|
/**
|
|
48
129
|
* Next enabled option index whose label starts with `buffer` (type-ahead),
|
|
49
130
|
* searching forward from just after `from` and wrapping. Returns -1 for no
|