@svgrid/grid 1.2.34 → 2.0.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 +6 -5
- package/dist/FlexRender.svelte +96 -96
- package/dist/GridFooter.svelte +178 -178
- package/dist/SvAccordion.svelte +109 -0
- package/dist/SvAccordion.svelte.d.ts +28 -0
- package/dist/SvAlert.svelte +92 -0
- package/dist/SvAlert.svelte.d.ts +27 -0
- package/dist/SvAutoComplete.svelte +47 -18
- package/dist/SvAutoComplete.svelte.d.ts +2 -5
- package/dist/SvAvatar.svelte +75 -0
- package/dist/SvAvatar.svelte.d.ts +14 -0
- package/dist/SvAvatarGroup.svelte +48 -0
- package/dist/SvAvatarGroup.svelte.d.ts +16 -0
- package/dist/SvBadge.svelte +54 -0
- package/dist/SvBadge.svelte.d.ts +23 -0
- package/dist/SvBreadcrumb.svelte +88 -0
- package/dist/SvBreadcrumb.svelte.d.ts +23 -0
- package/dist/SvButtonGroup.svelte +121 -0
- package/dist/SvButtonGroup.svelte.d.ts +16 -0
- package/dist/SvCalendar.svelte +72 -9
- package/dist/SvCalendar.svelte.d.ts +34 -1
- package/dist/SvCard.svelte +71 -0
- package/dist/SvCard.svelte.d.ts +28 -0
- package/dist/SvCarousel.svelte +134 -0
- package/dist/SvCarousel.svelte.d.ts +28 -0
- package/dist/SvCheckBox.svelte +36 -21
- package/dist/SvCheckBox.svelte.d.ts +2 -5
- package/dist/SvChip.svelte +88 -0
- package/dist/SvChip.svelte.d.ts +31 -0
- package/dist/SvCircularProgress.svelte +109 -0
- package/dist/SvCircularProgress.svelte.d.ts +31 -0
- package/dist/SvColorInput.svelte +54 -27
- package/dist/SvColorInput.svelte.d.ts +10 -6
- package/dist/SvComboBox.svelte +119 -33
- package/dist/SvComboBox.svelte.d.ts +18 -6
- package/dist/SvCommand.svelte +197 -0
- package/dist/SvCommand.svelte.d.ts +26 -0
- package/dist/SvContextMenu.svelte +96 -0
- package/dist/SvContextMenu.svelte.d.ts +25 -0
- package/dist/SvCountryInput.svelte +56 -21
- package/dist/SvCountryInput.svelte.d.ts +9 -5
- package/dist/SvDateRangeInput.svelte +256 -0
- package/dist/SvDateRangeInput.svelte.d.ts +37 -0
- package/dist/SvDateTimePicker.svelte +57 -20
- package/dist/SvDateTimePicker.svelte.d.ts +12 -4
- package/dist/SvDivider.svelte +61 -0
- package/dist/SvDivider.svelte.d.ts +22 -0
- package/dist/SvDrawer.svelte +226 -0
- package/dist/SvDrawer.svelte.d.ts +43 -0
- package/dist/SvDropDownList.svelte +114 -35
- package/dist/SvDropDownList.svelte.d.ts +7 -6
- package/dist/SvDurationInput.svelte +108 -0
- package/dist/SvDurationInput.svelte.d.ts +15 -0
- package/dist/SvEmptyState.svelte +59 -0
- package/dist/SvEmptyState.svelte.d.ts +25 -0
- package/dist/SvField.svelte +85 -0
- package/dist/SvField.svelte.d.ts +30 -0
- package/dist/SvFileUpload.svelte +169 -0
- package/dist/SvFileUpload.svelte.d.ts +24 -0
- package/dist/SvForm.svelte +7 -1
- package/dist/SvForm.svelte.d.ts +3 -0
- package/dist/SvGauge.svelte +7 -1
- package/dist/SvGauge.svelte.d.ts +11 -0
- package/dist/SvGrid.controller.svelte.d.ts +6 -0
- package/dist/SvGrid.controller.svelte.js +64 -9
- package/dist/SvGrid.css +2376 -2343
- package/dist/SvGrid.svelte +2764 -2651
- package/dist/SvGrid.types.d.ts +366 -0
- package/dist/SvGridBoard.svelte +2281 -0
- package/dist/SvGridBoard.svelte.d.ts +31 -0
- package/dist/SvGridDropdown.svelte +666 -665
- package/dist/SvGridSelect.svelte +255 -0
- package/dist/SvGridSelect.svelte.d.ts +32 -0
- package/dist/SvGroupCell.svelte +109 -0
- package/dist/SvGroupCell.svelte.d.ts +41 -0
- package/dist/SvListBox.svelte +123 -36
- package/dist/SvListBox.svelte.d.ts +12 -8
- package/dist/SvMaskedInput.svelte +53 -13
- package/dist/SvMaskedInput.svelte.d.ts +16 -6
- package/dist/SvMenu.svelte +101 -0
- package/dist/SvMenu.svelte.d.ts +26 -0
- package/dist/SvMenuList.svelte +143 -0
- package/dist/SvMenuList.svelte.d.ts +24 -0
- package/dist/SvModal.svelte +220 -0
- package/dist/SvModal.svelte.d.ts +31 -0
- package/dist/SvMultiSelect.svelte +277 -0
- package/dist/SvMultiSelect.svelte.d.ts +31 -0
- package/dist/SvNavPane.svelte +314 -0
- package/dist/SvNavPane.svelte.d.ts +59 -0
- package/dist/SvNumberInput.svelte +23 -9
- package/dist/SvNumberInput.svelte.d.ts +2 -0
- package/dist/SvOtpInput.svelte +157 -0
- package/dist/SvOtpInput.svelte.d.ts +16 -0
- package/dist/SvPagination.svelte +100 -0
- package/dist/SvPagination.svelte.d.ts +16 -0
- package/dist/SvPasswordInput.svelte +42 -12
- package/dist/SvPasswordInput.svelte.d.ts +13 -6
- package/dist/SvPhoneInput.svelte +43 -11
- package/dist/SvPhoneInput.svelte.d.ts +10 -11
- package/dist/SvPopover.svelte +127 -0
- package/dist/SvPopover.svelte.d.ts +35 -0
- package/dist/SvProgress.svelte +116 -0
- package/dist/SvProgress.svelte.d.ts +32 -0
- package/dist/SvRadioGroup.svelte +40 -22
- package/dist/SvRadioGroup.svelte.d.ts +2 -10
- package/dist/SvRating.svelte +51 -30
- package/dist/SvRating.svelte.d.ts +10 -7
- package/dist/SvRichText.svelte +192 -0
- package/dist/SvRichText.svelte.d.ts +16 -0
- package/dist/SvRowGroupPanel.svelte +147 -0
- package/dist/SvRowGroupPanel.svelte.d.ts +17 -0
- package/dist/SvScrollArea.svelte +50 -0
- package/dist/SvScrollArea.svelte.d.ts +22 -0
- package/dist/SvSkeleton.svelte +76 -0
- package/dist/SvSkeleton.svelte.d.ts +25 -0
- package/dist/SvSlider.svelte +41 -9
- package/dist/SvSlider.svelte.d.ts +4 -4
- package/dist/SvSparkline.svelte +77 -0
- package/dist/SvSparkline.svelte.d.ts +28 -0
- package/dist/SvSplitter.svelte +138 -0
- package/dist/SvSplitter.svelte.d.ts +32 -0
- package/dist/SvStat.svelte +86 -0
- package/dist/SvStat.svelte.d.ts +30 -0
- package/dist/SvStepper.svelte +91 -0
- package/dist/SvStepper.svelte.d.ts +24 -0
- package/dist/SvSwitchButton.svelte +39 -24
- package/dist/SvSwitchButton.svelte.d.ts +2 -5
- package/dist/SvTabs.svelte +147 -28
- package/dist/SvTabs.svelte.d.ts +16 -4
- package/dist/SvTagsInput.svelte +59 -22
- package/dist/SvTagsInput.svelte.d.ts +9 -4
- package/dist/SvTextArea.svelte +113 -0
- package/dist/SvTextArea.svelte.d.ts +18 -0
- package/dist/SvTextInput.svelte +115 -0
- package/dist/SvTextInput.svelte.d.ts +20 -0
- package/dist/SvTimePicker.svelte +19 -5
- package/dist/SvTimePicker.svelte.d.ts +12 -0
- package/dist/SvTimeline.svelte +75 -0
- package/dist/SvTimeline.svelte.d.ts +24 -0
- package/dist/SvToaster.svelte +124 -0
- package/dist/SvToaster.svelte.d.ts +10 -0
- package/dist/SvToggleButton.svelte +37 -12
- package/dist/SvToggleButton.svelte.d.ts +2 -4
- package/dist/SvTooltip.svelte +113 -0
- package/dist/SvTooltip.svelte.d.ts +22 -0
- package/dist/SvTour.svelte +151 -0
- package/dist/SvTour.svelte.d.ts +25 -0
- package/dist/SvTree.svelte +286 -44
- package/dist/SvTree.svelte.d.ts +26 -9
- package/dist/SvTreeSelect.svelte +222 -0
- package/dist/SvTreeSelect.svelte.d.ts +24 -0
- package/dist/a11y/dismissable.d.ts +32 -0
- package/dist/a11y/dismissable.js +54 -0
- package/dist/a11y/focus-trap.d.ts +39 -0
- package/dist/a11y/focus-trap.js +142 -0
- package/dist/a11y/live-region.d.ts +8 -0
- package/dist/a11y/live-region.js +60 -0
- package/dist/a11y/scroll-lock.d.ts +4 -0
- package/dist/a11y/scroll-lock.js +41 -0
- package/dist/a11y.d.ts +3 -1
- package/dist/a11y.js +1 -1
- package/dist/avatar.d.ts +10 -0
- package/dist/avatar.js +22 -0
- package/dist/builtin-editors.d.ts +5 -0
- package/dist/builtin-editors.js +55 -0
- package/dist/cdn/svgrid.js +16022 -8235
- package/dist/cdn/svgrid.svelte-external.js +14543 -6765
- package/dist/columns.js +3 -2
- package/dist/core.d.ts +7 -1
- package/dist/countries.d.ts +12 -0
- package/dist/countries.js +24 -0
- package/dist/createAccordion.svelte.d.ts +71 -0
- package/dist/createAccordion.svelte.js +99 -0
- package/dist/createAutocomplete.svelte.d.ts +13 -4
- package/dist/createAutocomplete.svelte.js +11 -1
- package/dist/createButtonGroup.svelte.d.ts +62 -0
- package/dist/createButtonGroup.svelte.js +154 -0
- package/dist/createCalendar.svelte.d.ts +5 -0
- package/dist/createCalendar.svelte.js +2 -0
- package/dist/createCheckbox.svelte.d.ts +9 -17
- package/dist/createCheckbox.svelte.js +28 -1
- package/dist/createColorInput.svelte.d.ts +12 -22
- package/dist/createColorInput.svelte.js +13 -0
- package/dist/createCombobox.svelte.d.ts +16 -5
- package/dist/createCombobox.svelte.js +13 -2
- package/dist/createCountryInput.svelte.d.ts +14 -3
- package/dist/createCountryInput.svelte.js +12 -2
- package/dist/createDateTimePicker.svelte.d.ts +17 -2
- package/dist/createDateTimePicker.svelte.js +12 -2
- package/dist/createDropdownList.svelte.d.ts +14 -5
- package/dist/createDropdownList.svelte.js +50 -1
- package/dist/createFileUpload.svelte.d.ts +68 -0
- package/dist/createFileUpload.svelte.js +122 -0
- package/dist/createListbox.svelte.d.ts +10 -1
- package/dist/createListbox.svelte.js +48 -1
- package/dist/createMaskedInput.svelte.d.ts +11 -2
- package/dist/createMaskedInput.svelte.js +11 -1
- package/dist/createNumberInput.svelte.d.ts +1 -0
- package/dist/createNumberInput.svelte.js +1 -0
- package/dist/createPasswordInput.svelte.d.ts +19 -21
- package/dist/createPasswordInput.svelte.js +16 -4
- package/dist/createPhoneInput.svelte.d.ts +26 -7
- package/dist/createPhoneInput.svelte.js +24 -3
- package/dist/createPopoverSelect.svelte.d.ts +86 -0
- package/dist/createPopoverSelect.svelte.js +196 -0
- package/dist/createRadioGroup.svelte.d.ts +9 -0
- package/dist/createRadioGroup.svelte.js +11 -1
- package/dist/createRating.svelte.d.ts +11 -0
- package/dist/createRating.svelte.js +12 -1
- package/dist/createSlider.svelte.d.ts +2 -0
- package/dist/createSlider.svelte.js +16 -5
- package/dist/createSplitter.svelte.d.ts +67 -0
- package/dist/createSplitter.svelte.js +99 -0
- package/dist/createSwitch.svelte.d.ts +9 -16
- package/dist/createSwitch.svelte.js +28 -1
- package/dist/createTabs.svelte.d.ts +7 -0
- package/dist/createTabs.svelte.js +17 -2
- package/dist/createTagsInput.svelte.d.ts +13 -1
- package/dist/createTagsInput.svelte.js +12 -3
- package/dist/createToggle.svelte.d.ts +9 -0
- package/dist/createToggle.svelte.js +12 -1
- package/dist/createTree.svelte.d.ts +19 -0
- package/dist/createTree.svelte.js +108 -15
- package/dist/debounce.d.ts +11 -0
- package/dist/debounce.js +30 -0
- package/dist/duration.d.ts +15 -0
- package/dist/duration.js +51 -0
- package/dist/editor-contract.d.ts +62 -7
- package/dist/editor-contract.js +45 -5
- package/dist/editor-registry.d.ts +67 -0
- package/dist/editor-registry.js +38 -0
- package/dist/editors/cell-editors.d.ts +1 -1
- package/dist/editors/cell-editors.js +5 -0
- package/dist/fuzzy.d.ts +3 -0
- package/dist/fuzzy.js +41 -0
- package/dist/grid-select.d.ts +9 -0
- package/dist/grid-select.js +8 -0
- package/dist/index.d.ts +79 -6
- package/dist/index.js +78 -4
- package/dist/keyboard-handlers.js +19 -0
- package/dist/list-option.d.ts +36 -0
- package/dist/list-option.js +68 -0
- package/dist/otp.d.ts +16 -0
- package/dist/otp.js +26 -0
- package/dist/paginate.d.ts +20 -0
- package/dist/paginate.js +40 -0
- package/dist/popover.d.ts +15 -1
- package/dist/popover.js +29 -3
- package/dist/recurrence.d.ts +36 -0
- package/dist/recurrence.js +90 -0
- package/dist/server-data-source.d.ts +89 -0
- package/dist/server-data-source.js +4 -0
- package/dist/server-group-model.d.ts +98 -0
- package/dist/server-group-model.js +263 -0
- package/dist/toast-store.svelte.d.ts +41 -0
- package/dist/toast-store.svelte.js +113 -0
- package/dist/tree-select.d.ts +19 -0
- package/dist/tree-select.js +40 -0
- package/dist/ui-app.types.d.ts +80 -0
- package/dist/ui-app.types.js +7 -0
- package/dist/validators.d.ts +43 -0
- package/dist/validators.js +53 -0
- package/dist/virtual.d.ts +35 -0
- package/dist/virtual.js +34 -0
- package/package.json +85 -85
- package/src/FlexRender.svelte +96 -96
- package/src/GridFooter.svelte +178 -178
- package/src/SvAccordion.svelte +109 -0
- package/src/SvAccordion.test.ts +66 -0
- package/src/SvAlert.svelte +92 -0
- package/src/SvAutoComplete.svelte +47 -18
- package/src/SvAvatar.svelte +75 -0
- package/src/SvAvatarGroup.svelte +48 -0
- package/src/SvBadge.svelte +54 -0
- package/src/SvBreadcrumb.svelte +88 -0
- package/src/SvButtonGroup.svelte +121 -0
- package/src/SvButtonGroup.test.ts +72 -0
- package/src/SvCalendar.svelte +72 -9
- package/src/SvCard.svelte +71 -0
- package/src/SvCarousel.svelte +134 -0
- package/src/SvCheckBox.svelte +36 -21
- package/src/SvChip.svelte +88 -0
- package/src/SvCircularProgress.svelte +109 -0
- package/src/SvColorInput.svelte +54 -27
- package/src/SvComboBox.svelte +119 -33
- package/src/SvCommand.svelte +197 -0
- package/src/SvContextMenu.svelte +96 -0
- package/src/SvCountryInput.svelte +56 -21
- package/src/SvDateRangeInput.svelte +256 -0
- package/src/SvDateRangeInput.test.ts +80 -0
- package/src/SvDateTimePicker.svelte +57 -20
- package/src/SvDivider.svelte +61 -0
- package/src/SvDrawer.svelte +226 -0
- package/src/SvDrawer.test.ts +125 -0
- package/src/SvDropDownList.svelte +114 -35
- package/src/SvDurationInput.svelte +108 -0
- package/src/SvEmptyState.svelte +59 -0
- package/src/SvField.svelte +85 -0
- package/src/SvFileUpload.svelte +169 -0
- package/src/SvFileUpload.test.ts +59 -0
- package/src/SvForm.svelte +7 -1
- package/src/SvGauge.svelte +7 -1
- package/src/SvGrid.controller.svelte.ts +2553 -2494
- package/src/SvGrid.css +2376 -2343
- package/src/SvGrid.svelte +2764 -2651
- package/src/SvGrid.types.ts +944 -570
- package/src/SvGridBoard.svelte +2281 -0
- package/src/SvGridDropdown.svelte +666 -665
- package/src/SvGridSelect.svelte +255 -0
- package/src/SvGridSelect.test.ts +100 -0
- package/src/SvGroupCell.svelte +109 -0
- package/src/SvListBox.svelte +123 -36
- package/src/SvMaskedInput.svelte +53 -13
- package/src/SvMenu.svelte +101 -0
- package/src/SvMenu.test.ts +73 -0
- package/src/SvMenuList.svelte +143 -0
- package/src/SvModal.svelte +220 -0
- package/src/SvMultiSelect.svelte +277 -0
- package/src/SvMultiSelect.test.ts +167 -0
- package/src/SvNavPane.svelte +314 -0
- package/src/SvNavPane.test.ts +70 -0
- package/src/SvNumberInput.svelte +23 -9
- package/src/SvOtpInput.svelte +157 -0
- package/src/SvPagination.svelte +100 -0
- package/src/SvPasswordInput.svelte +42 -12
- package/src/SvPhoneInput.svelte +43 -11
- package/src/SvPopover.svelte +127 -0
- package/src/SvProgress.svelte +116 -0
- package/src/SvRadioGroup.svelte +40 -22
- package/src/SvRating.svelte +51 -30
- package/src/SvRichText.svelte +192 -0
- package/src/SvRowGroupPanel.svelte +147 -0
- package/src/SvScrollArea.svelte +50 -0
- package/src/SvSkeleton.svelte +76 -0
- package/src/SvSlider.svelte +41 -9
- package/src/SvSparkline.svelte +77 -0
- package/src/SvSplitter.svelte +138 -0
- package/src/SvSplitter.test.ts +55 -0
- package/src/SvStat.svelte +86 -0
- package/src/SvStepper.svelte +91 -0
- package/src/SvSwitchButton.svelte +39 -24
- package/src/SvTabs.svelte +147 -28
- package/src/SvTagsInput.svelte +59 -22
- package/src/SvTextArea.svelte +113 -0
- package/src/SvTextInput.svelte +115 -0
- package/src/SvTimePicker.svelte +19 -5
- package/src/SvTimeline.svelte +75 -0
- package/src/SvToaster.svelte +124 -0
- package/src/SvToaster.test.ts +63 -0
- package/src/SvToggleButton.svelte +37 -12
- package/src/SvTooltip.svelte +113 -0
- package/src/SvTour.svelte +151 -0
- package/src/SvTree.svelte +286 -44
- package/src/SvTreeSelect.svelte +222 -0
- package/src/SvTreeSelect.test.ts +87 -0
- package/src/a11y/dismissable.test.ts +95 -0
- package/src/a11y/dismissable.ts +94 -0
- package/src/a11y/focus-trap.test.ts +110 -0
- package/src/a11y/focus-trap.ts +134 -0
- package/src/a11y/live-region.test.ts +40 -0
- package/src/a11y/live-region.ts +65 -0
- package/src/a11y/scroll-lock.test.ts +38 -0
- package/src/a11y/scroll-lock.ts +41 -0
- package/src/a11y.contract.test.ts +49 -49
- package/src/a11y.test.ts +59 -59
- package/src/a11y.ts +61 -59
- package/src/avatar.test.ts +32 -0
- package/src/avatar.ts +21 -0
- package/src/board.test.ts +927 -0
- package/src/build-api.ts +798 -798
- package/src/builtin-editors.grid.test.ts +85 -0
- package/src/builtin-editors.test.ts +58 -0
- package/src/builtin-editors.ts +56 -0
- package/src/cell-formatting.ts +169 -169
- package/src/cell-render.ts +469 -469
- package/src/collaboration.test.ts +104 -104
- package/src/collaboration.ts +167 -167
- package/src/columns.ts +3 -2
- package/src/core.performance.test.ts +30 -30
- package/src/core.ts +1111 -1101
- package/src/countries.ts +25 -0
- package/src/createAccordion.svelte.ts +126 -0
- package/src/createAutocomplete.svelte.ts +18 -1
- package/src/createButtonGroup.svelte.ts +166 -0
- package/src/createCalendar.svelte.ts +6 -0
- package/src/createCheckbox.svelte.ts +23 -1
- package/src/createColorInput.svelte.ts +20 -0
- package/src/createCombobox.svelte.ts +22 -2
- package/src/createCountryInput.svelte.ts +21 -2
- package/src/createDateTimePicker.svelte.ts +23 -2
- package/src/createDropdownList.svelte.ts +31 -2
- package/src/createFileUpload.svelte.ts +144 -0
- 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 +35 -3
- package/src/createMaskedInput.svelte.ts +18 -1
- package/src/createNumberInput.svelte.ts +2 -0
- package/src/createPasswordInput.svelte.ts +31 -5
- package/src/createPhoneInput.svelte.ts +38 -4
- package/src/createPopoverSelect.svelte.ts +208 -0
- package/src/createRadioGroup.svelte.ts +23 -1
- package/src/createRating.svelte.ts +26 -1
- package/src/createSlider.svelte.ts +14 -3
- package/src/createSplitter.svelte.ts +119 -0
- package/src/createSwitch.svelte.ts +26 -3
- package/src/createTabs.svelte.ts +20 -3
- package/src/createTagsInput.svelte.ts +24 -3
- package/src/createToggle.svelte.ts +25 -2
- package/src/createTree.svelte.ts +104 -14
- package/src/debounce.test.ts +50 -0
- package/src/debounce.ts +41 -0
- package/src/duration.test.ts +41 -0
- package/src/duration.ts +51 -0
- package/src/editing.test.ts +859 -859
- package/src/editing.ts +675 -675
- package/src/editor-contract.ts +87 -8
- package/src/editor-registry.grid.test.ts +98 -0
- package/src/editor-registry.test.ts +86 -0
- package/src/editor-registry.ts +106 -0
- package/src/editors/cell-editors.ts +10 -1
- package/src/export-data-api.test.ts +126 -126
- package/src/export-format.test.ts +107 -107
- package/src/export-format.ts +598 -598
- package/src/flex-render.ts +3 -3
- package/src/fuzzy.test.ts +40 -0
- package/src/fuzzy.ts +39 -0
- package/src/grid-select.test.ts +28 -0
- package/src/grid-select.ts +20 -0
- package/src/index.ts +463 -318
- package/src/keyboard-handlers.ts +20 -0
- package/src/keyboard.test.ts +59 -59
- package/src/keyboard.ts +97 -97
- package/src/list-option.ts +74 -0
- package/src/list-power.test.ts +71 -0
- package/src/menus.ts +582 -582
- package/src/merge-objects.ts +48 -48
- package/src/otp.test.ts +28 -0
- package/src/otp.ts +28 -0
- package/src/overlays.test.ts +85 -0
- package/src/paginate.test.ts +41 -0
- package/src/paginate.ts +61 -0
- package/src/phone-valid.test.ts +26 -0
- package/src/popover.ts +31 -3
- package/src/progress.test.ts +55 -0
- package/src/recurrence.test.ts +91 -0
- package/src/recurrence.ts +111 -0
- package/src/render-component.ts +28 -28
- package/src/selection.test.ts +754 -754
- package/src/selection.ts +600 -600
- package/src/server-data-source.test.ts +289 -289
- package/src/server-data-source.ts +413 -311
- package/src/server-group-model.test.ts +294 -0
- package/src/server-group-model.ts +370 -0
- package/src/sparkline.test.ts +68 -68
- package/src/sparkline.ts +169 -169
- package/src/spreadsheet.test.ts +489 -489
- package/src/spreadsheet.ts +304 -304
- package/src/static-functions.ts +11 -11
- package/src/subscribe.ts +38 -38
- package/src/svgrid-wrapper.types.ts +439 -439
- package/src/svgrid.behavior.test.ts +706 -706
- package/src/svgrid.features.test.ts +157 -157
- package/src/svgrid.new-features.wrapper.test.ts +251 -251
- package/src/svgrid.responsive.test.ts +72 -0
- package/src/svgrid.wrapper.test.ts +40 -40
- package/src/toast-store.svelte.ts +148 -0
- package/src/toast-store.test.ts +86 -0
- package/src/tree-edit.test.ts +49 -0
- package/src/tree-filter.test.ts +56 -0
- package/src/tree-select.test.ts +48 -0
- package/src/tree-select.ts +62 -0
- package/src/ui-app.test.ts +130 -0
- package/src/ui-app.types.ts +88 -0
- package/src/ui-display.test.ts +79 -0
- package/src/ui-inputs.test.ts +1 -1
- package/src/ui-status.test.ts +141 -0
- package/src/ui-tier1.test.ts +142 -0
- package/src/ui-tier2.test.ts +124 -0
- package/src/ui-tier3.test.ts +139 -0
- package/src/validators.test.ts +58 -0
- package/src/validators.ts +105 -0
- package/src/virtual.test.ts +72 -0
- package/src/virtual.ts +67 -0
- 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
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Component tests for the Tier-3 batch: SvRichText, SvCarousel, SvTour.
|
|
3
|
+
* (execCommand-driven formatting is a no-op in jsdom, so RichText tests cover
|
|
4
|
+
* structure + value/onChange, not the rendered rich output.)
|
|
5
|
+
*/
|
|
6
|
+
import { afterEach, describe, expect, it, vi } from 'vitest'
|
|
7
|
+
import { mount, unmount, flushSync } from 'svelte'
|
|
8
|
+
import SvRichText from './SvRichText.svelte'
|
|
9
|
+
import SvCarousel from './SvCarousel.svelte'
|
|
10
|
+
import SvTour from './SvTour.svelte'
|
|
11
|
+
|
|
12
|
+
function mountOn(Comp: any, props: Record<string, unknown>) {
|
|
13
|
+
const target = document.createElement('div')
|
|
14
|
+
document.body.appendChild(target)
|
|
15
|
+
const app = mount(Comp, { target, props: props as any })
|
|
16
|
+
flushSync()
|
|
17
|
+
return { target, destroy: () => { unmount(app); target.remove() } }
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
afterEach(() => { document.body.innerHTML = '' })
|
|
21
|
+
|
|
22
|
+
describe('SvRichText', () => {
|
|
23
|
+
it('renders a toolbar and an editable region seeded from value', () => {
|
|
24
|
+
const { target, destroy } = mountOn(SvRichText, { value: '<p>Hello</p>' })
|
|
25
|
+
try {
|
|
26
|
+
expect(target.querySelector('.sv-rt__toolbar')).not.toBeNull()
|
|
27
|
+
expect(target.querySelectorAll('.sv-rt__btn').length).toBeGreaterThan(6)
|
|
28
|
+
const body = target.querySelector<HTMLElement>('.sv-rt__body')!
|
|
29
|
+
expect(body.getAttribute('contenteditable')).toBe('true')
|
|
30
|
+
expect(body.innerHTML).toBe('<p>Hello</p>')
|
|
31
|
+
} finally { destroy() }
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
it('emits onChange on input', () => {
|
|
35
|
+
const onChange = vi.fn()
|
|
36
|
+
const { target, destroy } = mountOn(SvRichText, { value: '', onChange })
|
|
37
|
+
try {
|
|
38
|
+
const body = target.querySelector<HTMLElement>('.sv-rt__body')!
|
|
39
|
+
body.innerHTML = '<b>hi</b>'
|
|
40
|
+
body.dispatchEvent(new Event('input', { bubbles: true }))
|
|
41
|
+
expect(onChange).toHaveBeenCalledWith('<b>hi</b>')
|
|
42
|
+
} finally { destroy() }
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
it('readonly hides the toolbar and disables editing', () => {
|
|
46
|
+
const { target, destroy } = mountOn(SvRichText, { value: 'x', readonly: true })
|
|
47
|
+
try {
|
|
48
|
+
expect(target.querySelector('.sv-rt__toolbar')).toBeNull()
|
|
49
|
+
expect(target.querySelector('.sv-rt__body')!.getAttribute('contenteditable')).toBe('false')
|
|
50
|
+
} finally { destroy() }
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
it('honours a custom tools list', () => {
|
|
54
|
+
const { target, destroy } = mountOn(SvRichText, { value: '', tools: ['bold', 'italic'] })
|
|
55
|
+
try {
|
|
56
|
+
expect(target.querySelectorAll('.sv-rt__btn')).toHaveLength(2)
|
|
57
|
+
} finally { destroy() }
|
|
58
|
+
})
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
describe('SvCarousel', () => {
|
|
62
|
+
const slideProps = { count: 3, slide: undefined as any }
|
|
63
|
+
// Provide a minimal slide snippet via a wrapper is awkward; render count-only.
|
|
64
|
+
|
|
65
|
+
it('renders one slide element per count with arrows + dots', () => {
|
|
66
|
+
const { target, destroy } = mountOn(SvCarousel, { ...slideProps })
|
|
67
|
+
try {
|
|
68
|
+
expect(target.querySelectorAll('.sv-carousel__slide')).toHaveLength(3)
|
|
69
|
+
expect(target.querySelectorAll('.sv-carousel__dot')).toHaveLength(3)
|
|
70
|
+
expect(target.querySelector('.sv-carousel__arrow.is-next')).not.toBeNull()
|
|
71
|
+
} finally { destroy() }
|
|
72
|
+
})
|
|
73
|
+
|
|
74
|
+
it('next advances the active dot (with loop wrap)', () => {
|
|
75
|
+
const { target, destroy } = mountOn(SvCarousel, { count: 3, current: 0 })
|
|
76
|
+
try {
|
|
77
|
+
target.querySelector<HTMLButtonElement>('.sv-carousel__arrow.is-next')!.click()
|
|
78
|
+
flushSync()
|
|
79
|
+
const active = [...target.querySelectorAll('.sv-carousel__dot')].findIndex((d) => d.classList.contains('is-active'))
|
|
80
|
+
expect(active).toBe(1)
|
|
81
|
+
} finally { destroy() }
|
|
82
|
+
})
|
|
83
|
+
|
|
84
|
+
it('clicking a dot jumps to that slide', () => {
|
|
85
|
+
const { target, destroy } = mountOn(SvCarousel, { count: 4, current: 0 })
|
|
86
|
+
try {
|
|
87
|
+
target.querySelectorAll<HTMLButtonElement>('.sv-carousel__dot')[2]!.click()
|
|
88
|
+
flushSync()
|
|
89
|
+
expect(target.querySelectorAll('.sv-carousel__dot')[2]!.classList.contains('is-active')).toBe(true)
|
|
90
|
+
} finally { destroy() }
|
|
91
|
+
})
|
|
92
|
+
})
|
|
93
|
+
|
|
94
|
+
describe('SvTour', () => {
|
|
95
|
+
const steps = [
|
|
96
|
+
{ title: 'Welcome', content: 'Intro.' },
|
|
97
|
+
{ title: 'Step two', content: 'More.' },
|
|
98
|
+
{ title: 'Done', content: 'Finish.' },
|
|
99
|
+
]
|
|
100
|
+
|
|
101
|
+
it('shows the first step with a counter and Next', () => {
|
|
102
|
+
const { destroy } = mountOn(SvTour, { open: true, steps })
|
|
103
|
+
try {
|
|
104
|
+
expect(document.body.querySelector('.sv-tour__title')!.textContent).toBe('Welcome')
|
|
105
|
+
expect(document.body.querySelector('.sv-tour__count')!.textContent).toBe('1 / 3')
|
|
106
|
+
expect(document.body.querySelector('.sv-tour__back')).toBeNull() // no Back on step 1
|
|
107
|
+
} finally { destroy() }
|
|
108
|
+
})
|
|
109
|
+
|
|
110
|
+
it('Next advances, Back returns, Done finishes', () => {
|
|
111
|
+
const onFinish = vi.fn()
|
|
112
|
+
const { destroy } = mountOn(SvTour, { open: true, steps, onFinish })
|
|
113
|
+
try {
|
|
114
|
+
document.body.querySelector<HTMLButtonElement>('.sv-tour__next')!.click()
|
|
115
|
+
flushSync()
|
|
116
|
+
expect(document.body.querySelector('.sv-tour__title')!.textContent).toBe('Step two')
|
|
117
|
+
document.body.querySelector<HTMLButtonElement>('.sv-tour__back')!.click()
|
|
118
|
+
flushSync()
|
|
119
|
+
expect(document.body.querySelector('.sv-tour__title')!.textContent).toBe('Welcome')
|
|
120
|
+
// to the end
|
|
121
|
+
document.body.querySelector<HTMLButtonElement>('.sv-tour__next')!.click(); flushSync()
|
|
122
|
+
document.body.querySelector<HTMLButtonElement>('.sv-tour__next')!.click(); flushSync()
|
|
123
|
+
document.body.querySelector<HTMLButtonElement>('.sv-tour__next')!.click(); flushSync() // Done
|
|
124
|
+
expect(onFinish).toHaveBeenCalled()
|
|
125
|
+
expect(document.body.querySelector('.sv-tour__pop')).toBeNull()
|
|
126
|
+
} finally { destroy() }
|
|
127
|
+
})
|
|
128
|
+
|
|
129
|
+
it('Skip fires onSkip and closes', () => {
|
|
130
|
+
const onSkip = vi.fn()
|
|
131
|
+
const { destroy } = mountOn(SvTour, { open: true, steps, onSkip })
|
|
132
|
+
try {
|
|
133
|
+
document.body.querySelector<HTMLButtonElement>('.sv-tour__skip')!.click()
|
|
134
|
+
flushSync()
|
|
135
|
+
expect(onSkip).toHaveBeenCalled()
|
|
136
|
+
expect(document.body.querySelector('.sv-tour__pop')).toBeNull()
|
|
137
|
+
} finally { destroy() }
|
|
138
|
+
})
|
|
139
|
+
})
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for the declarative validation-rule library: each builder skips empty
|
|
3
|
+
* values (except required), enforces its constraint, honors a custom message,
|
|
4
|
+
* and cross-field compare reads sibling values. runRules returns the first error.
|
|
5
|
+
*/
|
|
6
|
+
import { describe, expect, it } from 'vitest'
|
|
7
|
+
import { rules, runRules, isEmptyValue } from './validators'
|
|
8
|
+
|
|
9
|
+
describe('validators', () => {
|
|
10
|
+
it('required flags empty values only', () => {
|
|
11
|
+
const r = rules.required()
|
|
12
|
+
expect(r('')).toBeTruthy()
|
|
13
|
+
expect(r(null)).toBeTruthy()
|
|
14
|
+
expect(r([])).toBeTruthy()
|
|
15
|
+
expect(r('x')).toBeNull()
|
|
16
|
+
expect(isEmptyValue([])).toBe(true)
|
|
17
|
+
})
|
|
18
|
+
|
|
19
|
+
it('format rules pass when empty, fail on bad input', () => {
|
|
20
|
+
expect(rules.email()('')).toBeNull()
|
|
21
|
+
expect(rules.email()('nope')).toBeTruthy()
|
|
22
|
+
expect(rules.email()('a@b.co')).toBeNull()
|
|
23
|
+
expect(rules.url()('example.com')).toBeNull()
|
|
24
|
+
expect(rules.pattern(/^\d+$/)('12a')).toBeTruthy()
|
|
25
|
+
expect(rules.zipCode()('90210')).toBeNull()
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
it('numeric ranges + lengths', () => {
|
|
29
|
+
expect(rules.min(10)(5)).toBeTruthy()
|
|
30
|
+
expect(rules.max(10)(11)).toBeTruthy()
|
|
31
|
+
expect(rules.range(1, 5)(3)).toBeNull()
|
|
32
|
+
expect(rules.range(1, 5)(9)).toBeTruthy()
|
|
33
|
+
expect(rules.minLength(3)('ab')).toBeTruthy()
|
|
34
|
+
expect(rules.stringLength(2, 4)('abc')).toBeNull()
|
|
35
|
+
expect(rules.integer()(3.5)).toBeTruthy()
|
|
36
|
+
expect(rules.oneOf(['a', 'b'])('c')).toBeTruthy()
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
it('compare reads a sibling field', () => {
|
|
40
|
+
const r = rules.compare('password')
|
|
41
|
+
expect(r('x', { password: 'x' })).toBeNull()
|
|
42
|
+
expect(r('y', { password: 'x' })).toBeTruthy()
|
|
43
|
+
expect(rules.compare('age', '>')(20, { age: 18 })).toBeNull()
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
it('custom message overrides the default', () => {
|
|
47
|
+
expect(rules.required({ message: 'Pick one' })('')).toBe('Pick one')
|
|
48
|
+
expect(rules.min(5, { message: 'too small' })(1)).toBe('too small')
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
it('runRules returns the first failing message in order', () => {
|
|
52
|
+
const list = [rules.required(), rules.minLength(4), rules.pattern(/[A-Z]/, { message: 'needs a capital' })]
|
|
53
|
+
expect(runRules('', list)).toBe('This field is required')
|
|
54
|
+
expect(runRules('ab', list)).toContain('at least 4')
|
|
55
|
+
expect(runRules('abcd', list)).toBe('needs a capital')
|
|
56
|
+
expect(runRules('Abcd', list)).toBeNull()
|
|
57
|
+
})
|
|
58
|
+
})
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* validators - a declarative, framework-free validation-rule library for SvForm
|
|
3
|
+
* (and any editor). Each rule builder returns a `Validator`: a pure function
|
|
4
|
+
* `(value, allValues?) => errorMessage | null`. Compose them per field; the form
|
|
5
|
+
* runs them in order and shows the first failure. Parity: Smart `smart.validator`
|
|
6
|
+
* (required / email / pattern / min / max / range / stringLength / compare ...).
|
|
7
|
+
*
|
|
8
|
+
* ```ts
|
|
9
|
+
* import { rules } from '@svgrid/grid'
|
|
10
|
+
* const field = { name: 'email', label: 'Email', rules: [rules.required(), rules.email()] }
|
|
11
|
+
* ```
|
|
12
|
+
*/
|
|
13
|
+
export type Validator = (value: any, values?: Record<string, any>) => string | null | undefined
|
|
14
|
+
export type RuleOptions = { message?: string }
|
|
15
|
+
export type CompareOp = '==' | '===' | '!=' | '<' | '<=' | '>' | '>='
|
|
16
|
+
|
|
17
|
+
const EMAIL_RE = /^[^@\s]+@[^@\s]+\.[^@\s]+$/
|
|
18
|
+
const URL_RE = /^(https?:\/\/)?([\w-]+\.)+[\w-]{2,}(\/\S*)?$/i
|
|
19
|
+
const ZIP_RE = /^\d{5}(-\d{4})?$/
|
|
20
|
+
|
|
21
|
+
/** Empty for validation purposes: null/undefined, '', or an empty array. */
|
|
22
|
+
export function isEmptyValue(v: any): boolean {
|
|
23
|
+
return v == null || v === '' || (Array.isArray(v) && v.length === 0)
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function compareOk(a: any, b: any, op: CompareOp): boolean {
|
|
27
|
+
switch (op) {
|
|
28
|
+
case '==': return a == b // eslint-disable-line eqeqeq
|
|
29
|
+
case '===': return a === b
|
|
30
|
+
case '!=': return a != b // eslint-disable-line eqeqeq
|
|
31
|
+
case '<': return Number(a) < Number(b)
|
|
32
|
+
case '<=': return Number(a) <= Number(b)
|
|
33
|
+
case '>': return Number(a) > Number(b)
|
|
34
|
+
case '>=': return Number(a) >= Number(b)
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** The declarative rule builders. Every builder skips empty values (except
|
|
39
|
+
* `required`) so optional fields validate format only when filled. */
|
|
40
|
+
export const rules = {
|
|
41
|
+
required: (o?: RuleOptions): Validator => (v) =>
|
|
42
|
+
isEmptyValue(v) ? o?.message ?? 'This field is required' : null,
|
|
43
|
+
|
|
44
|
+
email: (o?: RuleOptions): Validator => (v) =>
|
|
45
|
+
isEmptyValue(v) || EMAIL_RE.test(String(v)) ? null : o?.message ?? 'Enter a valid email address',
|
|
46
|
+
|
|
47
|
+
url: (o?: RuleOptions): Validator => (v) =>
|
|
48
|
+
isEmptyValue(v) || URL_RE.test(String(v)) ? null : o?.message ?? 'Enter a valid URL',
|
|
49
|
+
|
|
50
|
+
zipCode: (o?: RuleOptions): Validator => (v) =>
|
|
51
|
+
isEmptyValue(v) || ZIP_RE.test(String(v)) ? null : o?.message ?? 'Enter a valid ZIP code',
|
|
52
|
+
|
|
53
|
+
pattern: (re: RegExp, o?: RuleOptions): Validator => (v) =>
|
|
54
|
+
isEmptyValue(v) || re.test(String(v)) ? null : o?.message ?? 'Invalid format',
|
|
55
|
+
|
|
56
|
+
numeric: (o?: RuleOptions): Validator => (v) =>
|
|
57
|
+
isEmptyValue(v) || !isNaN(Number(v)) ? null : o?.message ?? 'Must be a number',
|
|
58
|
+
|
|
59
|
+
integer: (o?: RuleOptions): Validator => (v) =>
|
|
60
|
+
isEmptyValue(v) || Number.isInteger(Number(v)) ? null : o?.message ?? 'Must be a whole number',
|
|
61
|
+
|
|
62
|
+
min: (n: number, o?: RuleOptions): Validator => (v) =>
|
|
63
|
+
isEmptyValue(v) || Number(v) >= n ? null : o?.message ?? `Must be at least ${n}`,
|
|
64
|
+
|
|
65
|
+
max: (n: number, o?: RuleOptions): Validator => (v) =>
|
|
66
|
+
isEmptyValue(v) || Number(v) <= n ? null : o?.message ?? `Must be at most ${n}`,
|
|
67
|
+
|
|
68
|
+
range: (lo: number, hi: number, o?: RuleOptions): Validator => (v) =>
|
|
69
|
+
isEmptyValue(v) || (Number(v) >= lo && Number(v) <= hi) ? null : o?.message ?? `Must be between ${lo} and ${hi}`,
|
|
70
|
+
|
|
71
|
+
minLength: (n: number, o?: RuleOptions): Validator => (v) =>
|
|
72
|
+
isEmptyValue(v) || String(v).length >= n ? null : o?.message ?? `Must be at least ${n} characters`,
|
|
73
|
+
|
|
74
|
+
maxLength: (n: number, o?: RuleOptions): Validator => (v) =>
|
|
75
|
+
isEmptyValue(v) || String(v).length <= n ? null : o?.message ?? `Must be at most ${n} characters`,
|
|
76
|
+
|
|
77
|
+
stringLength: (lo: number, hi: number, o?: RuleOptions): Validator => (v) =>
|
|
78
|
+
isEmptyValue(v) || (String(v).length >= lo && String(v).length <= hi)
|
|
79
|
+
? null
|
|
80
|
+
: o?.message ?? `Must be ${lo}-${hi} characters`,
|
|
81
|
+
|
|
82
|
+
oneOf: (allowed: ReadonlyArray<string | number>, o?: RuleOptions): Validator => (v) =>
|
|
83
|
+
isEmptyValue(v) || allowed.includes(v) ? null : o?.message ?? 'Invalid choice',
|
|
84
|
+
|
|
85
|
+
/** Compare against another field's value (cross-field): e.g. confirm password. */
|
|
86
|
+
compare: (otherField: string, op: CompareOp = '===', o?: RuleOptions): Validator => (v, values) =>
|
|
87
|
+
compareOk(v, values?.[otherField], op) ? null : o?.message ?? 'Values do not match',
|
|
88
|
+
|
|
89
|
+
/** Wrap an arbitrary predicate/function as a rule. */
|
|
90
|
+
custom: (fn: Validator): Validator => fn,
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/** Run a list of rules against a value; return the first error message, or null. */
|
|
94
|
+
export function runRules(
|
|
95
|
+
value: any,
|
|
96
|
+
ruleList: ReadonlyArray<Validator> | undefined,
|
|
97
|
+
values?: Record<string, any>,
|
|
98
|
+
): string | null {
|
|
99
|
+
if (!ruleList) return null
|
|
100
|
+
for (const rule of ruleList) {
|
|
101
|
+
const err = rule(value, values)
|
|
102
|
+
if (err) return err
|
|
103
|
+
}
|
|
104
|
+
return null
|
|
105
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for the fixed-row windowing helper and SvListBox `virtual` mode: a huge
|
|
3
|
+
* option set renders only a small window (plus spacer rows), and itemTemplate
|
|
4
|
+
* customizes the row.
|
|
5
|
+
*/
|
|
6
|
+
import { describe, expect, it } from 'vitest'
|
|
7
|
+
import { mount, unmount, flushSync, createRawSnippet } from 'svelte'
|
|
8
|
+
import SvListBox from './SvListBox.svelte'
|
|
9
|
+
import { virtualRange, scrollToIndex } from './virtual'
|
|
10
|
+
|
|
11
|
+
describe('virtualRange', () => {
|
|
12
|
+
it('returns the visible window + spacer heights for a fixed-row list', () => {
|
|
13
|
+
const r = virtualRange({ scrollTop: 0, viewportHeight: 320, rowHeight: 32, count: 1000, overscan: 2 })
|
|
14
|
+
expect(r.start).toBe(0)
|
|
15
|
+
expect(r.totalHeight).toBe(32000)
|
|
16
|
+
expect(r.padTop).toBe(0)
|
|
17
|
+
// ~10 visible + 2 overscan.
|
|
18
|
+
expect(r.end).toBeLessThanOrEqual(14)
|
|
19
|
+
expect(r.padBottom).toBe((1000 - r.end) * 32)
|
|
20
|
+
})
|
|
21
|
+
it('scrolls the window when scrolled down', () => {
|
|
22
|
+
const r = virtualRange({ scrollTop: 3200, viewportHeight: 320, rowHeight: 32, count: 1000, overscan: 2 })
|
|
23
|
+
expect(r.start).toBe(98) // floor(3200/32)=100, minus overscan 2
|
|
24
|
+
expect(r.padTop).toBe(98 * 32)
|
|
25
|
+
})
|
|
26
|
+
it('handles empty + zero-height gracefully', () => {
|
|
27
|
+
expect(virtualRange({ scrollTop: 0, viewportHeight: 100, rowHeight: 32, count: 0 })).toMatchObject({ start: 0, end: 0, totalHeight: 0 })
|
|
28
|
+
expect(virtualRange({ scrollTop: 0, viewportHeight: 100, rowHeight: 0, count: 5 }).totalHeight).toBe(0)
|
|
29
|
+
})
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
describe('scrollToIndex', () => {
|
|
33
|
+
it('scrolls up to an item above and down to an item below the viewport', () => {
|
|
34
|
+
expect(scrollToIndex(2, 320, 320, 32)).toBe(64) // item 2 above -> its top
|
|
35
|
+
expect(scrollToIndex(20, 0, 320, 32)).toBe(20 * 32 + 32 - 320) // item 20 below -> bottom-align
|
|
36
|
+
expect(scrollToIndex(5, 100, 320, 32)).toBe(100) // already visible -> unchanged
|
|
37
|
+
})
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
describe('SvListBox virtual', () => {
|
|
41
|
+
it('renders only a small window of a huge list', () => {
|
|
42
|
+
const options = Array.from({ length: 5000 }, (_, i) => ({ value: i, label: `Item ${i}` }))
|
|
43
|
+
const target = document.createElement('div')
|
|
44
|
+
document.body.appendChild(target)
|
|
45
|
+
const app = mount(SvListBox, { target, props: { options, virtual: true } as any })
|
|
46
|
+
flushSync()
|
|
47
|
+
try {
|
|
48
|
+
const rendered = target.querySelectorAll('.sv-listbox__opt').length
|
|
49
|
+
expect(rendered).toBeGreaterThan(0)
|
|
50
|
+
expect(rendered).toBeLessThan(60) // nowhere near 5000
|
|
51
|
+
// A fixed-height sizer keeps the scrollbar honest.
|
|
52
|
+
const sizer = target.querySelector<HTMLElement>('.sv-listbox__sizer')!
|
|
53
|
+
expect(sizer).not.toBeNull()
|
|
54
|
+
expect(sizer.style.height).toBe(`${5000 * 32}px`)
|
|
55
|
+
} finally { unmount(app); target.remove() }
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
it('renders a custom itemTemplate per option', () => {
|
|
59
|
+
const options = [{ value: 'a', label: 'Apple' }, { value: 'b', label: 'Banana' }]
|
|
60
|
+
const itemTemplate = createRawSnippet((opt: () => { label: string }) => ({
|
|
61
|
+
render: () => `<span class="tpl">${opt().label.toUpperCase()}</span>`,
|
|
62
|
+
}))
|
|
63
|
+
const target = document.createElement('div')
|
|
64
|
+
document.body.appendChild(target)
|
|
65
|
+
const app = mount(SvListBox, { target, props: { options, itemTemplate } as any })
|
|
66
|
+
flushSync()
|
|
67
|
+
try {
|
|
68
|
+
const tpls = [...target.querySelectorAll('.tpl')].map((n) => n.textContent)
|
|
69
|
+
expect(tpls).toEqual(['APPLE', 'BANANA'])
|
|
70
|
+
} finally { unmount(app); target.remove() }
|
|
71
|
+
})
|
|
72
|
+
})
|
package/src/virtual.ts
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* virtual - a tiny, framework-free fixed-row windowing helper shared by the
|
|
3
|
+
* selection controls so their option lists scale to tens of thousands of rows
|
|
4
|
+
* without rendering them all. The renderer measures the viewport + tracks
|
|
5
|
+
* scrollTop (DOM concerns) and passes them in; this returns the slice to render
|
|
6
|
+
* plus the top/bottom spacer heights that keep the scrollbar honest.
|
|
7
|
+
*/
|
|
8
|
+
export type VirtualRange = {
|
|
9
|
+
/** First item index to render (inclusive). */
|
|
10
|
+
start: number
|
|
11
|
+
/** Last item index to render (exclusive). */
|
|
12
|
+
end: number
|
|
13
|
+
/** Spacer height above the rendered slice (px). */
|
|
14
|
+
padTop: number
|
|
15
|
+
/** Spacer height below the rendered slice (px). */
|
|
16
|
+
padBottom: number
|
|
17
|
+
/** Total scrollable height of all rows (px). */
|
|
18
|
+
totalHeight: number
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export type VirtualParams = {
|
|
22
|
+
scrollTop: number
|
|
23
|
+
viewportHeight: number
|
|
24
|
+
rowHeight: number
|
|
25
|
+
count: number
|
|
26
|
+
/** Extra rows rendered above/below the viewport to avoid blank flashes. */
|
|
27
|
+
overscan?: number
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** Compute the visible window for a fixed-row-height list (pure). */
|
|
31
|
+
export function virtualRange(params: VirtualParams): VirtualRange {
|
|
32
|
+
const { scrollTop, viewportHeight, rowHeight, count } = params
|
|
33
|
+
const overscan = params.overscan ?? 6
|
|
34
|
+
const total = Math.max(0, count) * Math.max(0, rowHeight)
|
|
35
|
+
if (rowHeight <= 0 || count <= 0) {
|
|
36
|
+
return { start: 0, end: Math.max(0, count), padTop: 0, padBottom: 0, totalHeight: total }
|
|
37
|
+
}
|
|
38
|
+
const first = Math.floor(Math.max(0, scrollTop) / rowHeight)
|
|
39
|
+
const start = Math.max(0, first - overscan)
|
|
40
|
+
const visible = Math.max(1, Math.ceil(Math.max(0, viewportHeight) / rowHeight))
|
|
41
|
+
const end = Math.min(count, first + visible + overscan)
|
|
42
|
+
return {
|
|
43
|
+
start,
|
|
44
|
+
end,
|
|
45
|
+
padTop: start * rowHeight,
|
|
46
|
+
padBottom: Math.max(0, (count - end) * rowHeight),
|
|
47
|
+
totalHeight: total,
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* The scrollTop that brings item `index` fully into a fixed-row viewport,
|
|
53
|
+
* scrolling the least amount (returns the current scrollTop if already visible).
|
|
54
|
+
* Pure - the renderer applies it to the element.
|
|
55
|
+
*/
|
|
56
|
+
export function scrollToIndex(
|
|
57
|
+
index: number,
|
|
58
|
+
scrollTop: number,
|
|
59
|
+
viewportHeight: number,
|
|
60
|
+
rowHeight: number,
|
|
61
|
+
): number {
|
|
62
|
+
const top = index * rowHeight
|
|
63
|
+
const bottom = top + rowHeight
|
|
64
|
+
if (top < scrollTop) return top
|
|
65
|
+
if (bottom > scrollTop + viewportHeight) return bottom - viewportHeight
|
|
66
|
+
return scrollTop
|
|
67
|
+
}
|
|
@@ -1,27 +1,27 @@
|
|
|
1
|
-
import { describe, expect, it } from 'vitest'
|
|
2
|
-
import { createColumnVirtualizer } from './column-virtualizer'
|
|
3
|
-
|
|
4
|
-
describe('column virtualizer', () => {
|
|
5
|
-
it('computes horizontal window', () => {
|
|
6
|
-
const v = createColumnVirtualizer({
|
|
7
|
-
count: 1000,
|
|
8
|
-
viewportWidth: 700,
|
|
9
|
-
overscan: 3,
|
|
10
|
-
estimateSize: () => 140,
|
|
11
|
-
})
|
|
12
|
-
const items = v.getVirtualItems()
|
|
13
|
-
expect(items[0]?.index).toBe(0)
|
|
14
|
-
expect(items.length).toBeGreaterThan(0)
|
|
15
|
-
})
|
|
16
|
-
|
|
17
|
-
it('updates indices on horizontal scroll', () => {
|
|
18
|
-
const v = createColumnVirtualizer({
|
|
19
|
-
count: 1000,
|
|
20
|
-
viewportWidth: 700,
|
|
21
|
-
estimateSize: () => 140,
|
|
22
|
-
})
|
|
23
|
-
v.setHorizontalOffset(2800)
|
|
24
|
-
const state = v.getState()
|
|
25
|
-
expect(state.startIndex).toBeGreaterThan(0)
|
|
26
|
-
})
|
|
27
|
-
})
|
|
1
|
+
import { describe, expect, it } from 'vitest'
|
|
2
|
+
import { createColumnVirtualizer } from './column-virtualizer'
|
|
3
|
+
|
|
4
|
+
describe('column virtualizer', () => {
|
|
5
|
+
it('computes horizontal window', () => {
|
|
6
|
+
const v = createColumnVirtualizer({
|
|
7
|
+
count: 1000,
|
|
8
|
+
viewportWidth: 700,
|
|
9
|
+
overscan: 3,
|
|
10
|
+
estimateSize: () => 140,
|
|
11
|
+
})
|
|
12
|
+
const items = v.getVirtualItems()
|
|
13
|
+
expect(items[0]?.index).toBe(0)
|
|
14
|
+
expect(items.length).toBeGreaterThan(0)
|
|
15
|
+
})
|
|
16
|
+
|
|
17
|
+
it('updates indices on horizontal scroll', () => {
|
|
18
|
+
const v = createColumnVirtualizer({
|
|
19
|
+
count: 1000,
|
|
20
|
+
viewportWidth: 700,
|
|
21
|
+
estimateSize: () => 140,
|
|
22
|
+
})
|
|
23
|
+
v.setHorizontalOffset(2800)
|
|
24
|
+
const state = v.getState()
|
|
25
|
+
expect(state.startIndex).toBeGreaterThan(0)
|
|
26
|
+
})
|
|
27
|
+
})
|
|
@@ -1,30 +1,30 @@
|
|
|
1
|
-
import { createVirtualizer } from './virtualizer'
|
|
2
|
-
|
|
3
|
-
export type ColumnSizeEstimator = (index: number) => number
|
|
4
|
-
|
|
5
|
-
export function createColumnVirtualizer(input: {
|
|
6
|
-
count: number
|
|
7
|
-
viewportWidth: number
|
|
8
|
-
scrollOffset?: number
|
|
9
|
-
overscan?: number
|
|
10
|
-
/** Either a uniform size (number) or a per-column size function. */
|
|
11
|
-
estimateSize?: number | ColumnSizeEstimator
|
|
12
|
-
}) {
|
|
13
|
-
const virtualizer = createVirtualizer({
|
|
14
|
-
count: input.count,
|
|
15
|
-
estimateSize: input.estimateSize ?? 140,
|
|
16
|
-
viewportHeight: input.viewportWidth,
|
|
17
|
-
scrollOffset: input.scrollOffset ?? 0,
|
|
18
|
-
overscan: input.overscan ?? 4,
|
|
19
|
-
})
|
|
20
|
-
|
|
21
|
-
return {
|
|
22
|
-
...virtualizer,
|
|
23
|
-
setViewportWidth(viewportWidth: number) {
|
|
24
|
-
virtualizer.setViewportHeight(viewportWidth)
|
|
25
|
-
},
|
|
26
|
-
setHorizontalOffset(scrollOffset: number) {
|
|
27
|
-
virtualizer.setScrollOffset(scrollOffset)
|
|
28
|
-
},
|
|
29
|
-
}
|
|
30
|
-
}
|
|
1
|
+
import { createVirtualizer } from './virtualizer'
|
|
2
|
+
|
|
3
|
+
export type ColumnSizeEstimator = (index: number) => number
|
|
4
|
+
|
|
5
|
+
export function createColumnVirtualizer(input: {
|
|
6
|
+
count: number
|
|
7
|
+
viewportWidth: number
|
|
8
|
+
scrollOffset?: number
|
|
9
|
+
overscan?: number
|
|
10
|
+
/** Either a uniform size (number) or a per-column size function. */
|
|
11
|
+
estimateSize?: number | ColumnSizeEstimator
|
|
12
|
+
}) {
|
|
13
|
+
const virtualizer = createVirtualizer({
|
|
14
|
+
count: input.count,
|
|
15
|
+
estimateSize: input.estimateSize ?? 140,
|
|
16
|
+
viewportHeight: input.viewportWidth,
|
|
17
|
+
scrollOffset: input.scrollOffset ?? 0,
|
|
18
|
+
overscan: input.overscan ?? 4,
|
|
19
|
+
})
|
|
20
|
+
|
|
21
|
+
return {
|
|
22
|
+
...virtualizer,
|
|
23
|
+
setViewportWidth(viewportWidth: number) {
|
|
24
|
+
virtualizer.setViewportHeight(viewportWidth)
|
|
25
|
+
},
|
|
26
|
+
setHorizontalOffset(scrollOffset: number) {
|
|
27
|
+
virtualizer.setScrollOffset(scrollOffset)
|
|
28
|
+
},
|
|
29
|
+
}
|
|
30
|
+
}
|
|
@@ -1,26 +1,26 @@
|
|
|
1
|
-
import { createVirtualizer } from './virtualizer'
|
|
2
|
-
import type { VirtualizerOptions } from './types'
|
|
3
|
-
|
|
4
|
-
export function createSvelteVirtualizer(options: VirtualizerOptions) {
|
|
5
|
-
const virtualizer = createVirtualizer(options)
|
|
6
|
-
let version = $state(0)
|
|
7
|
-
|
|
8
|
-
virtualizer.subscribe(() => {
|
|
9
|
-
version += 1
|
|
10
|
-
})
|
|
11
|
-
|
|
12
|
-
return {
|
|
13
|
-
get version() {
|
|
14
|
-
return version
|
|
15
|
-
},
|
|
16
|
-
setOptions: virtualizer.setOptions,
|
|
17
|
-
setScrollOffset: virtualizer.setScrollOffset,
|
|
18
|
-
setViewportHeight: virtualizer.setViewportHeight,
|
|
19
|
-
scrollToIndex: virtualizer.scrollToIndex,
|
|
20
|
-
getVirtualItems: virtualizer.getVirtualItems,
|
|
21
|
-
getTotalSize: virtualizer.getTotalSize,
|
|
22
|
-
getOffsetForIndex: virtualizer.getOffsetForIndex,
|
|
23
|
-
getSizeForIndex: virtualizer.getSizeForIndex,
|
|
24
|
-
getState: virtualizer.getState,
|
|
25
|
-
}
|
|
26
|
-
}
|
|
1
|
+
import { createVirtualizer } from './virtualizer'
|
|
2
|
+
import type { VirtualizerOptions } from './types'
|
|
3
|
+
|
|
4
|
+
export function createSvelteVirtualizer(options: VirtualizerOptions) {
|
|
5
|
+
const virtualizer = createVirtualizer(options)
|
|
6
|
+
let version = $state(0)
|
|
7
|
+
|
|
8
|
+
virtualizer.subscribe(() => {
|
|
9
|
+
version += 1
|
|
10
|
+
})
|
|
11
|
+
|
|
12
|
+
return {
|
|
13
|
+
get version() {
|
|
14
|
+
return version
|
|
15
|
+
},
|
|
16
|
+
setOptions: virtualizer.setOptions,
|
|
17
|
+
setScrollOffset: virtualizer.setScrollOffset,
|
|
18
|
+
setViewportHeight: virtualizer.setViewportHeight,
|
|
19
|
+
scrollToIndex: virtualizer.scrollToIndex,
|
|
20
|
+
getVirtualItems: virtualizer.getVirtualItems,
|
|
21
|
+
getTotalSize: virtualizer.getTotalSize,
|
|
22
|
+
getOffsetForIndex: virtualizer.getOffsetForIndex,
|
|
23
|
+
getSizeForIndex: virtualizer.getSizeForIndex,
|
|
24
|
+
getState: virtualizer.getState,
|
|
25
|
+
}
|
|
26
|
+
}
|
|
@@ -1,30 +1,30 @@
|
|
|
1
|
-
export type VirtualItem = {
|
|
2
|
-
index: number
|
|
3
|
-
start: number
|
|
4
|
-
size: number
|
|
5
|
-
end: number
|
|
6
|
-
key: string
|
|
7
|
-
}
|
|
8
|
-
|
|
9
|
-
export type VirtualizerOptions = {
|
|
10
|
-
count: number
|
|
11
|
-
/**
|
|
12
|
-
* Per-item size. Pass a number for a uniform layout (fast path) or a
|
|
13
|
-
* function for variable sizing - the virtualizer will build cumulative
|
|
14
|
-
* offsets and use binary lookup to find the visible window. The function
|
|
15
|
-
* receives the item index and must return its pixel size.
|
|
16
|
-
*/
|
|
17
|
-
estimateSize: number | ((index: number) => number)
|
|
18
|
-
overscan?: number
|
|
19
|
-
viewportHeight: number
|
|
20
|
-
scrollOffset?: number
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
export type VirtualizerState = {
|
|
24
|
-
items: Array<VirtualItem>
|
|
25
|
-
totalSize: number
|
|
26
|
-
startIndex: number
|
|
27
|
-
endIndex: number
|
|
28
|
-
scrollOffset: number
|
|
29
|
-
viewportHeight: number
|
|
30
|
-
}
|
|
1
|
+
export type VirtualItem = {
|
|
2
|
+
index: number
|
|
3
|
+
start: number
|
|
4
|
+
size: number
|
|
5
|
+
end: number
|
|
6
|
+
key: string
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export type VirtualizerOptions = {
|
|
10
|
+
count: number
|
|
11
|
+
/**
|
|
12
|
+
* Per-item size. Pass a number for a uniform layout (fast path) or a
|
|
13
|
+
* function for variable sizing - the virtualizer will build cumulative
|
|
14
|
+
* offsets and use binary lookup to find the visible window. The function
|
|
15
|
+
* receives the item index and must return its pixel size.
|
|
16
|
+
*/
|
|
17
|
+
estimateSize: number | ((index: number) => number)
|
|
18
|
+
overscan?: number
|
|
19
|
+
viewportHeight: number
|
|
20
|
+
scrollOffset?: number
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export type VirtualizerState = {
|
|
24
|
+
items: Array<VirtualItem>
|
|
25
|
+
totalSize: number
|
|
26
|
+
startIndex: number
|
|
27
|
+
endIndex: number
|
|
28
|
+
scrollOffset: number
|
|
29
|
+
viewportHeight: number
|
|
30
|
+
}
|