@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
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* board-view registry - the injection seam for the Kanban *board view of the
|
|
3
|
+
* grid*. Setting the `board` prop on `<SvGrid>` switches the table for lanes of
|
|
4
|
+
* cards, but the renderer itself ships in `@svgrid/enterprise` (a paid feature).
|
|
5
|
+
* This tiny reactive holder lets enterprise register that renderer at import
|
|
6
|
+
* time; the grid looks it up and mounts it, or shows an upsell placeholder when
|
|
7
|
+
* it is absent. Mirrors the `scheduler-view` / `editor-registry` pattern.
|
|
8
|
+
*
|
|
9
|
+
* ```ts
|
|
10
|
+
* // in @svgrid/enterprise, at module load:
|
|
11
|
+
* import { registerBoardView } from '@svgrid/grid'
|
|
12
|
+
* import SvGridBoard from './SvGridBoard.svelte'
|
|
13
|
+
* registerBoardView(SvGridBoard)
|
|
14
|
+
* ```
|
|
15
|
+
*/
|
|
16
|
+
import type { Component } from 'svelte'
|
|
17
|
+
|
|
18
|
+
let renderer = $state<Component<any> | null>(null)
|
|
19
|
+
|
|
20
|
+
/** Register the component that renders `board` mode. Enterprise calls this. */
|
|
21
|
+
export function registerBoardView(component: Component<any>): void {
|
|
22
|
+
renderer = component
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** The registered board renderer, or null when enterprise is not installed. */
|
|
26
|
+
export function getBoardView(): Component<any> | null {
|
|
27
|
+
return renderer
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** Whether a board renderer has been registered. */
|
|
31
|
+
export function hasBoardView(): boolean {
|
|
32
|
+
return renderer != null
|
|
33
|
+
}
|
|
@@ -530,3 +530,104 @@ describe("SvGridApi - sort opt-out + refresh", () => {
|
|
|
530
530
|
}
|
|
531
531
|
});
|
|
532
532
|
});
|
|
533
|
+
|
|
534
|
+
describe("SvGridApi - setOption / getOption (runtime prop overrides)", () => {
|
|
535
|
+
it("getOption returns the incoming prop when no override is set", async () => {
|
|
536
|
+
const { api, destroy } = await mountGrid();
|
|
537
|
+
try {
|
|
538
|
+
expect(api.getOption("rowHeight")).toBe(36);
|
|
539
|
+
expect(api.getOption("zebraRows")).toBeUndefined();
|
|
540
|
+
} finally {
|
|
541
|
+
destroy();
|
|
542
|
+
}
|
|
543
|
+
});
|
|
544
|
+
|
|
545
|
+
it("setOption overrides a prop, getOption reflects it, undefined clears it", async () => {
|
|
546
|
+
const { api, destroy } = await mountGrid();
|
|
547
|
+
try {
|
|
548
|
+
api.setOption("rowHeight", 48);
|
|
549
|
+
await flush();
|
|
550
|
+
expect(api.getOption("rowHeight")).toBe(48);
|
|
551
|
+
|
|
552
|
+
api.setOption("zebraRows", true);
|
|
553
|
+
await flush();
|
|
554
|
+
expect(api.getOption("zebraRows")).toBe(true);
|
|
555
|
+
|
|
556
|
+
// Clearing an override falls back to the incoming prop.
|
|
557
|
+
api.setOption("rowHeight", undefined);
|
|
558
|
+
await flush();
|
|
559
|
+
expect(api.getOption("rowHeight")).toBe(36);
|
|
560
|
+
} finally {
|
|
561
|
+
destroy();
|
|
562
|
+
}
|
|
563
|
+
});
|
|
564
|
+
|
|
565
|
+
it("setOption('sortable', true) turns sorting on at runtime; false turns it off", async () => {
|
|
566
|
+
// Mount with NO features so sorting starts disabled - only the runtime
|
|
567
|
+
// override can inject rowSortingFeature (mirrors resolveEffectiveFeatures).
|
|
568
|
+
const { api, destroy } = await mountGrid({}, tableFeatures({}));
|
|
569
|
+
try {
|
|
570
|
+
// Off by default: setSort is guarded to a no-op.
|
|
571
|
+
api.setSort("salary", "asc");
|
|
572
|
+
await flush();
|
|
573
|
+
expect(api.getState().sorting).toEqual([]);
|
|
574
|
+
|
|
575
|
+
// Turn sorting on via the override, then setSort takes effect.
|
|
576
|
+
api.setOption("sortable", true);
|
|
577
|
+
await flush();
|
|
578
|
+
api.setSort("salary", "asc");
|
|
579
|
+
await flush();
|
|
580
|
+
expect(api.getState().sorting).toEqual([{ id: "salary", desc: false }]);
|
|
581
|
+
|
|
582
|
+
// Turn it back off: setSort is guarded again.
|
|
583
|
+
api.setOption("sortable", false);
|
|
584
|
+
await flush();
|
|
585
|
+
api.clearSort();
|
|
586
|
+
await flush();
|
|
587
|
+
api.setSort("team", "asc");
|
|
588
|
+
await flush();
|
|
589
|
+
expect(api.getState().sorting).toEqual([]);
|
|
590
|
+
} finally {
|
|
591
|
+
destroy();
|
|
592
|
+
}
|
|
593
|
+
});
|
|
594
|
+
|
|
595
|
+
it("a view-direct prop (zebraRows) visually re-renders on override", async () => {
|
|
596
|
+
const { api, target, destroy } = await mountGrid();
|
|
597
|
+
try {
|
|
598
|
+
// No striping to start.
|
|
599
|
+
expect(target.querySelectorAll(".sv-grid-row-alt").length).toBe(0);
|
|
600
|
+
|
|
601
|
+
api.setOption("zebraRows", true);
|
|
602
|
+
await flush();
|
|
603
|
+
await flush();
|
|
604
|
+
// Odd rows now carry the alternate-row class (view reads props.zebraRows,
|
|
605
|
+
// which is the effective proxy).
|
|
606
|
+
expect(target.querySelectorAll(".sv-grid-row-alt").length).toBeGreaterThan(0);
|
|
607
|
+
|
|
608
|
+
api.setOption("zebraRows", undefined);
|
|
609
|
+
await flush();
|
|
610
|
+
await flush();
|
|
611
|
+
expect(target.querySelectorAll(".sv-grid-row-alt").length).toBe(0);
|
|
612
|
+
} finally {
|
|
613
|
+
destroy();
|
|
614
|
+
}
|
|
615
|
+
});
|
|
616
|
+
|
|
617
|
+
it("resetOptions clears every override", async () => {
|
|
618
|
+
const { api, destroy } = await mountGrid();
|
|
619
|
+
try {
|
|
620
|
+
api.setOption("rowHeight", 60);
|
|
621
|
+
api.setOption("zebraRows", true);
|
|
622
|
+
await flush();
|
|
623
|
+
expect(api.getOption("rowHeight")).toBe(60);
|
|
624
|
+
|
|
625
|
+
api.resetOptions();
|
|
626
|
+
await flush();
|
|
627
|
+
expect(api.getOption("rowHeight")).toBe(36);
|
|
628
|
+
expect(api.getOption("zebraRows")).toBeUndefined();
|
|
629
|
+
} finally {
|
|
630
|
+
destroy();
|
|
631
|
+
}
|
|
632
|
+
});
|
|
633
|
+
});
|
package/src/build-api.ts
CHANGED
|
@@ -48,17 +48,14 @@ import {
|
|
|
48
48
|
getDateFormatter,
|
|
49
49
|
resolveDatePattern,
|
|
50
50
|
} from "./cell-formatting";
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
type GridExportScope,
|
|
60
|
-
type GridExportOptions,
|
|
61
|
-
type GridClipboardOptions,
|
|
51
|
+
// The export serializers load lazily (their own chunk) so export-format.ts stays
|
|
52
|
+
// out of the base bundle - it is fetched only when an export/copy method is
|
|
53
|
+
// actually invoked (an explicit, already-async user action).
|
|
54
|
+
import type {
|
|
55
|
+
GridExportColumn,
|
|
56
|
+
GridExportScope,
|
|
57
|
+
GridExportOptions,
|
|
58
|
+
GridClipboardOptions,
|
|
62
59
|
} from "./export-format";
|
|
63
60
|
import {
|
|
64
61
|
RenderSnippetConfig,
|
|
@@ -166,16 +163,18 @@ export function createGridApi<
|
|
|
166
163
|
}
|
|
167
164
|
return out;
|
|
168
165
|
};
|
|
169
|
-
const projectForExport = (
|
|
166
|
+
const projectForExport = async (
|
|
170
167
|
scope: GridExportScope,
|
|
171
168
|
columns: ReadonlyArray<string> | undefined,
|
|
172
169
|
rawValues: boolean | undefined,
|
|
173
|
-
) =>
|
|
174
|
-
projectGridRows(
|
|
170
|
+
) => {
|
|
171
|
+
const { projectGridRows } = await import("./export-format");
|
|
172
|
+
return projectGridRows(
|
|
175
173
|
exportScopeRows(scope) as ReadonlyArray<Record<string, unknown>>,
|
|
176
174
|
exportColumns(),
|
|
177
175
|
{ columns, rawValues },
|
|
178
176
|
);
|
|
177
|
+
};
|
|
179
178
|
|
|
180
179
|
return {
|
|
181
180
|
getCellValue(rowIndex, columnId) {
|
|
@@ -372,6 +371,28 @@ export function createGridApi<
|
|
|
372
371
|
setGroupBy(columnIds) {
|
|
373
372
|
ctx.grid.setGrouping([...columnIds]);
|
|
374
373
|
},
|
|
374
|
+
setOption(key, value) {
|
|
375
|
+
// Runtime prop override. Writes into the controller's `optionOverrides`
|
|
376
|
+
// store, which the effective-props proxy merges over the incoming prop, so
|
|
377
|
+
// the grid re-renders exactly as if the parent had changed that prop.
|
|
378
|
+
// `undefined` clears the override (falls back to the prop). Reassign the
|
|
379
|
+
// whole object for reliable reactivity; `untrack` per the `setFilter` note.
|
|
380
|
+
untrack(() => {
|
|
381
|
+
const next: Record<string, unknown> = { ...ctx.optionOverrides };
|
|
382
|
+
if (value === undefined) delete next[key as string];
|
|
383
|
+
else next[key as string] = value;
|
|
384
|
+
ctx.optionOverrides = next;
|
|
385
|
+
});
|
|
386
|
+
},
|
|
387
|
+
getOption(key) {
|
|
388
|
+
// The effective value: the override if set, otherwise the incoming prop.
|
|
389
|
+
return (ctx.props as Record<string, unknown>)[key as string] as never;
|
|
390
|
+
},
|
|
391
|
+
resetOptions() {
|
|
392
|
+
untrack(() => {
|
|
393
|
+
ctx.optionOverrides = {};
|
|
394
|
+
});
|
|
395
|
+
},
|
|
375
396
|
setFilter(columnId, filter) {
|
|
376
397
|
// `untrack`: these imperative mutators read the same filter state they
|
|
377
398
|
// write (read-modify-write on `filterMenuValues`). When a consumer
|
|
@@ -511,7 +532,8 @@ export function createGridApi<
|
|
|
511
532
|
},
|
|
512
533
|
// ---- Free data export (CSV / TSV / JSON + clipboard) -----------------
|
|
513
534
|
async exportCsv(options: GridExportOptions = {}) {
|
|
514
|
-
const {
|
|
535
|
+
const { serializeDelimited, downloadTextFile } = await import("./export-format");
|
|
536
|
+
const { records, fields } = await projectForExport(
|
|
515
537
|
options.rows ?? "displayed",
|
|
516
538
|
options.columns,
|
|
517
539
|
options.rawValues,
|
|
@@ -526,7 +548,8 @@ export function createGridApi<
|
|
|
526
548
|
return text;
|
|
527
549
|
},
|
|
528
550
|
async exportTsv(options: GridExportOptions = {}) {
|
|
529
|
-
const {
|
|
551
|
+
const { serializeDelimited, downloadTextFile } = await import("./export-format");
|
|
552
|
+
const { records, fields } = await projectForExport(
|
|
530
553
|
options.rows ?? "displayed",
|
|
531
554
|
options.columns,
|
|
532
555
|
options.rawValues,
|
|
@@ -541,7 +564,8 @@ export function createGridApi<
|
|
|
541
564
|
return text;
|
|
542
565
|
},
|
|
543
566
|
async exportJson(options: GridExportOptions = {}) {
|
|
544
|
-
const {
|
|
567
|
+
const { serializeJson, downloadTextFile } = await import("./export-format");
|
|
568
|
+
const { records, fields } = await projectForExport(
|
|
545
569
|
options.rows ?? "displayed",
|
|
546
570
|
options.columns,
|
|
547
571
|
options.rawValues,
|
|
@@ -555,7 +579,8 @@ export function createGridApi<
|
|
|
555
579
|
return text;
|
|
556
580
|
},
|
|
557
581
|
async copyToClipboard(options: GridClipboardOptions = {}) {
|
|
558
|
-
const {
|
|
582
|
+
const { serializeDelimited, serializeMarkdown, copyTextToClipboard } = await import("./export-format");
|
|
583
|
+
const { records, fields, align } = await projectForExport(
|
|
559
584
|
options.rows ?? "displayed",
|
|
560
585
|
options.columns,
|
|
561
586
|
options.rawValues,
|
|
@@ -77,7 +77,7 @@ describe('registerBuiltinEditors in SvGrid', () => {
|
|
|
77
77
|
api.startEditing(0, 'estimate')
|
|
78
78
|
await tick()
|
|
79
79
|
await tick()
|
|
80
|
-
expect(target.querySelector('.sv-
|
|
80
|
+
expect(target.querySelector('.sv-dur__input')).not.toBeNull()
|
|
81
81
|
} finally {
|
|
82
82
|
destroy()
|
|
83
83
|
}
|
package/src/cell-formatting.ts
CHANGED
|
@@ -60,7 +60,7 @@ export function resolveDatePattern(
|
|
|
60
60
|
|
|
61
61
|
type NumericFormatInput = {
|
|
62
62
|
type: 'number' | 'currency' | 'percent'
|
|
63
|
-
locales?: string | string[]
|
|
63
|
+
locales?: string | readonly string[]
|
|
64
64
|
currency?: string
|
|
65
65
|
/** When type is percent: treat value as 0–100 instead of Intl’s 0–1 fraction */
|
|
66
66
|
valueIsPercentPoints?: boolean
|
|
@@ -75,7 +75,7 @@ type NumericFormatInput = {
|
|
|
75
75
|
*/
|
|
76
76
|
const numberFormatterCache = new Map<string, Intl.NumberFormat>()
|
|
77
77
|
function getNumberFormatter(
|
|
78
|
-
locales: string | string[] | undefined,
|
|
78
|
+
locales: string | readonly string[] | undefined,
|
|
79
79
|
options: Intl.NumberFormatOptions,
|
|
80
80
|
): Intl.NumberFormat {
|
|
81
81
|
// Stable key - for normal column configs the options object is reused
|
|
@@ -98,7 +98,7 @@ function getNumberFormatter(
|
|
|
98
98
|
(options.notation ?? '')
|
|
99
99
|
let fmt = numberFormatterCache.get(key)
|
|
100
100
|
if (!fmt) {
|
|
101
|
-
fmt = new Intl.NumberFormat(locales, options)
|
|
101
|
+
fmt = new Intl.NumberFormat(locales as string | string[] | undefined, options)
|
|
102
102
|
numberFormatterCache.set(key, fmt)
|
|
103
103
|
}
|
|
104
104
|
return fmt
|
|
@@ -107,7 +107,7 @@ function getNumberFormatter(
|
|
|
107
107
|
/** Cache of `Intl.DateTimeFormat` by `(locale, options)` signature. */
|
|
108
108
|
const dateFormatterCache = new Map<string, Intl.DateTimeFormat>()
|
|
109
109
|
export function getDateFormatter(
|
|
110
|
-
locales: string | string[] | undefined,
|
|
110
|
+
locales: string | readonly string[] | undefined,
|
|
111
111
|
options: Intl.DateTimeFormatOptions,
|
|
112
112
|
): Intl.DateTimeFormat {
|
|
113
113
|
const key =
|
|
@@ -134,7 +134,7 @@ export function getDateFormatter(
|
|
|
134
134
|
(options.hour12 ?? '')
|
|
135
135
|
let fmt = dateFormatterCache.get(key)
|
|
136
136
|
if (!fmt) {
|
|
137
|
-
fmt = new Intl.DateTimeFormat(locales, options)
|
|
137
|
+
fmt = new Intl.DateTimeFormat(locales as string | string[] | undefined, options)
|
|
138
138
|
dateFormatterCache.set(key, fmt)
|
|
139
139
|
}
|
|
140
140
|
return fmt
|
package/src/cell-render.ts
CHANGED
|
@@ -332,7 +332,7 @@ export function createCellRender<
|
|
|
332
332
|
) {
|
|
333
333
|
return formatNumericWithConfig(value, {
|
|
334
334
|
type: formatConfig.type,
|
|
335
|
-
locales: formatConfig.locales,
|
|
335
|
+
locales: (formatConfig.locales ?? ctx.props.localization?.locale),
|
|
336
336
|
currency:
|
|
337
337
|
formatConfig.type === "currency"
|
|
338
338
|
? (formatConfig.currency ?? "USD")
|
|
@@ -365,7 +365,7 @@ export function createCellRender<
|
|
|
365
365
|
minute: "2-digit",
|
|
366
366
|
};
|
|
367
367
|
|
|
368
|
-
return getDateFormatter(formatConfig.locales, merged).format(
|
|
368
|
+
return getDateFormatter((formatConfig.locales ?? ctx.props.localization?.locale), merged).format(
|
|
369
369
|
parsedDate,
|
|
370
370
|
);
|
|
371
371
|
}
|
|
@@ -406,7 +406,7 @@ export function createCellRender<
|
|
|
406
406
|
) {
|
|
407
407
|
return formatNumericWithConfig(value, {
|
|
408
408
|
type: formatConfig.type,
|
|
409
|
-
locales: formatConfig.locales,
|
|
409
|
+
locales: (formatConfig.locales ?? ctx.props.localization?.locale),
|
|
410
410
|
currency:
|
|
411
411
|
formatConfig.type === "currency"
|
|
412
412
|
? (formatConfig.currency ?? "USD")
|
|
@@ -428,7 +428,7 @@ export function createCellRender<
|
|
|
428
428
|
: formatConfig.type === "date"
|
|
429
429
|
? { year: "numeric", month: "2-digit", day: "2-digit" }
|
|
430
430
|
: { year: "numeric", month: "2-digit", day: "2-digit", hour: "2-digit", minute: "2-digit" };
|
|
431
|
-
return getDateFormatter(formatConfig.locales, merged).format(parsedDate);
|
|
431
|
+
return getDateFormatter((formatConfig.locales ?? ctx.props.localization?.locale), merged).format(parsedDate);
|
|
432
432
|
}
|
|
433
433
|
}
|
|
434
434
|
return String(value ?? "");
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* chart-view registry - the injection seam for the *chart view of the grid*.
|
|
3
|
+
* Setting the `chart` prop on `<SvGrid>` swaps the table for a chart driven by
|
|
4
|
+
* the grid's filtered + sorted rows. Mirrors the `board-view` / `scheduler-view`
|
|
5
|
+
* seam, with one difference: the chart renderer is NOT a paid feature. The chart
|
|
6
|
+
* primitive (`SvChart` / `SvGridChart`) already ships in the free `@svgrid/grid`,
|
|
7
|
+
* so the grid lazy-loads a built-in default renderer (`SvGridChartView`) out of
|
|
8
|
+
* the box. This seam exists only so a host (or enterprise) can *override* that
|
|
9
|
+
* default with a richer renderer - it is not required for the view to work.
|
|
10
|
+
*
|
|
11
|
+
* ```ts
|
|
12
|
+
* // optional: replace the built-in chart view with your own renderer
|
|
13
|
+
* import { registerChartView } from '@svgrid/grid'
|
|
14
|
+
* import MyChartView from './MyChartView.svelte'
|
|
15
|
+
* registerChartView(MyChartView)
|
|
16
|
+
* ```
|
|
17
|
+
*/
|
|
18
|
+
import type { Component } from 'svelte'
|
|
19
|
+
|
|
20
|
+
let renderer = $state<Component<any> | null>(null)
|
|
21
|
+
|
|
22
|
+
/** Override the built-in chart-view renderer. Optional - the grid ships a
|
|
23
|
+
* free default, so this is only for hosts that want a custom renderer. */
|
|
24
|
+
export function registerChartView(component: Component<any>): void {
|
|
25
|
+
renderer = component
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** The registered chart-view override, or null to use the built-in default. */
|
|
29
|
+
export function getChartView(): Component<any> | null {
|
|
30
|
+
return renderer
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** Whether a chart-view override has been registered. */
|
|
34
|
+
export function hasChartView(): boolean {
|
|
35
|
+
return renderer != null
|
|
36
|
+
}
|
|
@@ -30,6 +30,8 @@ export type ComboboxConfig = {
|
|
|
30
30
|
value: () => ComboboxValue
|
|
31
31
|
onChange?: (value: ComboboxValue) => void
|
|
32
32
|
disabled?: () => boolean
|
|
33
|
+
/** Read-only: value shown, input focusable, but not editable and the panel will not open. */
|
|
34
|
+
readonly?: () => boolean
|
|
33
35
|
ariaLabel?: () => string | undefined
|
|
34
36
|
/** Filter the options locally by the query. Set false for server-side search. */
|
|
35
37
|
localFilter?: () => boolean
|
|
@@ -51,6 +53,7 @@ export function createCombobox(config: ComboboxConfig) {
|
|
|
51
53
|
const listId = `${id}-list`
|
|
52
54
|
const opts = () => config.options()
|
|
53
55
|
const disabled = () => config.disabled?.() ?? false
|
|
56
|
+
const readonly = () => config.readonly?.() ?? false
|
|
54
57
|
|
|
55
58
|
let open = $state(false)
|
|
56
59
|
let query = $state('')
|
|
@@ -76,25 +79,31 @@ export function createCombobox(config: ComboboxConfig) {
|
|
|
76
79
|
ariaLabel: config.ariaLabel?.(),
|
|
77
80
|
})
|
|
78
81
|
|
|
79
|
-
function openPanel() { if (disabled() || open) return; open = true; active = 0 }
|
|
82
|
+
function openPanel() { if (disabled() || readonly() || open) return; open = true; active = 0 }
|
|
80
83
|
function close(revert = true) {
|
|
81
84
|
open = false; editing = false
|
|
82
85
|
if (revert) query = selected?.label ?? ''
|
|
83
86
|
}
|
|
84
87
|
function toggle() { if (open) { close() } else { config.focusInput?.(); openPanel() } }
|
|
85
88
|
function pick(o: ListOption | undefined) {
|
|
86
|
-
if (!o || o.disabled) return
|
|
89
|
+
if (readonly() || !o || o.disabled) return
|
|
87
90
|
config.onChange?.(o.value); query = o.label; editing = false; open = false; config.blurInput?.()
|
|
88
91
|
}
|
|
92
|
+
/** Clear the selection (value -> null) and reset the shown text. */
|
|
93
|
+
function clear() {
|
|
94
|
+
if (readonly() || disabled()) return
|
|
95
|
+
config.onChange?.(null); query = ''; editing = false; open = false
|
|
96
|
+
}
|
|
89
97
|
function setActive(i: number) { const o = filtered[i]; if (o && !o.disabled) active = i }
|
|
90
98
|
function onInput(e: Event) {
|
|
99
|
+
if (readonly()) return
|
|
91
100
|
query = (e.currentTarget as HTMLInputElement).value
|
|
92
101
|
editing = true; active = 0
|
|
93
102
|
if (!open) openPanel()
|
|
94
103
|
}
|
|
95
|
-
function onFocus() { editing = true; query = selected?.label ?? ''; openPanel() }
|
|
104
|
+
function onFocus() { if (readonly()) return; editing = true; query = selected?.label ?? ''; openPanel() }
|
|
96
105
|
function onKeydown(e: KeyboardEvent) {
|
|
97
|
-
if (disabled()) return
|
|
106
|
+
if (disabled() || readonly()) return
|
|
98
107
|
if (!open && e.key === 'ArrowDown') { e.preventDefault(); editing = true; openPanel(); return }
|
|
99
108
|
if (e.key === 'ArrowDown') { e.preventDefault(); active = Math.min(active + 1, filtered.length - 1) }
|
|
100
109
|
else if (e.key === 'ArrowUp') { e.preventDefault(); active = Math.max(active - 1, 0) }
|
|
@@ -128,6 +137,7 @@ export function createCombobox(config: ComboboxConfig) {
|
|
|
128
137
|
close,
|
|
129
138
|
toggle,
|
|
130
139
|
pick,
|
|
140
|
+
clear,
|
|
131
141
|
onInput,
|
|
132
142
|
onFocus,
|
|
133
143
|
onKeydown,
|
|
@@ -140,8 +150,10 @@ export function createCombobox(config: ComboboxConfig) {
|
|
|
140
150
|
'aria-autocomplete': 'list' as const,
|
|
141
151
|
'aria-activedescendant': open && filtered[active] ? optionId(active) : undefined,
|
|
142
152
|
...editorAria(ariaState()),
|
|
153
|
+
'aria-readonly': readonly() || undefined,
|
|
143
154
|
value: shownText,
|
|
144
155
|
disabled: disabled(),
|
|
156
|
+
readonly: readonly() || undefined,
|
|
145
157
|
oninput: onInput,
|
|
146
158
|
onfocus: onFocus,
|
|
147
159
|
onkeydown: onKeydown,
|
|
@@ -151,7 +163,7 @@ export function createCombobox(config: ComboboxConfig) {
|
|
|
151
163
|
type: 'button' as const,
|
|
152
164
|
tabindex: -1,
|
|
153
165
|
'aria-label': 'Toggle',
|
|
154
|
-
disabled: disabled(),
|
|
166
|
+
disabled: disabled() || readonly(),
|
|
155
167
|
onclick: toggle,
|
|
156
168
|
}),
|
|
157
169
|
/** Spread onto the listbox container. */
|
|
@@ -30,6 +30,8 @@ export type DropdownListConfig = {
|
|
|
30
30
|
value: () => DropdownValue
|
|
31
31
|
onChange?: (value: string | number) => void
|
|
32
32
|
disabled?: () => boolean
|
|
33
|
+
/** Read-only: value shown, trigger focusable, but the panel will not open. */
|
|
34
|
+
readonly?: () => boolean
|
|
33
35
|
ariaLabel?: () => string | undefined
|
|
34
36
|
/** DOM focus hook provided by the renderer; the core never touches the DOM. */
|
|
35
37
|
focusTrigger?: () => void
|
|
@@ -48,6 +50,7 @@ export function createDropdownList(config: DropdownListConfig) {
|
|
|
48
50
|
const optionId = (index: number) => `${listId}-opt-${index}`
|
|
49
51
|
const opts = () => config.options()
|
|
50
52
|
const disabled = () => config.disabled?.() ?? false
|
|
53
|
+
const readonly = () => config.readonly?.() ?? false
|
|
51
54
|
|
|
52
55
|
const ariaState = (): EditorAriaState => ({
|
|
53
56
|
id: config.id?.(),
|
|
@@ -65,13 +68,14 @@ export function createDropdownList(config: DropdownListConfig) {
|
|
|
65
68
|
const enabledIdx = $derived(opts().map((o, i) => (o.disabled ? -1 : i)).filter((i) => i >= 0))
|
|
66
69
|
|
|
67
70
|
function openPanel() {
|
|
68
|
-
if (disabled() || open) return
|
|
71
|
+
if (disabled() || readonly() || open) return
|
|
69
72
|
open = true
|
|
70
73
|
active = Math.max(0, opts().findIndex((o) => o.value === config.value()))
|
|
71
74
|
}
|
|
72
75
|
function close() { open = false }
|
|
73
76
|
function toggle() { if (open) { close() } else { openPanel() } }
|
|
74
77
|
function pick(i: number) {
|
|
78
|
+
if (readonly()) return
|
|
75
79
|
const o = opts()[i]
|
|
76
80
|
if (!o || o.disabled) return
|
|
77
81
|
config.onChange?.(o.value); close(); config.focusTrigger?.()
|
|
@@ -85,6 +89,7 @@ export function createDropdownList(config: DropdownListConfig) {
|
|
|
85
89
|
// when the list is closed, matching a native <select>).
|
|
86
90
|
const typeahead = createTypeaheadBuffer()
|
|
87
91
|
function onTypeahead(char: string) {
|
|
92
|
+
if (readonly()) return
|
|
88
93
|
const buffer = typeahead.push(char.toLowerCase())
|
|
89
94
|
const from = active >= 0 ? active : opts().findIndex((o) => o.value === config.value())
|
|
90
95
|
const next = nextTypeaheadIndex(opts(), buffer, from)
|
|
@@ -93,7 +98,7 @@ export function createDropdownList(config: DropdownListConfig) {
|
|
|
93
98
|
if (!open) config.onChange?.(opts()[next]!.value)
|
|
94
99
|
}
|
|
95
100
|
function onKeydown(e: KeyboardEvent) {
|
|
96
|
-
if (disabled()) return
|
|
101
|
+
if (disabled() || readonly()) return
|
|
97
102
|
if (isTypeaheadKey(e)) { onTypeahead(e.key); return }
|
|
98
103
|
if (!open && (e.key === 'ArrowDown' || e.key === 'Enter' || e.key === ' ')) { e.preventDefault(); openPanel(); return }
|
|
99
104
|
if (!open) return
|
|
@@ -132,6 +137,7 @@ export function createDropdownList(config: DropdownListConfig) {
|
|
|
132
137
|
'aria-controls': listId,
|
|
133
138
|
'aria-activedescendant': open && opts()[active] ? optionId(active) : undefined,
|
|
134
139
|
...editorAria(ariaState()),
|
|
140
|
+
'aria-readonly': readonly() || undefined,
|
|
135
141
|
disabled: disabled(),
|
|
136
142
|
onclick: toggle,
|
|
137
143
|
onkeydown: onKeydown,
|
package/src/createForm.svelte.ts
CHANGED
|
@@ -22,6 +22,22 @@
|
|
|
22
22
|
import { flattenFields, type FormField, type FormEntry } from './form-field'
|
|
23
23
|
import { runRules } from './validators'
|
|
24
24
|
|
|
25
|
+
/** Localizable strings the form generates itself (override via `messages`). */
|
|
26
|
+
export type FormMessages = {
|
|
27
|
+
required: (label: string) => string
|
|
28
|
+
minItems: (label: string, n: number) => string
|
|
29
|
+
maxItems: (label: string, n: number) => string
|
|
30
|
+
/** Shown by SvForm while an async validator runs. */
|
|
31
|
+
checking: string
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const DEFAULT_FORM_MESSAGES: FormMessages = {
|
|
35
|
+
required: (l) => `${l} is required`,
|
|
36
|
+
minItems: (_l, n) => `Add at least ${n} item(s)`,
|
|
37
|
+
maxItems: (_l, n) => `At most ${n} item(s)`,
|
|
38
|
+
checking: 'Checking…',
|
|
39
|
+
}
|
|
40
|
+
|
|
25
41
|
export type FormConfig = {
|
|
26
42
|
/** The schema - a mix of flat fields and titled sections. */
|
|
27
43
|
fields: () => ReadonlyArray<FormEntry>
|
|
@@ -31,6 +47,8 @@ export type FormConfig = {
|
|
|
31
47
|
* `submitting` stays true until the returned promise settles. */
|
|
32
48
|
onSubmit?: (values: Record<string, any>) => void | Promise<void>
|
|
33
49
|
onChange?: (values: Record<string, any>) => void
|
|
50
|
+
/** Override the built-in generated strings (required / min-max items / checking). */
|
|
51
|
+
messages?: Partial<FormMessages>
|
|
34
52
|
}
|
|
35
53
|
|
|
36
54
|
/** Deep-ish equality for dirty tracking: identity for primitives, structural
|
|
@@ -54,6 +72,7 @@ function resolveFlag(
|
|
|
54
72
|
}
|
|
55
73
|
|
|
56
74
|
export function createForm(config: FormConfig) {
|
|
75
|
+
const M: FormMessages = { ...DEFAULT_FORM_MESSAGES, ...(config.messages ?? {}) }
|
|
57
76
|
let values = $state<Record<string, any>>({ ...(config.initial ?? {}) })
|
|
58
77
|
let errors = $state<Record<string, string>>({})
|
|
59
78
|
let touched = $state<Record<string, boolean>>({})
|
|
@@ -85,7 +104,7 @@ export function createForm(config: FormConfig) {
|
|
|
85
104
|
* rows (so within-row cross-field rules work). */
|
|
86
105
|
function syncError(field: FormField, v: any, ctx: Record<string, any> = values): string | null {
|
|
87
106
|
if (field.required && (v == null || v === '' || (Array.isArray(v) && !v.length)))
|
|
88
|
-
return
|
|
107
|
+
return M.required(field.label)
|
|
89
108
|
if (field.rules) { const e = runRules(v, field.rules, ctx); if (e) return e }
|
|
90
109
|
if (field.validate) { const e = field.validate(v, ctx); if (e) return e }
|
|
91
110
|
return null
|
|
@@ -128,6 +147,8 @@ export function createForm(config: FormConfig) {
|
|
|
128
147
|
function validateField(name: string): boolean {
|
|
129
148
|
const field = fieldOf(name)
|
|
130
149
|
if (!field) return true
|
|
150
|
+
// Computed fields are derived + read-only - never validated, never block submit.
|
|
151
|
+
if (field.computed) { setError(name, null); return true }
|
|
131
152
|
// Hidden fields never block submit; clear any stale error + pending check.
|
|
132
153
|
if (!isVisible(name)) { setError(name, null); cancelAsync(name); return true }
|
|
133
154
|
if (field.type === 'array') return validateArray(field)
|
|
@@ -196,9 +217,9 @@ export function createForm(config: FormConfig) {
|
|
|
196
217
|
const name = field.name
|
|
197
218
|
const items = arrayItems(name)
|
|
198
219
|
let arrErr: string | null = null
|
|
199
|
-
if (field.required && items.length === 0) arrErr =
|
|
200
|
-
else if (field.minItems != null && items.length < field.minItems) arrErr =
|
|
201
|
-
else if (field.maxItems != null && items.length > field.maxItems) arrErr =
|
|
220
|
+
if (field.required && items.length === 0) arrErr = M.required(field.label)
|
|
221
|
+
else if (field.minItems != null && items.length < field.minItems) arrErr = M.minItems(field.label, field.minItems)
|
|
222
|
+
else if (field.maxItems != null && items.length > field.maxItems) arrErr = M.maxItems(field.label, field.maxItems)
|
|
202
223
|
setError(name, arrErr)
|
|
203
224
|
let ok = !arrErr
|
|
204
225
|
clearNamespacedErrors(name) // rebuild fresh per-item errors
|
|
@@ -208,8 +229,23 @@ export function createForm(config: FormConfig) {
|
|
|
208
229
|
return ok
|
|
209
230
|
}
|
|
210
231
|
|
|
232
|
+
/** True when `field` depends on `name` (cascading). */
|
|
233
|
+
function dependsOnName(field: FormField, name: string): boolean {
|
|
234
|
+
const dep = field.dependsOn
|
|
235
|
+
return dep == null ? false : Array.isArray(dep) ? dep.includes(name) : dep === name
|
|
236
|
+
}
|
|
237
|
+
|
|
211
238
|
function setValue(name: string, v: any) {
|
|
212
|
-
|
|
239
|
+
let next = { ...values, [name]: v }
|
|
240
|
+
// Cascading: clear any fields that depend on this one so a stale child
|
|
241
|
+
// selection (e.g. a city under the old country) does not linger.
|
|
242
|
+
for (const f of flat()) {
|
|
243
|
+
if (dependsOnName(f, name) && next[f.name] != null) {
|
|
244
|
+
next = { ...next, [f.name]: null }
|
|
245
|
+
setError(f.name, null)
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
values = next
|
|
213
249
|
config.onChange?.(values)
|
|
214
250
|
// Re-validate live once the field has been blurred at least once.
|
|
215
251
|
if (touched[name]) validateField(name)
|
|
@@ -240,6 +276,8 @@ export function createForm(config: FormConfig) {
|
|
|
240
276
|
// Exclude hidden fields from the submitted payload; keep any non-field keys.
|
|
241
277
|
const payload = { ...values }
|
|
242
278
|
for (const f of flat()) if (!isVisible(f.name)) delete payload[f.name]
|
|
279
|
+
// Inject computed (derived) values so the payload carries them.
|
|
280
|
+
for (const f of flat()) if (f.computed && isVisible(f.name)) payload[f.name] = f.computed(values)
|
|
243
281
|
submitting = true
|
|
244
282
|
try {
|
|
245
283
|
await config.onSubmit?.(payload)
|
|
@@ -294,6 +332,17 @@ export function createForm(config: FormConfig) {
|
|
|
294
332
|
|
|
295
333
|
const isFieldDirty = (name: string) => !eq(values[name], baseline[name])
|
|
296
334
|
|
|
335
|
+
/** Top-level field errors (visible fields only), for a form-level summary. */
|
|
336
|
+
function errorList(): { name: string; label: string; message: string }[] {
|
|
337
|
+
const out: { name: string; label: string; message: string }[] = []
|
|
338
|
+
for (const f of flat()) {
|
|
339
|
+
const m = errors[f.name]
|
|
340
|
+
if (m && isVisible(f.name)) out.push({ name: f.name, label: f.label, message: m })
|
|
341
|
+
}
|
|
342
|
+
return out
|
|
343
|
+
}
|
|
344
|
+
const firstErrorField = (): string | undefined => errorList()[0]?.name
|
|
345
|
+
|
|
297
346
|
return {
|
|
298
347
|
get values() { return values },
|
|
299
348
|
get submitting() { return submitting },
|
|
@@ -302,7 +351,7 @@ export function createForm(config: FormConfig) {
|
|
|
302
351
|
for (const k of keys) if (!eq(values[k], baseline[k])) return true
|
|
303
352
|
return false
|
|
304
353
|
},
|
|
305
|
-
value: (name: string) => values[name],
|
|
354
|
+
value: (name: string) => { const f = fieldOf(name); return f?.computed ? f.computed(values) : values[name] },
|
|
306
355
|
error: (name: string): string | undefined => errors[name],
|
|
307
356
|
isTouched: (name: string) => !!touched[name],
|
|
308
357
|
hasError: (name: string) => !!errors[name],
|
|
@@ -327,6 +376,10 @@ export function createForm(config: FormConfig) {
|
|
|
327
376
|
submit,
|
|
328
377
|
reset,
|
|
329
378
|
setErrors,
|
|
379
|
+
errorList,
|
|
380
|
+
firstErrorField,
|
|
381
|
+
/** The resolved message strings (defaults merged with `config.messages`). */
|
|
382
|
+
messages: M,
|
|
330
383
|
}
|
|
331
384
|
}
|
|
332
385
|
|