@svgrid/grid 2.2.31 → 2.2.32
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/SvAvatar.svelte +1 -1
- package/dist/SvCheckBox.svelte +2 -1
- package/dist/SvCircularProgress.svelte +1 -1
- package/dist/SvGrid.controller.svelte.d.ts +20 -0
- package/dist/SvGrid.controller.svelte.js +358 -18
- package/dist/SvGrid.css +103 -3
- package/dist/SvGrid.svelte +162 -10
- package/dist/SvGrid.types.d.ts +81 -0
- package/dist/SvGridDropdown.svelte +38 -6
- package/dist/SvGridDropdown.svelte.d.ts +4 -0
- package/dist/SvGridSelect.svelte +12 -3
- package/dist/SvGridSelect.svelte.d.ts +6 -0
- package/dist/SvGroupCell.svelte +9 -2
- package/dist/SvMultiSelect.svelte +12 -3
- package/dist/SvMultiSelect.svelte.d.ts +6 -0
- package/dist/SvPasswordInput.svelte +1 -1
- package/dist/SvProgress.svelte +1 -1
- package/dist/SvRating.svelte +1 -1
- package/dist/SvResult.svelte +1 -1
- package/dist/SvRichText.svelte +31 -16
- package/dist/SvRichText.svelte.d.ts +9 -0
- package/dist/SvSwitchButton.svelte +2 -1
- package/dist/SvToaster.svelte +1 -1
- package/dist/SvTree.svelte +9 -2
- package/dist/SvTree.svelte.d.ts +5 -0
- package/dist/SvTreeSelect.svelte +6 -2
- package/dist/build-api.js +16 -0
- package/dist/cdn/{GridMenus-Ds3OpzZT.js → GridMenus-C6ixSGWW.js} +33 -33
- package/dist/cdn/{GridMenus-Da_p7SKS.js → GridMenus-Cyk3qpJ7.js} +33 -33
- package/dist/cdn/{SvDateTimePicker-B8GopjhC.js → SvDateTimePicker-CdqaP61H.js} +2 -1
- package/dist/cdn/{SvDateTimePicker-DB0aIEk8.js → SvDateTimePicker-ClqtVIj3.js} +2 -1
- package/dist/cdn/{SvGridDropdown-DjALc_4f.js → SvGridDropdown-D0VdjeR8.js} +129 -123
- package/dist/cdn/{SvGridDropdown-hpnHvOGt.js → SvGridDropdown-Dh-2ej8j.js} +92 -87
- package/dist/cdn/{src-BLJfdyL0.js → src-BgHjWF0Q.js} +4193 -3837
- package/dist/cdn/{src-DYDCiC0D.js → src-CgD-wiuS.js} +5956 -5602
- package/dist/cdn/svgrid.js +8 -8
- package/dist/cdn/svgrid.svelte-external.js +8 -8
- package/dist/cell-render.d.ts +1 -0
- package/dist/cell-render.js +115 -1
- package/dist/cell-values.js +5 -0
- package/dist/core.d.ts +80 -9
- package/dist/core.js +89 -0
- package/dist/editor-contract.d.ts +12 -5
- package/dist/editor-registry.d.ts +18 -7
- package/dist/editor-registry.js +10 -1
- package/dist/group-display.d.ts +39 -0
- package/dist/group-display.js +52 -0
- package/dist/index.d.ts +2 -2
- package/dist/index.js +2 -2
- package/dist/keyboard-handlers.js +16 -0
- package/dist/recurrence.js +3 -2
- package/dist/svgrid-wrapper.types.d.ts +8 -2
- package/dist/themes/index.d.ts +10 -0
- package/dist/themes/index.js +26 -0
- package/package.json +1 -1
- package/src/SvAccordion.test.ts +5 -5
- package/src/SvAvatar.svelte +1 -1
- package/src/SvButtonGroup.test.ts +6 -6
- package/src/SvCheckBox.svelte +2 -1
- package/src/SvCircularProgress.svelte +1 -1
- package/src/SvFileUpload.test.ts +1 -1
- package/src/SvGrid.controller.svelte.ts +367 -17
- package/src/SvGrid.css +103 -3
- package/src/SvGrid.svelte +162 -10
- package/src/SvGrid.types.ts +81 -0
- package/src/SvGridDropdown.svelte +38 -6
- package/src/SvGridSelect.svelte +12 -3
- package/src/SvGroupCell.svelte +9 -2
- package/src/SvMultiSelect.svelte +12 -3
- package/src/SvPasswordInput.svelte +1 -1
- package/src/SvProgress.svelte +1 -1
- package/src/SvRating.svelte +1 -1
- package/src/SvResult.svelte +1 -1
- package/src/SvRichText.svelte +31 -16
- package/src/SvSwitchButton.svelte +2 -1
- package/src/SvToaster.svelte +1 -1
- package/src/SvTree.svelte +9 -2
- package/src/SvTreeSelect.svelte +6 -2
- package/src/build-api.ts +15 -0
- package/src/builtin-editors.test.ts +3 -1
- package/src/cell-render.test.ts +27 -3
- package/src/cell-render.ts +122 -1
- package/src/cell-values.ts +4 -0
- package/src/chart-export.test.ts +2 -2
- package/src/clipboard.test.ts +0 -2
- package/src/core.ts +157 -8
- package/src/editing.test.ts +1 -1
- package/src/editor-contract.ts +12 -5
- package/src/editor-registry.grid.test.ts +48 -0
- package/src/editor-registry.test.ts +3 -1
- package/src/editor-registry.ts +23 -7
- package/src/facet-buckets.test.ts +33 -33
- package/src/group-display.test.ts +91 -1
- package/src/group-display.ts +83 -0
- package/src/index.ts +9 -0
- package/src/keyboard-handlers.ts +17 -0
- package/src/list-power.test.ts +2 -2
- package/src/menus.test.ts +4 -4
- package/src/named-views.coverage.test.ts +3 -3
- package/src/recurrence.ts +3 -2
- package/src/row-resize.test.ts +40 -40
- package/src/scheduler-model.ts +1 -1
- package/src/server-data-source.coverage.test.ts +1 -1
- package/src/server-group-model.test.ts +6 -6
- package/src/spreadsheet.test.ts +0 -1
- package/src/svgrid-wrapper.types.ts +11 -9
- package/src/svgrid.async-editor-options.test.ts +271 -0
- package/src/svgrid.auto-row-height.test.ts +204 -0
- package/src/svgrid.charting.test.ts +4 -4
- package/src/svgrid.comments-autocomplete.test.ts +8 -2
- package/src/svgrid.context-menu.test.ts +25 -9
- package/src/svgrid.filter-depth.test.ts +163 -0
- package/src/svgrid.filter-menu-listbox.svelte.test.ts +16 -2
- package/src/svgrid.filter-menu-scroll.test.ts +10 -2
- package/src/svgrid.grand-total.test.ts +188 -0
- package/src/svgrid.group-display-mode.test.ts +171 -0
- package/src/svgrid.group-footers.test.ts +121 -0
- package/src/svgrid.group-pagination.test.ts +153 -0
- package/src/svgrid.tree-data.test.ts +186 -0
- package/src/svgriddropdown.async-panel.svelte.test.ts +195 -0
- package/src/themes/index.ts +42 -0
- package/src/tree-edit.test.ts +4 -4
- package/src/tree-row-model.test.ts +169 -0
- package/src/ui-localization.test.ts +113 -0
- package/themes/ag-alpine.css +22 -0
- package/themes/antd.css +22 -0
- package/themes/atlassian.css +22 -0
- package/themes/bootstrap.css +22 -0
- package/themes/carbon.css +22 -0
- package/themes/catppuccin.css +22 -0
- package/themes/dracula.css +22 -0
- package/themes/ember.css +22 -0
- package/themes/excel.css +22 -0
- package/themes/fluent.css +22 -0
- package/themes/github.css +22 -0
- package/themes/linear.css +22 -0
- package/themes/material.css +22 -0
- package/themes/nord.css +22 -0
- package/themes/notion.css +22 -0
- package/themes/salesforce.css +22 -0
- package/themes/sap.css +22 -0
- package/themes/shadcn.css +22 -0
- package/themes/tailwind.css +22 -0
- package/themes/vercel.css +22 -0
|
@@ -28,10 +28,10 @@ describe('SvButtonGroup', () => {
|
|
|
28
28
|
expect(group.getAttribute('role')).toBe('radiogroup')
|
|
29
29
|
const btns = [...target.querySelectorAll<HTMLButtonElement>('.sv-bg__btn')]
|
|
30
30
|
expect(btns.map((b) => b.getAttribute('role'))).toEqual(['radio', 'radio', 'radio'])
|
|
31
|
-
expect(btns[1]
|
|
31
|
+
expect(btns[1]!.getAttribute('aria-checked')).toBe('true')
|
|
32
32
|
// Roving tabindex: only the checked/active button is tabbable.
|
|
33
|
-
expect(btns[1]
|
|
34
|
-
expect(btns[0]
|
|
33
|
+
expect(btns[1]!.tabIndex).toBe(0)
|
|
34
|
+
expect(btns[0]!.tabIndex).toBe(-1)
|
|
35
35
|
} finally { destroy() }
|
|
36
36
|
})
|
|
37
37
|
|
|
@@ -40,7 +40,7 @@ describe('SvButtonGroup', () => {
|
|
|
40
40
|
const { target, destroy } = mountBg({ items, value: 'day', mode: 'single', onChange: (v: unknown) => (got = v) })
|
|
41
41
|
try {
|
|
42
42
|
const btns = target.querySelectorAll<HTMLButtonElement>('.sv-bg__btn')
|
|
43
|
-
btns[2]
|
|
43
|
+
btns[2]!.click()
|
|
44
44
|
flushSync()
|
|
45
45
|
expect(got).toBe('month')
|
|
46
46
|
} finally { destroy() }
|
|
@@ -53,8 +53,8 @@ describe('SvButtonGroup', () => {
|
|
|
53
53
|
const group = target.querySelector<HTMLElement>('.sv-bg')!
|
|
54
54
|
expect(group.getAttribute('role')).toBe('group')
|
|
55
55
|
const btns = target.querySelectorAll<HTMLButtonElement>('.sv-bg__btn')
|
|
56
|
-
expect(btns[0]
|
|
57
|
-
btns[1]
|
|
56
|
+
expect(btns[0]!.getAttribute('aria-pressed')).toBe('true')
|
|
57
|
+
btns[1]!.click()
|
|
58
58
|
flushSync()
|
|
59
59
|
expect(got).toEqual(['day', 'week'])
|
|
60
60
|
} finally { destroy() }
|
package/src/SvCheckBox.svelte
CHANGED
|
@@ -56,8 +56,9 @@
|
|
|
56
56
|
|
|
57
57
|
<SvField id={uid} {hint} {error} {required} {dir}>
|
|
58
58
|
<label class="sv-check sv-check--{size}" class:is-disabled={disabled} class:is-readonly={readonly} class:is-invalid={invalid}>
|
|
59
|
+
<!-- role="checkbox" arrives via boxProps(), which the a11y pass cannot see through -->
|
|
60
|
+
<!-- svelte-ignore a11y_role_supports_aria_props_implicit -->
|
|
59
61
|
<button
|
|
60
|
-
role="checkbox"
|
|
61
62
|
class="sv-check__box"
|
|
62
63
|
class:is-checked={checked && !indeterminate}
|
|
63
64
|
class:is-indeterminate={indeterminate}
|
|
@@ -89,7 +89,7 @@
|
|
|
89
89
|
position: relative; display: inline-flex; flex: none;
|
|
90
90
|
}
|
|
91
91
|
.sv-circ--success { --_c: var(--sg-success, #16a34a); }
|
|
92
|
-
.sv-circ--warning { --_c: var(--sg-warning, #
|
|
92
|
+
.sv-circ--warning { --_c: var(--sg-warning, #d97706); }
|
|
93
93
|
.sv-circ--danger { --_c: var(--sg-danger, #dc2626); }
|
|
94
94
|
.sv-circ__svg { display: block; transform: rotate(-90deg); }
|
|
95
95
|
.sv-circ__track { stroke: var(--sg-border, #e2e8f0); }
|
package/src/SvFileUpload.test.ts
CHANGED
|
@@ -53,7 +53,7 @@ describe('SvFileUpload', () => {
|
|
|
53
53
|
target.querySelector<HTMLButtonElement>('.sv-file__remove')!.click()
|
|
54
54
|
flushSync()
|
|
55
55
|
expect(got).toHaveLength(1)
|
|
56
|
-
expect(got![0]
|
|
56
|
+
expect(got![0]!.name).toBe('b.png')
|
|
57
57
|
} finally { destroy() }
|
|
58
58
|
})
|
|
59
59
|
})
|
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
import {
|
|
2
2
|
applyExcelFilter,
|
|
3
|
+
applyGroupAggregate,
|
|
3
4
|
normalizeForFilter,
|
|
4
5
|
createColumnVirtualizer,
|
|
5
6
|
createCoreRowModel,
|
|
6
7
|
createExpandedRowModel,
|
|
7
8
|
createFilteredRowModel,
|
|
8
9
|
createGroupedRowModel,
|
|
10
|
+
createTreeRowModel,
|
|
9
11
|
createSvelteVirtualizer,
|
|
10
12
|
createSortedRowModel,
|
|
11
13
|
createSvGrid,
|
|
@@ -88,6 +90,7 @@ import {
|
|
|
88
90
|
hiddenLeavesForCollapse,
|
|
89
91
|
} from "./column-groups";
|
|
90
92
|
import { resolveColumnId } from "./column-id";
|
|
93
|
+
import { buildAutoGroupColumns, insertGroupFooters, paginateGroupedRows } from "./group-display";
|
|
91
94
|
import {
|
|
92
95
|
createGridApi,
|
|
93
96
|
} from "./build-api";
|
|
@@ -667,6 +670,10 @@ export function createSvGridController<
|
|
|
667
670
|
const externalPaginationEnabled = $derived(props.externalPagination === true);
|
|
668
671
|
const passthroughSortedRowModel = ({ rows }: { rows: Array<Row<TData>> }) =>
|
|
669
672
|
rows;
|
|
673
|
+
// Client tree data. Structural like external sort, so captured once at mount -
|
|
674
|
+
// the row-model pipeline is read before any later prop change could apply.
|
|
675
|
+
// svelte-ignore state_referenced_locally
|
|
676
|
+
const treeDataConfig = props.treeData;
|
|
670
677
|
|
|
671
678
|
|
|
672
679
|
const grid = createSvGrid({
|
|
@@ -687,7 +694,14 @@ export function createSvGridController<
|
|
|
687
694
|
sortedRowModel: externalSortEnabled
|
|
688
695
|
? passthroughSortedRowModel
|
|
689
696
|
: createSortedRowModel<TData>(sortFns),
|
|
690
|
-
|
|
697
|
+
// Tree nesting replaces grouping when `treeData` is set - a row cannot
|
|
698
|
+
// be both a hierarchy node and bucketed under a group banner.
|
|
699
|
+
groupedRowModel: treeDataConfig
|
|
700
|
+
? createTreeRowModel<TData>({
|
|
701
|
+
parentField: treeDataConfig.parentField,
|
|
702
|
+
idField: treeDataConfig.idField,
|
|
703
|
+
})
|
|
704
|
+
: createGroupedRowModel<TData>(),
|
|
691
705
|
expandedRowModel: createExpandedRowModel<TData>(),
|
|
692
706
|
};
|
|
693
707
|
},
|
|
@@ -771,6 +785,103 @@ export function createSvGridController<
|
|
|
771
785
|
userColumnOrder = props.columnOrder ? [...props.columnOrder] : [];
|
|
772
786
|
});
|
|
773
787
|
|
|
788
|
+
// Declared here rather than beside the other pagination/state derivations:
|
|
789
|
+
// the auto-group column pipeline below reads it, and a `const` used above its
|
|
790
|
+
// declaration is a type error even though `$derived` is lazy enough at runtime.
|
|
791
|
+
const groupingColumns = $derived.by(() => {
|
|
792
|
+
gridStateVersion;
|
|
793
|
+
return grid.getState().grouping ?? [];
|
|
794
|
+
});
|
|
795
|
+
|
|
796
|
+
/**
|
|
797
|
+
* Row-grouping display mode. `groupRows` (the default) renders a full-width
|
|
798
|
+
* banner per group - the historical behaviour. `singleColumn` folds every
|
|
799
|
+
* grouping level into one synthetic column; `multipleColumns` gives each
|
|
800
|
+
* grouped field its own. Both hide the grouped SOURCE columns, since their
|
|
801
|
+
* values now live in the auto column.
|
|
802
|
+
*/
|
|
803
|
+
const groupDisplayMode = $derived(props.groupDisplayMode ?? "groupRows");
|
|
804
|
+
const autoGroupSpec = $derived(
|
|
805
|
+
buildAutoGroupColumns(groupingColumns, groupDisplayMode),
|
|
806
|
+
);
|
|
807
|
+
/** True when group state is shown in columns rather than banner rows. */
|
|
808
|
+
const groupColumnMode = $derived(autoGroupSpec.autoColumns.length > 0);
|
|
809
|
+
|
|
810
|
+
/**
|
|
811
|
+
* The synthetic auto-group columns as real `Column` objects, so the rest of
|
|
812
|
+
* the pipeline (widths, pinning, virtualization, headers) treats them like
|
|
813
|
+
* any other column. They carry no `field` - their cell content is resolved
|
|
814
|
+
* from the row's group state at render time.
|
|
815
|
+
*/
|
|
816
|
+
const autoGroupColumns = $derived.by(() => {
|
|
817
|
+
if (!groupColumnMode) return [] as Column<TData>[];
|
|
818
|
+
const sourceById = new Map(grid.getAllColumns().map((c) => [c.id, c]));
|
|
819
|
+
return autoGroupSpec.autoColumns.map((spec) => {
|
|
820
|
+
const source = spec.field ? sourceById.get(spec.field) : undefined;
|
|
821
|
+
const sourceHeader = source?.columnDef.header;
|
|
822
|
+
const header =
|
|
823
|
+
spec.field == null
|
|
824
|
+
? (props.autoGroupColumnHeader ?? "Group")
|
|
825
|
+
: typeof sourceHeader === "string"
|
|
826
|
+
? sourceHeader
|
|
827
|
+
: spec.field;
|
|
828
|
+
const column: Column<TData> = {
|
|
829
|
+
id: spec.id,
|
|
830
|
+
depth: 0,
|
|
831
|
+
columnDef: {
|
|
832
|
+
header,
|
|
833
|
+
width: props.autoGroupColumnWidth ?? 220,
|
|
834
|
+
sortable: false,
|
|
835
|
+
filterable: false,
|
|
836
|
+
} as never,
|
|
837
|
+
getCanSort: () => false,
|
|
838
|
+
getCanFilter: () => false,
|
|
839
|
+
getIsSorted: () => false,
|
|
840
|
+
getToggleSortingHandler: () => () => {},
|
|
841
|
+
};
|
|
842
|
+
return column;
|
|
843
|
+
});
|
|
844
|
+
});
|
|
845
|
+
|
|
846
|
+
/**
|
|
847
|
+
* What an auto-group column shows for a given row.
|
|
848
|
+
*
|
|
849
|
+
* `singleColumn` puts every level in one column, so a group row renders its
|
|
850
|
+
* own level (indented by depth) and leaf rows stay blank - the ancestry is
|
|
851
|
+
* already visible in the banner rows above.
|
|
852
|
+
*
|
|
853
|
+
* `multipleColumns` gives each grouped field a column, so a group row only
|
|
854
|
+
* writes into the column matching ITS level and leaves the others blank.
|
|
855
|
+
*
|
|
856
|
+
* Returns null when the cell should render nothing.
|
|
857
|
+
*/
|
|
858
|
+
function autoGroupCell(
|
|
859
|
+
row: Row<TData>,
|
|
860
|
+
columnId: string,
|
|
861
|
+
): { label: string; count: number; depth: number } | null {
|
|
862
|
+
if (!isGroupRow(row)) return null;
|
|
863
|
+
const depth = row.depth ?? 0;
|
|
864
|
+
if (columnId !== "__autoGroup") {
|
|
865
|
+
// multipleColumns: only the column for this row's own level renders.
|
|
866
|
+
const field = groupingColumns[depth];
|
|
867
|
+
if (!field || `__group_${field}` !== columnId) return null;
|
|
868
|
+
}
|
|
869
|
+
const groupingColumnId = groupingColumns[depth] ?? "";
|
|
870
|
+
const sourceColumn = grid
|
|
871
|
+
.getAllColumns()
|
|
872
|
+
.find((c) => c.id === groupingColumnId);
|
|
873
|
+
const raw = row.getCellValueByColumnId(groupingColumnId);
|
|
874
|
+
const label = sourceColumn
|
|
875
|
+
? formatCellValue(sourceColumn, raw, row)
|
|
876
|
+
: String(raw ?? "");
|
|
877
|
+
return {
|
|
878
|
+
label,
|
|
879
|
+
count: row.leafCount ?? row.subRows?.length ?? 0,
|
|
880
|
+
// singleColumn indents by level; multipleColumns already separates them.
|
|
881
|
+
depth: columnId === "__autoGroup" ? depth : 0,
|
|
882
|
+
};
|
|
883
|
+
}
|
|
884
|
+
|
|
774
885
|
const allColumns = $derived.by(() => {
|
|
775
886
|
let raw = grid
|
|
776
887
|
.getAllColumns()
|
|
@@ -778,8 +889,13 @@ export function createSvGridController<
|
|
|
778
889
|
(column) =>
|
|
779
890
|
!hiddenColumns[column.id] &&
|
|
780
891
|
!hiddenByGroupCollapse[column.id] &&
|
|
781
|
-
!isHiddenByResponsive(column)
|
|
892
|
+
!isHiddenByResponsive(column) &&
|
|
893
|
+
// In a column display mode the grouped columns are folded into the
|
|
894
|
+
// auto column(s), so showing them too would just duplicate the value.
|
|
895
|
+
!autoGroupSpec.hiddenSourceIds.has(column.id),
|
|
782
896
|
);
|
|
897
|
+
// Auto-group columns lead, like a row header.
|
|
898
|
+
if (autoGroupColumns.length) raw = [...autoGroupColumns, ...raw];
|
|
783
899
|
// Apply user reorder (if any). Unknown ids in userColumnOrder are
|
|
784
900
|
// skipped; columns not in userColumnOrder keep their original
|
|
785
901
|
// relative order after the user-ordered ones.
|
|
@@ -821,7 +937,21 @@ export function createSvGridController<
|
|
|
821
937
|
const headers: (typeof base)[number]["headers"] = [];
|
|
822
938
|
for (const column of allColumns) {
|
|
823
939
|
const header = byId.get(column.id);
|
|
824
|
-
if (header)
|
|
940
|
+
if (header) {
|
|
941
|
+
headers.push(header);
|
|
942
|
+
continue;
|
|
943
|
+
}
|
|
944
|
+
// Auto-group columns have no engine header. Synthesize one rather than
|
|
945
|
+
// skipping: the header row indexes `headers` BY POSITION against the
|
|
946
|
+
// rendered columns, so a gap would shift every later header by one.
|
|
947
|
+
const synthetic = {
|
|
948
|
+
id: column.id,
|
|
949
|
+
isPlaceholder: false,
|
|
950
|
+
colSpan: 1,
|
|
951
|
+
column,
|
|
952
|
+
getContext: () => ({ header: synthetic, column, table: grid }),
|
|
953
|
+
} as (typeof base)[number]["headers"][number];
|
|
954
|
+
headers.push(synthetic);
|
|
825
955
|
}
|
|
826
956
|
return [{ id: base[0]!.id, headers }];
|
|
827
957
|
});
|
|
@@ -1098,10 +1228,6 @@ export function createSvGridController<
|
|
|
1098
1228
|
return directions;
|
|
1099
1229
|
});
|
|
1100
1230
|
|
|
1101
|
-
const groupingColumns = $derived.by(() => {
|
|
1102
|
-
gridStateVersion;
|
|
1103
|
-
return grid.getState().grouping ?? [];
|
|
1104
|
-
});
|
|
1105
1231
|
|
|
1106
1232
|
const paginationState = $derived.by(() => {
|
|
1107
1233
|
gridStateVersion;
|
|
@@ -1221,22 +1347,137 @@ export function createSvGridController<
|
|
|
1221
1347
|
* the full dataset rather than the current page (see the comment above
|
|
1222
1348
|
* `_rowModels`).
|
|
1223
1349
|
*/
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1350
|
+
/**
|
|
1351
|
+
* Grouped pagination. `allRowsBeforePagination` interleaves group banners with
|
|
1352
|
+
* data rows, so a plain slice spends the page budget on banners - ten groups of
|
|
1353
|
+
* two at `pageSize: 10` used to show about three data rows (#73). When grouping
|
|
1354
|
+
* is active we page by DATA rows and reprint each page's ancestor banners.
|
|
1355
|
+
* Null when grouping is off, so the flat path stays a cheap slice.
|
|
1356
|
+
*/
|
|
1357
|
+
const groupedPage = $derived.by(() => {
|
|
1358
|
+
if (!paginationEnabled || externalPaginationEnabled) return null;
|
|
1359
|
+
if (!groupingColumns.length) return null;
|
|
1228
1360
|
const { pageIndex, pageSize } = paginationState;
|
|
1229
|
-
|
|
1230
|
-
|
|
1361
|
+
return paginateGroupedRows(allRowsBeforePagination, {
|
|
1362
|
+
getDepth: (row: any) => row.depth ?? 0,
|
|
1363
|
+
isGroup: (row: any) => isGroupRow(row),
|
|
1364
|
+
// A collapsed group is the visible unit and takes a page slot; an expanded
|
|
1365
|
+
// one is a banner reprinted above its children.
|
|
1366
|
+
isExpanded: (row: any) => row.getIsExpanded?.() === true,
|
|
1367
|
+
pageIndex,
|
|
1368
|
+
pageSize,
|
|
1369
|
+
});
|
|
1231
1370
|
});
|
|
1232
1371
|
|
|
1372
|
+
/**
|
|
1373
|
+
* The grand-total row: one synthetic row carrying each column's `aggregate`
|
|
1374
|
+
* over the whole filtered set. Shaped like a group footer so it renders
|
|
1375
|
+
* through the normal cell path - totals land under the columns they belong
|
|
1376
|
+
* to, and `getCanExpand`/`getIsExpanded` are off so nothing treats it as a
|
|
1377
|
+
* banner. Returns null when no column aggregates (nothing to total).
|
|
1378
|
+
*
|
|
1379
|
+
* Aggregates the LEAF rows: `allRowsBeforePagination` interleaves group
|
|
1380
|
+
* banners once grouping is on, and those carry per-group subtotals that
|
|
1381
|
+
* would otherwise be summed a second time.
|
|
1382
|
+
*/
|
|
1383
|
+
const buildGrandTotalRow = () => {
|
|
1384
|
+
const leaves = allRowsBeforePagination.filter((row: any) => !isGroupRow(row));
|
|
1385
|
+
const columns = grid.getAllColumns();
|
|
1386
|
+
const original: Record<string, unknown> = {};
|
|
1387
|
+
let hasAggregate = false;
|
|
1388
|
+
for (const column of columns) {
|
|
1389
|
+
const field = column.columnDef.field;
|
|
1390
|
+
const agg = column.columnDef.aggregate;
|
|
1391
|
+
if (!field || !agg) continue;
|
|
1392
|
+
hasAggregate = true;
|
|
1393
|
+
original[field] = applyGroupAggregate(agg, column.id, leaves as any);
|
|
1394
|
+
}
|
|
1395
|
+
if (!hasAggregate) return null;
|
|
1396
|
+
return {
|
|
1397
|
+
id: "__grand_total__",
|
|
1398
|
+
index: -1,
|
|
1399
|
+
original,
|
|
1400
|
+
depth: 0,
|
|
1401
|
+
subRows: [],
|
|
1402
|
+
leafCount: leaves.length,
|
|
1403
|
+
__groupFooter: true,
|
|
1404
|
+
__grandTotal: true,
|
|
1405
|
+
getCanExpand: () => false,
|
|
1406
|
+
getIsExpanded: () => false,
|
|
1407
|
+
toggleExpanded: () => {},
|
|
1408
|
+
getIsSelected: () => false,
|
|
1409
|
+
toggleSelected: () => {},
|
|
1410
|
+
getAllCells: () => [],
|
|
1411
|
+
getCellValueByColumnId: (columnId: string) => {
|
|
1412
|
+
const col = columns.find((c) => c.id === columnId);
|
|
1413
|
+
const field = col?.columnDef.field;
|
|
1414
|
+
return field && field in original ? original[field] : undefined;
|
|
1415
|
+
},
|
|
1416
|
+
} as any;
|
|
1417
|
+
};
|
|
1418
|
+
|
|
1419
|
+
/**
|
|
1420
|
+
* Visible rows for the current page. Applied last so filters operate on
|
|
1421
|
+
* the full dataset rather than the current page (see the comment above
|
|
1422
|
+
* `_rowModels`).
|
|
1423
|
+
*/
|
|
1424
|
+
const allRows = $derived.by(() => {
|
|
1425
|
+
const paged = (() => {
|
|
1426
|
+
const rows = allRowsBeforePagination;
|
|
1427
|
+
// External pagination: `data` already IS the current page - never slice.
|
|
1428
|
+
if (!paginationEnabled || externalPaginationEnabled) return rows;
|
|
1429
|
+
if (groupedPage) return groupedPage.rows;
|
|
1430
|
+
const { pageIndex, pageSize } = paginationState;
|
|
1431
|
+
const start = pageIndex * pageSize;
|
|
1432
|
+
return rows.slice(start, start + pageSize);
|
|
1433
|
+
})();
|
|
1434
|
+
const wantGroupFooters = Boolean(props.groupFooters) && groupingColumns.length > 0;
|
|
1435
|
+
// The grand total belongs at the END of the dataset, so it is appended only
|
|
1436
|
+
// on the last page - otherwise every page would end in a "total" that isn't
|
|
1437
|
+
// one. With pagination off (or external, where the consumer owns paging and
|
|
1438
|
+
// `data` is already the page) there is only one page to be last.
|
|
1439
|
+
const isLastPage = (() => {
|
|
1440
|
+
if (!paginationEnabled || externalPaginationEnabled) return true;
|
|
1441
|
+
const { pageIndex, pageSize } = paginationState;
|
|
1442
|
+
const total = groupedPage ? groupedPage.dataRowCount : allRowsBeforePagination.length;
|
|
1443
|
+
return pageIndex >= Math.ceil(total / pageSize) - 1;
|
|
1444
|
+
})();
|
|
1445
|
+
const wantGrandTotal = Boolean(props.grandTotalRow) && isLastPage;
|
|
1446
|
+
if (!wantGroupFooters && !wantGrandTotal) return paged;
|
|
1447
|
+
// Footers are inserted AFTER paging so they never eat page budget. A footer
|
|
1448
|
+
// is a clone of its group row, which already carries the group's aggregate
|
|
1449
|
+
// values - so it renders through the normal cell path and the totals land
|
|
1450
|
+
// under the columns they belong to, with `getCanExpand` off so nothing
|
|
1451
|
+
// treats it as a banner.
|
|
1452
|
+
return insertGroupFooters(paged, {
|
|
1453
|
+
getDepth: (row: any) => row.depth ?? 0,
|
|
1454
|
+
isGroup: (row: any) => isGroupRow(row),
|
|
1455
|
+
includeGroupFooter: wantGroupFooters,
|
|
1456
|
+
includeGrandTotalFooter: wantGrandTotal,
|
|
1457
|
+
makeGrandTotal: buildGrandTotalRow,
|
|
1458
|
+
makeFooter: (group: any) => ({
|
|
1459
|
+
...group,
|
|
1460
|
+
id: `${group.id}__footer`,
|
|
1461
|
+
subRows: [],
|
|
1462
|
+
__groupFooter: true,
|
|
1463
|
+
getCanExpand: () => false,
|
|
1464
|
+
getIsExpanded: () => false,
|
|
1465
|
+
}),
|
|
1466
|
+
}) as typeof paged;
|
|
1467
|
+
});
|
|
1468
|
+
|
|
1469
|
+
/** Rows the pager divides into pages: data rows only once grouping is on. */
|
|
1470
|
+
const paginatedRowTotal = $derived(
|
|
1471
|
+
groupedPage ? groupedPage.dataRowCount : allRowsBeforePagination.length,
|
|
1472
|
+
);
|
|
1473
|
+
|
|
1233
1474
|
// When a filter reduces the dataset, the stored pageIndex can point beyond
|
|
1234
1475
|
// the last valid page. Reset to page 0 so the grid never shows a blank body.
|
|
1235
1476
|
// Skipped for external pagination, where the consumer owns pageIndex.
|
|
1236
1477
|
$effect(() => {
|
|
1237
1478
|
if (!paginationEnabled || externalPaginationEnabled) return;
|
|
1238
1479
|
const { pageIndex, pageSize } = paginationState;
|
|
1239
|
-
const pageCount = Math.ceil(
|
|
1480
|
+
const pageCount = Math.ceil(paginatedRowTotal / pageSize);
|
|
1240
1481
|
if (pageCount > 0 && pageIndex >= pageCount) {
|
|
1241
1482
|
grid.setPagination({ pageIndex: 0, pageSize });
|
|
1242
1483
|
}
|
|
@@ -1245,7 +1486,7 @@ export function createSvGridController<
|
|
|
1245
1486
|
// Footer-facing pagination values. In external mode they come from the
|
|
1246
1487
|
// consumer-controlled props; otherwise from the local row model + state.
|
|
1247
1488
|
const paginationTotalRows = $derived(
|
|
1248
|
-
externalPaginationEnabled ? (props.rowCount ?? 0) :
|
|
1489
|
+
externalPaginationEnabled ? (props.rowCount ?? 0) : paginatedRowTotal,
|
|
1249
1490
|
);
|
|
1250
1491
|
const paginationPageIndex = $derived(
|
|
1251
1492
|
externalPaginationEnabled ? (props.pageIndex ?? 0) : paginationState.pageIndex,
|
|
@@ -2223,6 +2464,75 @@ export function createSvGridController<
|
|
|
2223
2464
|
el.removeEventListener("scroll-change", onHorizontal as EventListener);
|
|
2224
2465
|
});
|
|
2225
2466
|
|
|
2467
|
+
// --- Auto row height -------------------------------------------------
|
|
2468
|
+
// Measured natural heights, keyed by row index. A plain Map (not $state):
|
|
2469
|
+
// it is written from a ResizeObserver during layout, and making it reactive
|
|
2470
|
+
// would re-run the render that is doing the measuring. `autoRowHeightVersion`
|
|
2471
|
+
// is the reactive signal instead, bumped only when a height actually changes.
|
|
2472
|
+
const measuredRowHeights = new Map<number, number>();
|
|
2473
|
+
let autoRowHeightVersion = $state(0);
|
|
2474
|
+
// A per-row `rowHeight` function already supplies heights, so auto-measuring
|
|
2475
|
+
// would fight it. Fixed numbers are fine - they become the pre-measure estimate.
|
|
2476
|
+
const autoRowHeightOn = $derived(
|
|
2477
|
+
props.autoRowHeight === true && typeof props.rowHeight !== "function",
|
|
2478
|
+
);
|
|
2479
|
+
const autoRowHeightFallback = $derived(
|
|
2480
|
+
typeof props.rowHeight === "number" ? props.rowHeight : 30,
|
|
2481
|
+
);
|
|
2482
|
+
|
|
2483
|
+
/** Report a row's measured height. Called by the view's measuring action.
|
|
2484
|
+
* Sub-pixel churn is ignored so a fractional layout can't loop. */
|
|
2485
|
+
function reportRowHeight(index: number, height: number): void {
|
|
2486
|
+
if (!autoRowHeightOn || height <= 0) return;
|
|
2487
|
+
const prev = measuredRowHeights.get(index);
|
|
2488
|
+
if (prev !== undefined && Math.abs(prev - height) < 1) return;
|
|
2489
|
+
measuredRowHeights.set(index, height);
|
|
2490
|
+
autoRowHeightVersion += 1;
|
|
2491
|
+
}
|
|
2492
|
+
|
|
2493
|
+
/**
|
|
2494
|
+
* Svelte action for a `<tr>` under `autoRowHeight`. Measures the row once on
|
|
2495
|
+
* mount and again whenever its content reflows (a resized column rewraps
|
|
2496
|
+
* text), reporting the natural height back to the virtualizer.
|
|
2497
|
+
*
|
|
2498
|
+
* Uses the shared rAF-deferred observer: the callback bumps state that
|
|
2499
|
+
* re-lays-out the observed element, which is exactly the pattern that trips
|
|
2500
|
+
* "ResizeObserver loop completed with undelivered notifications" when handled
|
|
2501
|
+
* synchronously.
|
|
2502
|
+
*/
|
|
2503
|
+
function measureRowHeight(node: HTMLElement, index: number) {
|
|
2504
|
+
let current = index;
|
|
2505
|
+
let stop: (() => void) | null = null;
|
|
2506
|
+
const measure = () => reportRowHeight(current, node.getBoundingClientRect().height);
|
|
2507
|
+
const attach = () => {
|
|
2508
|
+
stop?.();
|
|
2509
|
+
stop = autoRowHeightOn ? observeSizeRaf(node, measure) : null;
|
|
2510
|
+
if (autoRowHeightOn) measure();
|
|
2511
|
+
};
|
|
2512
|
+
attach();
|
|
2513
|
+
return {
|
|
2514
|
+
update(next: number) {
|
|
2515
|
+
if (next === current) return;
|
|
2516
|
+
current = next;
|
|
2517
|
+
// A recycled row now shows different data - re-measure under its new index.
|
|
2518
|
+
measure();
|
|
2519
|
+
},
|
|
2520
|
+
destroy() {
|
|
2521
|
+
stop?.();
|
|
2522
|
+
},
|
|
2523
|
+
};
|
|
2524
|
+
}
|
|
2525
|
+
|
|
2526
|
+
// Row index -> row identity changes when the data does, so stale measurements
|
|
2527
|
+
// would size new rows by the old ones. Drop them when the row set changes.
|
|
2528
|
+
$effect(() => {
|
|
2529
|
+
void allRows.length;
|
|
2530
|
+
void internalData;
|
|
2531
|
+
if (!autoRowHeightOn || measuredRowHeights.size === 0) return;
|
|
2532
|
+
measuredRowHeights.clear();
|
|
2533
|
+
autoRowHeightVersion += 1;
|
|
2534
|
+
});
|
|
2535
|
+
|
|
2226
2536
|
$effect(() => {
|
|
2227
2537
|
// When containerHeight is a string (e.g. "100%") the actual pixel height
|
|
2228
2538
|
// depends on the parent layout - read it from the live scroll container.
|
|
@@ -2236,9 +2546,18 @@ export function createSvGridController<
|
|
|
2236
2546
|
? (scrollContainer?.clientHeight ?? 520)
|
|
2237
2547
|
: (props.containerHeight ?? 520);
|
|
2238
2548
|
const rh = props.rowHeight;
|
|
2549
|
+
// Auto height: read measured sizes, falling back to the fixed rowHeight as
|
|
2550
|
+
// the estimate for rows that have not rendered yet. Touch the version so a
|
|
2551
|
+
// new measurement re-runs this effect (the Map itself is not reactive).
|
|
2552
|
+
autoRowHeightVersion;
|
|
2553
|
+
const estimateSize = autoRowHeightOn
|
|
2554
|
+
? (index: number) => measuredRowHeights.get(index) ?? autoRowHeightFallback
|
|
2555
|
+
: typeof rh === "function"
|
|
2556
|
+
? rh
|
|
2557
|
+
: (rh ?? 30);
|
|
2239
2558
|
virtualizer.setOptions({
|
|
2240
2559
|
count: allRows.length,
|
|
2241
|
-
estimateSize
|
|
2560
|
+
estimateSize,
|
|
2242
2561
|
overscan: props.overscan ?? 8,
|
|
2243
2562
|
viewportHeight,
|
|
2244
2563
|
});
|
|
@@ -2313,6 +2632,20 @@ export function createSvGridController<
|
|
|
2313
2632
|
* are resolved on every call because they can change as other cells in
|
|
2314
2633
|
* the same row change (the whole point of cascading editors). */
|
|
2315
2634
|
const editorOptionsCache: Record<string, CellEditorOption[]> = {};
|
|
2635
|
+
// Resolved async `editorOptions`, keyed by column id (static source) or
|
|
2636
|
+
// `columnId::rowId` (per-row cascade). Reactive so a fetch landing re-renders
|
|
2637
|
+
// the open editor; `...Pending` both drives the loading state and dedupes the
|
|
2638
|
+
// request across the many synchronous render calls.
|
|
2639
|
+
let asyncEditorOptions = $state<Record<string, CellEditorOption[]>>({});
|
|
2640
|
+
// Deliberately NOT reactive: this is written during render (the resolver runs
|
|
2641
|
+
// inside the template), and mutating $state there is a Svelte 5 error that
|
|
2642
|
+
// takes the whole editor down with it. Only the resolved result needs to be
|
|
2643
|
+
// reactive; the loading flag is derived from "thenable source, no result yet".
|
|
2644
|
+
const asyncEditorOptionsPending = new Set<string>();
|
|
2645
|
+
// Columns whose per-row `editorOptions` has been observed returning a Promise.
|
|
2646
|
+
// Lets the resolver answer from cache without re-invoking the source, so an
|
|
2647
|
+
// async source fires one request per row instead of one per render.
|
|
2648
|
+
const asyncEditorColumns = new Set<string>();
|
|
2316
2649
|
|
|
2317
2650
|
|
|
2318
2651
|
|
|
@@ -2950,6 +3283,9 @@ export function createSvGridController<
|
|
|
2950
3283
|
get isCellEditableAt() { return isCellEditableAt; },
|
|
2951
3284
|
get sortDirectionByColumn() { return sortDirectionByColumn; },
|
|
2952
3285
|
get groupingColumns() { return groupingColumns; },
|
|
3286
|
+
get groupDisplayMode() { return groupDisplayMode; },
|
|
3287
|
+
get groupColumnMode() { return groupColumnMode; },
|
|
3288
|
+
get autoGroupCell() { return autoGroupCell; },
|
|
2953
3289
|
get paginationState() { return paginationState; },
|
|
2954
3290
|
get externalPaginationEnabled() { return externalPaginationEnabled; },
|
|
2955
3291
|
get paginationTotalRows() { return paginationTotalRows; },
|
|
@@ -3120,6 +3456,15 @@ export function createSvGridController<
|
|
|
3120
3456
|
get rowVirtualizationEnabled() { return rowVirtualizationEnabled; },
|
|
3121
3457
|
get columnVirtualizationEnabled() { return columnVirtualizationEnabled; },
|
|
3122
3458
|
get virtualRows() { return virtualRows; },
|
|
3459
|
+
get autoRowHeightOn() { return autoRowHeightOn; },
|
|
3460
|
+
/** Svelte action: measure this row and report its natural height. */
|
|
3461
|
+
get measureRowHeight() { return measureRowHeight; },
|
|
3462
|
+
/** A row's measured height, or undefined before it has been measured.
|
|
3463
|
+
* The non-virtualized body reads this directly (it has no virtual item). */
|
|
3464
|
+
get measuredRowHeightPx() {
|
|
3465
|
+
autoRowHeightVersion;
|
|
3466
|
+
return (index: number): number | undefined => measuredRowHeights.get(index);
|
|
3467
|
+
},
|
|
3123
3468
|
get virtualRowTotalSize() { return virtualRowTotalSize; },
|
|
3124
3469
|
get virtualRowStart() { return virtualRowStart; },
|
|
3125
3470
|
get virtualRowEnd() { return virtualRowEnd; },
|
|
@@ -3161,7 +3506,12 @@ export function createSvGridController<
|
|
|
3161
3506
|
get getCellDisplayValue() { return getCellDisplayValue; },
|
|
3162
3507
|
get getColumnAlign() { return getColumnAlign; },
|
|
3163
3508
|
get editorOptionsCache() { return editorOptionsCache; },
|
|
3509
|
+
get asyncEditorOptions() { return asyncEditorOptions; },
|
|
3510
|
+
set asyncEditorOptions(v) { asyncEditorOptions = v as never; },
|
|
3511
|
+
get asyncEditorOptionsPending() { return asyncEditorOptionsPending; },
|
|
3512
|
+
get asyncEditorColumns() { return asyncEditorColumns; },
|
|
3164
3513
|
get getColumnEditorOptions() { return getColumnEditorOptions; },
|
|
3514
|
+
get areEditorOptionsLoading() { return areEditorOptionsLoading; },
|
|
3165
3515
|
get formatListCellValue() { return formatListCellValue; },
|
|
3166
3516
|
get formatCellValue() { return formatCellValue; },
|
|
3167
3517
|
get getPinnedCellValue() { return getPinnedCellValue; },
|
|
@@ -3297,7 +3647,7 @@ export function createSvGridController<
|
|
|
3297
3647
|
const { onGridKeyDown, onWindowKeydown, onHeaderSortClick } = createKeyboard<TFeatures, TData>(ctx);
|
|
3298
3648
|
const { computeSummaries, hasRenderedColumn } = createSummaries<TFeatures, TData>(ctx);
|
|
3299
3649
|
const { updateFilterRow, updateFilterOperator, updateFilterMenuValue, updateFilterMenuValueTo, addFilterToken, removeFilterToken, toggleFilterToken, toggleCheckboxWithKeyboard, isColumnFiltered, closeMenus, openInSuggest, closeInSuggest, openChooseColumns, openColumnMenu, openFilterMenu, openOperatorMenu, sortColumnFromMenu, clearColumnSort, groupByColumnFromMenu, clearGroupingFromMenu, isFacetChecked, toggleFacetValue, setFacetSelection, setFilterTokens, isAllFacetsChecked, toggleAllFacets, clearColumnFilter, changePage, goToPage, setPageSize, openContextMenu, closeContextMenu, contextMenuItems, saveComment, removeComment, closeCommentEditor } = createMenus<TFeatures, TData>(ctx);
|
|
3300
|
-
const { cellConditionalFormat, computeRowClass, computeCellClass, computeCellTooltip, computeCellValidity, computeCellNote, getColumnEditorOptions, formatListCellValue, formatCellValue, formatPinnedValue, computePinnedCellClass } = createCellRender<TFeatures, TData>(ctx);
|
|
3650
|
+
const { cellConditionalFormat, computeRowClass, computeCellClass, computeCellTooltip, computeCellValidity, computeCellNote, getColumnEditorOptions, areEditorOptionsLoading, formatListCellValue, formatCellValue, formatPinnedValue, computePinnedCellClass } = createCellRender<TFeatures, TData>(ctx);
|
|
3301
3651
|
const { isCellEditable, isCellEditableAt, getRowColumnValue, getCellDisplayValue, startEditingWithChar, startEditing, stopEditing, startFullRowEdit, setFullRowDraft, commitFullRowEdit, cancelFullRowEdit, saveEditingCell, applyHistoryStep, updateEditingCellValue, onEditorKeyDown, commitAndMoveByTab, focusOnMount, onCellDoubleClick, pasteFromClipboard, onGridPaste } = createEditing<TFeatures, TData>(ctx);
|
|
3302
3652
|
const { isRowSelected, toggleRowSelectionById, toggleSelectAllRows, setActiveCell, scrollActiveCellIntoView, setSelection, extendSelection, isCellInSelectedRange, getCellRangeEdges, getSelectionRects, isInFillPreview, fillMarqueeEdges, findColumnById, onCellPointerDown, onCellPointerEnter, endDragSelection, onWindowPointerMove, onCellClick, emitCellDoubleClick } = createSelection<TFeatures, TData>(ctx);
|
|
3303
3653
|
const { cellPinStyle, isColumnPinned, getCurrentColumnOrder, emitColumnOrder, setColumnOrderInternal, applyColumnDrop, onColumnHeaderDragStart, onColumnHeaderDragOver, onColumnHeaderDragLeave, onColumnHeaderDrop, onColumnHeaderDragEnd, pinColumnLeft, pinColumnRight, unpinColumn, toggleColumnVisibleInPanel, moveColumnInPanel, toggleGroupInPanel, getColumnBaseWidth, getColumnWidth, startColumnResize, onColumnResizeMove, endColumnResize, measureText, autosizeColumn, autosizeAllColumns, resetColumns } = createColumns<TFeatures, TData>(ctx);
|