@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/dist/SvForm.svelte
CHANGED
|
@@ -12,18 +12,30 @@
|
|
|
12
12
|
* a flat `FormField[]` or mix in titled `FormSection`s; with `stepper` the
|
|
13
13
|
* sections become a validated wizard. Emits `onSubmit(values)` only when valid.
|
|
14
14
|
*/
|
|
15
|
+
import { untrack } from 'svelte'
|
|
15
16
|
import SvNumberInput from './SvNumberInput.svelte'
|
|
16
17
|
import SvPasswordInput from './SvPasswordInput.svelte'
|
|
17
18
|
import SvDropDownList from './SvDropDownList.svelte'
|
|
19
|
+
import SvMultiSelect from './SvMultiSelect.svelte'
|
|
18
20
|
import SvCheckBox from './SvCheckBox.svelte'
|
|
19
21
|
import SvSwitchButton from './SvSwitchButton.svelte'
|
|
20
22
|
import SvDateTimePicker from './SvDateTimePicker.svelte'
|
|
21
23
|
import SvColorInput from './SvColorInput.svelte'
|
|
22
24
|
import SvRating from './SvRating.svelte'
|
|
25
|
+
import SvRadioGroup from './SvRadioGroup.svelte'
|
|
26
|
+
import SvSlider from './SvSlider.svelte'
|
|
27
|
+
import SvTagsInput from './SvTagsInput.svelte'
|
|
28
|
+
import SvPhoneInput from './SvPhoneInput.svelte'
|
|
29
|
+
import SvCountryInput from './SvCountryInput.svelte'
|
|
30
|
+
import SvMaskedInput from './SvMaskedInput.svelte'
|
|
31
|
+
import SvComboBox from './SvComboBox.svelte'
|
|
32
|
+
import SvFileUpload from './SvFileUpload.svelte'
|
|
23
33
|
import SvButton from './SvButton.svelte'
|
|
24
34
|
import SvStepper from './SvStepper.svelte'
|
|
35
|
+
import SvTabs from './SvTabs.svelte'
|
|
25
36
|
import { isFormSection, type FormField, type FormEntry, type FormSection } from './form-field'
|
|
26
|
-
import { createForm } from './createForm.svelte'
|
|
37
|
+
import { createForm, type FormMessages } from './createForm.svelte'
|
|
38
|
+
import type { EditorDir } from './editor-contract'
|
|
27
39
|
|
|
28
40
|
type Props = {
|
|
29
41
|
fields: ReadonlyArray<FormEntry>
|
|
@@ -41,25 +53,97 @@
|
|
|
41
53
|
resetLabel?: string
|
|
42
54
|
/** Render titled sections as a validated multi-step wizard (requires sections). */
|
|
43
55
|
stepper?: boolean
|
|
56
|
+
/** Render titled sections as tabs (requires sections; ignored with `stepper`). */
|
|
57
|
+
tabs?: boolean
|
|
44
58
|
/** Columns in the responsive field grid (a section can override its own). */
|
|
45
59
|
columns?: number
|
|
46
60
|
disabled?: boolean
|
|
61
|
+
/** Inject server-side errors (`{ field: message }`); applied reactively. */
|
|
62
|
+
serverErrors?: Record<string, string>
|
|
63
|
+
/** A form-level error message shown in a banner above the fields. */
|
|
64
|
+
formError?: string
|
|
65
|
+
/** Show a summary of field errors (with focus links) above the form after a
|
|
66
|
+
* failed submit. */
|
|
67
|
+
errorSummary?: boolean
|
|
68
|
+
/** Heading for the error summary. Default "Please fix the following:". */
|
|
69
|
+
errorSummaryTitle?: string
|
|
70
|
+
/**
|
|
71
|
+
* Re-seed the form when the `initial` object identity changes (e.g. editing a
|
|
72
|
+
* different record). `off` (default) seeds once; `always` re-seeds on every
|
|
73
|
+
* change; `ifPristine` re-seeds only when the user has no unsaved edits.
|
|
74
|
+
*/
|
|
75
|
+
reinitialize?: 'off' | 'always' | 'ifPristine'
|
|
76
|
+
/** Text direction; `rtl` mirrors the form layout. */
|
|
77
|
+
dir?: EditorDir
|
|
78
|
+
/** Override the built-in generated strings (required / min-max items / checking). */
|
|
79
|
+
messages?: Partial<FormMessages>
|
|
47
80
|
}
|
|
48
81
|
|
|
49
82
|
let {
|
|
50
83
|
fields, initial = {}, onSubmit, onChange, onCancel,
|
|
51
84
|
submitLabel = 'Submit', cancelLabel = 'Cancel', showReset = false, resetLabel = 'Reset',
|
|
52
|
-
stepper = false, columns = 1, disabled = false,
|
|
85
|
+
stepper = false, tabs = false, columns = 1, disabled = false,
|
|
86
|
+
serverErrors, formError, errorSummary = false, errorSummaryTitle = 'Please fix the following:',
|
|
87
|
+
reinitialize = 'off', dir, messages,
|
|
53
88
|
}: Props = $props()
|
|
54
89
|
|
|
55
|
-
const
|
|
56
|
-
const
|
|
90
|
+
const resolvedDir = $derived(dir === 'ltr' || dir === 'rtl' ? dir : undefined)
|
|
91
|
+
const form = createForm({ fields: () => fields, initial, onSubmit, onChange, messages })
|
|
92
|
+
async function submit(e: Event) {
|
|
93
|
+
e.preventDefault()
|
|
94
|
+
const ok = await form.submit()
|
|
95
|
+
// In tabs mode, surface the first tab that has an error so it is not hidden.
|
|
96
|
+
if (!ok && tabbed) {
|
|
97
|
+
const first = form.firstErrorField()
|
|
98
|
+
const s = first ? sections.find((sec) => sec.fields.some((f) => f.name === first)) : undefined
|
|
99
|
+
if (s) activeTab = s.section
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
let formEl = $state<HTMLFormElement | null>(null)
|
|
104
|
+
// Apply server-side errors reactively (e.g. after a failed API submit). The
|
|
105
|
+
// write is untracked so reading `errors` inside setErrors does not make this
|
|
106
|
+
// effect depend on (and re-run from) its own write.
|
|
107
|
+
$effect(() => {
|
|
108
|
+
const se = serverErrors
|
|
109
|
+
if (se) untrack(() => form.setErrors(se))
|
|
110
|
+
})
|
|
111
|
+
|
|
112
|
+
// Opt-in re-seed when the `initial` identity changes. Only `initial` is tracked
|
|
113
|
+
// (read outside `untrack`); the isDirty check + reset run untracked so this
|
|
114
|
+
// never re-fires from its own writes.
|
|
115
|
+
let seeded = false
|
|
116
|
+
$effect(() => {
|
|
117
|
+
const init = initial
|
|
118
|
+
untrack(() => {
|
|
119
|
+
if (!seeded) { seeded = true; return } // the seed-once mount pass
|
|
120
|
+
if (reinitialize === 'off') return
|
|
121
|
+
if (reinitialize === 'ifPristine' && form.isDirty) return
|
|
122
|
+
form.reset(init)
|
|
123
|
+
})
|
|
124
|
+
})
|
|
125
|
+
|
|
126
|
+
const summaryItems = $derived(errorSummary ? form.errorList() : [])
|
|
127
|
+
function focusField(name: string) {
|
|
128
|
+
const wrap = formEl?.querySelector<HTMLElement>(`[data-field="${name}"]`)
|
|
129
|
+
wrap?.scrollIntoView({ block: 'nearest', behavior: 'smooth' })
|
|
130
|
+
wrap?.querySelector<HTMLElement>('input, select, textarea, button, [tabindex]')?.focus()
|
|
131
|
+
}
|
|
57
132
|
const dateVal = (v: any): Date | null => (v instanceof Date ? v : v ? new Date(v) : null)
|
|
133
|
+
// Resolve a field's options, running the function form (cascading) against the
|
|
134
|
+
// live values so a child list updates when its parent changes.
|
|
135
|
+
const resolveOptions = (field: FormField) =>
|
|
136
|
+
typeof field.options === 'function' ? field.options(form.values) : (field.options ?? [])
|
|
58
137
|
|
|
59
138
|
const sections = $derived(fields.filter(isFormSection) as FormSection[])
|
|
60
139
|
const wizard = $derived(stepper && sections.length > 0)
|
|
140
|
+
const tabbed = $derived(tabs && !stepper && sections.length > 0)
|
|
61
141
|
const stepItems = $derived(sections.map((s) => ({ label: s.section, description: s.description })))
|
|
62
142
|
|
|
143
|
+
let activeTab = $state<string | undefined>(undefined)
|
|
144
|
+
const sectionHasError = (s: FormSection) => s.fields.some((f) => form.error(f.name))
|
|
145
|
+
const tabItems = $derived(sections.map((s) => ({ id: s.section, label: sectionHasError(s) ? `${s.section} ⚠` : s.section })))
|
|
146
|
+
|
|
63
147
|
let step = $state(0)
|
|
64
148
|
async function next() {
|
|
65
149
|
const s = sections[step]
|
|
@@ -79,32 +163,57 @@
|
|
|
79
163
|
onChange: (v: any) => void
|
|
80
164
|
onBlur?: () => void
|
|
81
165
|
disabled: boolean
|
|
166
|
+
readonly?: boolean
|
|
167
|
+
invalid?: boolean
|
|
168
|
+
describedby?: string
|
|
82
169
|
placeholder?: string
|
|
83
170
|
options?: ReadonlyArray<{ value: string | number; label: string; color?: string }>
|
|
171
|
+
/** The source field, for type-specific extras (mask, min/max, loadOptions...). */
|
|
172
|
+
field?: FormField
|
|
84
173
|
}
|
|
85
174
|
</script>
|
|
86
175
|
|
|
87
176
|
{#snippet control(o: ControlOpts)}
|
|
88
177
|
{#if o.type === 'textarea'}
|
|
89
|
-
<textarea id={o.id} class="sv-form__control sv-form__textarea" rows="3" disabled={o.disabled} placeholder={o.placeholder} value={o.value ?? ''} oninput={(e) => o.onChange(e.currentTarget.value)} onblur={o.onBlur}></textarea>
|
|
178
|
+
<textarea id={o.id} class="sv-form__control sv-form__textarea" rows="3" disabled={o.disabled} readonly={o.readonly} placeholder={o.placeholder} value={o.value ?? ''} aria-invalid={o.invalid ? 'true' : undefined} aria-describedby={o.describedby} oninput={(e) => o.onChange(e.currentTarget.value)} onblur={o.onBlur}></textarea>
|
|
90
179
|
{:else if o.type === 'number'}
|
|
91
|
-
<SvNumberInput value={o.value ?? null} disabled={o.disabled} onChange={o.onChange} />
|
|
180
|
+
<SvNumberInput id={o.id} value={o.value ?? null} disabled={o.disabled} readonly={o.readonly} invalid={o.invalid} block min={o.field?.min ?? -Infinity} max={o.field?.max ?? Infinity} step={o.field?.step ?? 1} precision={o.field?.precision} prefix={o.field?.prefix ?? ''} suffix={o.field?.suffix ?? ''} onChange={o.onChange} />
|
|
92
181
|
{:else if o.type === 'password'}
|
|
93
|
-
<SvPasswordInput value={o.value ?? ''} disabled={o.disabled} onChange={o.onChange} />
|
|
182
|
+
<SvPasswordInput id={o.id} value={o.value ?? ''} disabled={o.disabled} readonly={o.readonly} invalid={o.invalid} block onChange={o.onChange} />
|
|
183
|
+
{:else if o.type === 'mask'}
|
|
184
|
+
<SvMaskedInput id={o.id} value={o.value ?? ''} mask={o.field?.mask ?? ''} disabled={o.disabled} readonly={o.readonly} invalid={o.invalid} block placeholder={o.placeholder} onChange={(m) => o.onChange(m)} />
|
|
185
|
+
{:else if o.type === 'phone'}
|
|
186
|
+
<SvPhoneInput id={o.id} value={o.value ?? ''} disabled={o.disabled} invalid={o.invalid} block placeholder={o.placeholder} onChange={(v) => o.onChange(v)} />
|
|
187
|
+
{:else if o.type === 'country'}
|
|
188
|
+
<SvCountryInput id={o.id} value={o.value ?? null} disabled={o.disabled} invalid={o.invalid} onChange={o.onChange} />
|
|
94
189
|
{:else if o.type === 'select'}
|
|
95
|
-
<SvDropDownList options={o.options ?? []} value={o.value ?? null} disabled={o.disabled} placeholder={o.placeholder} onChange={o.onChange} />
|
|
190
|
+
<SvDropDownList id={o.id} options={o.options ?? []} value={o.value ?? null} disabled={o.disabled} readonly={o.readonly} invalid={o.invalid} placeholder={o.placeholder} onChange={o.onChange} />
|
|
191
|
+
{:else if o.type === 'combobox'}
|
|
192
|
+
<SvComboBox id={o.id} options={o.options ?? []} value={o.value ?? null} disabled={o.disabled} readonly={o.readonly} invalid={o.invalid} clearable placeholder={o.placeholder} loadOptions={o.field?.loadOptions ? (q) => o.field!.loadOptions!(q, form.values) : undefined} onChange={o.onChange} />
|
|
193
|
+
{:else if o.type === 'multiselect'}
|
|
194
|
+
<SvMultiSelect id={o.id} options={o.options ?? []} value={Array.isArray(o.value) ? o.value : []} disabled={o.disabled} readonly={o.readonly} invalid={o.invalid} placeholder={o.placeholder} onChange={o.onChange} />
|
|
195
|
+
{:else if o.type === 'radio'}
|
|
196
|
+
<SvRadioGroup id={o.id} options={o.options ?? []} value={o.value ?? null} disabled={o.disabled} readonly={o.readonly} invalid={o.invalid} orientation="horizontal" onChange={o.onChange} />
|
|
197
|
+
{:else if o.type === 'tags'}
|
|
198
|
+
<SvTagsInput id={o.id} value={Array.isArray(o.value) ? o.value : []} disabled={o.disabled} invalid={o.invalid} placeholder={o.placeholder} onChange={o.onChange} />
|
|
199
|
+
{:else if o.type === 'slider'}
|
|
200
|
+
<SvSlider id={o.id} value={o.value ?? o.field?.min ?? 0} min={o.field?.min ?? 0} max={o.field?.max ?? 100} step={o.field?.step ?? 1} disabled={o.disabled} readonly={o.readonly} invalid={o.invalid} showValue onChange={o.onChange} />
|
|
96
201
|
{:else if o.type === 'checkbox'}
|
|
97
|
-
<SvCheckBox checked={!!o.value} disabled={o.disabled} onChange={o.onChange}>{o.label}</SvCheckBox>
|
|
202
|
+
<SvCheckBox checked={!!o.value} disabled={o.disabled} readonly={o.readonly} invalid={o.invalid} onChange={o.onChange}>{o.label}</SvCheckBox>
|
|
98
203
|
{:else if o.type === 'switch'}
|
|
99
|
-
<div class="sv-form__inline"><SvSwitchButton checked={!!o.value} disabled={o.disabled} onChange={o.onChange} ariaLabel={o.label} /><span>{o.label}</span></div>
|
|
204
|
+
<div class="sv-form__inline"><SvSwitchButton checked={!!o.value} disabled={o.disabled} readonly={o.readonly} onChange={o.onChange} ariaLabel={o.label} /><span>{o.label}</span></div>
|
|
100
205
|
{:else if o.type === 'date'}
|
|
101
206
|
<SvDateTimePicker value={dateVal(o.value)} dropDownDisplayMode="calendar" formatString="yyyy-MM-dd" disabled={o.disabled} onChange={o.onChange} />
|
|
207
|
+
{:else if o.type === 'datetime'}
|
|
208
|
+
<SvDateTimePicker value={dateVal(o.value)} dropDownDisplayMode="both" disabled={o.disabled} onChange={o.onChange} />
|
|
102
209
|
{:else if o.type === 'color'}
|
|
103
|
-
<SvColorInput value={o.value ?? '#3b82f6'} disabled={o.disabled} onChange={o.onChange} />
|
|
210
|
+
<SvColorInput id={o.id} value={o.value ?? '#3b82f6'} disabled={o.disabled} invalid={o.invalid} onChange={o.onChange} />
|
|
211
|
+
{:else if o.type === 'file'}
|
|
212
|
+
<SvFileUpload accept={o.field?.accept} multiple={o.field?.multiple ?? false} onChange={o.onChange} />
|
|
104
213
|
{:else if o.type === 'rating'}
|
|
105
|
-
<SvRating value={o.value ?? 0} disabled={o.disabled} onChange={o.onChange} />
|
|
214
|
+
<SvRating id={o.id} value={o.value ?? 0} disabled={o.disabled} readonly={o.readonly} invalid={o.invalid} onChange={o.onChange} />
|
|
106
215
|
{:else}
|
|
107
|
-
<input id={o.id} class="sv-form__control" type={o.type} disabled={o.disabled} placeholder={o.placeholder} value={o.value ?? ''} oninput={(e) => o.onChange(e.currentTarget.value)} onblur={o.onBlur} />
|
|
216
|
+
<input id={o.id} class="sv-form__control" type={o.type} disabled={o.disabled} readonly={o.readonly} placeholder={o.placeholder} value={o.value ?? ''} aria-invalid={o.invalid ? 'true' : undefined} aria-describedby={o.describedby} oninput={(e) => o.onChange(e.currentTarget.value)} onblur={o.onBlur} />
|
|
108
217
|
{/if}
|
|
109
218
|
{/snippet}
|
|
110
219
|
|
|
@@ -116,12 +225,13 @@
|
|
|
116
225
|
{#if type !== 'checkbox' && type !== 'switch'}
|
|
117
226
|
<label class="sv-form__label" for={id}>{itf.label}{#if itf.required}<span class="sv-form__req" aria-hidden="true"> *</span>{/if}</label>
|
|
118
227
|
{/if}
|
|
119
|
-
{@render control({ id, type, label: itf.label, value: form.itemValue(arrayName, i, itf.name), onChange: (v) => form.setItemValue(arrayName, i, itf.name, v), onBlur: () => form.handleItemBlur(arrayName, i, itf.name), disabled, placeholder: itf.placeholder, options: itf
|
|
120
|
-
{#if err}<span class="sv-form__error" role="alert">{err}</span>{/if}
|
|
228
|
+
{@render control({ id, type, label: itf.label, value: form.itemValue(arrayName, i, itf.name), onChange: (v) => form.setItemValue(arrayName, i, itf.name, v), onBlur: () => form.handleItemBlur(arrayName, i, itf.name), disabled, readonly: itf.readonly, invalid: !!err, describedby: err ? `${id}__error` : undefined, placeholder: itf.placeholder, options: resolveOptions(itf), field: itf })}
|
|
229
|
+
{#if err}<span class="sv-form__error" id={`${id}__error`} role="alert">{err}</span>{/if}
|
|
121
230
|
</div>
|
|
122
231
|
{/snippet}
|
|
123
232
|
|
|
124
233
|
{#snippet renderArray(field: FormField)}
|
|
234
|
+
{@const count = form.arrayItems(field.name).length}
|
|
125
235
|
<div class="sv-form__field is-full sv-form__array" class:has-error={form.error(field.name)}>
|
|
126
236
|
<label class="sv-form__label">{field.label}{#if field.required}<span class="sv-form__req" aria-hidden="true"> *</span>{/if}</label>
|
|
127
237
|
{#each form.arrayItems(field.name) as _item, i (i)}
|
|
@@ -129,7 +239,15 @@
|
|
|
129
239
|
<div class="sv-form__grid" style:--cols={field.itemFields?.length ?? 1}>
|
|
130
240
|
{#each field.itemFields ?? [] as itf (itf.name)}{@render renderItemField(field.name, i, itf)}{/each}
|
|
131
241
|
</div>
|
|
132
|
-
<
|
|
242
|
+
<div class="sv-form__arow-tools">
|
|
243
|
+
{#if count > 1}
|
|
244
|
+
<div class="sv-form__amove">
|
|
245
|
+
<button type="button" class="sv-form__amove-btn" disabled={disabled || i === 0} onclick={() => form.moveItem(field.name, i, i - 1)} aria-label="Move up" title="Move up">↑</button>
|
|
246
|
+
<button type="button" class="sv-form__amove-btn" disabled={disabled || i === count - 1} onclick={() => form.moveItem(field.name, i, i + 1)} aria-label="Move down" title="Move down">↓</button>
|
|
247
|
+
</div>
|
|
248
|
+
{/if}
|
|
249
|
+
<button type="button" class="sv-form__aremove" disabled={disabled} onclick={() => form.removeItem(field.name, i)} aria-label="Remove item">×</button>
|
|
250
|
+
</div>
|
|
133
251
|
</div>
|
|
134
252
|
{/each}
|
|
135
253
|
<div><SvButton type="button" variant="outline" size="sm" disabled={disabled} onclick={() => form.addItem(field.name)}>{field.addLabel ?? '+ Add'}</SvButton></div>
|
|
@@ -144,17 +262,19 @@
|
|
|
144
262
|
{@render renderArray(field)}
|
|
145
263
|
{:else}
|
|
146
264
|
{@const fieldDisabled = disabled || form.isDisabled(field.name)}
|
|
147
|
-
<div class="sv-form__field" class:is-full={field.full || cols === 1} class:has-error={form.error(field.name)}>
|
|
265
|
+
<div class="sv-form__field" data-field={field.name} class:is-full={(field.full || (field.span ?? 1) >= cols) || cols === 1} class:has-error={form.error(field.name)} style:grid-column={field.span && field.span > 1 && field.span < cols ? `span ${field.span}` : undefined}>
|
|
148
266
|
{#if type !== 'checkbox' && type !== 'switch'}
|
|
149
267
|
<label class="sv-form__label" for={`f-${field.name}`}>
|
|
150
268
|
{field.label}{#if field.required}<span class="sv-form__req" aria-hidden="true"> *</span>{/if}
|
|
151
269
|
</label>
|
|
152
270
|
{/if}
|
|
153
|
-
{@render control({ id: `f-${field.name}`, type, label: field.label, value: form.value(field.name), onChange: (v) => form.setValue(field.name, v), onBlur: () => form.handleBlur(field.name), disabled: fieldDisabled, placeholder: field.placeholder, options: field
|
|
271
|
+
{@render control({ id: `f-${field.name}`, type, label: field.label, value: form.value(field.name), onChange: (v) => form.setValue(field.name, v), onBlur: () => form.handleBlur(field.name), disabled: fieldDisabled, readonly: field.readonly || !!field.computed, invalid: !!form.error(field.name), describedby: form.error(field.name) ? `f-${field.name}__error` : undefined, placeholder: field.placeholder, options: resolveOptions(field), field })}
|
|
154
272
|
{#if form.error(field.name)}
|
|
155
|
-
<span class="sv-form__error" role="alert">{form.error(field.name)}</span>
|
|
273
|
+
<span class="sv-form__error" id={`f-${field.name}__error`} role="alert">{form.error(field.name)}</span>
|
|
156
274
|
{:else if form.isValidating(field.name)}
|
|
157
|
-
<span class="sv-form__checking" role="status"><span class="sv-form__spinner" aria-hidden="true"></span>
|
|
275
|
+
<span class="sv-form__checking" role="status"><span class="sv-form__spinner" aria-hidden="true"></span>{form.messages.checking}</span>
|
|
276
|
+
{:else if field.help}
|
|
277
|
+
<span class="sv-form__help">{field.help}</span>
|
|
158
278
|
{/if}
|
|
159
279
|
</div>
|
|
160
280
|
{/if}
|
|
@@ -179,7 +299,20 @@
|
|
|
179
299
|
</div>
|
|
180
300
|
{/snippet}
|
|
181
301
|
|
|
182
|
-
<form class="sv-form" onsubmit={submit} novalidate>
|
|
302
|
+
<form class="sv-form" bind:this={formEl} dir={resolvedDir} onsubmit={submit} novalidate>
|
|
303
|
+
{#if formError}
|
|
304
|
+
<div class="sv-form__banner" role="alert">{formError}</div>
|
|
305
|
+
{/if}
|
|
306
|
+
{#if errorSummary && summaryItems.length}
|
|
307
|
+
<div class="sv-form__summary" role="alert">
|
|
308
|
+
<p class="sv-form__summary-title">{errorSummaryTitle}</p>
|
|
309
|
+
<ul>
|
|
310
|
+
{#each summaryItems as it (it.name)}
|
|
311
|
+
<li><button type="button" class="sv-form__summary-link" onclick={() => focusField(it.name)}>{it.label}: {it.message}</button></li>
|
|
312
|
+
{/each}
|
|
313
|
+
</ul>
|
|
314
|
+
</div>
|
|
315
|
+
{/if}
|
|
183
316
|
{#if wizard}
|
|
184
317
|
{@const s = sections[Math.min(step, sections.length - 1)]!}
|
|
185
318
|
<SvStepper steps={stepItems} current={step} onChange={(n) => (step = n)} linear />
|
|
@@ -201,6 +334,17 @@
|
|
|
201
334
|
<SvButton type="submit" loading={form.submitting} disabled={disabled || form.submitting}>{submitLabel}</SvButton>
|
|
202
335
|
{/if}
|
|
203
336
|
</div>
|
|
337
|
+
{:else if tabbed}
|
|
338
|
+
<SvTabs tabs={tabItems} bind:value={activeTab} ariaLabel="Form sections">
|
|
339
|
+
{#snippet panel(id)}
|
|
340
|
+
{@const s = sections.find((sec) => sec.section === id)}
|
|
341
|
+
{#if s}
|
|
342
|
+
{#if s.description}<p class="sv-form__section-desc">{s.description}</p>{/if}
|
|
343
|
+
{@render grid(s.fields, s.columns ?? columns)}
|
|
344
|
+
{/if}
|
|
345
|
+
{/snippet}
|
|
346
|
+
</SvTabs>
|
|
347
|
+
{@render actions()}
|
|
204
348
|
{:else if sections.length > 0}
|
|
205
349
|
{#each fields as entry (isFormSection(entry) ? entry.section : entry.name)}
|
|
206
350
|
{#if isFormSection(entry)}
|
|
@@ -240,6 +384,24 @@
|
|
|
240
384
|
.sv-form__field.has-error .sv-form__control { border-color: var(--sg-danger, #dc2626); }
|
|
241
385
|
.sv-form__inline { display: flex; align-items: center; gap: 8px; font-size: 13px; }
|
|
242
386
|
.sv-form__error { font-size: 11.5px; color: var(--sg-danger, #dc2626); }
|
|
387
|
+
.sv-form__help { font-size: 11.5px; color: var(--sg-muted, #64748b); }
|
|
388
|
+
.sv-form__banner {
|
|
389
|
+
padding: 10px 12px; border-radius: 8px; font-size: 13px;
|
|
390
|
+
background: color-mix(in srgb, var(--sg-danger, #dc2626) 10%, transparent);
|
|
391
|
+
color: var(--sg-danger, #dc2626); border: 1px solid color-mix(in srgb, var(--sg-danger, #dc2626) 35%, transparent);
|
|
392
|
+
}
|
|
393
|
+
.sv-form__summary {
|
|
394
|
+
padding: 12px 14px; border-radius: 8px;
|
|
395
|
+
background: color-mix(in srgb, var(--sg-danger, #dc2626) 7%, transparent);
|
|
396
|
+
border: 1px solid color-mix(in srgb, var(--sg-danger, #dc2626) 30%, transparent);
|
|
397
|
+
}
|
|
398
|
+
.sv-form__summary-title { margin: 0 0 6px; font-size: 13px; font-weight: 700; color: var(--sg-danger, #dc2626); }
|
|
399
|
+
.sv-form__summary ul { margin: 0; padding-inline-start: 18px; display: flex; flex-direction: column; gap: 3px; }
|
|
400
|
+
.sv-form__summary-link {
|
|
401
|
+
background: none; border: 0; padding: 0; font: inherit; font-size: 12.5px; text-align: start;
|
|
402
|
+
color: var(--sg-danger, #dc2626); cursor: pointer; text-decoration: underline;
|
|
403
|
+
}
|
|
404
|
+
.sv-form__summary-link:hover { color: var(--sg-fg, #0f172a); }
|
|
243
405
|
.sv-form__checking { display: inline-flex; align-items: center; gap: 5px; font-size: 11.5px; color: var(--sg-muted, #64748b); }
|
|
244
406
|
.sv-form__spinner {
|
|
245
407
|
width: 11px; height: 11px; border-radius: 50%;
|
|
@@ -260,4 +422,16 @@
|
|
|
260
422
|
}
|
|
261
423
|
.sv-form__aremove:hover:not(:disabled) { color: var(--sg-danger, #dc2626); border-color: var(--sg-danger, #dc2626); }
|
|
262
424
|
.sv-form__aremove:disabled { opacity: 0.5; cursor: default; }
|
|
425
|
+
.sv-form__arow-tools { display: flex; align-items: center; gap: 4px; margin-top: 10px; }
|
|
426
|
+
.sv-form__amove { display: inline-flex; flex-direction: column; }
|
|
427
|
+
.sv-form__amove-btn {
|
|
428
|
+
width: 22px; height: 14px; display: grid; place-items: center; padding: 0; cursor: pointer;
|
|
429
|
+
border: 1px solid var(--sg-border, #cbd5e1); background: var(--sg-input-bg, #fff); color: var(--sg-muted, #64748b);
|
|
430
|
+
font-size: 10px; line-height: 1;
|
|
431
|
+
}
|
|
432
|
+
.sv-form__amove-btn:first-child { border-radius: 6px 6px 0 0; border-bottom: 0; }
|
|
433
|
+
.sv-form__amove-btn:last-child { border-radius: 0 0 6px 6px; }
|
|
434
|
+
.sv-form__amove-btn:hover:not(:disabled) { color: var(--sg-accent, #2563eb); }
|
|
435
|
+
.sv-form__amove-btn:disabled { opacity: 0.4; cursor: default; }
|
|
436
|
+
.sv-form__arow-tools .sv-form__aremove { margin-top: 0; }
|
|
263
437
|
</style>
|
package/dist/SvForm.svelte.d.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
export type { FormField, FormFieldType, FormSection, FormEntry } from './form-field';
|
|
2
2
|
import { type FormEntry } from './form-field';
|
|
3
|
+
import { type FormMessages } from './createForm.svelte';
|
|
4
|
+
import type { EditorDir } from './editor-contract';
|
|
3
5
|
type Props = {
|
|
4
6
|
fields: ReadonlyArray<FormEntry>;
|
|
5
7
|
initial?: Record<string, any>;
|
|
@@ -16,9 +18,30 @@ type Props = {
|
|
|
16
18
|
resetLabel?: string;
|
|
17
19
|
/** Render titled sections as a validated multi-step wizard (requires sections). */
|
|
18
20
|
stepper?: boolean;
|
|
21
|
+
/** Render titled sections as tabs (requires sections; ignored with `stepper`). */
|
|
22
|
+
tabs?: boolean;
|
|
19
23
|
/** Columns in the responsive field grid (a section can override its own). */
|
|
20
24
|
columns?: number;
|
|
21
25
|
disabled?: boolean;
|
|
26
|
+
/** Inject server-side errors (`{ field: message }`); applied reactively. */
|
|
27
|
+
serverErrors?: Record<string, string>;
|
|
28
|
+
/** A form-level error message shown in a banner above the fields. */
|
|
29
|
+
formError?: string;
|
|
30
|
+
/** Show a summary of field errors (with focus links) above the form after a
|
|
31
|
+
* failed submit. */
|
|
32
|
+
errorSummary?: boolean;
|
|
33
|
+
/** Heading for the error summary. Default "Please fix the following:". */
|
|
34
|
+
errorSummaryTitle?: string;
|
|
35
|
+
/**
|
|
36
|
+
* Re-seed the form when the `initial` object identity changes (e.g. editing a
|
|
37
|
+
* different record). `off` (default) seeds once; `always` re-seeds on every
|
|
38
|
+
* change; `ifPristine` re-seeds only when the user has no unsaved edits.
|
|
39
|
+
*/
|
|
40
|
+
reinitialize?: 'off' | 'always' | 'ifPristine';
|
|
41
|
+
/** Text direction; `rtl` mirrors the form layout. */
|
|
42
|
+
dir?: EditorDir;
|
|
43
|
+
/** Override the built-in generated strings (required / min-max items / checking). */
|
|
44
|
+
messages?: Partial<FormMessages>;
|
|
22
45
|
};
|
|
23
46
|
declare const SvForm: import("svelte").Component<Props, {}, "">;
|
|
24
47
|
type SvForm = ReturnType<typeof SvForm>;
|
|
@@ -12,8 +12,9 @@ import { getColumnBaseValue, isGroupRow, toolPanelHeaderLabel, formatSummaryNume
|
|
|
12
12
|
* the component lifecycle) and consumed through the returned getters.
|
|
13
13
|
*/
|
|
14
14
|
export type SvGridController<TFeatures extends TableFeatures = TableFeatures, TData extends RowData = RowData> = ReturnType<typeof createSvGridController<TFeatures, TData>>;
|
|
15
|
-
export declare function createSvGridController<TFeatures extends TableFeatures = TableFeatures, TData extends RowData = RowData>(
|
|
15
|
+
export declare function createSvGridController<TFeatures extends TableFeatures = TableFeatures, TData extends RowData = RowData>(rawProps: Props<TFeatures, TData>): {
|
|
16
16
|
readonly props: Props<TFeatures, TData>;
|
|
17
|
+
optionOverrides: Record<string, unknown>;
|
|
17
18
|
readonly editingEnabled: boolean;
|
|
18
19
|
readonly paginationEnabled: boolean;
|
|
19
20
|
readonly groupingControlsEnabled: boolean;
|
|
@@ -283,6 +284,16 @@ export declare function createSvGridController<TFeatures extends TableFeatures =
|
|
|
283
284
|
readonly toolPanelHeaderLabel: typeof toolPanelHeaderLabel;
|
|
284
285
|
readonly toggleColumnVisibleInPanel: (columnId: string) => void;
|
|
285
286
|
readonly moveColumnInPanel: (columnId: string, dir: -1 | 1) => void;
|
|
287
|
+
readonly messages: import("./grid-messages").GridMessages;
|
|
288
|
+
readonly pivotConfig: import("./pivot-view.svelte").GridPivotConfig<TData> | null;
|
|
289
|
+
readonly pivotActive: boolean;
|
|
290
|
+
readonly pivotResult: {
|
|
291
|
+
rows: import("./pivot-view.svelte").GridPivotRow[];
|
|
292
|
+
columns: ColumnDef<TableFeatures, import("./pivot-view.svelte").GridPivotRow>[];
|
|
293
|
+
} | null;
|
|
294
|
+
readonly pivotModeOn: boolean;
|
|
295
|
+
readonly hasPivotEngine: boolean;
|
|
296
|
+
togglePivotMode: () => void;
|
|
286
297
|
readonly chartingEnabled: boolean;
|
|
287
298
|
readonly chartingConfig: {
|
|
288
299
|
position: "right" | "bottom";
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { applyExcelFilter, normalizeForFilter, createColumnVirtualizer, createCoreRowModel, createExpandedRowModel, createFilteredRowModel, createGroupedRowModel, createSvelteVirtualizer, createSortedRowModel, createSvGrid, getGridCellDomId,
|
|
1
|
+
import { applyExcelFilter, normalizeForFilter, createColumnVirtualizer, createCoreRowModel, createExpandedRowModel, createFilteredRowModel, createGroupedRowModel, createSvelteVirtualizer, createSortedRowModel, createSvGrid, getGridCellDomId, sortFns, } from "./index";
|
|
2
2
|
import { createRowScrollScaling, resolveMaxDomHeight, } from "./virtualization/scroll-scaling";
|
|
3
3
|
import "./sv-grid-scrollbar";
|
|
4
4
|
import { computeColumnStat, formatsNeedingStats, formatNeedsStats, } from "./conditional-formatting";
|
|
@@ -18,6 +18,8 @@ import { resolveColumnTypes, } from "./column-types";
|
|
|
18
18
|
import { computeColumnGroupMeta, hiddenLeavesForCollapse, } from "./column-groups";
|
|
19
19
|
import { createGridApi, } from "./build-api";
|
|
20
20
|
import { createClipboard, } from "./clipboard";
|
|
21
|
+
import { resolveGridMessages } from "./grid-messages";
|
|
22
|
+
import { getPivotEngine, hasPivotEngine } from "./pivot-view.svelte";
|
|
21
23
|
import { filterOperatorOptions, fallbackOperatorOption, TEXT_OPERATORS, NUMBER_OPERATORS, DATE_OPERATORS, CHECKBOX_OPERATORS, operatorOption, operatorsForColumn, defaultOperatorFor, operatorLabelFor, } from "./filter-operators";
|
|
22
24
|
import { isBucketableColumn, buildBuckets, isInBucket, } from "./facet-buckets";
|
|
23
25
|
import { getColumnBaseValue, isGroupRow, toolPanelHeaderLabel, formatSummaryNumeric, getColumnAlign, getPinnedCellValue, getColumnAccessorValue, columnDefMatchesId, } from "./cell-values";
|
|
@@ -143,7 +145,26 @@ function observeSizeRaf(el, cb) {
|
|
|
143
145
|
observer.disconnect();
|
|
144
146
|
};
|
|
145
147
|
}
|
|
146
|
-
export function createSvGridController(
|
|
148
|
+
export function createSvGridController(rawProps) {
|
|
149
|
+
// Runtime option overrides set via the imperative api (`api.setOption(key, value)`).
|
|
150
|
+
// Every controller read of a prop goes through the `props` proxy below (and the view
|
|
151
|
+
// reads controller-resolved gates like `_features` / `editingEnabled`, not the raw
|
|
152
|
+
// prop), so an override wins over the incoming prop and re-renders reactively -
|
|
153
|
+
// identical to the parent flipping that prop. A get-only Proxy (not a fresh spread)
|
|
154
|
+
// preserves reference identity for non-overridden keys, which the columns-identity
|
|
155
|
+
// column cache and the `data`/`columns` sync effects rely on.
|
|
156
|
+
let optionOverrides = $state({});
|
|
157
|
+
const props = new Proxy(rawProps, {
|
|
158
|
+
// Read the override as a PROPERTY (tracked by `$state`), not via `in` (the `has`
|
|
159
|
+
// trap is not tracked) - so a reactive read of `props.X` re-runs when an override
|
|
160
|
+
// is set. `undefined` means "no override" and falls back to the incoming prop;
|
|
161
|
+
// `setOption(key, undefined)` clears by removing the key, which lands here as
|
|
162
|
+
// undefined too. Whole-object reassignment on write makes the signal fire.
|
|
163
|
+
get: (t, k) => {
|
|
164
|
+
const v = typeof k === "string" ? optionOverrides[k] : undefined;
|
|
165
|
+
return v !== undefined ? v : Reflect.get(t, k);
|
|
166
|
+
},
|
|
167
|
+
});
|
|
147
168
|
// Resolved capability gates. Capabilities are OFF by default - a bare
|
|
148
169
|
// grid is a plain read-only table, and each power feature is opted into
|
|
149
170
|
// via its shortcut (`editable` / `pageable` / `groupable`) or the matching
|
|
@@ -466,7 +487,8 @@ export function createSvGridController(props) {
|
|
|
466
487
|
state: {
|
|
467
488
|
columnFilters: [],
|
|
468
489
|
grouping: [],
|
|
469
|
-
|
|
490
|
+
// svelte-ignore state_referenced_locally
|
|
491
|
+
sorting: props.initialSorting ?? [],
|
|
470
492
|
// svelte-ignore state_referenced_locally
|
|
471
493
|
pagination: { pageIndex: 0, pageSize: props.pageSize ?? 10 },
|
|
472
494
|
rowSelection: {},
|
|
@@ -808,10 +830,10 @@ export function createSvGridController(props) {
|
|
|
808
830
|
return rawRows;
|
|
809
831
|
let rows = rawRows;
|
|
810
832
|
if (globalFilter.trim()) {
|
|
811
|
-
const needle = normalizeForFilter(globalFilter, props.filterLocale);
|
|
833
|
+
const needle = normalizeForFilter(globalFilter, (props.filterLocale ?? props.localization?.locale));
|
|
812
834
|
rows = rows.filter((row) => row
|
|
813
835
|
.getAllCells()
|
|
814
|
-
.some((cell) => normalizeForFilter(String(cell.getValue() ?? ""), props.filterLocale)
|
|
836
|
+
.some((cell) => normalizeForFilter(String(cell.getValue() ?? ""), (props.filterLocale ?? props.localization?.locale))
|
|
815
837
|
.includes(needle)));
|
|
816
838
|
}
|
|
817
839
|
// A single condition is "active" if it has the value(s) it needs.
|
|
@@ -822,7 +844,7 @@ export function createSvGridController(props) {
|
|
|
822
844
|
return value.trim().length > 0 && (valueTo ?? "").trim().length > 0;
|
|
823
845
|
return value.trim().length > 0;
|
|
824
846
|
};
|
|
825
|
-
const evalCond = (cellValue, columnId, op, value, valueTo) => applyExcelFilter(cellValue, { id: columnId, operator: op, value, valueTo: op === "between" ? valueTo : undefined }, { locale: props.filterLocale });
|
|
847
|
+
const evalCond = (cellValue, columnId, op, value, valueTo) => applyExcelFilter(cellValue, { id: columnId, operator: op, value, valueTo: op === "between" ? valueTo : undefined }, { locale: (props.filterLocale ?? props.localization?.locale) });
|
|
826
848
|
// A column filter is active if either of its (up to two) conditions is.
|
|
827
849
|
const menuFilters = Object.entries(filterMenuValues).filter(([_, f]) => {
|
|
828
850
|
const a = condActive(f.operator, f.value, f.valueTo);
|
|
@@ -1048,6 +1070,39 @@ export function createSvGridController(props) {
|
|
|
1048
1070
|
return c === true ? {} : c;
|
|
1049
1071
|
});
|
|
1050
1072
|
const chartingEnabled = $derived(!!chartCfg);
|
|
1073
|
+
// Resolved chrome messages (English defaults merged with `localeText`). One
|
|
1074
|
+
// map every consumer (SvGrid.svelte, GridFooter, menus, filter labels) reads.
|
|
1075
|
+
const gridMessages = $derived(resolveGridMessages(props.localization?.text));
|
|
1076
|
+
// ---- In-grid pivot mode ----
|
|
1077
|
+
// The pivot ENGINE ships in @svgrid/enterprise (registered via the pivot-view
|
|
1078
|
+
// seam). When `pivot` is set and the engine is present, the grid runs it over
|
|
1079
|
+
// the already filtered + sorted leaf rows and renders the pivot result in
|
|
1080
|
+
// place. `pivotModeOn` is uncontrolled (seeded from the prop) so a toolbar
|
|
1081
|
+
// toggle can flip between the pivot and the flat table over the same data.
|
|
1082
|
+
const pivotConfig = $derived(props.pivot ?? null);
|
|
1083
|
+
// Controlled when `pivotMode` prop is provided; otherwise internal state
|
|
1084
|
+
// (seeded to on when `pivot` is set) drives it, flipped by the toolbar toggle.
|
|
1085
|
+
let pivotModeInternal = $state(props.pivotMode ?? (props.pivot != null));
|
|
1086
|
+
const pivotModeOn = $derived(props.pivotMode ?? pivotModeInternal);
|
|
1087
|
+
function togglePivotMode() {
|
|
1088
|
+
const next = !pivotModeOn;
|
|
1089
|
+
pivotModeInternal = next;
|
|
1090
|
+
props.onPivotModeChange?.(next);
|
|
1091
|
+
}
|
|
1092
|
+
const pivotActive = $derived(!!pivotConfig && pivotModeOn && hasPivotEngine());
|
|
1093
|
+
const pivotResult = $derived.by(() => {
|
|
1094
|
+
if (!pivotActive || !pivotConfig)
|
|
1095
|
+
return null;
|
|
1096
|
+
const engine = getPivotEngine();
|
|
1097
|
+
if (!engine)
|
|
1098
|
+
return null;
|
|
1099
|
+
// Feed the pivot the filtered + sorted LEAF rows (drop any group rows); its
|
|
1100
|
+
// output must not be re-run through the column-filter pipeline.
|
|
1101
|
+
const leaves = allRowsBeforePagination
|
|
1102
|
+
.filter((r) => !r.subRows)
|
|
1103
|
+
.map((r) => r.original);
|
|
1104
|
+
return engine(leaves, pivotConfig);
|
|
1105
|
+
});
|
|
1051
1106
|
const chartIsCustom = $derived(typeof chartCfg?.buildSpec === "function");
|
|
1052
1107
|
const chartingConfig = $derived(chartCfg
|
|
1053
1108
|
? {
|
|
@@ -1300,6 +1355,17 @@ export function createSvGridController(props) {
|
|
|
1300
1355
|
})),
|
|
1301
1356
|
};
|
|
1302
1357
|
};
|
|
1358
|
+
// The chart derivation engine (rowsToChartSpec) is loaded lazily the first time
|
|
1359
|
+
// charting is enabled, keeping chart.ts (~11 KB gzip) out of the base <SvGrid>
|
|
1360
|
+
// bundle - it rides in the same lazy chunk as the chart panel. `chartSpec` stays
|
|
1361
|
+
// null until it resolves, a tick after the panel itself starts loading (which it
|
|
1362
|
+
// already does). The `getAggregate` server path uses the local bucketsToSpec and
|
|
1363
|
+
// does not need the engine.
|
|
1364
|
+
let chartEngine = $state(null);
|
|
1365
|
+
$effect(() => {
|
|
1366
|
+
if (chartingEnabled && !chartEngine)
|
|
1367
|
+
import("./chart").then((m) => (chartEngine = m.rowsToChartSpec));
|
|
1368
|
+
});
|
|
1303
1369
|
const chartSpec = $derived.by(() => {
|
|
1304
1370
|
if (!chartingEnabled || !chartCfg)
|
|
1305
1371
|
return null;
|
|
@@ -1327,8 +1393,11 @@ export function createSvGridController(props) {
|
|
|
1327
1393
|
const values = effectiveChartMeasureIds.map((id) => fieldOf(id)).filter((f) => !!f);
|
|
1328
1394
|
if (!category || !values.length)
|
|
1329
1395
|
return null;
|
|
1396
|
+
// Engine not loaded yet (lazy): render nothing until it resolves.
|
|
1397
|
+
if (!chartEngine)
|
|
1398
|
+
return null;
|
|
1330
1399
|
const seriesField = fieldOf(effectiveChartSeriesId);
|
|
1331
|
-
const spec =
|
|
1400
|
+
const spec = chartEngine(chartRows, {
|
|
1332
1401
|
type: chartType,
|
|
1333
1402
|
category: category,
|
|
1334
1403
|
value: values.length === 1 ? values[0] : values,
|
|
@@ -2142,6 +2211,8 @@ export function createSvGridController(props) {
|
|
|
2142
2211
|
});
|
|
2143
2212
|
const ctx = {
|
|
2144
2213
|
get props() { return props; },
|
|
2214
|
+
get optionOverrides() { return optionOverrides; },
|
|
2215
|
+
set optionOverrides(v) { optionOverrides = v; },
|
|
2145
2216
|
get editingEnabled() { return editingEnabled; },
|
|
2146
2217
|
get paginationEnabled() { return paginationEnabled; },
|
|
2147
2218
|
get groupingControlsEnabled() { return groupingControlsEnabled; },
|
|
@@ -2379,6 +2450,15 @@ export function createSvGridController(props) {
|
|
|
2379
2450
|
get toolPanelHeaderLabel() { return toolPanelHeaderLabel; },
|
|
2380
2451
|
get toggleColumnVisibleInPanel() { return toggleColumnVisibleInPanel; },
|
|
2381
2452
|
get moveColumnInPanel() { return moveColumnInPanel; },
|
|
2453
|
+
// ---- Localized chrome strings ----
|
|
2454
|
+
get messages() { return gridMessages; },
|
|
2455
|
+
// ---- In-grid pivot mode ----
|
|
2456
|
+
get pivotConfig() { return pivotConfig; },
|
|
2457
|
+
get pivotActive() { return pivotActive; },
|
|
2458
|
+
get pivotResult() { return pivotResult; },
|
|
2459
|
+
get pivotModeOn() { return pivotModeOn; },
|
|
2460
|
+
get hasPivotEngine() { return hasPivotEngine(); },
|
|
2461
|
+
togglePivotMode,
|
|
2382
2462
|
// ---- Integrated charting ----
|
|
2383
2463
|
get chartingEnabled() { return chartingEnabled; },
|
|
2384
2464
|
get chartingConfig() { return chartingConfig; },
|