@svgrid/grid 2.2.32 → 2.4.0
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 +199 -199
- package/dist/DockNodeView.svelte +1 -1
- package/dist/FlexRender.svelte +96 -96
- package/dist/GridFooter.svelte +178 -179
- package/dist/GridMenus.svelte +705 -782
- package/dist/SvAutoComplete.svelte +165 -165
- package/dist/SvAvatar.svelte +75 -75
- package/dist/SvCalendar.svelte +503 -503
- package/dist/SvCarousel.svelte +141 -141
- package/dist/SvCheckBox.svelte +102 -102
- package/dist/SvCircularProgress.svelte +109 -109
- package/dist/SvColorInput.svelte +181 -181
- package/dist/SvComboBox.svelte +275 -275
- package/dist/SvContextMenu.svelte +116 -116
- package/dist/SvCountryInput.svelte +160 -160
- package/dist/SvDateRangeInput.svelte +1 -1
- package/dist/SvDrawer.svelte +254 -254
- package/dist/SvDropDownList.svelte +374 -374
- package/dist/SvDurationInput.svelte +126 -126
- package/dist/SvField.svelte +293 -293
- package/dist/SvForm.svelte +437 -437
- package/dist/SvGrid.css +2814 -2804
- package/dist/SvGrid.svelte +3490 -3513
- package/dist/SvGridChart.svelte +1716 -1724
- package/dist/SvGridChartPanel.svelte +485 -485
- package/dist/SvGridChartView.svelte +70 -70
- package/dist/SvGridDropdown.svelte +728 -728
- package/dist/SvGridSelect.svelte +266 -268
- package/dist/SvGroupCell.svelte +116 -116
- package/dist/SvKbd.svelte +1 -1
- package/dist/SvList.svelte +1 -1
- package/dist/SvListBox.svelte +326 -326
- package/dist/SvMaskedInput.svelte +122 -122
- package/dist/SvMenu.svelte +124 -124
- package/dist/SvMenuList.svelte +146 -146
- package/dist/SvMenubar.svelte +1 -1
- package/dist/SvMultiSelect.svelte +289 -290
- package/dist/SvNumberInput.svelte +168 -168
- package/dist/SvOtpInput.svelte +158 -158
- package/dist/SvPasswordInput.svelte +151 -151
- package/dist/SvPhoneInput.svelte +133 -133
- package/dist/SvPopover.svelte +197 -197
- package/dist/SvProgress.svelte +116 -116
- package/dist/SvRadioGroup.svelte +107 -107
- package/dist/SvRating.svelte +112 -112
- package/dist/SvResult.svelte +73 -73
- package/dist/SvRichText.svelte +211 -207
- package/dist/SvRowGroupPanel.svelte +170 -170
- package/dist/SvScrollArea.svelte +61 -61
- package/dist/SvSlider.svelte +200 -200
- package/dist/SvSwitchButton.svelte +108 -108
- package/dist/SvTagsInput.svelte +112 -112
- package/dist/SvTextInput.svelte +147 -147
- package/dist/SvTimePicker.svelte +245 -245
- package/dist/SvToaster.svelte +159 -159
- package/dist/SvToggleButton.svelte +85 -85
- package/dist/SvTooltip.svelte +161 -161
- package/dist/SvTour.svelte +208 -204
- package/dist/SvTree.svelte +444 -444
- package/dist/SvTreeSelect.svelte +235 -237
- package/dist/cdn/{GridMenus-C6ixSGWW.js → GridMenus-BpVcKyUr.js} +5 -7
- package/dist/cdn/{GridMenus-Cyk3qpJ7.js → GridMenus-CeXL5IW4.js} +153 -152
- package/dist/cdn/{SvDateTimePicker-ClqtVIj3.js → SvDateTimePicker-DSUhIta8.js} +165 -165
- package/dist/cdn/{SvDateTimePicker-CdqaP61H.js → SvDateTimePicker-h9kPyszT.js} +572 -555
- package/dist/cdn/{SvGridChart-BcLCYvw9.js → SvGridChart-Bs2GIR2Q.js} +28 -21
- package/dist/cdn/{SvGridChart-BhnFdLd0.js → SvGridChart-CwhFz7GV.js} +2 -2
- package/dist/cdn/{SvGridChartPanel-Cmx9DHAS.js → SvGridChartPanel-1WXuStzM.js} +2 -2
- package/dist/cdn/{SvGridChartPanel-DvjwxLvK.js → SvGridChartPanel-e5_nqNP9.js} +22 -14
- package/dist/cdn/{SvGridChartView-PwqI4s02.js → SvGridChartView-SmPW10dI.js} +3 -3
- package/dist/cdn/{SvGridChartView-B40DTJSX.js → SvGridChartView-eSPuJE6g.js} +2 -2
- package/dist/cdn/{SvGridDropdown-D0VdjeR8.js → SvGridDropdown-B1ZcLpgs.js} +1 -1
- package/dist/cdn/{SvGridDropdown-Dh-2ej8j.js → SvGridDropdown-CtdbHcIS.js} +6 -5
- package/dist/cdn/{chart-T1usDN0o.js → chart-i4XcXyHJ.js} +12 -10
- package/dist/cdn/disclose-version-Dr7rEVv-.js +2866 -0
- package/dist/cdn/{export-format-JT5pWVT8.js → export-format-Cv1Dll6k.js} +66 -245
- package/dist/cdn/{src-CgD-wiuS.js → src-DbeN1ReZ.js} +9223 -8944
- package/dist/cdn/{src-BgHjWF0Q.js → src-DmEQFW5_.js} +9023 -8828
- package/dist/cdn/svgrid.js +9 -9
- package/dist/cdn/svgrid.svelte-external.js +9 -9
- package/dist/chart-export.js +8 -8
- package/dist/chart.d.ts +2 -0
- package/dist/chart.js +1 -4
- package/dist/columns.js +9 -2
- package/dist/createMenu.svelte.js +0 -1
- package/dist/createPopoverSelect.svelte.d.ts +2 -2
- package/dist/editing.js +1 -1
- package/dist/export-format.js +3 -0
- package/dist/node_modules/.vite/vitest/67d8ed22ed986c5e2e948f64d892f30be166cc36/_svelte_metadata.json +1 -0
- package/dist/node_modules/.vite/vitest/67d8ed22ed986c5e2e948f64d892f30be166cc36/deps___vitest__/_metadata.json +178 -0
- package/dist/node_modules/.vite/vitest/67d8ed22ed986c5e2e948f64d892f30be166cc36/deps___vitest__/attachments-qzRy9DX4.js +3843 -0
- package/dist/node_modules/.vite/vitest/67d8ed22ed986c5e2e948f64d892f30be166cc36/deps___vitest__/attachments-qzRy9DX4.js.map +1 -0
- package/dist/node_modules/.vite/vitest/67d8ed22ed986c5e2e948f64d892f30be166cc36/deps___vitest__/constants-RLNOXCpz.js +23 -0
- package/dist/node_modules/.vite/vitest/67d8ed22ed986c5e2e948f64d892f30be166cc36/deps___vitest__/constants-RLNOXCpz.js.map +1 -0
- package/dist/node_modules/.vite/vitest/67d8ed22ed986c5e2e948f64d892f30be166cc36/deps___vitest__/dev-DPd12uEU.js +1925 -0
- package/dist/node_modules/.vite/vitest/67d8ed22ed986c5e2e948f64d892f30be166cc36/deps___vitest__/dev-DPd12uEU.js.map +1 -0
- package/dist/node_modules/.vite/vitest/67d8ed22ed986c5e2e948f64d892f30be166cc36/deps___vitest__/events-CDC4DKuQ.js +206 -0
- package/dist/node_modules/.vite/vitest/67d8ed22ed986c5e2e948f64d892f30be166cc36/deps___vitest__/events-CDC4DKuQ.js.map +1 -0
- package/dist/node_modules/.vite/vitest/67d8ed22ed986c5e2e948f64d892f30be166cc36/deps___vitest__/flags-BD-lYX7s.js +20 -0
- package/dist/node_modules/.vite/vitest/67d8ed22ed986c5e2e948f64d892f30be166cc36/deps___vitest__/flags-BD-lYX7s.js.map +1 -0
- package/dist/node_modules/.vite/vitest/67d8ed22ed986c5e2e948f64d892f30be166cc36/deps___vitest__/hydration-D3UBU2t8.js +264 -0
- package/dist/node_modules/.vite/vitest/67d8ed22ed986c5e2e948f64d892f30be166cc36/deps___vitest__/hydration-D3UBU2t8.js.map +1 -0
- package/dist/node_modules/.vite/vitest/67d8ed22ed986c5e2e948f64d892f30be166cc36/deps___vitest__/legacy-client-B0ZUlbSQ.js +698 -0
- package/dist/node_modules/.vite/vitest/67d8ed22ed986c5e2e948f64d892f30be166cc36/deps___vitest__/legacy-client-B0ZUlbSQ.js.map +1 -0
- package/dist/node_modules/.vite/vitest/67d8ed22ed986c5e2e948f64d892f30be166cc36/deps___vitest__/package.json +3 -0
- package/dist/node_modules/.vite/vitest/67d8ed22ed986c5e2e948f64d892f30be166cc36/deps___vitest__/svelte.js +135 -0
- package/dist/node_modules/.vite/vitest/67d8ed22ed986c5e2e948f64d892f30be166cc36/deps___vitest__/svelte.js.map +1 -0
- package/dist/node_modules/.vite/vitest/67d8ed22ed986c5e2e948f64d892f30be166cc36/deps___vitest__/svelte_animate.js +57 -0
- package/dist/node_modules/.vite/vitest/67d8ed22ed986c5e2e948f64d892f30be166cc36/deps___vitest__/svelte_animate.js.map +1 -0
- package/dist/node_modules/.vite/vitest/67d8ed22ed986c5e2e948f64d892f30be166cc36/deps___vitest__/svelte_attachments.js +2 -0
- package/dist/node_modules/.vite/vitest/67d8ed22ed986c5e2e948f64d892f30be166cc36/deps___vitest__/svelte_compiler.js +46691 -0
- package/dist/node_modules/.vite/vitest/67d8ed22ed986c5e2e948f64d892f30be166cc36/deps___vitest__/svelte_compiler.js.map +1 -0
- package/dist/node_modules/.vite/vitest/67d8ed22ed986c5e2e948f64d892f30be166cc36/deps___vitest__/svelte_easing.js +239 -0
- package/dist/node_modules/.vite/vitest/67d8ed22ed986c5e2e948f64d892f30be166cc36/deps___vitest__/svelte_easing.js.map +1 -0
- package/dist/node_modules/.vite/vitest/67d8ed22ed986c5e2e948f64d892f30be166cc36/deps___vitest__/svelte_events.js +2 -0
- package/dist/node_modules/.vite/vitest/67d8ed22ed986c5e2e948f64d892f30be166cc36/deps___vitest__/svelte_internal.js +6 -0
- package/dist/node_modules/.vite/vitest/67d8ed22ed986c5e2e948f64d892f30be166cc36/deps___vitest__/svelte_internal.js.map +1 -0
- package/dist/node_modules/.vite/vitest/67d8ed22ed986c5e2e948f64d892f30be166cc36/deps___vitest__/svelte_internal_client.js +7 -0
- package/dist/node_modules/.vite/vitest/67d8ed22ed986c5e2e948f64d892f30be166cc36/deps___vitest__/svelte_internal_disclose-version.js +6 -0
- package/dist/node_modules/.vite/vitest/67d8ed22ed986c5e2e948f64d892f30be166cc36/deps___vitest__/svelte_internal_disclose-version.js.map +1 -0
- package/dist/node_modules/.vite/vitest/67d8ed22ed986c5e2e948f64d892f30be166cc36/deps___vitest__/svelte_internal_flags_async.js +7 -0
- package/dist/node_modules/.vite/vitest/67d8ed22ed986c5e2e948f64d892f30be166cc36/deps___vitest__/svelte_internal_flags_async.js.map +1 -0
- package/dist/node_modules/.vite/vitest/67d8ed22ed986c5e2e948f64d892f30be166cc36/deps___vitest__/svelte_internal_flags_legacy.js +7 -0
- package/dist/node_modules/.vite/vitest/67d8ed22ed986c5e2e948f64d892f30be166cc36/deps___vitest__/svelte_internal_flags_legacy.js.map +1 -0
- package/dist/node_modules/.vite/vitest/67d8ed22ed986c5e2e948f64d892f30be166cc36/deps___vitest__/svelte_internal_flags_tracing.js +7 -0
- package/dist/node_modules/.vite/vitest/67d8ed22ed986c5e2e948f64d892f30be166cc36/deps___vitest__/svelte_internal_flags_tracing.js.map +1 -0
- package/dist/node_modules/.vite/vitest/67d8ed22ed986c5e2e948f64d892f30be166cc36/deps___vitest__/svelte_internal_server.js +6 -0
- package/dist/node_modules/.vite/vitest/67d8ed22ed986c5e2e948f64d892f30be166cc36/deps___vitest__/svelte_legacy.js +85 -0
- package/dist/node_modules/.vite/vitest/67d8ed22ed986c5e2e948f64d892f30be166cc36/deps___vitest__/svelte_legacy.js.map +1 -0
- package/dist/node_modules/.vite/vitest/67d8ed22ed986c5e2e948f64d892f30be166cc36/deps___vitest__/svelte_motion.js +561 -0
- package/dist/node_modules/.vite/vitest/67d8ed22ed986c5e2e948f64d892f30be166cc36/deps___vitest__/svelte_motion.js.map +1 -0
- package/dist/node_modules/.vite/vitest/67d8ed22ed986c5e2e948f64d892f30be166cc36/deps___vitest__/svelte_reactivity.js +26 -0
- package/dist/node_modules/.vite/vitest/67d8ed22ed986c5e2e948f64d892f30be166cc36/deps___vitest__/svelte_reactivity.js.map +1 -0
- package/dist/node_modules/.vite/vitest/67d8ed22ed986c5e2e948f64d892f30be166cc36/deps___vitest__/svelte_reactivity_window.js +124 -0
- package/dist/node_modules/.vite/vitest/67d8ed22ed986c5e2e948f64d892f30be166cc36/deps___vitest__/svelte_reactivity_window.js.map +1 -0
- package/dist/node_modules/.vite/vitest/67d8ed22ed986c5e2e948f64d892f30be166cc36/deps___vitest__/svelte_server.js +2 -0
- package/dist/node_modules/.vite/vitest/67d8ed22ed986c5e2e948f64d892f30be166cc36/deps___vitest__/svelte_store.js +91 -0
- package/dist/node_modules/.vite/vitest/67d8ed22ed986c5e2e948f64d892f30be166cc36/deps___vitest__/svelte_store.js.map +1 -0
- package/dist/node_modules/.vite/vitest/67d8ed22ed986c5e2e948f64d892f30be166cc36/deps___vitest__/svelte_transition.js +242 -0
- package/dist/node_modules/.vite/vitest/67d8ed22ed986c5e2e948f64d892f30be166cc36/deps___vitest__/svelte_transition.js.map +1 -0
- package/dist/node_modules/.vite/vitest/67d8ed22ed986c5e2e948f64d892f30be166cc36/deps___vitest__/utils-lObbumRX.js +497 -0
- package/dist/node_modules/.vite/vitest/67d8ed22ed986c5e2e948f64d892f30be166cc36/deps___vitest__/utils-lObbumRX.js.map +1 -0
- package/dist/node_modules/.vite/vitest/67d8ed22ed986c5e2e948f64d892f30be166cc36/deps___vitest__/utils-mR9mAWyK.js +4929 -0
- package/dist/node_modules/.vite/vitest/67d8ed22ed986c5e2e948f64d892f30be166cc36/deps___vitest__/utils-mR9mAWyK.js.map +1 -0
- package/dist/node_modules/.vite/vitest/67d8ed22ed986c5e2e948f64d892f30be166cc36/deps___vitest__/validate-CezNmRkM.js +192 -0
- package/dist/node_modules/.vite/vitest/67d8ed22ed986c5e2e948f64d892f30be166cc36/deps___vitest__/validate-CezNmRkM.js.map +1 -0
- package/dist/node_modules/.vite/vitest/67d8ed22ed986c5e2e948f64d892f30be166cc36/deps___vitest__/version-BHHo0WE-.js +10 -0
- package/dist/node_modules/.vite/vitest/67d8ed22ed986c5e2e948f64d892f30be166cc36/deps___vitest__/version-BHHo0WE-.js.map +1 -0
- package/dist/node_modules/.vite/vitest/67d8ed22ed986c5e2e948f64d892f30be166cc36/deps___vitest__/warnings-yiExAB06.js +140 -0
- package/dist/node_modules/.vite/vitest/67d8ed22ed986c5e2e948f64d892f30be166cc36/deps___vitest__/warnings-yiExAB06.js.map +1 -0
- package/dist/selection.js +0 -1
- package/package.json +11 -10
- package/src/DockNodeView.svelte +1 -1
- package/src/FlexRender.svelte +96 -96
- package/src/GridFooter.svelte +178 -179
- package/src/GridMenus.svelte +705 -782
- package/src/SvAutoComplete.svelte +165 -165
- package/src/SvAvatar.svelte +75 -75
- package/src/SvCalendar.svelte +503 -503
- package/src/SvCalendar.test.ts +226 -226
- package/src/SvCarousel.svelte +141 -141
- package/src/SvCheckBox.svelte +102 -102
- package/src/SvCircularProgress.svelte +109 -109
- package/src/SvColorInput.svelte +181 -181
- package/src/SvComboBox.svelte +275 -275
- package/src/SvContextMenu.svelte +116 -116
- package/src/SvCountryInput.svelte +160 -160
- package/src/SvDateRangeInput.svelte +1 -1
- package/src/SvDrawer.svelte +254 -254
- package/src/SvDropDownList.svelte +374 -374
- package/src/SvDurationInput.svelte +126 -126
- package/src/SvField.svelte +293 -293
- package/src/SvForm.svelte +437 -437
- package/src/SvForm.test.ts +411 -411
- package/src/SvGrid.controller.svelte.ts +3680 -3685
- package/src/SvGrid.css +2814 -2804
- package/src/SvGrid.svelte +3490 -3513
- package/src/SvGrid.types.ts +1708 -1709
- package/src/SvGridChart.svelte +1716 -1724
- package/src/SvGridChartPanel.svelte +485 -485
- package/src/SvGridChartView.svelte +70 -70
- package/src/SvGridDropdown.svelte +728 -728
- package/src/SvGridSelect.svelte +266 -268
- package/src/SvGroupCell.svelte +116 -116
- package/src/SvKbd.svelte +1 -1
- package/src/SvList.svelte +1 -1
- package/src/SvListBox.svelte +326 -326
- package/src/SvMaskedInput.svelte +122 -122
- package/src/SvMenu.svelte +124 -124
- package/src/SvMenu.test.ts +97 -97
- package/src/SvMenuList.svelte +146 -146
- package/src/SvMenubar.svelte +1 -1
- package/src/SvMultiSelect.svelte +289 -290
- package/src/SvNumberInput.svelte +168 -168
- package/src/SvOtpInput.svelte +158 -158
- package/src/SvPasswordInput.svelte +151 -151
- package/src/SvPhoneInput.svelte +133 -133
- package/src/SvPopover.svelte +197 -197
- package/src/SvProgress.svelte +116 -116
- package/src/SvRadioGroup.svelte +107 -107
- package/src/SvRating.svelte +112 -112
- package/src/SvResult.svelte +73 -73
- package/src/SvRichText.svelte +211 -207
- package/src/SvRowGroupPanel.svelte +170 -170
- package/src/SvScrollArea.svelte +61 -61
- package/src/SvSlider.svelte +200 -200
- package/src/SvSwitchButton.svelte +108 -108
- package/src/SvTagsInput.svelte +112 -112
- package/src/SvTextInput.svelte +147 -147
- package/src/SvTimePicker.svelte +245 -245
- package/src/SvToaster.svelte +159 -159
- package/src/SvToaster.test.ts +95 -95
- package/src/SvToggleButton.svelte +85 -85
- package/src/SvTooltip.svelte +161 -161
- package/src/SvTour.svelte +208 -204
- package/src/SvTree.svelte +444 -444
- package/src/SvTreeSelect.svelte +235 -237
- package/src/a11y/dismissable.test.ts +119 -119
- package/src/a11y/dismissable.ts +114 -114
- package/src/a11y.contract.test.ts +49 -49
- package/src/a11y.test.ts +59 -59
- package/src/a11y.ts +61 -61
- package/src/ai.test.ts +502 -502
- package/src/ai.ts +1391 -1391
- package/src/build-api.coverage.test.ts +633 -633
- package/src/build-api.ts +805 -910
- package/src/builtin-editors.grid.test.ts +85 -85
- package/src/cell-formatting.test.ts +1 -1
- package/src/cell-formatting.ts +169 -169
- package/src/cell-render.test.ts +513 -513
- package/src/cell-render.ts +496 -590
- package/src/cell-values.ts +118 -118
- package/src/chart-export.ts +201 -201
- package/src/chart-view.svelte.ts +36 -36
- package/src/chart.ts +2295 -2296
- package/src/clipboard.ts +0 -102
- package/src/collaboration.test.ts +104 -104
- package/src/collaboration.ts +167 -167
- package/src/columns.ts +7 -117
- package/src/core.performance.test.ts +30 -30
- package/src/core.ts +1261 -1261
- package/src/createAutocomplete.svelte.ts +132 -132
- package/src/createCombobox.svelte.ts +191 -191
- package/src/createCountryInput.svelte.ts +157 -157
- package/src/createDropdownList.svelte.ts +168 -168
- package/src/createForm.svelte.ts +386 -386
- package/src/createGrid.svelte.ts +42 -42
- package/src/createGrid.test.ts +10 -10
- package/src/createGridState.svelte.ts +17 -17
- package/src/createListbox.svelte.ts +250 -250
- package/src/createMenu.svelte.ts +224 -225
- package/src/createPopoverSelect.svelte.ts +213 -213
- package/src/createSlider.svelte.ts +191 -191
- package/src/createTooltip.svelte.ts +144 -144
- package/src/createTree.svelte.ts +319 -319
- package/src/editing.test.ts +949 -949
- package/src/editing.ts +606 -709
- package/src/editor-contract.ts +164 -164
- package/src/editor-registry.grid.test.ts +144 -146
- package/src/editor-registry.ts +122 -122
- package/src/export-data-api.test.ts +126 -126
- package/src/export-format.test.ts +107 -107
- package/src/export-format.ts +601 -598
- package/src/features.ts +0 -90
- package/src/filter-operators.ts +157 -157
- package/src/flex-render.ts +3 -3
- package/src/form-field.ts +127 -127
- package/src/group-display.test.ts +167 -167
- package/src/group-display.ts +200 -200
- package/src/index.ts +724 -724
- package/src/js-scroller.svelte.ts +173 -173
- package/src/keyboard-handlers.coverage.test.ts +1 -1
- package/src/keyboard-handlers.ts +270 -390
- package/src/keyboard.test.ts +59 -59
- package/src/keyboard.ts +97 -97
- package/src/list-nav.test.ts +49 -49
- package/src/list-nav.ts +29 -29
- package/src/list-option.test.ts +56 -56
- package/src/list-option.ts +172 -172
- package/src/menus.test.ts +0 -1
- package/src/menus.ts +567 -679
- package/src/merge-objects.ts +48 -48
- package/src/new-features.test.ts +3 -1
- package/src/node_modules/.vite/vitest/67d8ed22ed986c5e2e948f64d892f30be166cc36/_svelte_metadata.json +1 -0
- package/src/node_modules/.vite/vitest/67d8ed22ed986c5e2e948f64d892f30be166cc36/deps___vitest__/_metadata.json +178 -0
- package/src/node_modules/.vite/vitest/67d8ed22ed986c5e2e948f64d892f30be166cc36/deps___vitest__/attachments-qzRy9DX4.js +3843 -0
- package/src/node_modules/.vite/vitest/67d8ed22ed986c5e2e948f64d892f30be166cc36/deps___vitest__/attachments-qzRy9DX4.js.map +1 -0
- package/src/node_modules/.vite/vitest/67d8ed22ed986c5e2e948f64d892f30be166cc36/deps___vitest__/constants-RLNOXCpz.js +23 -0
- package/src/node_modules/.vite/vitest/67d8ed22ed986c5e2e948f64d892f30be166cc36/deps___vitest__/constants-RLNOXCpz.js.map +1 -0
- package/src/node_modules/.vite/vitest/67d8ed22ed986c5e2e948f64d892f30be166cc36/deps___vitest__/dev-DPd12uEU.js +1925 -0
- package/src/node_modules/.vite/vitest/67d8ed22ed986c5e2e948f64d892f30be166cc36/deps___vitest__/dev-DPd12uEU.js.map +1 -0
- package/src/node_modules/.vite/vitest/67d8ed22ed986c5e2e948f64d892f30be166cc36/deps___vitest__/events-CDC4DKuQ.js +206 -0
- package/src/node_modules/.vite/vitest/67d8ed22ed986c5e2e948f64d892f30be166cc36/deps___vitest__/events-CDC4DKuQ.js.map +1 -0
- package/src/node_modules/.vite/vitest/67d8ed22ed986c5e2e948f64d892f30be166cc36/deps___vitest__/flags-BD-lYX7s.js +20 -0
- package/src/node_modules/.vite/vitest/67d8ed22ed986c5e2e948f64d892f30be166cc36/deps___vitest__/flags-BD-lYX7s.js.map +1 -0
- package/src/node_modules/.vite/vitest/67d8ed22ed986c5e2e948f64d892f30be166cc36/deps___vitest__/hydration-D3UBU2t8.js +264 -0
- package/src/node_modules/.vite/vitest/67d8ed22ed986c5e2e948f64d892f30be166cc36/deps___vitest__/hydration-D3UBU2t8.js.map +1 -0
- package/src/node_modules/.vite/vitest/67d8ed22ed986c5e2e948f64d892f30be166cc36/deps___vitest__/legacy-client-B0ZUlbSQ.js +698 -0
- package/src/node_modules/.vite/vitest/67d8ed22ed986c5e2e948f64d892f30be166cc36/deps___vitest__/legacy-client-B0ZUlbSQ.js.map +1 -0
- package/src/node_modules/.vite/vitest/67d8ed22ed986c5e2e948f64d892f30be166cc36/deps___vitest__/package.json +3 -0
- package/src/node_modules/.vite/vitest/67d8ed22ed986c5e2e948f64d892f30be166cc36/deps___vitest__/svelte.js +135 -0
- package/src/node_modules/.vite/vitest/67d8ed22ed986c5e2e948f64d892f30be166cc36/deps___vitest__/svelte.js.map +1 -0
- package/src/node_modules/.vite/vitest/67d8ed22ed986c5e2e948f64d892f30be166cc36/deps___vitest__/svelte_animate.js +57 -0
- package/src/node_modules/.vite/vitest/67d8ed22ed986c5e2e948f64d892f30be166cc36/deps___vitest__/svelte_animate.js.map +1 -0
- package/src/node_modules/.vite/vitest/67d8ed22ed986c5e2e948f64d892f30be166cc36/deps___vitest__/svelte_attachments.js +2 -0
- package/src/node_modules/.vite/vitest/67d8ed22ed986c5e2e948f64d892f30be166cc36/deps___vitest__/svelte_compiler.js +46691 -0
- package/src/node_modules/.vite/vitest/67d8ed22ed986c5e2e948f64d892f30be166cc36/deps___vitest__/svelte_compiler.js.map +1 -0
- package/src/node_modules/.vite/vitest/67d8ed22ed986c5e2e948f64d892f30be166cc36/deps___vitest__/svelte_easing.js +239 -0
- package/src/node_modules/.vite/vitest/67d8ed22ed986c5e2e948f64d892f30be166cc36/deps___vitest__/svelte_easing.js.map +1 -0
- package/src/node_modules/.vite/vitest/67d8ed22ed986c5e2e948f64d892f30be166cc36/deps___vitest__/svelte_events.js +2 -0
- package/src/node_modules/.vite/vitest/67d8ed22ed986c5e2e948f64d892f30be166cc36/deps___vitest__/svelte_internal.js +6 -0
- package/src/node_modules/.vite/vitest/67d8ed22ed986c5e2e948f64d892f30be166cc36/deps___vitest__/svelte_internal.js.map +1 -0
- package/src/node_modules/.vite/vitest/67d8ed22ed986c5e2e948f64d892f30be166cc36/deps___vitest__/svelte_internal_client.js +7 -0
- package/src/node_modules/.vite/vitest/67d8ed22ed986c5e2e948f64d892f30be166cc36/deps___vitest__/svelte_internal_disclose-version.js +6 -0
- package/src/node_modules/.vite/vitest/67d8ed22ed986c5e2e948f64d892f30be166cc36/deps___vitest__/svelte_internal_disclose-version.js.map +1 -0
- package/src/node_modules/.vite/vitest/67d8ed22ed986c5e2e948f64d892f30be166cc36/deps___vitest__/svelte_internal_flags_async.js +7 -0
- package/src/node_modules/.vite/vitest/67d8ed22ed986c5e2e948f64d892f30be166cc36/deps___vitest__/svelte_internal_flags_async.js.map +1 -0
- package/src/node_modules/.vite/vitest/67d8ed22ed986c5e2e948f64d892f30be166cc36/deps___vitest__/svelte_internal_flags_legacy.js +7 -0
- package/src/node_modules/.vite/vitest/67d8ed22ed986c5e2e948f64d892f30be166cc36/deps___vitest__/svelte_internal_flags_legacy.js.map +1 -0
- package/src/node_modules/.vite/vitest/67d8ed22ed986c5e2e948f64d892f30be166cc36/deps___vitest__/svelte_internal_flags_tracing.js +7 -0
- package/src/node_modules/.vite/vitest/67d8ed22ed986c5e2e948f64d892f30be166cc36/deps___vitest__/svelte_internal_flags_tracing.js.map +1 -0
- package/src/node_modules/.vite/vitest/67d8ed22ed986c5e2e948f64d892f30be166cc36/deps___vitest__/svelte_internal_server.js +6 -0
- package/src/node_modules/.vite/vitest/67d8ed22ed986c5e2e948f64d892f30be166cc36/deps___vitest__/svelte_legacy.js +85 -0
- package/src/node_modules/.vite/vitest/67d8ed22ed986c5e2e948f64d892f30be166cc36/deps___vitest__/svelte_legacy.js.map +1 -0
- package/src/node_modules/.vite/vitest/67d8ed22ed986c5e2e948f64d892f30be166cc36/deps___vitest__/svelte_motion.js +561 -0
- package/src/node_modules/.vite/vitest/67d8ed22ed986c5e2e948f64d892f30be166cc36/deps___vitest__/svelte_motion.js.map +1 -0
- package/src/node_modules/.vite/vitest/67d8ed22ed986c5e2e948f64d892f30be166cc36/deps___vitest__/svelte_reactivity.js +26 -0
- package/src/node_modules/.vite/vitest/67d8ed22ed986c5e2e948f64d892f30be166cc36/deps___vitest__/svelte_reactivity.js.map +1 -0
- package/src/node_modules/.vite/vitest/67d8ed22ed986c5e2e948f64d892f30be166cc36/deps___vitest__/svelte_reactivity_window.js +124 -0
- package/src/node_modules/.vite/vitest/67d8ed22ed986c5e2e948f64d892f30be166cc36/deps___vitest__/svelte_reactivity_window.js.map +1 -0
- package/src/node_modules/.vite/vitest/67d8ed22ed986c5e2e948f64d892f30be166cc36/deps___vitest__/svelte_server.js +2 -0
- package/src/node_modules/.vite/vitest/67d8ed22ed986c5e2e948f64d892f30be166cc36/deps___vitest__/svelte_store.js +91 -0
- package/src/node_modules/.vite/vitest/67d8ed22ed986c5e2e948f64d892f30be166cc36/deps___vitest__/svelte_store.js.map +1 -0
- package/src/node_modules/.vite/vitest/67d8ed22ed986c5e2e948f64d892f30be166cc36/deps___vitest__/svelte_transition.js +242 -0
- package/src/node_modules/.vite/vitest/67d8ed22ed986c5e2e948f64d892f30be166cc36/deps___vitest__/svelte_transition.js.map +1 -0
- package/src/node_modules/.vite/vitest/67d8ed22ed986c5e2e948f64d892f30be166cc36/deps___vitest__/utils-lObbumRX.js +497 -0
- package/src/node_modules/.vite/vitest/67d8ed22ed986c5e2e948f64d892f30be166cc36/deps___vitest__/utils-lObbumRX.js.map +1 -0
- package/src/node_modules/.vite/vitest/67d8ed22ed986c5e2e948f64d892f30be166cc36/deps___vitest__/utils-mR9mAWyK.js +4929 -0
- package/src/node_modules/.vite/vitest/67d8ed22ed986c5e2e948f64d892f30be166cc36/deps___vitest__/utils-mR9mAWyK.js.map +1 -0
- package/src/node_modules/.vite/vitest/67d8ed22ed986c5e2e948f64d892f30be166cc36/deps___vitest__/validate-CezNmRkM.js +192 -0
- package/src/node_modules/.vite/vitest/67d8ed22ed986c5e2e948f64d892f30be166cc36/deps___vitest__/validate-CezNmRkM.js.map +1 -0
- package/src/node_modules/.vite/vitest/67d8ed22ed986c5e2e948f64d892f30be166cc36/deps___vitest__/version-BHHo0WE-.js +10 -0
- package/src/node_modules/.vite/vitest/67d8ed22ed986c5e2e948f64d892f30be166cc36/deps___vitest__/version-BHHo0WE-.js.map +1 -0
- package/src/node_modules/.vite/vitest/67d8ed22ed986c5e2e948f64d892f30be166cc36/deps___vitest__/warnings-yiExAB06.js +140 -0
- package/src/node_modules/.vite/vitest/67d8ed22ed986c5e2e948f64d892f30be166cc36/deps___vitest__/warnings-yiExAB06.js.map +1 -0
- package/src/overlays.test.ts +90 -90
- package/src/render-component.ts +28 -28
- package/src/row-drag.test.ts +1 -1
- package/src/row-resize.test.ts +1 -2
- package/src/row-resize.ts +210 -210
- package/src/scheduler-model.test.ts +562 -562
- package/src/scheduler-model.ts +872 -872
- package/src/scroll-sync.test.ts +4 -7
- package/src/scroll-sync.ts +0 -122
- package/src/selection.test.ts +754 -754
- package/src/selection.ts +491 -600
- package/src/server-data-source.test.ts +289 -289
- package/src/server-data-source.ts +413 -413
- package/src/server-group-model.test.ts +2 -2
- package/src/sparkline.test.ts +68 -68
- package/src/sparkline.ts +169 -169
- package/src/spreadsheet.test.ts +488 -488
- package/src/spreadsheet.ts +312 -312
- package/src/static-functions.ts +11 -11
- package/src/subscribe.ts +38 -38
- package/src/summaries.ts +0 -112
- package/src/svgrid-wrapper.types.ts +526 -526
- package/src/svgrid.async-editor-options.test.ts +271 -271
- package/src/svgrid.auto-row-height.test.ts +204 -204
- package/src/svgrid.behavior.test.ts +705 -706
- package/src/svgrid.charting.test.ts +534 -534
- package/src/svgrid.column-reorder.test.ts +0 -8
- package/src/svgrid.comments-autocomplete.test.ts +123 -123
- package/src/svgrid.context-menu.test.ts +147 -147
- package/src/svgrid.features.test.ts +157 -157
- package/src/svgrid.filter-depth.test.ts +163 -163
- package/src/svgrid.filter-menu-listbox.svelte.test.ts +322 -322
- package/src/svgrid.filter-menu-scroll.test.ts +112 -112
- package/src/svgrid.footer-pagesize.svelte.test.ts +86 -0
- package/src/svgrid.grand-total.test.ts +188 -188
- package/src/svgrid.group-display-mode.test.ts +171 -171
- package/src/svgrid.group-footers.test.ts +121 -121
- package/src/svgrid.group-pagination.test.ts +153 -153
- package/src/svgrid.new-features.wrapper.test.ts +251 -251
- package/src/svgrid.tree-data.test.ts +186 -186
- package/src/svgrid.wrapper.test.ts +40 -40
- package/src/svgriddropdown.async-panel.svelte.test.ts +195 -195
- package/src/test-setup.ts +62 -62
- package/src/themes/index.ts +257 -257
- package/src/toast-store.svelte.ts +246 -246
- package/src/toast-store.test.ts +147 -147
- package/src/tree-row-model.test.ts +168 -169
- package/src/ui-buttons.test.ts +144 -144
- package/src/ui-inputs.test.ts +118 -118
- package/src/ui-localization.test.ts +113 -113
- package/src/ui-range.test.ts +70 -70
- package/src/ui-selection.test.ts +155 -155
- package/src/ui-tier1.test.ts +142 -142
- package/src/virtual.test.ts +88 -88
- package/src/virtualization/column-virtualizer.test.ts +27 -27
- package/src/virtualization/column-virtualizer.ts +30 -30
- package/src/virtualization/svelte-virtualizer.svelte.ts +26 -26
- package/src/virtualization/types.ts +30 -30
- package/src/virtualization/virtualizer.test.ts +47 -47
- package/src/virtualization/virtualizer.ts +296 -296
- package/dist/cdn/disclose-version-DoD9AFD_.js +0 -2958
package/src/createForm.svelte.ts
CHANGED
|
@@ -1,386 +1,386 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* createForm - the HEADLESS core behind <SvForm>: values / errors / touched
|
|
3
|
-
* state, required + rule + custom validation (validate-on-blur-then-live), and a
|
|
4
|
-
* submit that validates every field first. No markup, no controls - you render
|
|
5
|
-
* the fields however you like and read/write through the core.
|
|
6
|
-
*
|
|
7
|
-
* ```svelte
|
|
8
|
-
* <script lang="ts">
|
|
9
|
-
* import { createForm } from '@svgrid/grid'
|
|
10
|
-
* const form = createForm({ fields: () => fields, onSubmit: (v) => save(v) })
|
|
11
|
-
* </script>
|
|
12
|
-
* <form onsubmit={(e) => { e.preventDefault(); form.submit() }}>
|
|
13
|
-
* {#each fields as f}
|
|
14
|
-
* <input value={form.value(f.name) ?? ''}
|
|
15
|
-
* oninput={(e) => form.setValue(f.name, e.currentTarget.value)}
|
|
16
|
-
* onblur={() => form.handleBlur(f.name)} />
|
|
17
|
-
* {#if form.error(f.name)}<span>{form.error(f.name)}</span>{/if}
|
|
18
|
-
* {/each}
|
|
19
|
-
* </form>
|
|
20
|
-
* ```
|
|
21
|
-
*/
|
|
22
|
-
import { flattenFields, type FormField, type FormEntry } from './form-field'
|
|
23
|
-
import { runRules } from './validators'
|
|
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
|
-
|
|
41
|
-
export type FormConfig = {
|
|
42
|
-
/** The schema - a mix of flat fields and titled sections. */
|
|
43
|
-
fields: () => ReadonlyArray<FormEntry>
|
|
44
|
-
/** Seed values on creation. */
|
|
45
|
-
initial?: Record<string, any>
|
|
46
|
-
/** Called with the (visible-field) values on a valid submit. May be async;
|
|
47
|
-
* `submitting` stays true until the returned promise settles. */
|
|
48
|
-
onSubmit?: (values: Record<string, any>) => void | Promise<void>
|
|
49
|
-
onChange?: (values: Record<string, any>) => void
|
|
50
|
-
/** Override the built-in generated strings (required / min-max items / checking). */
|
|
51
|
-
messages?: Partial<FormMessages>
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
/** Deep-ish equality for dirty tracking: identity for primitives, structural
|
|
55
|
-
* (JSON) for arrays/objects. Good enough for form values. */
|
|
56
|
-
function eq(a: unknown, b: unknown): boolean {
|
|
57
|
-
if (Object.is(a, b)) return true
|
|
58
|
-
if (a && b && typeof a === 'object' && typeof b === 'object') {
|
|
59
|
-
try { return JSON.stringify(a) === JSON.stringify(b) } catch { return false }
|
|
60
|
-
}
|
|
61
|
-
return false
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
/** Resolve a `boolean | (values) => boolean` field flag. */
|
|
65
|
-
function resolveFlag(
|
|
66
|
-
flag: boolean | ((values: Record<string, any>) => boolean) | undefined,
|
|
67
|
-
dflt: boolean,
|
|
68
|
-
values: Record<string, any>,
|
|
69
|
-
): boolean {
|
|
70
|
-
if (flag == null) return dflt
|
|
71
|
-
return typeof flag === 'function' ? !!flag(values) : !!flag
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
export function createForm(config: FormConfig) {
|
|
75
|
-
const M: FormMessages = { ...DEFAULT_FORM_MESSAGES, ...(config.messages ?? {}) }
|
|
76
|
-
let values = $state<Record<string, any>>({ ...(config.initial ?? {}) })
|
|
77
|
-
let errors = $state<Record<string, string>>({})
|
|
78
|
-
let touched = $state<Record<string, boolean>>({})
|
|
79
|
-
let submitting = $state(false)
|
|
80
|
-
let validating = $state<Record<string, boolean>>({})
|
|
81
|
-
// Baseline for dirty tracking + reset. Rebased when `reset(next)` is passed.
|
|
82
|
-
let baseline = { ...(config.initial ?? {}) }
|
|
83
|
-
// Async-validation bookkeeping: debounce timers + a per-field run sequence so
|
|
84
|
-
// only the latest response wins (stale responses are dropped).
|
|
85
|
-
const asyncTimers = new Map<string, ReturnType<typeof setTimeout>>()
|
|
86
|
-
const asyncSeq = new Map<string, number>()
|
|
87
|
-
|
|
88
|
-
// The flat field list (sections flattened) drives all values/validation.
|
|
89
|
-
const flat = () => flattenFields(config.fields())
|
|
90
|
-
const fieldOf = (name: string) => flat().find((f) => f.name === name)
|
|
91
|
-
const isVisible = (name: string) => resolveFlag(fieldOf(name)?.visible, true, values)
|
|
92
|
-
const isDisabled = (name: string) => resolveFlag(fieldOf(name)?.disabled, false, values)
|
|
93
|
-
|
|
94
|
-
function setError(name: string, err: string | null | undefined) {
|
|
95
|
-
const next = { ...errors }
|
|
96
|
-
if (err) next[name] = err
|
|
97
|
-
else delete next[name]
|
|
98
|
-
errors = next
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
/** The synchronous checks: required, then declarative rules, then a sync
|
|
102
|
-
* `validate`. Returns the first error message, or null. `ctx` is the values
|
|
103
|
-
* object rules/validate see - the form values, or a single item for array
|
|
104
|
-
* rows (so within-row cross-field rules work). */
|
|
105
|
-
function syncError(field: FormField, v: any, ctx: Record<string, any> = values): string | null {
|
|
106
|
-
if (field.required && (v == null || v === '' || (Array.isArray(v) && !v.length)))
|
|
107
|
-
return M.required(field.label)
|
|
108
|
-
if (field.rules) { const e = runRules(v, field.rules, ctx); if (e) return e }
|
|
109
|
-
if (field.validate) { const e = field.validate(v, ctx); if (e) return e }
|
|
110
|
-
return null
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
function cancelAsync(name: string) {
|
|
114
|
-
const t = asyncTimers.get(name)
|
|
115
|
-
if (t) { clearTimeout(t); asyncTimers.delete(name) }
|
|
116
|
-
// Bump the sequence so any in-flight response is treated as stale.
|
|
117
|
-
asyncSeq.set(name, (asyncSeq.get(name) ?? 0) + 1)
|
|
118
|
-
if (validating[name]) validating = { ...validating, [name]: false }
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
/** Run a field's async validator now (guarded against stale responses). */
|
|
122
|
-
async function runAsync(name: string): Promise<boolean> {
|
|
123
|
-
const field = fieldOf(name)
|
|
124
|
-
if (!field?.asyncValidate || !isVisible(name)) return true
|
|
125
|
-
if (syncError(field, values[name])) return false // sync error already shown
|
|
126
|
-
const seq = (asyncSeq.get(name) ?? 0) + 1
|
|
127
|
-
asyncSeq.set(name, seq)
|
|
128
|
-
validating = { ...validating, [name]: true }
|
|
129
|
-
try {
|
|
130
|
-
const err = await field.asyncValidate(values[name], values)
|
|
131
|
-
if (asyncSeq.get(name) !== seq) return true // superseded - ignore
|
|
132
|
-
setError(name, err ?? null)
|
|
133
|
-
return !err
|
|
134
|
-
} finally {
|
|
135
|
-
if (asyncSeq.get(name) === seq) validating = { ...validating, [name]: false }
|
|
136
|
-
}
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
function scheduleAsync(name: string) {
|
|
140
|
-
const field = fieldOf(name)
|
|
141
|
-
if (!field?.asyncValidate) return
|
|
142
|
-
const prev = asyncTimers.get(name)
|
|
143
|
-
if (prev) clearTimeout(prev)
|
|
144
|
-
asyncTimers.set(name, setTimeout(() => { asyncTimers.delete(name); runAsync(name) }, field.asyncDebounce ?? 300))
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
function validateField(name: string): boolean {
|
|
148
|
-
const field = fieldOf(name)
|
|
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 }
|
|
152
|
-
// Hidden fields never block submit; clear any stale error + pending check.
|
|
153
|
-
if (!isVisible(name)) { setError(name, null); cancelAsync(name); return true }
|
|
154
|
-
if (field.type === 'array') return validateArray(field)
|
|
155
|
-
const err = syncError(field, values[name])
|
|
156
|
-
setError(name, err)
|
|
157
|
-
if (err) { cancelAsync(name); return false }
|
|
158
|
-
// Sync passed - kick off the debounced async check if there is one.
|
|
159
|
-
if (field.asyncValidate) scheduleAsync(name)
|
|
160
|
-
return true
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
// --- Field arrays (repeatable groups) -----------------------------------
|
|
164
|
-
const arrayItems = (name: string): Record<string, any>[] =>
|
|
165
|
-
Array.isArray(values[name]) ? values[name] : []
|
|
166
|
-
const itemValue = (name: string, i: number, field: string) => arrayItems(name)[i]?.[field]
|
|
167
|
-
const itemError = (name: string, i: number, field: string) => errors[`${name}.${i}.${field}`]
|
|
168
|
-
|
|
169
|
-
function clearNamespacedErrors(name: string) {
|
|
170
|
-
const prefix = `${name}.`
|
|
171
|
-
const next = { ...errors }
|
|
172
|
-
let changed = false
|
|
173
|
-
for (const k of Object.keys(next)) if (k.startsWith(prefix)) { delete next[k]; changed = true }
|
|
174
|
-
if (changed) errors = next
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
function setItems(name: string, next: Record<string, any>[]) {
|
|
178
|
-
values = { ...values, [name]: next }
|
|
179
|
-
config.onChange?.(values)
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
function addItem(name: string, item?: Record<string, any>) {
|
|
183
|
-
const seed = item ?? Object.fromEntries((fieldOf(name)?.itemFields ?? []).map((f) => [f.name, undefined]))
|
|
184
|
-
setItems(name, [...arrayItems(name), seed])
|
|
185
|
-
}
|
|
186
|
-
function removeItem(name: string, i: number) {
|
|
187
|
-
const items = arrayItems(name).slice()
|
|
188
|
-
items.splice(i, 1)
|
|
189
|
-
clearNamespacedErrors(name) // item indices shift - drop stale keys
|
|
190
|
-
setItems(name, items)
|
|
191
|
-
}
|
|
192
|
-
function moveItem(name: string, from: number, to: number) {
|
|
193
|
-
const items = arrayItems(name).slice()
|
|
194
|
-
if (from < 0 || from >= items.length || to < 0 || to >= items.length) return
|
|
195
|
-
const [it] = items.splice(from, 1)
|
|
196
|
-
items.splice(to, 0, it as Record<string, any>)
|
|
197
|
-
clearNamespacedErrors(name)
|
|
198
|
-
setItems(name, items)
|
|
199
|
-
}
|
|
200
|
-
function setItemValue(name: string, i: number, field: string, v: any) {
|
|
201
|
-
setItems(name, arrayItems(name).map((it, ix) => (ix === i ? { ...it, [field]: v } : it)))
|
|
202
|
-
if (touched[`${name}.${i}.${field}`]) validateItemField(name, i, field)
|
|
203
|
-
}
|
|
204
|
-
function handleItemBlur(name: string, i: number, field: string) {
|
|
205
|
-
touched = { ...touched, [`${name}.${i}.${field}`]: true }
|
|
206
|
-
validateItemField(name, i, field)
|
|
207
|
-
}
|
|
208
|
-
function validateItemField(name: string, i: number, fieldName: string): boolean {
|
|
209
|
-
const itemField = fieldOf(name)?.itemFields?.find((f) => f.name === fieldName)
|
|
210
|
-
if (!itemField) return true
|
|
211
|
-
const item = arrayItems(name)[i] ?? {}
|
|
212
|
-
const err = syncError(itemField, item[fieldName], item)
|
|
213
|
-
setError(`${name}.${i}.${fieldName}`, err)
|
|
214
|
-
return !err
|
|
215
|
-
}
|
|
216
|
-
function validateArray(field: FormField): boolean {
|
|
217
|
-
const name = field.name
|
|
218
|
-
const items = arrayItems(name)
|
|
219
|
-
let arrErr: string | null = null
|
|
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)
|
|
223
|
-
setError(name, arrErr)
|
|
224
|
-
let ok = !arrErr
|
|
225
|
-
clearNamespacedErrors(name) // rebuild fresh per-item errors
|
|
226
|
-
for (let i = 0; i < items.length; i++)
|
|
227
|
-
for (const itf of field.itemFields ?? [])
|
|
228
|
-
if (!validateItemField(name, i, itf.name)) ok = false
|
|
229
|
-
return ok
|
|
230
|
-
}
|
|
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
|
-
|
|
238
|
-
function setValue(name: string, v: any) {
|
|
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
|
|
249
|
-
config.onChange?.(values)
|
|
250
|
-
// Re-validate live once the field has been blurred at least once.
|
|
251
|
-
if (touched[name]) validateField(name)
|
|
252
|
-
}
|
|
253
|
-
|
|
254
|
-
function handleBlur(name: string) {
|
|
255
|
-
touched = { ...touched, [name]: true }
|
|
256
|
-
validateField(name)
|
|
257
|
-
}
|
|
258
|
-
|
|
259
|
-
async function submit(): Promise<boolean> {
|
|
260
|
-
let ok = true
|
|
261
|
-
const t: Record<string, boolean> = { ...touched }
|
|
262
|
-
for (const f of flat()) {
|
|
263
|
-
if (!isVisible(f.name)) continue
|
|
264
|
-
t[f.name] = true
|
|
265
|
-
if (!validateField(f.name)) ok = false
|
|
266
|
-
}
|
|
267
|
-
touched = t
|
|
268
|
-
if (!ok) return false
|
|
269
|
-
// Await async validators (run now, skipping their debounce). Any failure blocks.
|
|
270
|
-
const asyncFields = flat().filter((f) => f.asyncValidate && isVisible(f.name))
|
|
271
|
-
if (asyncFields.length) {
|
|
272
|
-
for (const f of asyncFields) { const timer = asyncTimers.get(f.name); if (timer) { clearTimeout(timer); asyncTimers.delete(f.name) } }
|
|
273
|
-
const results = await Promise.all(asyncFields.map((f) => runAsync(f.name)))
|
|
274
|
-
if (results.some((r) => !r)) return false
|
|
275
|
-
}
|
|
276
|
-
// Exclude hidden fields from the submitted payload; keep any non-field keys.
|
|
277
|
-
const payload = { ...values }
|
|
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)
|
|
281
|
-
submitting = true
|
|
282
|
-
try {
|
|
283
|
-
await config.onSubmit?.(payload)
|
|
284
|
-
} finally {
|
|
285
|
-
submitting = false
|
|
286
|
-
}
|
|
287
|
-
return true
|
|
288
|
-
}
|
|
289
|
-
|
|
290
|
-
/** Validate a subset of fields by name (sync + their async validators). Used
|
|
291
|
-
* to gate a wizard step before advancing. Returns true when all pass. */
|
|
292
|
-
async function validateFields(names: string[]): Promise<boolean> {
|
|
293
|
-
let ok = true
|
|
294
|
-
const t = { ...touched }
|
|
295
|
-
for (const name of names) {
|
|
296
|
-
if (!isVisible(name)) continue
|
|
297
|
-
t[name] = true
|
|
298
|
-
if (!validateField(name)) ok = false
|
|
299
|
-
}
|
|
300
|
-
touched = t
|
|
301
|
-
if (!ok) return false
|
|
302
|
-
const asyncNames = names.filter((n) => fieldOf(n)?.asyncValidate && isVisible(n))
|
|
303
|
-
if (asyncNames.length) {
|
|
304
|
-
for (const n of asyncNames) { const timer = asyncTimers.get(n); if (timer) { clearTimeout(timer); asyncTimers.delete(n) } }
|
|
305
|
-
const results = await Promise.all(asyncNames.map((n) => runAsync(n)))
|
|
306
|
-
if (results.some((r) => !r)) return false
|
|
307
|
-
}
|
|
308
|
-
return true
|
|
309
|
-
}
|
|
310
|
-
|
|
311
|
-
/** Restore values to the baseline (or a new one), clearing errors + touched. */
|
|
312
|
-
function reset(next?: Record<string, any>) {
|
|
313
|
-
if (next) baseline = { ...next }
|
|
314
|
-
values = { ...baseline }
|
|
315
|
-
errors = {}
|
|
316
|
-
touched = {}
|
|
317
|
-
config.onChange?.(values)
|
|
318
|
-
}
|
|
319
|
-
|
|
320
|
-
/** Inject errors (e.g. server-side validation after submit) and mark those
|
|
321
|
-
* fields touched so the messages show. Empty message clears a field's error. */
|
|
322
|
-
function setErrors(map: Record<string, string>) {
|
|
323
|
-
const nextErr = { ...errors }
|
|
324
|
-
const nextTouched = { ...touched }
|
|
325
|
-
for (const [k, msg] of Object.entries(map)) {
|
|
326
|
-
if (msg) { nextErr[k] = msg; nextTouched[k] = true }
|
|
327
|
-
else delete nextErr[k]
|
|
328
|
-
}
|
|
329
|
-
errors = nextErr
|
|
330
|
-
touched = nextTouched
|
|
331
|
-
}
|
|
332
|
-
|
|
333
|
-
const isFieldDirty = (name: string) => !eq(values[name], baseline[name])
|
|
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
|
-
|
|
346
|
-
return {
|
|
347
|
-
get values() { return values },
|
|
348
|
-
get submitting() { return submitting },
|
|
349
|
-
get isDirty() {
|
|
350
|
-
const keys = new Set([...Object.keys(values), ...Object.keys(baseline)])
|
|
351
|
-
for (const k of keys) if (!eq(values[k], baseline[k])) return true
|
|
352
|
-
return false
|
|
353
|
-
},
|
|
354
|
-
value: (name: string) => { const f = fieldOf(name); return f?.computed ? f.computed(values) : values[name] },
|
|
355
|
-
error: (name: string): string | undefined => errors[name],
|
|
356
|
-
isTouched: (name: string) => !!touched[name],
|
|
357
|
-
hasError: (name: string) => !!errors[name],
|
|
358
|
-
isValidating: (name: string) => !!validating[name],
|
|
359
|
-
isFieldDirty,
|
|
360
|
-
isVisible,
|
|
361
|
-
isDisabled,
|
|
362
|
-
entries: () => config.fields(),
|
|
363
|
-
// Field arrays
|
|
364
|
-
arrayItems,
|
|
365
|
-
itemValue,
|
|
366
|
-
itemError,
|
|
367
|
-
addItem,
|
|
368
|
-
removeItem,
|
|
369
|
-
moveItem,
|
|
370
|
-
setItemValue,
|
|
371
|
-
handleItemBlur,
|
|
372
|
-
setValue,
|
|
373
|
-
handleBlur,
|
|
374
|
-
validateField,
|
|
375
|
-
validateFields,
|
|
376
|
-
submit,
|
|
377
|
-
reset,
|
|
378
|
-
setErrors,
|
|
379
|
-
errorList,
|
|
380
|
-
firstErrorField,
|
|
381
|
-
/** The resolved message strings (defaults merged with `config.messages`). */
|
|
382
|
-
messages: M,
|
|
383
|
-
}
|
|
384
|
-
}
|
|
385
|
-
|
|
386
|
-
export type Form = ReturnType<typeof createForm>
|
|
1
|
+
/**
|
|
2
|
+
* createForm - the HEADLESS core behind <SvForm>: values / errors / touched
|
|
3
|
+
* state, required + rule + custom validation (validate-on-blur-then-live), and a
|
|
4
|
+
* submit that validates every field first. No markup, no controls - you render
|
|
5
|
+
* the fields however you like and read/write through the core.
|
|
6
|
+
*
|
|
7
|
+
* ```svelte
|
|
8
|
+
* <script lang="ts">
|
|
9
|
+
* import { createForm } from '@svgrid/grid'
|
|
10
|
+
* const form = createForm({ fields: () => fields, onSubmit: (v) => save(v) })
|
|
11
|
+
* </script>
|
|
12
|
+
* <form onsubmit={(e) => { e.preventDefault(); form.submit() }}>
|
|
13
|
+
* {#each fields as f}
|
|
14
|
+
* <input value={form.value(f.name) ?? ''}
|
|
15
|
+
* oninput={(e) => form.setValue(f.name, e.currentTarget.value)}
|
|
16
|
+
* onblur={() => form.handleBlur(f.name)} />
|
|
17
|
+
* {#if form.error(f.name)}<span>{form.error(f.name)}</span>{/if}
|
|
18
|
+
* {/each}
|
|
19
|
+
* </form>
|
|
20
|
+
* ```
|
|
21
|
+
*/
|
|
22
|
+
import { flattenFields, type FormField, type FormEntry } from './form-field'
|
|
23
|
+
import { runRules } from './validators'
|
|
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
|
+
|
|
41
|
+
export type FormConfig = {
|
|
42
|
+
/** The schema - a mix of flat fields and titled sections. */
|
|
43
|
+
fields: () => ReadonlyArray<FormEntry>
|
|
44
|
+
/** Seed values on creation. */
|
|
45
|
+
initial?: Record<string, any>
|
|
46
|
+
/** Called with the (visible-field) values on a valid submit. May be async;
|
|
47
|
+
* `submitting` stays true until the returned promise settles. */
|
|
48
|
+
onSubmit?: (values: Record<string, any>) => void | Promise<void>
|
|
49
|
+
onChange?: (values: Record<string, any>) => void
|
|
50
|
+
/** Override the built-in generated strings (required / min-max items / checking). */
|
|
51
|
+
messages?: Partial<FormMessages>
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** Deep-ish equality for dirty tracking: identity for primitives, structural
|
|
55
|
+
* (JSON) for arrays/objects. Good enough for form values. */
|
|
56
|
+
function eq(a: unknown, b: unknown): boolean {
|
|
57
|
+
if (Object.is(a, b)) return true
|
|
58
|
+
if (a && b && typeof a === 'object' && typeof b === 'object') {
|
|
59
|
+
try { return JSON.stringify(a) === JSON.stringify(b) } catch { return false }
|
|
60
|
+
}
|
|
61
|
+
return false
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** Resolve a `boolean | (values) => boolean` field flag. */
|
|
65
|
+
function resolveFlag(
|
|
66
|
+
flag: boolean | ((values: Record<string, any>) => boolean) | undefined,
|
|
67
|
+
dflt: boolean,
|
|
68
|
+
values: Record<string, any>,
|
|
69
|
+
): boolean {
|
|
70
|
+
if (flag == null) return dflt
|
|
71
|
+
return typeof flag === 'function' ? !!flag(values) : !!flag
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export function createForm(config: FormConfig) {
|
|
75
|
+
const M: FormMessages = { ...DEFAULT_FORM_MESSAGES, ...(config.messages ?? {}) }
|
|
76
|
+
let values = $state<Record<string, any>>({ ...(config.initial ?? {}) })
|
|
77
|
+
let errors = $state<Record<string, string>>({})
|
|
78
|
+
let touched = $state<Record<string, boolean>>({})
|
|
79
|
+
let submitting = $state(false)
|
|
80
|
+
let validating = $state<Record<string, boolean>>({})
|
|
81
|
+
// Baseline for dirty tracking + reset. Rebased when `reset(next)` is passed.
|
|
82
|
+
let baseline = { ...(config.initial ?? {}) }
|
|
83
|
+
// Async-validation bookkeeping: debounce timers + a per-field run sequence so
|
|
84
|
+
// only the latest response wins (stale responses are dropped).
|
|
85
|
+
const asyncTimers = new Map<string, ReturnType<typeof setTimeout>>()
|
|
86
|
+
const asyncSeq = new Map<string, number>()
|
|
87
|
+
|
|
88
|
+
// The flat field list (sections flattened) drives all values/validation.
|
|
89
|
+
const flat = () => flattenFields(config.fields())
|
|
90
|
+
const fieldOf = (name: string) => flat().find((f) => f.name === name)
|
|
91
|
+
const isVisible = (name: string) => resolveFlag(fieldOf(name)?.visible, true, values)
|
|
92
|
+
const isDisabled = (name: string) => resolveFlag(fieldOf(name)?.disabled, false, values)
|
|
93
|
+
|
|
94
|
+
function setError(name: string, err: string | null | undefined) {
|
|
95
|
+
const next = { ...errors }
|
|
96
|
+
if (err) next[name] = err
|
|
97
|
+
else delete next[name]
|
|
98
|
+
errors = next
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/** The synchronous checks: required, then declarative rules, then a sync
|
|
102
|
+
* `validate`. Returns the first error message, or null. `ctx` is the values
|
|
103
|
+
* object rules/validate see - the form values, or a single item for array
|
|
104
|
+
* rows (so within-row cross-field rules work). */
|
|
105
|
+
function syncError(field: FormField, v: any, ctx: Record<string, any> = values): string | null {
|
|
106
|
+
if (field.required && (v == null || v === '' || (Array.isArray(v) && !v.length)))
|
|
107
|
+
return M.required(field.label)
|
|
108
|
+
if (field.rules) { const e = runRules(v, field.rules, ctx); if (e) return e }
|
|
109
|
+
if (field.validate) { const e = field.validate(v, ctx); if (e) return e }
|
|
110
|
+
return null
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function cancelAsync(name: string) {
|
|
114
|
+
const t = asyncTimers.get(name)
|
|
115
|
+
if (t) { clearTimeout(t); asyncTimers.delete(name) }
|
|
116
|
+
// Bump the sequence so any in-flight response is treated as stale.
|
|
117
|
+
asyncSeq.set(name, (asyncSeq.get(name) ?? 0) + 1)
|
|
118
|
+
if (validating[name]) validating = { ...validating, [name]: false }
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/** Run a field's async validator now (guarded against stale responses). */
|
|
122
|
+
async function runAsync(name: string): Promise<boolean> {
|
|
123
|
+
const field = fieldOf(name)
|
|
124
|
+
if (!field?.asyncValidate || !isVisible(name)) return true
|
|
125
|
+
if (syncError(field, values[name])) return false // sync error already shown
|
|
126
|
+
const seq = (asyncSeq.get(name) ?? 0) + 1
|
|
127
|
+
asyncSeq.set(name, seq)
|
|
128
|
+
validating = { ...validating, [name]: true }
|
|
129
|
+
try {
|
|
130
|
+
const err = await field.asyncValidate(values[name], values)
|
|
131
|
+
if (asyncSeq.get(name) !== seq) return true // superseded - ignore
|
|
132
|
+
setError(name, err ?? null)
|
|
133
|
+
return !err
|
|
134
|
+
} finally {
|
|
135
|
+
if (asyncSeq.get(name) === seq) validating = { ...validating, [name]: false }
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function scheduleAsync(name: string) {
|
|
140
|
+
const field = fieldOf(name)
|
|
141
|
+
if (!field?.asyncValidate) return
|
|
142
|
+
const prev = asyncTimers.get(name)
|
|
143
|
+
if (prev) clearTimeout(prev)
|
|
144
|
+
asyncTimers.set(name, setTimeout(() => { asyncTimers.delete(name); runAsync(name) }, field.asyncDebounce ?? 300))
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function validateField(name: string): boolean {
|
|
148
|
+
const field = fieldOf(name)
|
|
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 }
|
|
152
|
+
// Hidden fields never block submit; clear any stale error + pending check.
|
|
153
|
+
if (!isVisible(name)) { setError(name, null); cancelAsync(name); return true }
|
|
154
|
+
if (field.type === 'array') return validateArray(field)
|
|
155
|
+
const err = syncError(field, values[name])
|
|
156
|
+
setError(name, err)
|
|
157
|
+
if (err) { cancelAsync(name); return false }
|
|
158
|
+
// Sync passed - kick off the debounced async check if there is one.
|
|
159
|
+
if (field.asyncValidate) scheduleAsync(name)
|
|
160
|
+
return true
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// --- Field arrays (repeatable groups) -----------------------------------
|
|
164
|
+
const arrayItems = (name: string): Record<string, any>[] =>
|
|
165
|
+
Array.isArray(values[name]) ? values[name] : []
|
|
166
|
+
const itemValue = (name: string, i: number, field: string) => arrayItems(name)[i]?.[field]
|
|
167
|
+
const itemError = (name: string, i: number, field: string) => errors[`${name}.${i}.${field}`]
|
|
168
|
+
|
|
169
|
+
function clearNamespacedErrors(name: string) {
|
|
170
|
+
const prefix = `${name}.`
|
|
171
|
+
const next = { ...errors }
|
|
172
|
+
let changed = false
|
|
173
|
+
for (const k of Object.keys(next)) if (k.startsWith(prefix)) { delete next[k]; changed = true }
|
|
174
|
+
if (changed) errors = next
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function setItems(name: string, next: Record<string, any>[]) {
|
|
178
|
+
values = { ...values, [name]: next }
|
|
179
|
+
config.onChange?.(values)
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function addItem(name: string, item?: Record<string, any>) {
|
|
183
|
+
const seed = item ?? Object.fromEntries((fieldOf(name)?.itemFields ?? []).map((f) => [f.name, undefined]))
|
|
184
|
+
setItems(name, [...arrayItems(name), seed])
|
|
185
|
+
}
|
|
186
|
+
function removeItem(name: string, i: number) {
|
|
187
|
+
const items = arrayItems(name).slice()
|
|
188
|
+
items.splice(i, 1)
|
|
189
|
+
clearNamespacedErrors(name) // item indices shift - drop stale keys
|
|
190
|
+
setItems(name, items)
|
|
191
|
+
}
|
|
192
|
+
function moveItem(name: string, from: number, to: number) {
|
|
193
|
+
const items = arrayItems(name).slice()
|
|
194
|
+
if (from < 0 || from >= items.length || to < 0 || to >= items.length) return
|
|
195
|
+
const [it] = items.splice(from, 1)
|
|
196
|
+
items.splice(to, 0, it as Record<string, any>)
|
|
197
|
+
clearNamespacedErrors(name)
|
|
198
|
+
setItems(name, items)
|
|
199
|
+
}
|
|
200
|
+
function setItemValue(name: string, i: number, field: string, v: any) {
|
|
201
|
+
setItems(name, arrayItems(name).map((it, ix) => (ix === i ? { ...it, [field]: v } : it)))
|
|
202
|
+
if (touched[`${name}.${i}.${field}`]) validateItemField(name, i, field)
|
|
203
|
+
}
|
|
204
|
+
function handleItemBlur(name: string, i: number, field: string) {
|
|
205
|
+
touched = { ...touched, [`${name}.${i}.${field}`]: true }
|
|
206
|
+
validateItemField(name, i, field)
|
|
207
|
+
}
|
|
208
|
+
function validateItemField(name: string, i: number, fieldName: string): boolean {
|
|
209
|
+
const itemField = fieldOf(name)?.itemFields?.find((f) => f.name === fieldName)
|
|
210
|
+
if (!itemField) return true
|
|
211
|
+
const item = arrayItems(name)[i] ?? {}
|
|
212
|
+
const err = syncError(itemField, item[fieldName], item)
|
|
213
|
+
setError(`${name}.${i}.${fieldName}`, err)
|
|
214
|
+
return !err
|
|
215
|
+
}
|
|
216
|
+
function validateArray(field: FormField): boolean {
|
|
217
|
+
const name = field.name
|
|
218
|
+
const items = arrayItems(name)
|
|
219
|
+
let arrErr: string | null = null
|
|
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)
|
|
223
|
+
setError(name, arrErr)
|
|
224
|
+
let ok = !arrErr
|
|
225
|
+
clearNamespacedErrors(name) // rebuild fresh per-item errors
|
|
226
|
+
for (let i = 0; i < items.length; i++)
|
|
227
|
+
for (const itf of field.itemFields ?? [])
|
|
228
|
+
if (!validateItemField(name, i, itf.name)) ok = false
|
|
229
|
+
return ok
|
|
230
|
+
}
|
|
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
|
+
|
|
238
|
+
function setValue(name: string, v: any) {
|
|
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
|
|
249
|
+
config.onChange?.(values)
|
|
250
|
+
// Re-validate live once the field has been blurred at least once.
|
|
251
|
+
if (touched[name]) validateField(name)
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
function handleBlur(name: string) {
|
|
255
|
+
touched = { ...touched, [name]: true }
|
|
256
|
+
validateField(name)
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
async function submit(): Promise<boolean> {
|
|
260
|
+
let ok = true
|
|
261
|
+
const t: Record<string, boolean> = { ...touched }
|
|
262
|
+
for (const f of flat()) {
|
|
263
|
+
if (!isVisible(f.name)) continue
|
|
264
|
+
t[f.name] = true
|
|
265
|
+
if (!validateField(f.name)) ok = false
|
|
266
|
+
}
|
|
267
|
+
touched = t
|
|
268
|
+
if (!ok) return false
|
|
269
|
+
// Await async validators (run now, skipping their debounce). Any failure blocks.
|
|
270
|
+
const asyncFields = flat().filter((f) => f.asyncValidate && isVisible(f.name))
|
|
271
|
+
if (asyncFields.length) {
|
|
272
|
+
for (const f of asyncFields) { const timer = asyncTimers.get(f.name); if (timer) { clearTimeout(timer); asyncTimers.delete(f.name) } }
|
|
273
|
+
const results = await Promise.all(asyncFields.map((f) => runAsync(f.name)))
|
|
274
|
+
if (results.some((r) => !r)) return false
|
|
275
|
+
}
|
|
276
|
+
// Exclude hidden fields from the submitted payload; keep any non-field keys.
|
|
277
|
+
const payload = { ...values }
|
|
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)
|
|
281
|
+
submitting = true
|
|
282
|
+
try {
|
|
283
|
+
await config.onSubmit?.(payload)
|
|
284
|
+
} finally {
|
|
285
|
+
submitting = false
|
|
286
|
+
}
|
|
287
|
+
return true
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
/** Validate a subset of fields by name (sync + their async validators). Used
|
|
291
|
+
* to gate a wizard step before advancing. Returns true when all pass. */
|
|
292
|
+
async function validateFields(names: string[]): Promise<boolean> {
|
|
293
|
+
let ok = true
|
|
294
|
+
const t = { ...touched }
|
|
295
|
+
for (const name of names) {
|
|
296
|
+
if (!isVisible(name)) continue
|
|
297
|
+
t[name] = true
|
|
298
|
+
if (!validateField(name)) ok = false
|
|
299
|
+
}
|
|
300
|
+
touched = t
|
|
301
|
+
if (!ok) return false
|
|
302
|
+
const asyncNames = names.filter((n) => fieldOf(n)?.asyncValidate && isVisible(n))
|
|
303
|
+
if (asyncNames.length) {
|
|
304
|
+
for (const n of asyncNames) { const timer = asyncTimers.get(n); if (timer) { clearTimeout(timer); asyncTimers.delete(n) } }
|
|
305
|
+
const results = await Promise.all(asyncNames.map((n) => runAsync(n)))
|
|
306
|
+
if (results.some((r) => !r)) return false
|
|
307
|
+
}
|
|
308
|
+
return true
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
/** Restore values to the baseline (or a new one), clearing errors + touched. */
|
|
312
|
+
function reset(next?: Record<string, any>) {
|
|
313
|
+
if (next) baseline = { ...next }
|
|
314
|
+
values = { ...baseline }
|
|
315
|
+
errors = {}
|
|
316
|
+
touched = {}
|
|
317
|
+
config.onChange?.(values)
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
/** Inject errors (e.g. server-side validation after submit) and mark those
|
|
321
|
+
* fields touched so the messages show. Empty message clears a field's error. */
|
|
322
|
+
function setErrors(map: Record<string, string>) {
|
|
323
|
+
const nextErr = { ...errors }
|
|
324
|
+
const nextTouched = { ...touched }
|
|
325
|
+
for (const [k, msg] of Object.entries(map)) {
|
|
326
|
+
if (msg) { nextErr[k] = msg; nextTouched[k] = true }
|
|
327
|
+
else delete nextErr[k]
|
|
328
|
+
}
|
|
329
|
+
errors = nextErr
|
|
330
|
+
touched = nextTouched
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
const isFieldDirty = (name: string) => !eq(values[name], baseline[name])
|
|
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
|
+
|
|
346
|
+
return {
|
|
347
|
+
get values() { return values },
|
|
348
|
+
get submitting() { return submitting },
|
|
349
|
+
get isDirty() {
|
|
350
|
+
const keys = new Set([...Object.keys(values), ...Object.keys(baseline)])
|
|
351
|
+
for (const k of keys) if (!eq(values[k], baseline[k])) return true
|
|
352
|
+
return false
|
|
353
|
+
},
|
|
354
|
+
value: (name: string) => { const f = fieldOf(name); return f?.computed ? f.computed(values) : values[name] },
|
|
355
|
+
error: (name: string): string | undefined => errors[name],
|
|
356
|
+
isTouched: (name: string) => !!touched[name],
|
|
357
|
+
hasError: (name: string) => !!errors[name],
|
|
358
|
+
isValidating: (name: string) => !!validating[name],
|
|
359
|
+
isFieldDirty,
|
|
360
|
+
isVisible,
|
|
361
|
+
isDisabled,
|
|
362
|
+
entries: () => config.fields(),
|
|
363
|
+
// Field arrays
|
|
364
|
+
arrayItems,
|
|
365
|
+
itemValue,
|
|
366
|
+
itemError,
|
|
367
|
+
addItem,
|
|
368
|
+
removeItem,
|
|
369
|
+
moveItem,
|
|
370
|
+
setItemValue,
|
|
371
|
+
handleItemBlur,
|
|
372
|
+
setValue,
|
|
373
|
+
handleBlur,
|
|
374
|
+
validateField,
|
|
375
|
+
validateFields,
|
|
376
|
+
submit,
|
|
377
|
+
reset,
|
|
378
|
+
setErrors,
|
|
379
|
+
errorList,
|
|
380
|
+
firstErrorField,
|
|
381
|
+
/** The resolved message strings (defaults merged with `config.messages`). */
|
|
382
|
+
messages: M,
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
export type Form = ReturnType<typeof createForm>
|