@svgrid/grid 2.2.16 → 2.2.18
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
package/src/menus.test.ts
CHANGED
|
@@ -666,3 +666,35 @@ describe('createMenus - comments', () => {
|
|
|
666
666
|
expect(ctx.commentEditFor).toBeNull()
|
|
667
667
|
})
|
|
668
668
|
})
|
|
669
|
+
|
|
670
|
+
describe('createMenus - chart context-menu item', () => {
|
|
671
|
+
const target = { rowIndex: 0, colIndex: 0, columnId: 'a', rowId: '0', row: {} }
|
|
672
|
+
|
|
673
|
+
it('appends "Chart selected range" to the default menu when charting is enabled', () => {
|
|
674
|
+
const ctx = makeCtx({ props: { contextMenu: true }, chartingEnabled: true, contextMenuFor: target })
|
|
675
|
+
const items = createMenus(ctx).contextMenuItems()
|
|
676
|
+
expect(items.map((i) => i.label)).toContain('Chart selected range')
|
|
677
|
+
})
|
|
678
|
+
|
|
679
|
+
it('omits the chart item when charting is disabled', () => {
|
|
680
|
+
const ctx = makeCtx({ props: { contextMenu: true }, chartingEnabled: false, contextMenuFor: target })
|
|
681
|
+
const items = createMenus(ctx).contextMenuItems()
|
|
682
|
+
expect(items.map((i) => i.label)).not.toContain('Chart selected range')
|
|
683
|
+
})
|
|
684
|
+
|
|
685
|
+
it('clicking the chart item opens the chart panel', () => {
|
|
686
|
+
const ctx = makeCtx({ props: { contextMenu: true }, chartingEnabled: true, chartPanelOpen: false, contextMenuFor: target })
|
|
687
|
+
const item = createMenus(ctx).contextMenuItems().find((i) => i.label === 'Chart selected range')
|
|
688
|
+
item?.run?.()
|
|
689
|
+
expect(ctx.chartPanelOpen).toBe(true)
|
|
690
|
+
})
|
|
691
|
+
|
|
692
|
+
it('localizes the chart item label via ctx.messages', () => {
|
|
693
|
+
const ctx = makeCtx({
|
|
694
|
+
props: { contextMenu: true }, chartingEnabled: true, contextMenuFor: target,
|
|
695
|
+
messages: { chartRange: 'Graphique de la selection' },
|
|
696
|
+
})
|
|
697
|
+
const items = createMenus(ctx).contextMenuItems()
|
|
698
|
+
expect(items.map((i) => i.label)).toContain('Graphique de la selection')
|
|
699
|
+
})
|
|
700
|
+
})
|
package/src/menus.ts
CHANGED
|
@@ -491,16 +491,21 @@ export function createMenus<
|
|
|
491
491
|
}
|
|
492
492
|
|
|
493
493
|
// Built-in items, keyed. `run` reads the live target passed at click time.
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
494
|
+
// `msg` names the GridMessages key so the label localizes (falls back to the
|
|
495
|
+
// English `label` when no `localeText` is set).
|
|
496
|
+
const builtins: Record<string, { label: string; msg?: string; run: (t: CtxTarget) => void }> = {
|
|
497
|
+
copy: { label: "Copy", msg: "menuCopy", run: () => ctx.copySelectionToClipboard() },
|
|
498
|
+
cut: { label: "Cut", msg: "menuCut", run: () => ctx.cutSelectionToClipboard() },
|
|
499
|
+
paste: { label: "Paste", msg: "menuPaste", run: () => void ctx.pasteFromClipboard() },
|
|
500
|
+
clear: { label: "Clear", msg: "menuClear", run: () => ctx.clearSelectedCells() },
|
|
501
|
+
row_above: { label: "Insert row above", msg: "menuInsertRowAbove", run: (t) => insertRowAt(t, 0) },
|
|
502
|
+
row_below: { label: "Insert row below", msg: "menuInsertRowBelow", run: (t) => insertRowAt(t, 1) },
|
|
503
|
+
remove_row: { label: "Remove row", msg: "menuRemoveRow", run: removeRowAt },
|
|
504
|
+
remove_col: { label: "Remove column", msg: "menuRemoveColumn", run: removeColAt },
|
|
505
|
+
comment: { label: "Edit comment", msg: "menuEditComment", run: (t) => openCommentEditor(t) },
|
|
506
|
+
// Charting: open the chart panel scoped to the current cell selection. Gated
|
|
507
|
+
// on `chartingEnabled` in the resolver below.
|
|
508
|
+
chart: { label: "Chart selected range", msg: "chartRange", run: () => { ctx.chartPanelOpen = true; } },
|
|
504
509
|
};
|
|
505
510
|
const DEFAULT_ITEMS = [
|
|
506
511
|
"copy", "cut", "paste", "clear", "separator",
|
|
@@ -574,16 +579,24 @@ export function createMenus<
|
|
|
574
579
|
const target = ctx.contextMenuFor as CtxTarget | null;
|
|
575
580
|
if (!target) return [];
|
|
576
581
|
const cfg = ctx.props.contextMenu;
|
|
582
|
+
const messages = ctx.messages;
|
|
577
583
|
const entries: any[] = Array.isArray(cfg)
|
|
578
584
|
? cfg
|
|
579
|
-
: [
|
|
585
|
+
: [
|
|
586
|
+
...DEFAULT_ITEMS,
|
|
587
|
+
...(ctx.chartingEnabled ? ["separator", "chart"] : []),
|
|
588
|
+
...(ctx.props.editableComments ? ["separator", "comment"] : []),
|
|
589
|
+
];
|
|
580
590
|
const out: ResolvedItem[] = [];
|
|
581
591
|
for (const entry of entries) {
|
|
582
592
|
if (entry === "separator") { out.push({ key: "sep" + out.length, label: "", separator: true }); continue; }
|
|
583
593
|
if (typeof entry === "string") {
|
|
594
|
+
// The chart item only makes sense when charting is enabled.
|
|
595
|
+
if (entry === "chart" && !ctx.chartingEnabled) continue;
|
|
584
596
|
const b = builtins[entry];
|
|
585
597
|
if (!b) continue;
|
|
586
|
-
|
|
598
|
+
const label = (b.msg && messages?.[b.msg]) || b.label;
|
|
599
|
+
out.push({ key: entry, label, run: () => b.run(target) });
|
|
587
600
|
continue;
|
|
588
601
|
}
|
|
589
602
|
if (entry.hidden?.(target)) continue;
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* pivot-engine registry - the injection seam for in-grid pivot mode. Setting the
|
|
3
|
+
* `pivot` prop on `<SvGrid>` turns the live grid into a pivot table in place, but
|
|
4
|
+
* the pivot ENGINE (which aggregates rows into a pivot result with nested column
|
|
5
|
+
* headers) ships in `@svgrid/enterprise`. This tiny reactive holder lets
|
|
6
|
+
* enterprise register that engine at import time; the grid looks it up and runs
|
|
7
|
+
* it over its filtered + sorted rows, or shows an upsell placeholder when the
|
|
8
|
+
* engine is absent. Mirrors the `scheduler-view` / `editor-registry` pattern.
|
|
9
|
+
*
|
|
10
|
+
* Unlike the scheduler (which swaps in a whole renderer component), the pivot
|
|
11
|
+
* engine is a PURE function - the pivot result renders through the grid's normal
|
|
12
|
+
* table + grouped-column-header path.
|
|
13
|
+
*
|
|
14
|
+
* ONE ENGINE, two entry points: the engine registered here is enterprise's
|
|
15
|
+
* `createPivotModel`. The `pivot` prop on `<SvGrid>` (declarative, no UI) reaches
|
|
16
|
+
* it through this seam; the visual `<SvPivotDesigner>` component calls the same
|
|
17
|
+
* `createPivotModel` directly and adds a drag-drop field designer, heat-tinting,
|
|
18
|
+
* expand/collapse and a chart view on top. Pick the prop for code-defined
|
|
19
|
+
* pivots, the designer for an end-user field picker - both aggregate identically.
|
|
20
|
+
*
|
|
21
|
+
* ```ts
|
|
22
|
+
* // in @svgrid/enterprise, at module load (via enablePivot()):
|
|
23
|
+
* import { registerPivotEngine } from '@svgrid/grid'
|
|
24
|
+
* import { createPivotModel } from './pivot'
|
|
25
|
+
* registerPivotEngine((data, config) => createPivotModel(data, config))
|
|
26
|
+
* ```
|
|
27
|
+
*/
|
|
28
|
+
import type { ColumnDef, RowData, TableFeatures } from './index'
|
|
29
|
+
|
|
30
|
+
/** One value measure in a pivot: which field, how to aggregate, how to label. */
|
|
31
|
+
export type GridPivotValue<TData = RowData> = {
|
|
32
|
+
field: keyof TData & string
|
|
33
|
+
agg?: 'sum' | 'avg' | 'min' | 'max' | 'count' | 'countDistinct' | 'first' | 'last'
|
|
34
|
+
label?: string
|
|
35
|
+
format?: unknown
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Free-grid pivot config. Structurally compatible with enterprise `PivotConfig`
|
|
40
|
+
* so the free package carries zero dependency on enterprise; the enterprise
|
|
41
|
+
* config remains assignable to this.
|
|
42
|
+
*/
|
|
43
|
+
export type GridPivotConfig<TData = RowData> = {
|
|
44
|
+
/** Row-axis dimensions, outer-most first. */
|
|
45
|
+
rows: ReadonlyArray<keyof TData & string>
|
|
46
|
+
/** Column-axis dimensions, outer-most first. */
|
|
47
|
+
cols: ReadonlyArray<keyof TData & string>
|
|
48
|
+
/** One or more measures aggregated under each column-axis leaf. */
|
|
49
|
+
values: ReadonlyArray<GridPivotValue<TData>>
|
|
50
|
+
grandTotalRow?: boolean
|
|
51
|
+
grandTotalCol?: boolean
|
|
52
|
+
rowSubtotals?: boolean
|
|
53
|
+
colSort?: (a: unknown, b: unknown, level: number) => number
|
|
54
|
+
rowSort?: (a: unknown, b: unknown, level: number) => number
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** A pivot row as emitted by the engine (plain object, keyed by leaf column id). */
|
|
58
|
+
export type GridPivotRow = Record<string, unknown> & {
|
|
59
|
+
__pivotId: string
|
|
60
|
+
__pivotDepth: number
|
|
61
|
+
__pivotLabel: string
|
|
62
|
+
__pivotParentId: string | null
|
|
63
|
+
__pivotExpandable: boolean
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** The registered pivot engine: pure `(data, config) => { rows, columns }`. */
|
|
67
|
+
export type PivotEngine = <
|
|
68
|
+
TFeatures extends TableFeatures = TableFeatures,
|
|
69
|
+
TData extends RowData = RowData,
|
|
70
|
+
>(
|
|
71
|
+
data: ReadonlyArray<TData>,
|
|
72
|
+
config: GridPivotConfig<TData>,
|
|
73
|
+
) => { rows: GridPivotRow[]; columns: Array<ColumnDef<TFeatures, GridPivotRow>> }
|
|
74
|
+
|
|
75
|
+
let engine = $state<PivotEngine | null>(null)
|
|
76
|
+
|
|
77
|
+
/** Register the pivot engine. Enterprise calls this from `enablePivot()`. */
|
|
78
|
+
export function registerPivotEngine(fn: PivotEngine): void {
|
|
79
|
+
engine = fn
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** The registered pivot engine, or null when enterprise is not installed. */
|
|
83
|
+
export function getPivotEngine(): PivotEngine | null {
|
|
84
|
+
return engine
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/** Whether a pivot engine has been registered. */
|
|
88
|
+
export function hasPivotEngine(): boolean {
|
|
89
|
+
return engine != null
|
|
90
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { afterEach, describe, expect, it } from 'vitest'
|
|
2
|
+
import {
|
|
3
|
+
getPivotEngine,
|
|
4
|
+
hasPivotEngine,
|
|
5
|
+
registerPivotEngine,
|
|
6
|
+
type GridPivotRow,
|
|
7
|
+
} from './pivot-view.svelte'
|
|
8
|
+
|
|
9
|
+
// A tiny stand-in engine so the seam can be exercised without @svgrid/enterprise.
|
|
10
|
+
const fakeEngine = (data: ReadonlyArray<Record<string, unknown>>) => {
|
|
11
|
+
const rows: GridPivotRow[] = [
|
|
12
|
+
{ __pivotId: 'g1', __pivotDepth: 0, __pivotLabel: 'All', __pivotParentId: null, __pivotExpandable: true, total: data.length },
|
|
13
|
+
]
|
|
14
|
+
return { rows, columns: [{ id: 'total', header: 'Total', field: 'total' }] as never }
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
afterEach(() => {
|
|
18
|
+
// reset the module singleton between tests
|
|
19
|
+
registerPivotEngine(null as never)
|
|
20
|
+
})
|
|
21
|
+
|
|
22
|
+
describe('pivot-view seam', () => {
|
|
23
|
+
it('reports no engine until one is registered', () => {
|
|
24
|
+
expect(hasPivotEngine()).toBe(false)
|
|
25
|
+
expect(getPivotEngine()).toBeNull()
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
it('round-trips a registered engine', () => {
|
|
29
|
+
registerPivotEngine(fakeEngine as never)
|
|
30
|
+
expect(hasPivotEngine()).toBe(true)
|
|
31
|
+
const engine = getPivotEngine()
|
|
32
|
+
expect(engine).not.toBeNull()
|
|
33
|
+
const result = engine!([{ a: 1 }, { a: 2 }], { rows: [], cols: [], values: [] })
|
|
34
|
+
expect(result.rows).toHaveLength(1)
|
|
35
|
+
expect(result.rows[0]!.total).toBe(2)
|
|
36
|
+
expect(result.columns[0]!.id).toBe('total')
|
|
37
|
+
})
|
|
38
|
+
})
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DOM: in-grid pivot mode. Without a registered engine the grid shows an upsell
|
|
3
|
+
* note; after `registerPivotEngine(...)` the pivot result renders in place (as a
|
|
4
|
+
* nested grid) and the toolbar toggle flips back to the flat table.
|
|
5
|
+
*/
|
|
6
|
+
import { afterEach, describe, expect, it } from 'vitest'
|
|
7
|
+
import { mount, unmount } from 'svelte'
|
|
8
|
+
import SvGrid from './SvGrid.svelte'
|
|
9
|
+
import {
|
|
10
|
+
createCoreRowModel,
|
|
11
|
+
createFilteredRowModel,
|
|
12
|
+
createPaginatedRowModel,
|
|
13
|
+
createSortedRowModel,
|
|
14
|
+
registerPivotEngine,
|
|
15
|
+
sortFns,
|
|
16
|
+
tableFeatures,
|
|
17
|
+
rowSortingFeature,
|
|
18
|
+
type ColumnDef,
|
|
19
|
+
type GridPivotRow,
|
|
20
|
+
} from './index'
|
|
21
|
+
|
|
22
|
+
type Row = { id: number; region: string; sales: number }
|
|
23
|
+
const features = tableFeatures({ rowSortingFeature })
|
|
24
|
+
const cols: ColumnDef<typeof features, Row>[] = [
|
|
25
|
+
{ field: 'region', header: 'Region', width: 140 },
|
|
26
|
+
{ field: 'sales', header: 'Sales', width: 120, editorType: 'number' },
|
|
27
|
+
]
|
|
28
|
+
const data: Row[] = [
|
|
29
|
+
{ id: 1, region: 'EU', sales: 100 },
|
|
30
|
+
{ id: 2, region: 'EU', sales: 40 },
|
|
31
|
+
{ id: 3, region: 'US', sales: 70 },
|
|
32
|
+
]
|
|
33
|
+
|
|
34
|
+
// A minimal pivot engine: one row per region with a summed `total` column.
|
|
35
|
+
const engine = (rows: ReadonlyArray<Record<string, unknown>>) => {
|
|
36
|
+
const byRegion = new Map<string, number>()
|
|
37
|
+
for (const r of rows) byRegion.set(String(r.region), (byRegion.get(String(r.region)) ?? 0) + Number(r.sales))
|
|
38
|
+
const pivotRows: GridPivotRow[] = [...byRegion].map(([region, total], i) => ({
|
|
39
|
+
__pivotId: `p${i}`, __pivotDepth: 0, __pivotLabel: region, __pivotParentId: null, __pivotExpandable: false,
|
|
40
|
+
region, total,
|
|
41
|
+
}))
|
|
42
|
+
return {
|
|
43
|
+
rows: pivotRows,
|
|
44
|
+
columns: [
|
|
45
|
+
{ id: 'region', header: 'Region', field: 'region', width: 140 },
|
|
46
|
+
{ id: 'total', header: 'Total sales', field: 'total', width: 140 },
|
|
47
|
+
] as never,
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const tick = () => new Promise<void>((r) => queueMicrotask(r))
|
|
52
|
+
|
|
53
|
+
function mountGrid() {
|
|
54
|
+
const target = document.createElement('div')
|
|
55
|
+
document.body.appendChild(target)
|
|
56
|
+
const app = mount(SvGrid, {
|
|
57
|
+
target,
|
|
58
|
+
props: {
|
|
59
|
+
data,
|
|
60
|
+
columns: cols,
|
|
61
|
+
features,
|
|
62
|
+
_rowModels: {
|
|
63
|
+
coreRowModel: createCoreRowModel(),
|
|
64
|
+
filteredRowModel: createFilteredRowModel(),
|
|
65
|
+
sortedRowModel: createSortedRowModel(sortFns),
|
|
66
|
+
paginatedRowModel: createPaginatedRowModel(),
|
|
67
|
+
},
|
|
68
|
+
containerHeight: 300,
|
|
69
|
+
virtualization: false,
|
|
70
|
+
pivot: { rows: ['region'], cols: [], values: [{ field: 'sales', agg: 'sum' }] },
|
|
71
|
+
pivotMode: true,
|
|
72
|
+
} as never,
|
|
73
|
+
})
|
|
74
|
+
return { target, destroy: () => { unmount(app); target.remove() } }
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
let cleanup: (() => void) | null = null
|
|
78
|
+
afterEach(() => {
|
|
79
|
+
registerPivotEngine(null as never)
|
|
80
|
+
cleanup?.(); cleanup = null
|
|
81
|
+
})
|
|
82
|
+
|
|
83
|
+
describe('in-grid pivot mode', () => {
|
|
84
|
+
it('shows an upsell note when no pivot engine is registered', async () => {
|
|
85
|
+
const { target, destroy } = mountGrid()
|
|
86
|
+
cleanup = destroy
|
|
87
|
+
await tick()
|
|
88
|
+
expect(target.querySelector('.sv-grid-pivot-upsell')).toBeTruthy()
|
|
89
|
+
})
|
|
90
|
+
|
|
91
|
+
it('renders the pivot result in place once an engine is registered', async () => {
|
|
92
|
+
registerPivotEngine(engine as never)
|
|
93
|
+
const { target, destroy } = mountGrid()
|
|
94
|
+
cleanup = destroy
|
|
95
|
+
await tick()
|
|
96
|
+
await tick()
|
|
97
|
+
expect(target.querySelector('.sv-grid-pivot-root')).toBeTruthy()
|
|
98
|
+
// The pivot's aggregated column header + a summed value render.
|
|
99
|
+
expect(target.textContent).toContain('Total sales')
|
|
100
|
+
expect(target.textContent).toContain('140') // EU 100 + 40
|
|
101
|
+
})
|
|
102
|
+
})
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest'
|
|
2
|
+
import { computePosition, parsePlacement, type Rect, type Viewport } from './positioning'
|
|
3
|
+
|
|
4
|
+
const VP: Viewport = { width: 1000, height: 800 }
|
|
5
|
+
// A reference in the middle of the viewport, 100x40.
|
|
6
|
+
const mid: Rect = { x: 450, y: 380, width: 100, height: 40 }
|
|
7
|
+
const floating: Viewport = { width: 200, height: 120 }
|
|
8
|
+
|
|
9
|
+
describe('parsePlacement', () => {
|
|
10
|
+
it('defaults the bare side to center alignment', () => {
|
|
11
|
+
expect(parsePlacement('bottom')).toEqual({ side: 'bottom', align: 'center' })
|
|
12
|
+
expect(parsePlacement('top-start')).toEqual({ side: 'top', align: 'start' })
|
|
13
|
+
expect(parsePlacement('right-end')).toEqual({ side: 'right', align: 'end' })
|
|
14
|
+
})
|
|
15
|
+
})
|
|
16
|
+
|
|
17
|
+
describe('computePosition - placements', () => {
|
|
18
|
+
it('bottom-start sits below, left-aligned to the reference', () => {
|
|
19
|
+
const r = computePosition(mid, floating, { placement: 'bottom-start', offset: 6, viewport: VP })
|
|
20
|
+
expect(r.side).toBe('bottom')
|
|
21
|
+
expect(r.y).toBe(mid.y + mid.height + 6) // 426
|
|
22
|
+
expect(r.x).toBe(mid.x) // 450
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
it('bottom (center) centers the floating over the reference', () => {
|
|
26
|
+
const r = computePosition(mid, floating, { placement: 'bottom', offset: 6, viewport: VP, shift: false })
|
|
27
|
+
expect(r.x).toBe(mid.x + mid.width / 2 - floating.width / 2) // 500 - 100 = 400
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
it('bottom-end right-aligns the floating to the reference', () => {
|
|
31
|
+
const r = computePosition(mid, floating, { placement: 'bottom-end', offset: 6, viewport: VP })
|
|
32
|
+
expect(r.x).toBe(mid.x + mid.width - floating.width) // 550 - 200 = 350
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
it('top places above the reference', () => {
|
|
36
|
+
const r = computePosition(mid, floating, { placement: 'top-start', offset: 6, viewport: VP })
|
|
37
|
+
expect(r.side).toBe('top')
|
|
38
|
+
expect(r.y).toBe(mid.y - floating.height - 6) // 380 - 126 = 254
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
it('right places to the right, vertically centered', () => {
|
|
42
|
+
const r = computePosition(mid, floating, { placement: 'right', offset: 6, viewport: VP, shift: false })
|
|
43
|
+
expect(r.side).toBe('right')
|
|
44
|
+
expect(r.x).toBe(mid.x + mid.width + 6) // 556
|
|
45
|
+
expect(r.y).toBe(mid.y + mid.height / 2 - floating.height / 2) // 400 - 60 = 340
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
it('left places to the left of the reference', () => {
|
|
49
|
+
const r = computePosition(mid, floating, { placement: 'left', offset: 6, viewport: VP, shift: false })
|
|
50
|
+
expect(r.x).toBe(mid.x - floating.width - 6) // 450 - 206 = 244
|
|
51
|
+
})
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
describe('computePosition - flip', () => {
|
|
55
|
+
it('flips bottom -> top when there is no room below', () => {
|
|
56
|
+
const nearBottom: Rect = { x: 450, y: 760, width: 100, height: 30 } // 30px below to viewport edge
|
|
57
|
+
const r = computePosition(nearBottom, floating, { placement: 'bottom-start', viewport: VP })
|
|
58
|
+
expect(r.side).toBe('top')
|
|
59
|
+
expect(r.y).toBe(nearBottom.y - floating.height - 6)
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
it('does NOT flip when the preferred side fits', () => {
|
|
63
|
+
const r = computePosition(mid, floating, { placement: 'bottom-start', viewport: VP })
|
|
64
|
+
expect(r.side).toBe('bottom')
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
it('keeps the preferred side when flip is disabled even if it overflows', () => {
|
|
68
|
+
const nearBottom: Rect = { x: 450, y: 760, width: 100, height: 30 }
|
|
69
|
+
const r = computePosition(nearBottom, floating, { placement: 'bottom-start', flip: false, viewport: VP })
|
|
70
|
+
expect(r.side).toBe('bottom')
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
it('flips right -> left when there is no room on the right', () => {
|
|
74
|
+
const nearRight: Rect = { x: 940, y: 380, width: 50, height: 40 } // 10px to right edge
|
|
75
|
+
const r = computePosition(nearRight, floating, { placement: 'right', viewport: VP })
|
|
76
|
+
expect(r.side).toBe('left')
|
|
77
|
+
})
|
|
78
|
+
})
|
|
79
|
+
|
|
80
|
+
describe('computePosition - shift', () => {
|
|
81
|
+
it('clamps a start-aligned floating back inside the right edge', () => {
|
|
82
|
+
const nearRight: Rect = { x: 950, y: 380, width: 40, height: 40 }
|
|
83
|
+
const r = computePosition(nearRight, floating, { placement: 'bottom-start', padding: 8, viewport: VP })
|
|
84
|
+
// Would be x=950, but must fit: max x = 1000 - 8 - 200 = 792
|
|
85
|
+
expect(r.x).toBe(792)
|
|
86
|
+
})
|
|
87
|
+
|
|
88
|
+
it('clamps to the left padding when the floating is wider than the room', () => {
|
|
89
|
+
const nearLeft: Rect = { x: 4, y: 380, width: 40, height: 40 }
|
|
90
|
+
const r = computePosition(nearLeft, floating, { placement: 'bottom-start', padding: 8, viewport: VP })
|
|
91
|
+
expect(r.x).toBe(8) // left padding
|
|
92
|
+
})
|
|
93
|
+
|
|
94
|
+
it('does not shift when disabled', () => {
|
|
95
|
+
const nearRight: Rect = { x: 950, y: 380, width: 40, height: 40 }
|
|
96
|
+
const r = computePosition(nearRight, floating, { placement: 'bottom-start', shift: false, viewport: VP })
|
|
97
|
+
expect(r.x).toBe(950)
|
|
98
|
+
})
|
|
99
|
+
})
|
|
100
|
+
|
|
101
|
+
describe('computePosition - size', () => {
|
|
102
|
+
it('reports the room below for a bottom placement', () => {
|
|
103
|
+
const r = computePosition(mid, floating, { placement: 'bottom-start', offset: 6, padding: 8, viewport: VP })
|
|
104
|
+
// 800 - 8 - (380 + 40 + 6) = 366
|
|
105
|
+
expect(r.maxHeight).toBe(366)
|
|
106
|
+
expect(r.maxWidth).toBe(VP.width - 16)
|
|
107
|
+
})
|
|
108
|
+
|
|
109
|
+
it('reports the room above for a top placement', () => {
|
|
110
|
+
const r = computePosition(mid, floating, { placement: 'top-start', offset: 6, padding: 8, viewport: VP })
|
|
111
|
+
// 380 - 6 - 8 = 366
|
|
112
|
+
expect(r.maxHeight).toBe(366)
|
|
113
|
+
})
|
|
114
|
+
|
|
115
|
+
it('honors the minMainAxis floor when the side is cramped', () => {
|
|
116
|
+
const nearBottom: Rect = { x: 450, y: 790, width: 100, height: 8 }
|
|
117
|
+
const r = computePosition(nearBottom, floating, { placement: 'bottom-start', flip: false, minMainAxis: 96, viewport: VP })
|
|
118
|
+
expect(r.maxHeight).toBe(96)
|
|
119
|
+
})
|
|
120
|
+
})
|
|
121
|
+
|
|
122
|
+
describe('computePosition - arrow', () => {
|
|
123
|
+
it('points the arrow at the reference center for a bottom placement', () => {
|
|
124
|
+
const r = computePosition(mid, floating, { placement: 'bottom-start', arrow: { size: 10 }, viewport: VP })
|
|
125
|
+
// reference center x = 500; floating x = 450; arrow = 500 - 450 - 5 = 45
|
|
126
|
+
expect(r.arrow?.x).toBe(45)
|
|
127
|
+
})
|
|
128
|
+
|
|
129
|
+
it('clamps the arrow inside the floating (minus padding)', () => {
|
|
130
|
+
// end-aligned so the reference center is near the floating's leading edge
|
|
131
|
+
const r = computePosition(mid, floating, { placement: 'bottom-end', arrow: { size: 10 }, padding: 8, viewport: VP })
|
|
132
|
+
// floating x = 350; center 500; raw arrow = 500 - 350 - 5 = 145; max = 200 - 10 - 8 = 182 -> stays 145
|
|
133
|
+
expect(r.arrow?.x).toBe(145)
|
|
134
|
+
})
|
|
135
|
+
|
|
136
|
+
it('produces a y arrow offset for side placements', () => {
|
|
137
|
+
const r = computePosition(mid, floating, { placement: 'right', arrow: { size: 10 }, shift: false, viewport: VP })
|
|
138
|
+
// reference center y = 400; floating y = 340; arrow y = 400 - 340 - 5 = 55
|
|
139
|
+
expect(r.arrow?.y).toBe(55)
|
|
140
|
+
})
|
|
141
|
+
})
|