@svgrid/grid 2.2.35 → 2.5.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/FlexRender.svelte +96 -96
- package/dist/GridFooter.svelte +178 -178
- package/dist/GridMenus.svelte +705 -705
- 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/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 -2814
- package/dist/SvGrid.svelte +3490 -3490
- package/dist/SvGridChart.svelte +1716 -1716
- package/dist/SvGridChartPanel.svelte +485 -485
- package/dist/SvGridChartView.svelte +70 -70
- package/dist/SvGridDropdown.svelte +728 -728
- package/dist/SvGridSelect.svelte +266 -266
- package/dist/SvGroupCell.svelte +116 -116
- 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/SvMultiSelect.svelte +289 -289
- 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 -211
- 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 -208
- package/dist/SvTree.svelte +444 -444
- package/dist/SvTreeSelect.svelte +235 -235
- package/dist/cdn/{GridMenus-oQYElQll.js → GridMenus-BpVcKyUr.js} +2 -2
- package/dist/cdn/{GridMenus-T-bZNprc.js → GridMenus-CeXL5IW4.js} +2 -2
- package/dist/cdn/{SvDateTimePicker-CHnUkWcH.js → SvDateTimePicker-DSUhIta8.js} +2 -2
- package/dist/cdn/{SvDateTimePicker-CRQH_U7E.js → SvDateTimePicker-h9kPyszT.js} +2 -2
- package/dist/cdn/{SvGridChart-BwVos976.js → SvGridChart-Bs2GIR2Q.js} +1 -1
- package/dist/cdn/{SvGridChart-BEJmNNx9.js → SvGridChart-CwhFz7GV.js} +1 -1
- package/dist/cdn/{SvGridChartPanel-BOknOkrP.js → SvGridChartPanel-1WXuStzM.js} +1 -1
- package/dist/cdn/{SvGridChartPanel-D2PjII4O.js → SvGridChartPanel-e5_nqNP9.js} +1 -1
- package/dist/cdn/{SvGridChartView-00LPUHL1.js → SvGridChartView-SmPW10dI.js} +1 -1
- package/dist/cdn/{SvGridChartView-BhUEJ5ki.js → SvGridChartView-eSPuJE6g.js} +1 -1
- package/dist/cdn/{SvGridDropdown-D0VdjeR8.js → SvGridDropdown-B1ZcLpgs.js} +1 -1
- package/dist/cdn/{SvGridDropdown-D13MtJ2j.js → SvGridDropdown-CtdbHcIS.js} +1 -1
- package/dist/cdn/{src-Cm7C_WOZ.js → src-DbeN1ReZ.js} +24 -24
- package/dist/cdn/{src-72QTH8lh.js → src-DmEQFW5_.js} +23 -23
- package/dist/cdn/svgrid.js +5 -5
- package/dist/cdn/svgrid.svelte-external.js +5 -5
- package/dist/chart-export.js +8 -8
- 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/themes/index.js +28 -9
- package/package.json +11 -11
- package/src/FlexRender.svelte +96 -96
- package/src/GridFooter.svelte +178 -178
- package/src/GridMenus.svelte +705 -705
- 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/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 -3680
- package/src/SvGrid.css +2814 -2814
- package/src/SvGrid.svelte +3490 -3490
- package/src/SvGrid.types.ts +1708 -1708
- package/src/SvGridChart.svelte +1716 -1716
- package/src/SvGridChartPanel.svelte +485 -485
- package/src/SvGridChartView.svelte +70 -70
- package/src/SvGridDropdown.svelte +728 -728
- package/src/SvGridSelect.svelte +266 -266
- package/src/SvGroupCell.svelte +116 -116
- 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/SvMultiSelect.svelte +289 -289
- 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 -211
- 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 -208
- package/src/SvTree.svelte +444 -444
- package/src/SvTreeSelect.svelte +235 -235
- 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 -805
- package/src/builtin-editors.grid.test.ts +85 -85
- package/src/cell-formatting.ts +169 -169
- package/src/cell-render.test.ts +513 -513
- package/src/cell-render.ts +496 -496
- 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 -2295
- package/src/collaboration.test.ts +104 -104
- package/src/collaboration.ts +167 -167
- 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 -224
- 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 -606
- package/src/editor-contract.ts +164 -164
- package/src/editor-registry.grid.test.ts +144 -144
- 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 -601
- 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.ts +270 -270
- 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.ts +567 -567
- package/src/merge-objects.ts +48 -48
- 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-resize.ts +210 -210
- package/src/scheduler-model.test.ts +562 -562
- package/src/scheduler-model.ts +872 -872
- package/src/selection.test.ts +754 -754
- package/src/selection.ts +491 -491
- package/src/server-data-source.test.ts +289 -289
- package/src/server-data-source.ts +413 -413
- 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/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 -705
- package/src/svgrid.charting.test.ts +534 -534
- 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 +331 -322
- package/src/svgrid.filter-menu-scroll.test.ts +112 -112
- 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 +278 -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 -168
- 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/themes/ag-alpine.css +2 -2
- package/themes/antd.css +4 -4
- package/themes/atlassian.css +1 -1
- package/themes/ember.css +2 -2
- package/themes/fluent.css +1 -1
- package/themes/github.css +1 -1
- package/themes/nord.css +2 -2
- package/themes/notion.css +2 -2
- package/themes/salesforce.css +1 -1
- package/themes/tailwind.css +1 -1
- package/themes/vercel.css +1 -1
package/dist/GridMenus.svelte
CHANGED
|
@@ -1,705 +1,705 @@
|
|
|
1
|
-
<script lang="ts" generics="TFeatures extends TableFeatures = TableFeatures, TData extends RowData = RowData">
|
|
2
|
-
import {
|
|
3
|
-
splitInTokens,
|
|
4
|
-
trailingInToken,
|
|
5
|
-
type CellEditorType,
|
|
6
|
-
type Column,
|
|
7
|
-
type RowData,
|
|
8
|
-
type TableFeatures,
|
|
9
|
-
} from "./index";
|
|
10
|
-
import "./sv-grid-scrollbar";
|
|
11
|
-
import { onScrollOutside } from "./a11y/dismissable";
|
|
12
|
-
import type { Snippet } from "svelte";
|
|
13
|
-
import SvListBox from "./SvListBox.svelte";
|
|
14
|
-
import type {
|
|
15
|
-
FilterOperator,
|
|
16
|
-
} from "./SvGrid.types";
|
|
17
|
-
import {
|
|
18
|
-
getEditorInputType,
|
|
19
|
-
} from "./SvGrid.helpers";
|
|
20
|
-
|
|
21
|
-
import type { SvGridController } from "./SvGrid.controller.svelte";
|
|
22
|
-
|
|
23
|
-
let { ctrl, icon }: { ctrl: SvGridController<TFeatures, TData>; icon: Snippet<[string]> } = $props();
|
|
24
|
-
|
|
25
|
-
// View facade: re-bind the controller's reactive members so the markup
|
|
26
|
-
// (moved verbatim from SvGrid.svelte) stays identical.
|
|
27
|
-
const groupingControlsEnabled = $derived(ctrl.groupingControlsEnabled);
|
|
28
|
-
const filterMenuValues = $derived(ctrl.filterMenuValues);
|
|
29
|
-
const tooltip = $derived(ctrl.tooltip);
|
|
30
|
-
const columnMenuFor = $derived(ctrl.columnMenuFor);
|
|
31
|
-
const columnMenuPos = $derived(ctrl.columnMenuPos);
|
|
32
|
-
const filterMenuFor = $derived(ctrl.filterMenuFor);
|
|
33
|
-
const filterMenuPos = $derived(ctrl.filterMenuPos);
|
|
34
|
-
const operatorMenuFor = $derived(ctrl.operatorMenuFor);
|
|
35
|
-
const operatorMenuPos = $derived(ctrl.operatorMenuPos);
|
|
36
|
-
const chooseColumnsPos = $derived(ctrl.chooseColumnsPos);
|
|
37
|
-
const showColumnFiltersEffective = $derived(ctrl.showColumnFiltersEffective);
|
|
38
|
-
const columnMenuTabsEnabled = $derived(ctrl.props.columnMenuTabs === true);
|
|
39
|
-
const grid = $derived(ctrl.grid);
|
|
40
|
-
const allColumns = $derived(ctrl.allColumns);
|
|
41
|
-
const isColumnPinned = $derived(ctrl.isColumnPinned);
|
|
42
|
-
const pinColumnLeft = $derived(ctrl.pinColumnLeft);
|
|
43
|
-
const pinColumnRight = $derived(ctrl.pinColumnRight);
|
|
44
|
-
const unpinColumn = $derived(ctrl.unpinColumn);
|
|
45
|
-
const sortDirectionByColumn = $derived(ctrl.sortDirectionByColumn);
|
|
46
|
-
const groupingColumns = $derived(ctrl.groupingColumns);
|
|
47
|
-
const columnVirtualizationEnabled = $derived(ctrl.columnVirtualizationEnabled);
|
|
48
|
-
const updateFilterOperator = $derived(ctrl.updateFilterOperator);
|
|
49
|
-
const updateFilterMenuValue = $derived(ctrl.updateFilterMenuValue);
|
|
50
|
-
const updateFilterMenuValueTo = $derived(ctrl.updateFilterMenuValueTo);
|
|
51
|
-
const operatorsForColumn = $derived(ctrl.operatorsForColumn);
|
|
52
|
-
const defaultOperatorFor = $derived(ctrl.defaultOperatorFor);
|
|
53
|
-
const operatorLabelFor = $derived(ctrl.operatorLabelFor);
|
|
54
|
-
const filterRowValues = $derived(ctrl.filterRowValues);
|
|
55
|
-
const inSuggestFor = $derived(ctrl.inSuggestFor);
|
|
56
|
-
const inSuggestPos = $derived(ctrl.inSuggestPos);
|
|
57
|
-
|
|
58
|
-
// Patch a column's menu filter (used for the optional 2nd condition + join
|
|
59
|
-
// of multi-condition filtering). Merges into the existing entry, creating a
|
|
60
|
-
// base entry from the column's default operator when none exists yet.
|
|
61
|
-
function patchFilter(columnId: string, patch: Record<string, unknown>) {
|
|
62
|
-
const col = allColumns.find((c) => c.id === columnId);
|
|
63
|
-
const cur = ctrl.filterMenuValues[columnId] ?? { operator: defaultOperatorFor(col), value: "" };
|
|
64
|
-
ctrl.filterMenuValues = { ...ctrl.filterMenuValues, [columnId]: { ...cur, ...patch } };
|
|
65
|
-
}
|
|
66
|
-
const isColumnFiltered = $derived(ctrl.isColumnFiltered);
|
|
67
|
-
const closeMenus = $derived(ctrl.closeMenus);
|
|
68
|
-
|
|
69
|
-
// Column/filter/operator menus are position:fixed and anchored to their
|
|
70
|
-
// trigger by a one-time measurement taken when they open. On scroll they'd
|
|
71
|
-
// otherwise detach and float over the wrong place (#88), so close them when
|
|
72
|
-
// anything OUTSIDE them scrolls, or on resize. Scrolling the menu's own body
|
|
73
|
-
// or its facet list has to keep it open - hence onScrollOutside rather than a
|
|
74
|
-
// bare capture-phase listener.
|
|
75
|
-
$effect(() => {
|
|
76
|
-
if (!(columnMenuFor || filterMenuFor || operatorMenuFor || chooseColumnsPos)) return;
|
|
77
|
-
const close = () => closeMenus();
|
|
78
|
-
const offScroll = onScrollOutside(
|
|
79
|
-
() => Array.from(document.querySelectorAll<HTMLElement>('.sv-grid-menu')),
|
|
80
|
-
close,
|
|
81
|
-
);
|
|
82
|
-
window.addEventListener('resize', close);
|
|
83
|
-
return () => {
|
|
84
|
-
offScroll();
|
|
85
|
-
window.removeEventListener('resize', close);
|
|
86
|
-
};
|
|
87
|
-
});
|
|
88
|
-
|
|
89
|
-
const autosizeColumn = $derived(ctrl.autosizeColumn);
|
|
90
|
-
const autosizeAllColumns = $derived(ctrl.autosizeAllColumns);
|
|
91
|
-
const resetColumns = $derived(ctrl.resetColumns);
|
|
92
|
-
const openChooseColumns = $derived(ctrl.openChooseColumns);
|
|
93
|
-
const sortColumnFromMenu = $derived(ctrl.sortColumnFromMenu);
|
|
94
|
-
const clearColumnSort = $derived(ctrl.clearColumnSort);
|
|
95
|
-
const groupByColumnFromMenu = $derived(ctrl.groupByColumnFromMenu);
|
|
96
|
-
const clearGroupingFromMenu = $derived(ctrl.clearGroupingFromMenu);
|
|
97
|
-
const columnMenuFacetValues = $derived(ctrl.columnMenuFacetValues);
|
|
98
|
-
const columnMenuVisibleFacets = $derived(ctrl.columnMenuVisibleFacets);
|
|
99
|
-
const columnMenuFacetOptions = $derived(ctrl.columnMenuFacetOptions);
|
|
100
|
-
const columnMenuSelectedFacets = $derived(ctrl.columnMenuSelectedFacets);
|
|
101
|
-
const setFacetSelection = $derived(ctrl.setFacetSelection);
|
|
102
|
-
const setFilterTokens = $derived(ctrl.setFilterTokens);
|
|
103
|
-
const inSuggestOptions = $derived(ctrl.inSuggestOptions);
|
|
104
|
-
const openInSuggest = $derived(ctrl.openInSuggest);
|
|
105
|
-
const closeInSuggest = $derived(ctrl.closeInSuggest);
|
|
106
|
-
const isAllFacetsChecked = $derived(ctrl.isAllFacetsChecked);
|
|
107
|
-
const toggleAllFacets = $derived(ctrl.toggleAllFacets);
|
|
108
|
-
|
|
109
|
-
/**
|
|
110
|
-
* Above this many rows the checklists switch to the windowed listbox. Short
|
|
111
|
-
* lists stay fully rendered so the popover hugs its content instead of
|
|
112
|
-
* reserving a fixed `rows`-tall box.
|
|
113
|
-
*/
|
|
114
|
-
const FACET_VIRTUAL_THRESHOLD = 40;
|
|
115
|
-
|
|
116
|
-
// The filter panel's `in` / `notIn` box holds the WHOLE token list
|
|
117
|
-
// ("AAPL, MSFT"), so the suggestion query is the trailing fragment being
|
|
118
|
-
// typed - not the full value, which would match nothing.
|
|
119
|
-
function onSetOpFilterFocus(input: HTMLInputElement, columnId: string) {
|
|
120
|
-
openInSuggest(input, columnId);
|
|
121
|
-
ctrl.inSuggestQuery = trailingInToken(input.value);
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
function onSetOpFilterInput(input: HTMLInputElement, columnId: string) {
|
|
125
|
-
ctrl.inSuggestQuery = trailingInToken(input.value);
|
|
126
|
-
if (ctrl.inSuggestFor !== columnId) openInSuggest(input, columnId);
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
/** Columns as checklist options, labelled by their header when it's a string. */
|
|
130
|
-
function columnVisibilityOptions(columns: Array<Column<TData>>) {
|
|
131
|
-
return columns.map((column) => ({
|
|
132
|
-
value: column.id,
|
|
133
|
-
label: typeof column.columnDef.header === "string" ? column.columnDef.header : column.id,
|
|
134
|
-
}));
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
/** Currently visible column ids - the checklist's selection set. */
|
|
138
|
-
function visibleColumnIds(columns: Array<Column<TData>>) {
|
|
139
|
-
const set = new Set<string | number>();
|
|
140
|
-
for (const column of columns) if (!ctrl.hiddenColumns[column.id]) set.add(column.id);
|
|
141
|
-
return set;
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
/** Write a checklist selection back as the complementary `hiddenColumns` map. */
|
|
145
|
-
function applyColumnVisibility(columns: Array<Column<TData>>, visible: Set<string>) {
|
|
146
|
-
const next: Record<string, boolean> = { ...ctrl.hiddenColumns };
|
|
147
|
-
for (const column of columns) {
|
|
148
|
-
if (visible.has(column.id)) delete next[column.id];
|
|
149
|
-
else next[column.id] = true;
|
|
150
|
-
}
|
|
151
|
-
ctrl.hiddenColumns = next;
|
|
152
|
-
}
|
|
153
|
-
const clearColumnFilter = $derived(ctrl.clearColumnFilter);
|
|
154
|
-
const contextMenuFor = $derived(ctrl.contextMenuFor);
|
|
155
|
-
const contextMenuPos = $derived(ctrl.contextMenuPos);
|
|
156
|
-
const contextMenuItems = $derived(ctrl.contextMenuItems);
|
|
157
|
-
const closeContextMenu = $derived(ctrl.closeContextMenu);
|
|
158
|
-
const commentEditFor = $derived(ctrl.commentEditFor);
|
|
159
|
-
const saveComment = $derived(ctrl.saveComment);
|
|
160
|
-
const removeComment = $derived(ctrl.removeComment);
|
|
161
|
-
const closeCommentEditor = $derived(ctrl.closeCommentEditor);
|
|
162
|
-
</script>
|
|
163
|
-
|
|
164
|
-
{#if tooltip}
|
|
165
|
-
<!-- Custom hover tooltip. Floats above the grid; positioned by
|
|
166
|
-
showTooltipFor(). Notes and column tooltips both surface here. -->
|
|
167
|
-
<div
|
|
168
|
-
class="sv-grid-tooltip"
|
|
169
|
-
class:sv-grid-tooltip-above={!tooltip.below}
|
|
170
|
-
style={`left: ${tooltip.x}px; top: ${tooltip.y}px; ${tooltip.below ? '' : 'transform: translateY(-100%);'}`}
|
|
171
|
-
role="tooltip"
|
|
172
|
-
>{tooltip.text}</div>
|
|
173
|
-
{/if}
|
|
174
|
-
|
|
175
|
-
{#if columnMenuFor || filterMenuFor || operatorMenuFor || chooseColumnsPos}
|
|
176
|
-
<div
|
|
177
|
-
class="sv-grid-menu-backdrop"
|
|
178
|
-
role="presentation"
|
|
179
|
-
onclick={closeMenus}
|
|
180
|
-
></div>
|
|
181
|
-
{/if}
|
|
182
|
-
|
|
183
|
-
<!-- Shared filter UI: rendered both in the funnel popover and in the column
|
|
184
|
-
menu's Filter tab, so the two never diverge. -->
|
|
185
|
-
{#snippet filterPanelBody(colId: string)}
|
|
186
|
-
{@const menuColumn = allColumns.find((c) => c.id === colId)}
|
|
187
|
-
{@const menuOperatorOptions = operatorsForColumn(menuColumn)}
|
|
188
|
-
{@const menuActiveOperator = filterMenuValues[colId]?.operator ?? defaultOperatorFor(menuColumn)}
|
|
189
|
-
{@const menuInputType = getEditorInputType((menuColumn?.columnDef.editorType ?? "text") as CellEditorType)}
|
|
190
|
-
<div class="sv-grid-menu-filter">
|
|
191
|
-
<div class="sv-grid-menu-filter-head">
|
|
192
|
-
<span class="sv-grid-header-icon">{@render icon("filter")}</span> Filter
|
|
193
|
-
condition
|
|
194
|
-
</div>
|
|
195
|
-
<select
|
|
196
|
-
class="sv-grid-menu-operator-select"
|
|
197
|
-
aria-label="Filter condition"
|
|
198
|
-
value={menuActiveOperator}
|
|
199
|
-
onchange={(event) =>
|
|
200
|
-
updateFilterOperator(colId, (event.currentTarget as HTMLSelectElement).value as FilterOperator)}
|
|
201
|
-
>
|
|
202
|
-
{#each menuOperatorOptions as option (option.value)}
|
|
203
|
-
<option value={option.value}>{operatorLabelFor(option, menuColumn)}</option>
|
|
204
|
-
{/each}
|
|
205
|
-
</select>
|
|
206
|
-
{#if menuActiveOperator !== "isBlank" && menuActiveOperator !== "isNotBlank"}
|
|
207
|
-
{@const isSetOp =
|
|
208
|
-
menuActiveOperator === "in" || menuActiveOperator === "notIn"}
|
|
209
|
-
<!-- in/notIn drives the shared value-suggestions dropdown (a windowed
|
|
210
|
-
checklist) rather than a native datalist, which capped at 200
|
|
211
|
-
values and couldn't show which are already selected. -->
|
|
212
|
-
<input
|
|
213
|
-
class="sv-grid-menu-condition-value"
|
|
214
|
-
type={menuActiveOperator === "regex" ? "text" : menuInputType}
|
|
215
|
-
value={filterMenuValues[colId]?.value ?? ""}
|
|
216
|
-
placeholder={menuActiveOperator === "between"
|
|
217
|
-
? "From"
|
|
218
|
-
: isSetOp
|
|
219
|
-
? "value, value..."
|
|
220
|
-
: menuActiveOperator === "regex"
|
|
221
|
-
? "Pattern..."
|
|
222
|
-
: "Filter value..."}
|
|
223
|
-
onfocus={isSetOp
|
|
224
|
-
? (event) => onSetOpFilterFocus(event.currentTarget as HTMLInputElement, colId)
|
|
225
|
-
: undefined}
|
|
226
|
-
onblur={isSetOp ? () => closeInSuggest() : undefined}
|
|
227
|
-
oninput={(event) => {
|
|
228
|
-
const input = event.currentTarget as HTMLInputElement;
|
|
229
|
-
updateFilterMenuValue(colId, input.value);
|
|
230
|
-
if (isSetOp) onSetOpFilterInput(input, colId);
|
|
231
|
-
}}
|
|
232
|
-
/>
|
|
233
|
-
{#if menuActiveOperator === "between"}
|
|
234
|
-
<input
|
|
235
|
-
class="sv-grid-menu-condition-value"
|
|
236
|
-
type={menuInputType}
|
|
237
|
-
value={filterMenuValues[colId]?.valueTo ?? ""}
|
|
238
|
-
placeholder="To"
|
|
239
|
-
oninput={(event) => updateFilterMenuValueTo(colId, (event.currentTarget as HTMLInputElement).value)}
|
|
240
|
-
/>
|
|
241
|
-
{/if}
|
|
242
|
-
{@const mf = filterMenuValues[colId]}
|
|
243
|
-
{#if mf?.operator2}
|
|
244
|
-
{@const op2 = mf.operator2 ?? defaultOperatorFor(menuColumn)}
|
|
245
|
-
<div class="sv-grid-menu-join" role="radiogroup" aria-label="Combine conditions">
|
|
246
|
-
<button type="button" class:is-on={(mf.join ?? "AND") === "AND"}
|
|
247
|
-
onclick={() => patchFilter(colId, { join: "AND" })}>AND</button>
|
|
248
|
-
<button type="button" class:is-on={mf.join === "OR"}
|
|
249
|
-
onclick={() => patchFilter(colId, { join: "OR" })}>OR</button>
|
|
250
|
-
<button type="button" class="sv-grid-menu-join-x" title="Remove second condition"
|
|
251
|
-
aria-label="Remove second condition"
|
|
252
|
-
onclick={() => patchFilter(colId, { operator2: undefined, value2: undefined, valueTo2: undefined, join: undefined })}>×</button>
|
|
253
|
-
</div>
|
|
254
|
-
<select
|
|
255
|
-
class="sv-grid-menu-operator-select"
|
|
256
|
-
aria-label="Second filter condition"
|
|
257
|
-
value={op2}
|
|
258
|
-
onchange={(event) =>
|
|
259
|
-
patchFilter(colId, { operator2: (event.currentTarget as HTMLSelectElement).value as FilterOperator })}
|
|
260
|
-
>
|
|
261
|
-
{#each menuOperatorOptions as option (option.value)}
|
|
262
|
-
<option value={option.value}>{operatorLabelFor(option, menuColumn)}</option>
|
|
263
|
-
{/each}
|
|
264
|
-
</select>
|
|
265
|
-
{#if op2 !== "isBlank"}
|
|
266
|
-
<input
|
|
267
|
-
class="sv-grid-menu-condition-value"
|
|
268
|
-
type={menuInputType}
|
|
269
|
-
value={mf.value2 ?? ""}
|
|
270
|
-
placeholder={op2 === "between" ? "From" : "Filter value..."}
|
|
271
|
-
oninput={(event) => patchFilter(colId, { value2: (event.currentTarget as HTMLInputElement).value })}
|
|
272
|
-
/>
|
|
273
|
-
{#if op2 === "between"}
|
|
274
|
-
<input
|
|
275
|
-
class="sv-grid-menu-condition-value"
|
|
276
|
-
type={menuInputType}
|
|
277
|
-
value={mf.valueTo2 ?? ""}
|
|
278
|
-
placeholder="To"
|
|
279
|
-
oninput={(event) => patchFilter(colId, { valueTo2: (event.currentTarget as HTMLInputElement).value })}
|
|
280
|
-
/>
|
|
281
|
-
{/if}
|
|
282
|
-
{/if}
|
|
283
|
-
{:else}
|
|
284
|
-
<button type="button" class="sv-grid-menu-add-cond"
|
|
285
|
-
onclick={() => patchFilter(colId, { operator2: defaultOperatorFor(menuColumn), value2: "", join: "AND" })}>
|
|
286
|
-
+ Add condition
|
|
287
|
-
</button>
|
|
288
|
-
{/if}
|
|
289
|
-
{/if}
|
|
290
|
-
<div class="sv-grid-menu-sep"></div>
|
|
291
|
-
<div class="sv-grid-menu-filter-head">Values</div>
|
|
292
|
-
<input
|
|
293
|
-
class="sv-grid-menu-search"
|
|
294
|
-
placeholder="Search values..."
|
|
295
|
-
bind:value={ctrl.columnMenuSearch}
|
|
296
|
-
/>
|
|
297
|
-
<label class="sv-grid-facet sv-grid-facet-all">
|
|
298
|
-
<input type="checkbox" checked={isAllFacetsChecked(colId)} onchange={() => toggleAllFacets(colId)} />
|
|
299
|
-
<span class="sv-grid-facet-label">(Select all)</span>
|
|
300
|
-
</label>
|
|
301
|
-
<!-- Windowed checklist. A high-cardinality column (10k distinct symbols
|
|
302
|
-
on a live feed) would otherwise mount a label + checkbox per value -
|
|
303
|
-
and 10k focusable checkboxes are 10k tab stops. SvListBox renders
|
|
304
|
-
only the visible rows and is one roving-tabindex stop. -->
|
|
305
|
-
{#if columnMenuFacetOptions.length}
|
|
306
|
-
<SvListBox
|
|
307
|
-
block
|
|
308
|
-
checkbox
|
|
309
|
-
multiple
|
|
310
|
-
virtual={columnMenuFacetOptions.length > FACET_VIRTUAL_THRESHOLD}
|
|
311
|
-
rows={7}
|
|
312
|
-
rowHeight={24}
|
|
313
|
-
size="sm"
|
|
314
|
-
ariaLabel="Filter values"
|
|
315
|
-
options={columnMenuFacetOptions}
|
|
316
|
-
value={columnMenuSelectedFacets}
|
|
317
|
-
onChange={(next) => setFacetSelection(colId, next as Set<string>)}
|
|
318
|
-
/>
|
|
319
|
-
{:else}
|
|
320
|
-
<div class="sv-grid-facet-empty">No values</div>
|
|
321
|
-
{/if}
|
|
322
|
-
{#if columnMenuFacetValues.length > columnMenuVisibleFacets.length}
|
|
323
|
-
<div class="sv-grid-facet-note">
|
|
324
|
-
Showing {columnMenuVisibleFacets.length} of {columnMenuFacetValues.length}
|
|
325
|
-
</div>
|
|
326
|
-
{/if}
|
|
327
|
-
<div class="sv-grid-menu-actions">
|
|
328
|
-
<button
|
|
329
|
-
type="button"
|
|
330
|
-
class="sv-grid-menu-btn"
|
|
331
|
-
disabled={!isColumnFiltered(colId)}
|
|
332
|
-
onclick={() => clearColumnFilter(colId)}
|
|
333
|
-
>
|
|
334
|
-
Clear filter
|
|
335
|
-
</button>
|
|
336
|
-
<button type="button" class="sv-grid-menu-btn sv-grid-menu-btn-primary" onclick={closeMenus}>
|
|
337
|
-
Done
|
|
338
|
-
</button>
|
|
339
|
-
</div>
|
|
340
|
-
</div>
|
|
341
|
-
{/snippet}
|
|
342
|
-
|
|
343
|
-
{#if columnMenuFor}
|
|
344
|
-
{@const menuColumnId = columnMenuFor}
|
|
345
|
-
{@const menuCol = allColumns.find((c) => c.id === menuColumnId)}
|
|
346
|
-
{@const menuCanSort = menuCol?.getCanSort?.() ?? false}
|
|
347
|
-
{@const menuCanFilter = (menuCol?.columnDef.field != null) && (menuCol?.columnDef.filterable !== false)}
|
|
348
|
-
{@const menuTab = ctrl.columnMenuTab}
|
|
349
|
-
<div
|
|
350
|
-
class="sv-grid-menu sv-grid-column-menu"
|
|
351
|
-
role="menu"
|
|
352
|
-
style={`left: ${columnMenuPos.x}px; top: ${columnMenuPos.y}px; max-height: calc(100vh - ${columnMenuPos.y}px - 12px);`}
|
|
353
|
-
>
|
|
354
|
-
{#if columnMenuTabsEnabled}
|
|
355
|
-
<div class="sv-grid-menu-tabs" role="tablist">
|
|
356
|
-
<button type="button" class="sv-grid-menu-tab" class:is-active={menuTab === "general"}
|
|
357
|
-
role="tab" aria-selected={menuTab === "general"}
|
|
358
|
-
onclick={() => (ctrl.columnMenuTab = "general")}>General</button>
|
|
359
|
-
{#if menuCanFilter && showColumnFiltersEffective}
|
|
360
|
-
<button type="button" class="sv-grid-menu-tab" class:is-active={menuTab === "filter"}
|
|
361
|
-
role="tab" aria-selected={menuTab === "filter"}
|
|
362
|
-
onclick={() => (ctrl.columnMenuTab = "filter")}>Filter</button>
|
|
363
|
-
{/if}
|
|
364
|
-
<button type="button" class="sv-grid-menu-tab" class:is-active={menuTab === "columns"}
|
|
365
|
-
role="tab" aria-selected={menuTab === "columns"}
|
|
366
|
-
onclick={() => (ctrl.columnMenuTab = "columns")}>Columns</button>
|
|
367
|
-
</div>
|
|
368
|
-
{/if}
|
|
369
|
-
{#if columnMenuTabsEnabled && menuTab === "filter" && menuCanFilter && showColumnFiltersEffective}
|
|
370
|
-
{@render filterPanelBody(menuColumnId)}
|
|
371
|
-
{:else if columnMenuTabsEnabled && menuTab === "columns"}
|
|
372
|
-
{@const tabColumns = grid.getAllColumns()}
|
|
373
|
-
<div class="sv-grid-menu-filter-head">Visible columns</div>
|
|
374
|
-
<SvListBox
|
|
375
|
-
block
|
|
376
|
-
checkbox
|
|
377
|
-
multiple
|
|
378
|
-
virtual={tabColumns.length > FACET_VIRTUAL_THRESHOLD}
|
|
379
|
-
rows={9}
|
|
380
|
-
rowHeight={24}
|
|
381
|
-
size="sm"
|
|
382
|
-
ariaLabel="Visible columns"
|
|
383
|
-
options={columnVisibilityOptions(tabColumns)}
|
|
384
|
-
value={visibleColumnIds(tabColumns)}
|
|
385
|
-
onChange={(next) => applyColumnVisibility(tabColumns, next as Set<string>)}
|
|
386
|
-
/>
|
|
387
|
-
{:else}
|
|
388
|
-
{#if menuCanSort}
|
|
389
|
-
<button
|
|
390
|
-
type="button"
|
|
391
|
-
class="sv-grid-menu-item"
|
|
392
|
-
role="menuitem"
|
|
393
|
-
onclick={() => sortColumnFromMenu(menuColumnId, false)}
|
|
394
|
-
>
|
|
395
|
-
<span class="sv-grid-header-icon">{@render icon("sort-asc")}</span> Sort
|
|
396
|
-
ascending
|
|
397
|
-
</button>
|
|
398
|
-
<button
|
|
399
|
-
type="button"
|
|
400
|
-
class="sv-grid-menu-item"
|
|
401
|
-
role="menuitem"
|
|
402
|
-
onclick={() => sortColumnFromMenu(menuColumnId, true)}
|
|
403
|
-
>
|
|
404
|
-
<span class="sv-grid-header-icon">{@render icon("sort-desc")}</span> Sort
|
|
405
|
-
descending
|
|
406
|
-
</button>
|
|
407
|
-
<button
|
|
408
|
-
type="button"
|
|
409
|
-
class="sv-grid-menu-item"
|
|
410
|
-
role="menuitem"
|
|
411
|
-
disabled={!sortDirectionByColumn[menuColumnId]}
|
|
412
|
-
onclick={() => clearColumnSort(menuColumnId)}
|
|
413
|
-
>
|
|
414
|
-
<span class="sv-grid-header-icon">{@render icon("x")}</span> Remove sort
|
|
415
|
-
</button>
|
|
416
|
-
{/if}
|
|
417
|
-
{#if !columnVirtualizationEnabled}
|
|
418
|
-
{@const menuPinSide = isColumnPinned(menuColumnId)}
|
|
419
|
-
<div class="sv-grid-menu-sep"></div>
|
|
420
|
-
<button
|
|
421
|
-
type="button"
|
|
422
|
-
class="sv-grid-menu-item"
|
|
423
|
-
role="menuitem"
|
|
424
|
-
disabled={menuPinSide === "left"}
|
|
425
|
-
onclick={() => pinColumnLeft(menuColumnId)}
|
|
426
|
-
>
|
|
427
|
-
<span class="sv-grid-header-icon"
|
|
428
|
-
>{@render icon("op-startsWith")}</span
|
|
429
|
-
> Pin to left
|
|
430
|
-
</button>
|
|
431
|
-
<button
|
|
432
|
-
type="button"
|
|
433
|
-
class="sv-grid-menu-item"
|
|
434
|
-
role="menuitem"
|
|
435
|
-
disabled={menuPinSide === "right"}
|
|
436
|
-
onclick={() => pinColumnRight(menuColumnId)}
|
|
437
|
-
>
|
|
438
|
-
<span class="sv-grid-header-icon"
|
|
439
|
-
>{@render icon("op-greaterThan")}</span
|
|
440
|
-
> Pin to right
|
|
441
|
-
</button>
|
|
442
|
-
<button
|
|
443
|
-
type="button"
|
|
444
|
-
class="sv-grid-menu-item"
|
|
445
|
-
role="menuitem"
|
|
446
|
-
disabled={!menuPinSide}
|
|
447
|
-
onclick={() => unpinColumn(menuColumnId)}
|
|
448
|
-
>
|
|
449
|
-
<span class="sv-grid-header-icon">{@render icon("x")}</span> Unpin column
|
|
450
|
-
</button>
|
|
451
|
-
{/if}
|
|
452
|
-
<div class="sv-grid-menu-sep"></div>
|
|
453
|
-
<button
|
|
454
|
-
type="button"
|
|
455
|
-
class="sv-grid-menu-item"
|
|
456
|
-
role="menuitem"
|
|
457
|
-
onclick={() => {
|
|
458
|
-
autosizeColumn(menuColumnId);
|
|
459
|
-
closeMenus();
|
|
460
|
-
}}
|
|
461
|
-
>
|
|
462
|
-
<span class="sv-grid-header-icon">{@render icon("autosize")}</span> Autosize
|
|
463
|
-
this column
|
|
464
|
-
</button>
|
|
465
|
-
<button
|
|
466
|
-
type="button"
|
|
467
|
-
class="sv-grid-menu-item"
|
|
468
|
-
role="menuitem"
|
|
469
|
-
onclick={() => {
|
|
470
|
-
autosizeAllColumns();
|
|
471
|
-
closeMenus();
|
|
472
|
-
}}
|
|
473
|
-
>
|
|
474
|
-
<span class="sv-grid-header-icon">{@render icon("autosize")}</span> Autosize
|
|
475
|
-
all columns
|
|
476
|
-
</button>
|
|
477
|
-
{#if groupingControlsEnabled}
|
|
478
|
-
<div class="sv-grid-menu-sep"></div>
|
|
479
|
-
<button
|
|
480
|
-
type="button"
|
|
481
|
-
class="sv-grid-menu-item"
|
|
482
|
-
role="menuitem"
|
|
483
|
-
disabled={groupingColumns.includes(menuColumnId)}
|
|
484
|
-
onclick={() => groupByColumnFromMenu(menuColumnId)}
|
|
485
|
-
>
|
|
486
|
-
<span class="sv-grid-header-icon">{@render icon("group")}</span> Group
|
|
487
|
-
by this column
|
|
488
|
-
</button>
|
|
489
|
-
<button
|
|
490
|
-
type="button"
|
|
491
|
-
class="sv-grid-menu-item"
|
|
492
|
-
role="menuitem"
|
|
493
|
-
disabled={!groupingColumns.includes(menuColumnId)}
|
|
494
|
-
onclick={() => clearGroupingFromMenu(menuColumnId)}
|
|
495
|
-
>
|
|
496
|
-
<span class="sv-grid-header-icon">{@render icon("x")}</span> Remove grouping
|
|
497
|
-
</button>
|
|
498
|
-
{/if}
|
|
499
|
-
<div class="sv-grid-menu-sep"></div>
|
|
500
|
-
{#if !columnMenuTabsEnabled}
|
|
501
|
-
<!-- Flat menu: reach the column chooser via a submenu (tabbed mode has
|
|
502
|
-
a Columns tab instead). -->
|
|
503
|
-
<button
|
|
504
|
-
type="button"
|
|
505
|
-
class="sv-grid-menu-item"
|
|
506
|
-
class:is-open={chooseColumnsPos !== null}
|
|
507
|
-
role="menuitem"
|
|
508
|
-
aria-haspopup="menu"
|
|
509
|
-
aria-expanded={chooseColumnsPos !== null}
|
|
510
|
-
onclick={(event) => openChooseColumns(event)}
|
|
511
|
-
>
|
|
512
|
-
<span class="sv-grid-header-icon">{@render icon("columns")}</span>
|
|
513
|
-
Choose columns
|
|
514
|
-
<span class="sv-grid-header-icon sv-grid-menu-item-chevron">{@render icon("chevron-down")}</span>
|
|
515
|
-
</button>
|
|
516
|
-
{/if}
|
|
517
|
-
<button
|
|
518
|
-
type="button"
|
|
519
|
-
class="sv-grid-menu-item"
|
|
520
|
-
role="menuitem"
|
|
521
|
-
onclick={() => {
|
|
522
|
-
resetColumns();
|
|
523
|
-
closeMenus();
|
|
524
|
-
}}
|
|
525
|
-
>
|
|
526
|
-
<span class="sv-grid-header-icon">{@render icon("reset")}</span> Reset columns
|
|
527
|
-
</button>
|
|
528
|
-
{/if}
|
|
529
|
-
</div>
|
|
530
|
-
{/if}
|
|
531
|
-
|
|
532
|
-
{#if filterMenuFor && showColumnFiltersEffective}
|
|
533
|
-
<div
|
|
534
|
-
class="sv-grid-menu sv-grid-filter-menu"
|
|
535
|
-
role="menu"
|
|
536
|
-
style={`left: ${filterMenuPos.x}px; top: ${filterMenuPos.y}px; max-height: calc(100vh - ${filterMenuPos.y}px - 12px);`}
|
|
537
|
-
>
|
|
538
|
-
{@render filterPanelBody(filterMenuFor)}
|
|
539
|
-
</div>
|
|
540
|
-
{/if}
|
|
541
|
-
|
|
542
|
-
{#if chooseColumnsPos}
|
|
543
|
-
{@const everyColumn = grid.getAllColumns()}
|
|
544
|
-
<div
|
|
545
|
-
class="sv-grid-menu sv-grid-choose-columns-menu"
|
|
546
|
-
role="menu"
|
|
547
|
-
style={`left: ${chooseColumnsPos.x}px; top: ${chooseColumnsPos.y}px;`}
|
|
548
|
-
>
|
|
549
|
-
<div class="sv-grid-menu-filter-head">Visible columns</div>
|
|
550
|
-
<SvListBox
|
|
551
|
-
block
|
|
552
|
-
checkbox
|
|
553
|
-
multiple
|
|
554
|
-
virtual={everyColumn.length > FACET_VIRTUAL_THRESHOLD}
|
|
555
|
-
rows={11}
|
|
556
|
-
rowHeight={24}
|
|
557
|
-
size="sm"
|
|
558
|
-
ariaLabel="Visible columns"
|
|
559
|
-
options={columnVisibilityOptions(everyColumn)}
|
|
560
|
-
value={visibleColumnIds(everyColumn)}
|
|
561
|
-
onChange={(next) => applyColumnVisibility(everyColumn, next as Set<string>)}
|
|
562
|
-
/>
|
|
563
|
-
</div>
|
|
564
|
-
{/if}
|
|
565
|
-
|
|
566
|
-
{#if operatorMenuFor}
|
|
567
|
-
{@const opColumnId = operatorMenuFor}
|
|
568
|
-
{@const opColumn = allColumns.find((c) => c.id === opColumnId)}
|
|
569
|
-
{@const opOptions = operatorsForColumn(opColumn)}
|
|
570
|
-
{@const opActive =
|
|
571
|
-
filterMenuValues[opColumnId]?.operator ?? defaultOperatorFor(opColumn)}
|
|
572
|
-
<div
|
|
573
|
-
class="sv-grid-menu sv-grid-operator-menu"
|
|
574
|
-
role="menu"
|
|
575
|
-
style={`left: ${operatorMenuPos.x}px; top: ${operatorMenuPos.y}px;`}
|
|
576
|
-
>
|
|
577
|
-
{#each opOptions as option (option.value)}
|
|
578
|
-
<button
|
|
579
|
-
type="button"
|
|
580
|
-
class="sv-grid-menu-item"
|
|
581
|
-
role="menuitemradio"
|
|
582
|
-
aria-checked={opActive === option.value}
|
|
583
|
-
onclick={() => {
|
|
584
|
-
updateFilterOperator(opColumnId, option.value);
|
|
585
|
-
closeMenus();
|
|
586
|
-
}}
|
|
587
|
-
>
|
|
588
|
-
<span class="sv-grid-header-icon"
|
|
589
|
-
>{@render icon(option.iconName)}</span
|
|
590
|
-
>
|
|
591
|
-
{operatorLabelFor(option, opColumn)}
|
|
592
|
-
</button>
|
|
593
|
-
{/each}
|
|
594
|
-
</div>
|
|
595
|
-
{/if}
|
|
596
|
-
|
|
597
|
-
{#if inSuggestFor}
|
|
598
|
-
{@const suggestColId = inSuggestFor}
|
|
599
|
-
{@const selectedTokens = new Set<string | number>(
|
|
600
|
-
splitInTokens(filterRowValues[suggestColId] ?? ""),
|
|
601
|
-
)}
|
|
602
|
-
<!-- Value suggestions for an `in` / `notIn` input. The wrapper swallows
|
|
603
|
-
`mousedown` so clicking a row keeps focus in the input and the
|
|
604
|
-
dropdown stays open for multi-select; the input's own blur handler
|
|
605
|
-
closes it when focus truly leaves. The list still picks on `click`.
|
|
606
|
-
Tokens the query is currently hiding ride along in the set, so a
|
|
607
|
-
click never silently drops them. -->
|
|
608
|
-
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
|
609
|
-
<div
|
|
610
|
-
class="sv-grid-menu sv-grid-in-suggest"
|
|
611
|
-
style={`left: ${inSuggestPos.x}px; top: ${inSuggestPos.y}px;`}
|
|
612
|
-
onmousedown={(event) => event.preventDefault()}
|
|
613
|
-
>
|
|
614
|
-
{#if inSuggestOptions.length}
|
|
615
|
-
<SvListBox
|
|
616
|
-
block
|
|
617
|
-
checkbox
|
|
618
|
-
multiple
|
|
619
|
-
virtual={inSuggestOptions.length > FACET_VIRTUAL_THRESHOLD}
|
|
620
|
-
rows={8}
|
|
621
|
-
rowHeight={26}
|
|
622
|
-
size="sm"
|
|
623
|
-
ariaLabel="Filter values"
|
|
624
|
-
options={inSuggestOptions}
|
|
625
|
-
value={selectedTokens}
|
|
626
|
-
onChange={(next) => {
|
|
627
|
-
const picked = new Set(next as Set<string>);
|
|
628
|
-
// Boxes that hold the whole list ("AAPL, MS") leave a half-typed
|
|
629
|
-
// fragment in the token set. The pick REPLACES that fragment
|
|
630
|
-
// rather than landing beside it as a stray filter value. The chip
|
|
631
|
-
// input keeps its fragment out of `filterRowValues`, so this is a
|
|
632
|
-
// no-op there.
|
|
633
|
-
const fragment = trailingInToken(filterRowValues[suggestColId] ?? "");
|
|
634
|
-
if (fragment) picked.delete(fragment);
|
|
635
|
-
setFilterTokens(suggestColId, [...picked]);
|
|
636
|
-
ctrl.inSuggestQuery = "";
|
|
637
|
-
}}
|
|
638
|
-
/>
|
|
639
|
-
{:else}
|
|
640
|
-
<div class="sv-grid-facet-empty">No values</div>
|
|
641
|
-
{/if}
|
|
642
|
-
</div>
|
|
643
|
-
{/if}
|
|
644
|
-
|
|
645
|
-
{#if contextMenuFor}
|
|
646
|
-
<div
|
|
647
|
-
class="sv-grid-menu-backdrop"
|
|
648
|
-
role="presentation"
|
|
649
|
-
oncontextmenu={(e) => { e.preventDefault(); closeContextMenu(); }}
|
|
650
|
-
onclick={closeContextMenu}
|
|
651
|
-
></div>
|
|
652
|
-
<div
|
|
653
|
-
class="sv-grid-menu sv-grid-context-menu"
|
|
654
|
-
role="menu"
|
|
655
|
-
style={`left: ${contextMenuPos.x}px; top: ${contextMenuPos.y}px;`}
|
|
656
|
-
>
|
|
657
|
-
{#each contextMenuItems() as item (item.key)}
|
|
658
|
-
{#if item.separator}
|
|
659
|
-
<div class="sv-grid-menu-sep"></div>
|
|
660
|
-
{:else}
|
|
661
|
-
<button
|
|
662
|
-
type="button"
|
|
663
|
-
class="sv-grid-menu-item"
|
|
664
|
-
role="menuitem"
|
|
665
|
-
disabled={item.disabled}
|
|
666
|
-
onclick={() => { item.run?.(); closeContextMenu(); }}
|
|
667
|
-
>{item.label}</button>
|
|
668
|
-
{/if}
|
|
669
|
-
{/each}
|
|
670
|
-
</div>
|
|
671
|
-
{/if}
|
|
672
|
-
|
|
673
|
-
{#if commentEditFor}
|
|
674
|
-
<div
|
|
675
|
-
class="sv-grid-menu-backdrop"
|
|
676
|
-
role="presentation"
|
|
677
|
-
onclick={closeCommentEditor}
|
|
678
|
-
></div>
|
|
679
|
-
<div
|
|
680
|
-
class="sv-grid-menu sv-grid-comment-editor"
|
|
681
|
-
role="dialog"
|
|
682
|
-
aria-label="Edit comment"
|
|
683
|
-
style={`left: ${commentEditFor.x}px; top: ${commentEditFor.y}px;`}
|
|
684
|
-
>
|
|
685
|
-
<!-- svelte-ignore a11y_autofocus -->
|
|
686
|
-
<textarea
|
|
687
|
-
class="sv-grid-comment-textarea"
|
|
688
|
-
rows="3"
|
|
689
|
-
placeholder="Add a comment…"
|
|
690
|
-
autofocus
|
|
691
|
-
bind:value={ctrl.commentDraft}
|
|
692
|
-
onkeydown={(e) => {
|
|
693
|
-
e.stopPropagation();
|
|
694
|
-
if (e.key === "Escape") { e.preventDefault(); closeCommentEditor(); }
|
|
695
|
-
if (e.key === "Enter" && (e.ctrlKey || e.metaKey)) { e.preventDefault(); saveComment(); }
|
|
696
|
-
}}
|
|
697
|
-
></textarea>
|
|
698
|
-
<div class="sv-grid-comment-actions">
|
|
699
|
-
<button type="button" class="sv-grid-comment-remove" onclick={removeComment}>Remove</button>
|
|
700
|
-
<span class="sv-grid-comment-spacer"></span>
|
|
701
|
-
<button type="button" class="sv-grid-comment-cancel" onclick={closeCommentEditor}>Cancel</button>
|
|
702
|
-
<button type="button" class="sv-grid-comment-save" onclick={saveComment}>Save</button>
|
|
703
|
-
</div>
|
|
704
|
-
</div>
|
|
705
|
-
{/if}
|
|
1
|
+
<script lang="ts" generics="TFeatures extends TableFeatures = TableFeatures, TData extends RowData = RowData">
|
|
2
|
+
import {
|
|
3
|
+
splitInTokens,
|
|
4
|
+
trailingInToken,
|
|
5
|
+
type CellEditorType,
|
|
6
|
+
type Column,
|
|
7
|
+
type RowData,
|
|
8
|
+
type TableFeatures,
|
|
9
|
+
} from "./index";
|
|
10
|
+
import "./sv-grid-scrollbar";
|
|
11
|
+
import { onScrollOutside } from "./a11y/dismissable";
|
|
12
|
+
import type { Snippet } from "svelte";
|
|
13
|
+
import SvListBox from "./SvListBox.svelte";
|
|
14
|
+
import type {
|
|
15
|
+
FilterOperator,
|
|
16
|
+
} from "./SvGrid.types";
|
|
17
|
+
import {
|
|
18
|
+
getEditorInputType,
|
|
19
|
+
} from "./SvGrid.helpers";
|
|
20
|
+
|
|
21
|
+
import type { SvGridController } from "./SvGrid.controller.svelte";
|
|
22
|
+
|
|
23
|
+
let { ctrl, icon }: { ctrl: SvGridController<TFeatures, TData>; icon: Snippet<[string]> } = $props();
|
|
24
|
+
|
|
25
|
+
// View facade: re-bind the controller's reactive members so the markup
|
|
26
|
+
// (moved verbatim from SvGrid.svelte) stays identical.
|
|
27
|
+
const groupingControlsEnabled = $derived(ctrl.groupingControlsEnabled);
|
|
28
|
+
const filterMenuValues = $derived(ctrl.filterMenuValues);
|
|
29
|
+
const tooltip = $derived(ctrl.tooltip);
|
|
30
|
+
const columnMenuFor = $derived(ctrl.columnMenuFor);
|
|
31
|
+
const columnMenuPos = $derived(ctrl.columnMenuPos);
|
|
32
|
+
const filterMenuFor = $derived(ctrl.filterMenuFor);
|
|
33
|
+
const filterMenuPos = $derived(ctrl.filterMenuPos);
|
|
34
|
+
const operatorMenuFor = $derived(ctrl.operatorMenuFor);
|
|
35
|
+
const operatorMenuPos = $derived(ctrl.operatorMenuPos);
|
|
36
|
+
const chooseColumnsPos = $derived(ctrl.chooseColumnsPos);
|
|
37
|
+
const showColumnFiltersEffective = $derived(ctrl.showColumnFiltersEffective);
|
|
38
|
+
const columnMenuTabsEnabled = $derived(ctrl.props.columnMenuTabs === true);
|
|
39
|
+
const grid = $derived(ctrl.grid);
|
|
40
|
+
const allColumns = $derived(ctrl.allColumns);
|
|
41
|
+
const isColumnPinned = $derived(ctrl.isColumnPinned);
|
|
42
|
+
const pinColumnLeft = $derived(ctrl.pinColumnLeft);
|
|
43
|
+
const pinColumnRight = $derived(ctrl.pinColumnRight);
|
|
44
|
+
const unpinColumn = $derived(ctrl.unpinColumn);
|
|
45
|
+
const sortDirectionByColumn = $derived(ctrl.sortDirectionByColumn);
|
|
46
|
+
const groupingColumns = $derived(ctrl.groupingColumns);
|
|
47
|
+
const columnVirtualizationEnabled = $derived(ctrl.columnVirtualizationEnabled);
|
|
48
|
+
const updateFilterOperator = $derived(ctrl.updateFilterOperator);
|
|
49
|
+
const updateFilterMenuValue = $derived(ctrl.updateFilterMenuValue);
|
|
50
|
+
const updateFilterMenuValueTo = $derived(ctrl.updateFilterMenuValueTo);
|
|
51
|
+
const operatorsForColumn = $derived(ctrl.operatorsForColumn);
|
|
52
|
+
const defaultOperatorFor = $derived(ctrl.defaultOperatorFor);
|
|
53
|
+
const operatorLabelFor = $derived(ctrl.operatorLabelFor);
|
|
54
|
+
const filterRowValues = $derived(ctrl.filterRowValues);
|
|
55
|
+
const inSuggestFor = $derived(ctrl.inSuggestFor);
|
|
56
|
+
const inSuggestPos = $derived(ctrl.inSuggestPos);
|
|
57
|
+
|
|
58
|
+
// Patch a column's menu filter (used for the optional 2nd condition + join
|
|
59
|
+
// of multi-condition filtering). Merges into the existing entry, creating a
|
|
60
|
+
// base entry from the column's default operator when none exists yet.
|
|
61
|
+
function patchFilter(columnId: string, patch: Record<string, unknown>) {
|
|
62
|
+
const col = allColumns.find((c) => c.id === columnId);
|
|
63
|
+
const cur = ctrl.filterMenuValues[columnId] ?? { operator: defaultOperatorFor(col), value: "" };
|
|
64
|
+
ctrl.filterMenuValues = { ...ctrl.filterMenuValues, [columnId]: { ...cur, ...patch } };
|
|
65
|
+
}
|
|
66
|
+
const isColumnFiltered = $derived(ctrl.isColumnFiltered);
|
|
67
|
+
const closeMenus = $derived(ctrl.closeMenus);
|
|
68
|
+
|
|
69
|
+
// Column/filter/operator menus are position:fixed and anchored to their
|
|
70
|
+
// trigger by a one-time measurement taken when they open. On scroll they'd
|
|
71
|
+
// otherwise detach and float over the wrong place (#88), so close them when
|
|
72
|
+
// anything OUTSIDE them scrolls, or on resize. Scrolling the menu's own body
|
|
73
|
+
// or its facet list has to keep it open - hence onScrollOutside rather than a
|
|
74
|
+
// bare capture-phase listener.
|
|
75
|
+
$effect(() => {
|
|
76
|
+
if (!(columnMenuFor || filterMenuFor || operatorMenuFor || chooseColumnsPos)) return;
|
|
77
|
+
const close = () => closeMenus();
|
|
78
|
+
const offScroll = onScrollOutside(
|
|
79
|
+
() => Array.from(document.querySelectorAll<HTMLElement>('.sv-grid-menu')),
|
|
80
|
+
close,
|
|
81
|
+
);
|
|
82
|
+
window.addEventListener('resize', close);
|
|
83
|
+
return () => {
|
|
84
|
+
offScroll();
|
|
85
|
+
window.removeEventListener('resize', close);
|
|
86
|
+
};
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
const autosizeColumn = $derived(ctrl.autosizeColumn);
|
|
90
|
+
const autosizeAllColumns = $derived(ctrl.autosizeAllColumns);
|
|
91
|
+
const resetColumns = $derived(ctrl.resetColumns);
|
|
92
|
+
const openChooseColumns = $derived(ctrl.openChooseColumns);
|
|
93
|
+
const sortColumnFromMenu = $derived(ctrl.sortColumnFromMenu);
|
|
94
|
+
const clearColumnSort = $derived(ctrl.clearColumnSort);
|
|
95
|
+
const groupByColumnFromMenu = $derived(ctrl.groupByColumnFromMenu);
|
|
96
|
+
const clearGroupingFromMenu = $derived(ctrl.clearGroupingFromMenu);
|
|
97
|
+
const columnMenuFacetValues = $derived(ctrl.columnMenuFacetValues);
|
|
98
|
+
const columnMenuVisibleFacets = $derived(ctrl.columnMenuVisibleFacets);
|
|
99
|
+
const columnMenuFacetOptions = $derived(ctrl.columnMenuFacetOptions);
|
|
100
|
+
const columnMenuSelectedFacets = $derived(ctrl.columnMenuSelectedFacets);
|
|
101
|
+
const setFacetSelection = $derived(ctrl.setFacetSelection);
|
|
102
|
+
const setFilterTokens = $derived(ctrl.setFilterTokens);
|
|
103
|
+
const inSuggestOptions = $derived(ctrl.inSuggestOptions);
|
|
104
|
+
const openInSuggest = $derived(ctrl.openInSuggest);
|
|
105
|
+
const closeInSuggest = $derived(ctrl.closeInSuggest);
|
|
106
|
+
const isAllFacetsChecked = $derived(ctrl.isAllFacetsChecked);
|
|
107
|
+
const toggleAllFacets = $derived(ctrl.toggleAllFacets);
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Above this many rows the checklists switch to the windowed listbox. Short
|
|
111
|
+
* lists stay fully rendered so the popover hugs its content instead of
|
|
112
|
+
* reserving a fixed `rows`-tall box.
|
|
113
|
+
*/
|
|
114
|
+
const FACET_VIRTUAL_THRESHOLD = 40;
|
|
115
|
+
|
|
116
|
+
// The filter panel's `in` / `notIn` box holds the WHOLE token list
|
|
117
|
+
// ("AAPL, MSFT"), so the suggestion query is the trailing fragment being
|
|
118
|
+
// typed - not the full value, which would match nothing.
|
|
119
|
+
function onSetOpFilterFocus(input: HTMLInputElement, columnId: string) {
|
|
120
|
+
openInSuggest(input, columnId);
|
|
121
|
+
ctrl.inSuggestQuery = trailingInToken(input.value);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function onSetOpFilterInput(input: HTMLInputElement, columnId: string) {
|
|
125
|
+
ctrl.inSuggestQuery = trailingInToken(input.value);
|
|
126
|
+
if (ctrl.inSuggestFor !== columnId) openInSuggest(input, columnId);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/** Columns as checklist options, labelled by their header when it's a string. */
|
|
130
|
+
function columnVisibilityOptions(columns: Array<Column<TData>>) {
|
|
131
|
+
return columns.map((column) => ({
|
|
132
|
+
value: column.id,
|
|
133
|
+
label: typeof column.columnDef.header === "string" ? column.columnDef.header : column.id,
|
|
134
|
+
}));
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/** Currently visible column ids - the checklist's selection set. */
|
|
138
|
+
function visibleColumnIds(columns: Array<Column<TData>>) {
|
|
139
|
+
const set = new Set<string | number>();
|
|
140
|
+
for (const column of columns) if (!ctrl.hiddenColumns[column.id]) set.add(column.id);
|
|
141
|
+
return set;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/** Write a checklist selection back as the complementary `hiddenColumns` map. */
|
|
145
|
+
function applyColumnVisibility(columns: Array<Column<TData>>, visible: Set<string>) {
|
|
146
|
+
const next: Record<string, boolean> = { ...ctrl.hiddenColumns };
|
|
147
|
+
for (const column of columns) {
|
|
148
|
+
if (visible.has(column.id)) delete next[column.id];
|
|
149
|
+
else next[column.id] = true;
|
|
150
|
+
}
|
|
151
|
+
ctrl.hiddenColumns = next;
|
|
152
|
+
}
|
|
153
|
+
const clearColumnFilter = $derived(ctrl.clearColumnFilter);
|
|
154
|
+
const contextMenuFor = $derived(ctrl.contextMenuFor);
|
|
155
|
+
const contextMenuPos = $derived(ctrl.contextMenuPos);
|
|
156
|
+
const contextMenuItems = $derived(ctrl.contextMenuItems);
|
|
157
|
+
const closeContextMenu = $derived(ctrl.closeContextMenu);
|
|
158
|
+
const commentEditFor = $derived(ctrl.commentEditFor);
|
|
159
|
+
const saveComment = $derived(ctrl.saveComment);
|
|
160
|
+
const removeComment = $derived(ctrl.removeComment);
|
|
161
|
+
const closeCommentEditor = $derived(ctrl.closeCommentEditor);
|
|
162
|
+
</script>
|
|
163
|
+
|
|
164
|
+
{#if tooltip}
|
|
165
|
+
<!-- Custom hover tooltip. Floats above the grid; positioned by
|
|
166
|
+
showTooltipFor(). Notes and column tooltips both surface here. -->
|
|
167
|
+
<div
|
|
168
|
+
class="sv-grid-tooltip"
|
|
169
|
+
class:sv-grid-tooltip-above={!tooltip.below}
|
|
170
|
+
style={`left: ${tooltip.x}px; top: ${tooltip.y}px; ${tooltip.below ? '' : 'transform: translateY(-100%);'}`}
|
|
171
|
+
role="tooltip"
|
|
172
|
+
>{tooltip.text}</div>
|
|
173
|
+
{/if}
|
|
174
|
+
|
|
175
|
+
{#if columnMenuFor || filterMenuFor || operatorMenuFor || chooseColumnsPos}
|
|
176
|
+
<div
|
|
177
|
+
class="sv-grid-menu-backdrop"
|
|
178
|
+
role="presentation"
|
|
179
|
+
onclick={closeMenus}
|
|
180
|
+
></div>
|
|
181
|
+
{/if}
|
|
182
|
+
|
|
183
|
+
<!-- Shared filter UI: rendered both in the funnel popover and in the column
|
|
184
|
+
menu's Filter tab, so the two never diverge. -->
|
|
185
|
+
{#snippet filterPanelBody(colId: string)}
|
|
186
|
+
{@const menuColumn = allColumns.find((c) => c.id === colId)}
|
|
187
|
+
{@const menuOperatorOptions = operatorsForColumn(menuColumn)}
|
|
188
|
+
{@const menuActiveOperator = filterMenuValues[colId]?.operator ?? defaultOperatorFor(menuColumn)}
|
|
189
|
+
{@const menuInputType = getEditorInputType((menuColumn?.columnDef.editorType ?? "text") as CellEditorType)}
|
|
190
|
+
<div class="sv-grid-menu-filter">
|
|
191
|
+
<div class="sv-grid-menu-filter-head">
|
|
192
|
+
<span class="sv-grid-header-icon">{@render icon("filter")}</span> Filter
|
|
193
|
+
condition
|
|
194
|
+
</div>
|
|
195
|
+
<select
|
|
196
|
+
class="sv-grid-menu-operator-select"
|
|
197
|
+
aria-label="Filter condition"
|
|
198
|
+
value={menuActiveOperator}
|
|
199
|
+
onchange={(event) =>
|
|
200
|
+
updateFilterOperator(colId, (event.currentTarget as HTMLSelectElement).value as FilterOperator)}
|
|
201
|
+
>
|
|
202
|
+
{#each menuOperatorOptions as option (option.value)}
|
|
203
|
+
<option value={option.value}>{operatorLabelFor(option, menuColumn)}</option>
|
|
204
|
+
{/each}
|
|
205
|
+
</select>
|
|
206
|
+
{#if menuActiveOperator !== "isBlank" && menuActiveOperator !== "isNotBlank"}
|
|
207
|
+
{@const isSetOp =
|
|
208
|
+
menuActiveOperator === "in" || menuActiveOperator === "notIn"}
|
|
209
|
+
<!-- in/notIn drives the shared value-suggestions dropdown (a windowed
|
|
210
|
+
checklist) rather than a native datalist, which capped at 200
|
|
211
|
+
values and couldn't show which are already selected. -->
|
|
212
|
+
<input
|
|
213
|
+
class="sv-grid-menu-condition-value"
|
|
214
|
+
type={menuActiveOperator === "regex" ? "text" : menuInputType}
|
|
215
|
+
value={filterMenuValues[colId]?.value ?? ""}
|
|
216
|
+
placeholder={menuActiveOperator === "between"
|
|
217
|
+
? "From"
|
|
218
|
+
: isSetOp
|
|
219
|
+
? "value, value..."
|
|
220
|
+
: menuActiveOperator === "regex"
|
|
221
|
+
? "Pattern..."
|
|
222
|
+
: "Filter value..."}
|
|
223
|
+
onfocus={isSetOp
|
|
224
|
+
? (event) => onSetOpFilterFocus(event.currentTarget as HTMLInputElement, colId)
|
|
225
|
+
: undefined}
|
|
226
|
+
onblur={isSetOp ? () => closeInSuggest() : undefined}
|
|
227
|
+
oninput={(event) => {
|
|
228
|
+
const input = event.currentTarget as HTMLInputElement;
|
|
229
|
+
updateFilterMenuValue(colId, input.value);
|
|
230
|
+
if (isSetOp) onSetOpFilterInput(input, colId);
|
|
231
|
+
}}
|
|
232
|
+
/>
|
|
233
|
+
{#if menuActiveOperator === "between"}
|
|
234
|
+
<input
|
|
235
|
+
class="sv-grid-menu-condition-value"
|
|
236
|
+
type={menuInputType}
|
|
237
|
+
value={filterMenuValues[colId]?.valueTo ?? ""}
|
|
238
|
+
placeholder="To"
|
|
239
|
+
oninput={(event) => updateFilterMenuValueTo(colId, (event.currentTarget as HTMLInputElement).value)}
|
|
240
|
+
/>
|
|
241
|
+
{/if}
|
|
242
|
+
{@const mf = filterMenuValues[colId]}
|
|
243
|
+
{#if mf?.operator2}
|
|
244
|
+
{@const op2 = mf.operator2 ?? defaultOperatorFor(menuColumn)}
|
|
245
|
+
<div class="sv-grid-menu-join" role="radiogroup" aria-label="Combine conditions">
|
|
246
|
+
<button type="button" class:is-on={(mf.join ?? "AND") === "AND"}
|
|
247
|
+
onclick={() => patchFilter(colId, { join: "AND" })}>AND</button>
|
|
248
|
+
<button type="button" class:is-on={mf.join === "OR"}
|
|
249
|
+
onclick={() => patchFilter(colId, { join: "OR" })}>OR</button>
|
|
250
|
+
<button type="button" class="sv-grid-menu-join-x" title="Remove second condition"
|
|
251
|
+
aria-label="Remove second condition"
|
|
252
|
+
onclick={() => patchFilter(colId, { operator2: undefined, value2: undefined, valueTo2: undefined, join: undefined })}>×</button>
|
|
253
|
+
</div>
|
|
254
|
+
<select
|
|
255
|
+
class="sv-grid-menu-operator-select"
|
|
256
|
+
aria-label="Second filter condition"
|
|
257
|
+
value={op2}
|
|
258
|
+
onchange={(event) =>
|
|
259
|
+
patchFilter(colId, { operator2: (event.currentTarget as HTMLSelectElement).value as FilterOperator })}
|
|
260
|
+
>
|
|
261
|
+
{#each menuOperatorOptions as option (option.value)}
|
|
262
|
+
<option value={option.value}>{operatorLabelFor(option, menuColumn)}</option>
|
|
263
|
+
{/each}
|
|
264
|
+
</select>
|
|
265
|
+
{#if op2 !== "isBlank"}
|
|
266
|
+
<input
|
|
267
|
+
class="sv-grid-menu-condition-value"
|
|
268
|
+
type={menuInputType}
|
|
269
|
+
value={mf.value2 ?? ""}
|
|
270
|
+
placeholder={op2 === "between" ? "From" : "Filter value..."}
|
|
271
|
+
oninput={(event) => patchFilter(colId, { value2: (event.currentTarget as HTMLInputElement).value })}
|
|
272
|
+
/>
|
|
273
|
+
{#if op2 === "between"}
|
|
274
|
+
<input
|
|
275
|
+
class="sv-grid-menu-condition-value"
|
|
276
|
+
type={menuInputType}
|
|
277
|
+
value={mf.valueTo2 ?? ""}
|
|
278
|
+
placeholder="To"
|
|
279
|
+
oninput={(event) => patchFilter(colId, { valueTo2: (event.currentTarget as HTMLInputElement).value })}
|
|
280
|
+
/>
|
|
281
|
+
{/if}
|
|
282
|
+
{/if}
|
|
283
|
+
{:else}
|
|
284
|
+
<button type="button" class="sv-grid-menu-add-cond"
|
|
285
|
+
onclick={() => patchFilter(colId, { operator2: defaultOperatorFor(menuColumn), value2: "", join: "AND" })}>
|
|
286
|
+
+ Add condition
|
|
287
|
+
</button>
|
|
288
|
+
{/if}
|
|
289
|
+
{/if}
|
|
290
|
+
<div class="sv-grid-menu-sep"></div>
|
|
291
|
+
<div class="sv-grid-menu-filter-head">Values</div>
|
|
292
|
+
<input
|
|
293
|
+
class="sv-grid-menu-search"
|
|
294
|
+
placeholder="Search values..."
|
|
295
|
+
bind:value={ctrl.columnMenuSearch}
|
|
296
|
+
/>
|
|
297
|
+
<label class="sv-grid-facet sv-grid-facet-all">
|
|
298
|
+
<input type="checkbox" checked={isAllFacetsChecked(colId)} onchange={() => toggleAllFacets(colId)} />
|
|
299
|
+
<span class="sv-grid-facet-label">(Select all)</span>
|
|
300
|
+
</label>
|
|
301
|
+
<!-- Windowed checklist. A high-cardinality column (10k distinct symbols
|
|
302
|
+
on a live feed) would otherwise mount a label + checkbox per value -
|
|
303
|
+
and 10k focusable checkboxes are 10k tab stops. SvListBox renders
|
|
304
|
+
only the visible rows and is one roving-tabindex stop. -->
|
|
305
|
+
{#if columnMenuFacetOptions.length}
|
|
306
|
+
<SvListBox
|
|
307
|
+
block
|
|
308
|
+
checkbox
|
|
309
|
+
multiple
|
|
310
|
+
virtual={columnMenuFacetOptions.length > FACET_VIRTUAL_THRESHOLD}
|
|
311
|
+
rows={7}
|
|
312
|
+
rowHeight={24}
|
|
313
|
+
size="sm"
|
|
314
|
+
ariaLabel="Filter values"
|
|
315
|
+
options={columnMenuFacetOptions}
|
|
316
|
+
value={columnMenuSelectedFacets}
|
|
317
|
+
onChange={(next) => setFacetSelection(colId, next as Set<string>)}
|
|
318
|
+
/>
|
|
319
|
+
{:else}
|
|
320
|
+
<div class="sv-grid-facet-empty">No values</div>
|
|
321
|
+
{/if}
|
|
322
|
+
{#if columnMenuFacetValues.length > columnMenuVisibleFacets.length}
|
|
323
|
+
<div class="sv-grid-facet-note">
|
|
324
|
+
Showing {columnMenuVisibleFacets.length} of {columnMenuFacetValues.length}
|
|
325
|
+
</div>
|
|
326
|
+
{/if}
|
|
327
|
+
<div class="sv-grid-menu-actions">
|
|
328
|
+
<button
|
|
329
|
+
type="button"
|
|
330
|
+
class="sv-grid-menu-btn"
|
|
331
|
+
disabled={!isColumnFiltered(colId)}
|
|
332
|
+
onclick={() => clearColumnFilter(colId)}
|
|
333
|
+
>
|
|
334
|
+
Clear filter
|
|
335
|
+
</button>
|
|
336
|
+
<button type="button" class="sv-grid-menu-btn sv-grid-menu-btn-primary" onclick={closeMenus}>
|
|
337
|
+
Done
|
|
338
|
+
</button>
|
|
339
|
+
</div>
|
|
340
|
+
</div>
|
|
341
|
+
{/snippet}
|
|
342
|
+
|
|
343
|
+
{#if columnMenuFor}
|
|
344
|
+
{@const menuColumnId = columnMenuFor}
|
|
345
|
+
{@const menuCol = allColumns.find((c) => c.id === menuColumnId)}
|
|
346
|
+
{@const menuCanSort = menuCol?.getCanSort?.() ?? false}
|
|
347
|
+
{@const menuCanFilter = (menuCol?.columnDef.field != null) && (menuCol?.columnDef.filterable !== false)}
|
|
348
|
+
{@const menuTab = ctrl.columnMenuTab}
|
|
349
|
+
<div
|
|
350
|
+
class="sv-grid-menu sv-grid-column-menu"
|
|
351
|
+
role="menu"
|
|
352
|
+
style={`left: ${columnMenuPos.x}px; top: ${columnMenuPos.y}px; max-height: calc(100vh - ${columnMenuPos.y}px - 12px);`}
|
|
353
|
+
>
|
|
354
|
+
{#if columnMenuTabsEnabled}
|
|
355
|
+
<div class="sv-grid-menu-tabs" role="tablist">
|
|
356
|
+
<button type="button" class="sv-grid-menu-tab" class:is-active={menuTab === "general"}
|
|
357
|
+
role="tab" aria-selected={menuTab === "general"}
|
|
358
|
+
onclick={() => (ctrl.columnMenuTab = "general")}>General</button>
|
|
359
|
+
{#if menuCanFilter && showColumnFiltersEffective}
|
|
360
|
+
<button type="button" class="sv-grid-menu-tab" class:is-active={menuTab === "filter"}
|
|
361
|
+
role="tab" aria-selected={menuTab === "filter"}
|
|
362
|
+
onclick={() => (ctrl.columnMenuTab = "filter")}>Filter</button>
|
|
363
|
+
{/if}
|
|
364
|
+
<button type="button" class="sv-grid-menu-tab" class:is-active={menuTab === "columns"}
|
|
365
|
+
role="tab" aria-selected={menuTab === "columns"}
|
|
366
|
+
onclick={() => (ctrl.columnMenuTab = "columns")}>Columns</button>
|
|
367
|
+
</div>
|
|
368
|
+
{/if}
|
|
369
|
+
{#if columnMenuTabsEnabled && menuTab === "filter" && menuCanFilter && showColumnFiltersEffective}
|
|
370
|
+
{@render filterPanelBody(menuColumnId)}
|
|
371
|
+
{:else if columnMenuTabsEnabled && menuTab === "columns"}
|
|
372
|
+
{@const tabColumns = grid.getAllColumns()}
|
|
373
|
+
<div class="sv-grid-menu-filter-head">Visible columns</div>
|
|
374
|
+
<SvListBox
|
|
375
|
+
block
|
|
376
|
+
checkbox
|
|
377
|
+
multiple
|
|
378
|
+
virtual={tabColumns.length > FACET_VIRTUAL_THRESHOLD}
|
|
379
|
+
rows={9}
|
|
380
|
+
rowHeight={24}
|
|
381
|
+
size="sm"
|
|
382
|
+
ariaLabel="Visible columns"
|
|
383
|
+
options={columnVisibilityOptions(tabColumns)}
|
|
384
|
+
value={visibleColumnIds(tabColumns)}
|
|
385
|
+
onChange={(next) => applyColumnVisibility(tabColumns, next as Set<string>)}
|
|
386
|
+
/>
|
|
387
|
+
{:else}
|
|
388
|
+
{#if menuCanSort}
|
|
389
|
+
<button
|
|
390
|
+
type="button"
|
|
391
|
+
class="sv-grid-menu-item"
|
|
392
|
+
role="menuitem"
|
|
393
|
+
onclick={() => sortColumnFromMenu(menuColumnId, false)}
|
|
394
|
+
>
|
|
395
|
+
<span class="sv-grid-header-icon">{@render icon("sort-asc")}</span> Sort
|
|
396
|
+
ascending
|
|
397
|
+
</button>
|
|
398
|
+
<button
|
|
399
|
+
type="button"
|
|
400
|
+
class="sv-grid-menu-item"
|
|
401
|
+
role="menuitem"
|
|
402
|
+
onclick={() => sortColumnFromMenu(menuColumnId, true)}
|
|
403
|
+
>
|
|
404
|
+
<span class="sv-grid-header-icon">{@render icon("sort-desc")}</span> Sort
|
|
405
|
+
descending
|
|
406
|
+
</button>
|
|
407
|
+
<button
|
|
408
|
+
type="button"
|
|
409
|
+
class="sv-grid-menu-item"
|
|
410
|
+
role="menuitem"
|
|
411
|
+
disabled={!sortDirectionByColumn[menuColumnId]}
|
|
412
|
+
onclick={() => clearColumnSort(menuColumnId)}
|
|
413
|
+
>
|
|
414
|
+
<span class="sv-grid-header-icon">{@render icon("x")}</span> Remove sort
|
|
415
|
+
</button>
|
|
416
|
+
{/if}
|
|
417
|
+
{#if !columnVirtualizationEnabled}
|
|
418
|
+
{@const menuPinSide = isColumnPinned(menuColumnId)}
|
|
419
|
+
<div class="sv-grid-menu-sep"></div>
|
|
420
|
+
<button
|
|
421
|
+
type="button"
|
|
422
|
+
class="sv-grid-menu-item"
|
|
423
|
+
role="menuitem"
|
|
424
|
+
disabled={menuPinSide === "left"}
|
|
425
|
+
onclick={() => pinColumnLeft(menuColumnId)}
|
|
426
|
+
>
|
|
427
|
+
<span class="sv-grid-header-icon"
|
|
428
|
+
>{@render icon("op-startsWith")}</span
|
|
429
|
+
> Pin to left
|
|
430
|
+
</button>
|
|
431
|
+
<button
|
|
432
|
+
type="button"
|
|
433
|
+
class="sv-grid-menu-item"
|
|
434
|
+
role="menuitem"
|
|
435
|
+
disabled={menuPinSide === "right"}
|
|
436
|
+
onclick={() => pinColumnRight(menuColumnId)}
|
|
437
|
+
>
|
|
438
|
+
<span class="sv-grid-header-icon"
|
|
439
|
+
>{@render icon("op-greaterThan")}</span
|
|
440
|
+
> Pin to right
|
|
441
|
+
</button>
|
|
442
|
+
<button
|
|
443
|
+
type="button"
|
|
444
|
+
class="sv-grid-menu-item"
|
|
445
|
+
role="menuitem"
|
|
446
|
+
disabled={!menuPinSide}
|
|
447
|
+
onclick={() => unpinColumn(menuColumnId)}
|
|
448
|
+
>
|
|
449
|
+
<span class="sv-grid-header-icon">{@render icon("x")}</span> Unpin column
|
|
450
|
+
</button>
|
|
451
|
+
{/if}
|
|
452
|
+
<div class="sv-grid-menu-sep"></div>
|
|
453
|
+
<button
|
|
454
|
+
type="button"
|
|
455
|
+
class="sv-grid-menu-item"
|
|
456
|
+
role="menuitem"
|
|
457
|
+
onclick={() => {
|
|
458
|
+
autosizeColumn(menuColumnId);
|
|
459
|
+
closeMenus();
|
|
460
|
+
}}
|
|
461
|
+
>
|
|
462
|
+
<span class="sv-grid-header-icon">{@render icon("autosize")}</span> Autosize
|
|
463
|
+
this column
|
|
464
|
+
</button>
|
|
465
|
+
<button
|
|
466
|
+
type="button"
|
|
467
|
+
class="sv-grid-menu-item"
|
|
468
|
+
role="menuitem"
|
|
469
|
+
onclick={() => {
|
|
470
|
+
autosizeAllColumns();
|
|
471
|
+
closeMenus();
|
|
472
|
+
}}
|
|
473
|
+
>
|
|
474
|
+
<span class="sv-grid-header-icon">{@render icon("autosize")}</span> Autosize
|
|
475
|
+
all columns
|
|
476
|
+
</button>
|
|
477
|
+
{#if groupingControlsEnabled}
|
|
478
|
+
<div class="sv-grid-menu-sep"></div>
|
|
479
|
+
<button
|
|
480
|
+
type="button"
|
|
481
|
+
class="sv-grid-menu-item"
|
|
482
|
+
role="menuitem"
|
|
483
|
+
disabled={groupingColumns.includes(menuColumnId)}
|
|
484
|
+
onclick={() => groupByColumnFromMenu(menuColumnId)}
|
|
485
|
+
>
|
|
486
|
+
<span class="sv-grid-header-icon">{@render icon("group")}</span> Group
|
|
487
|
+
by this column
|
|
488
|
+
</button>
|
|
489
|
+
<button
|
|
490
|
+
type="button"
|
|
491
|
+
class="sv-grid-menu-item"
|
|
492
|
+
role="menuitem"
|
|
493
|
+
disabled={!groupingColumns.includes(menuColumnId)}
|
|
494
|
+
onclick={() => clearGroupingFromMenu(menuColumnId)}
|
|
495
|
+
>
|
|
496
|
+
<span class="sv-grid-header-icon">{@render icon("x")}</span> Remove grouping
|
|
497
|
+
</button>
|
|
498
|
+
{/if}
|
|
499
|
+
<div class="sv-grid-menu-sep"></div>
|
|
500
|
+
{#if !columnMenuTabsEnabled}
|
|
501
|
+
<!-- Flat menu: reach the column chooser via a submenu (tabbed mode has
|
|
502
|
+
a Columns tab instead). -->
|
|
503
|
+
<button
|
|
504
|
+
type="button"
|
|
505
|
+
class="sv-grid-menu-item"
|
|
506
|
+
class:is-open={chooseColumnsPos !== null}
|
|
507
|
+
role="menuitem"
|
|
508
|
+
aria-haspopup="menu"
|
|
509
|
+
aria-expanded={chooseColumnsPos !== null}
|
|
510
|
+
onclick={(event) => openChooseColumns(event)}
|
|
511
|
+
>
|
|
512
|
+
<span class="sv-grid-header-icon">{@render icon("columns")}</span>
|
|
513
|
+
Choose columns
|
|
514
|
+
<span class="sv-grid-header-icon sv-grid-menu-item-chevron">{@render icon("chevron-down")}</span>
|
|
515
|
+
</button>
|
|
516
|
+
{/if}
|
|
517
|
+
<button
|
|
518
|
+
type="button"
|
|
519
|
+
class="sv-grid-menu-item"
|
|
520
|
+
role="menuitem"
|
|
521
|
+
onclick={() => {
|
|
522
|
+
resetColumns();
|
|
523
|
+
closeMenus();
|
|
524
|
+
}}
|
|
525
|
+
>
|
|
526
|
+
<span class="sv-grid-header-icon">{@render icon("reset")}</span> Reset columns
|
|
527
|
+
</button>
|
|
528
|
+
{/if}
|
|
529
|
+
</div>
|
|
530
|
+
{/if}
|
|
531
|
+
|
|
532
|
+
{#if filterMenuFor && showColumnFiltersEffective}
|
|
533
|
+
<div
|
|
534
|
+
class="sv-grid-menu sv-grid-filter-menu"
|
|
535
|
+
role="menu"
|
|
536
|
+
style={`left: ${filterMenuPos.x}px; top: ${filterMenuPos.y}px; max-height: calc(100vh - ${filterMenuPos.y}px - 12px);`}
|
|
537
|
+
>
|
|
538
|
+
{@render filterPanelBody(filterMenuFor)}
|
|
539
|
+
</div>
|
|
540
|
+
{/if}
|
|
541
|
+
|
|
542
|
+
{#if chooseColumnsPos}
|
|
543
|
+
{@const everyColumn = grid.getAllColumns()}
|
|
544
|
+
<div
|
|
545
|
+
class="sv-grid-menu sv-grid-choose-columns-menu"
|
|
546
|
+
role="menu"
|
|
547
|
+
style={`left: ${chooseColumnsPos.x}px; top: ${chooseColumnsPos.y}px;`}
|
|
548
|
+
>
|
|
549
|
+
<div class="sv-grid-menu-filter-head">Visible columns</div>
|
|
550
|
+
<SvListBox
|
|
551
|
+
block
|
|
552
|
+
checkbox
|
|
553
|
+
multiple
|
|
554
|
+
virtual={everyColumn.length > FACET_VIRTUAL_THRESHOLD}
|
|
555
|
+
rows={11}
|
|
556
|
+
rowHeight={24}
|
|
557
|
+
size="sm"
|
|
558
|
+
ariaLabel="Visible columns"
|
|
559
|
+
options={columnVisibilityOptions(everyColumn)}
|
|
560
|
+
value={visibleColumnIds(everyColumn)}
|
|
561
|
+
onChange={(next) => applyColumnVisibility(everyColumn, next as Set<string>)}
|
|
562
|
+
/>
|
|
563
|
+
</div>
|
|
564
|
+
{/if}
|
|
565
|
+
|
|
566
|
+
{#if operatorMenuFor}
|
|
567
|
+
{@const opColumnId = operatorMenuFor}
|
|
568
|
+
{@const opColumn = allColumns.find((c) => c.id === opColumnId)}
|
|
569
|
+
{@const opOptions = operatorsForColumn(opColumn)}
|
|
570
|
+
{@const opActive =
|
|
571
|
+
filterMenuValues[opColumnId]?.operator ?? defaultOperatorFor(opColumn)}
|
|
572
|
+
<div
|
|
573
|
+
class="sv-grid-menu sv-grid-operator-menu"
|
|
574
|
+
role="menu"
|
|
575
|
+
style={`left: ${operatorMenuPos.x}px; top: ${operatorMenuPos.y}px;`}
|
|
576
|
+
>
|
|
577
|
+
{#each opOptions as option (option.value)}
|
|
578
|
+
<button
|
|
579
|
+
type="button"
|
|
580
|
+
class="sv-grid-menu-item"
|
|
581
|
+
role="menuitemradio"
|
|
582
|
+
aria-checked={opActive === option.value}
|
|
583
|
+
onclick={() => {
|
|
584
|
+
updateFilterOperator(opColumnId, option.value);
|
|
585
|
+
closeMenus();
|
|
586
|
+
}}
|
|
587
|
+
>
|
|
588
|
+
<span class="sv-grid-header-icon"
|
|
589
|
+
>{@render icon(option.iconName)}</span
|
|
590
|
+
>
|
|
591
|
+
{operatorLabelFor(option, opColumn)}
|
|
592
|
+
</button>
|
|
593
|
+
{/each}
|
|
594
|
+
</div>
|
|
595
|
+
{/if}
|
|
596
|
+
|
|
597
|
+
{#if inSuggestFor}
|
|
598
|
+
{@const suggestColId = inSuggestFor}
|
|
599
|
+
{@const selectedTokens = new Set<string | number>(
|
|
600
|
+
splitInTokens(filterRowValues[suggestColId] ?? ""),
|
|
601
|
+
)}
|
|
602
|
+
<!-- Value suggestions for an `in` / `notIn` input. The wrapper swallows
|
|
603
|
+
`mousedown` so clicking a row keeps focus in the input and the
|
|
604
|
+
dropdown stays open for multi-select; the input's own blur handler
|
|
605
|
+
closes it when focus truly leaves. The list still picks on `click`.
|
|
606
|
+
Tokens the query is currently hiding ride along in the set, so a
|
|
607
|
+
click never silently drops them. -->
|
|
608
|
+
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
|
609
|
+
<div
|
|
610
|
+
class="sv-grid-menu sv-grid-in-suggest"
|
|
611
|
+
style={`left: ${inSuggestPos.x}px; top: ${inSuggestPos.y}px;`}
|
|
612
|
+
onmousedown={(event) => event.preventDefault()}
|
|
613
|
+
>
|
|
614
|
+
{#if inSuggestOptions.length}
|
|
615
|
+
<SvListBox
|
|
616
|
+
block
|
|
617
|
+
checkbox
|
|
618
|
+
multiple
|
|
619
|
+
virtual={inSuggestOptions.length > FACET_VIRTUAL_THRESHOLD}
|
|
620
|
+
rows={8}
|
|
621
|
+
rowHeight={26}
|
|
622
|
+
size="sm"
|
|
623
|
+
ariaLabel="Filter values"
|
|
624
|
+
options={inSuggestOptions}
|
|
625
|
+
value={selectedTokens}
|
|
626
|
+
onChange={(next) => {
|
|
627
|
+
const picked = new Set(next as Set<string>);
|
|
628
|
+
// Boxes that hold the whole list ("AAPL, MS") leave a half-typed
|
|
629
|
+
// fragment in the token set. The pick REPLACES that fragment
|
|
630
|
+
// rather than landing beside it as a stray filter value. The chip
|
|
631
|
+
// input keeps its fragment out of `filterRowValues`, so this is a
|
|
632
|
+
// no-op there.
|
|
633
|
+
const fragment = trailingInToken(filterRowValues[suggestColId] ?? "");
|
|
634
|
+
if (fragment) picked.delete(fragment);
|
|
635
|
+
setFilterTokens(suggestColId, [...picked]);
|
|
636
|
+
ctrl.inSuggestQuery = "";
|
|
637
|
+
}}
|
|
638
|
+
/>
|
|
639
|
+
{:else}
|
|
640
|
+
<div class="sv-grid-facet-empty">No values</div>
|
|
641
|
+
{/if}
|
|
642
|
+
</div>
|
|
643
|
+
{/if}
|
|
644
|
+
|
|
645
|
+
{#if contextMenuFor}
|
|
646
|
+
<div
|
|
647
|
+
class="sv-grid-menu-backdrop"
|
|
648
|
+
role="presentation"
|
|
649
|
+
oncontextmenu={(e) => { e.preventDefault(); closeContextMenu(); }}
|
|
650
|
+
onclick={closeContextMenu}
|
|
651
|
+
></div>
|
|
652
|
+
<div
|
|
653
|
+
class="sv-grid-menu sv-grid-context-menu"
|
|
654
|
+
role="menu"
|
|
655
|
+
style={`left: ${contextMenuPos.x}px; top: ${contextMenuPos.y}px;`}
|
|
656
|
+
>
|
|
657
|
+
{#each contextMenuItems() as item (item.key)}
|
|
658
|
+
{#if item.separator}
|
|
659
|
+
<div class="sv-grid-menu-sep"></div>
|
|
660
|
+
{:else}
|
|
661
|
+
<button
|
|
662
|
+
type="button"
|
|
663
|
+
class="sv-grid-menu-item"
|
|
664
|
+
role="menuitem"
|
|
665
|
+
disabled={item.disabled}
|
|
666
|
+
onclick={() => { item.run?.(); closeContextMenu(); }}
|
|
667
|
+
>{item.label}</button>
|
|
668
|
+
{/if}
|
|
669
|
+
{/each}
|
|
670
|
+
</div>
|
|
671
|
+
{/if}
|
|
672
|
+
|
|
673
|
+
{#if commentEditFor}
|
|
674
|
+
<div
|
|
675
|
+
class="sv-grid-menu-backdrop"
|
|
676
|
+
role="presentation"
|
|
677
|
+
onclick={closeCommentEditor}
|
|
678
|
+
></div>
|
|
679
|
+
<div
|
|
680
|
+
class="sv-grid-menu sv-grid-comment-editor"
|
|
681
|
+
role="dialog"
|
|
682
|
+
aria-label="Edit comment"
|
|
683
|
+
style={`left: ${commentEditFor.x}px; top: ${commentEditFor.y}px;`}
|
|
684
|
+
>
|
|
685
|
+
<!-- svelte-ignore a11y_autofocus -->
|
|
686
|
+
<textarea
|
|
687
|
+
class="sv-grid-comment-textarea"
|
|
688
|
+
rows="3"
|
|
689
|
+
placeholder="Add a comment…"
|
|
690
|
+
autofocus
|
|
691
|
+
bind:value={ctrl.commentDraft}
|
|
692
|
+
onkeydown={(e) => {
|
|
693
|
+
e.stopPropagation();
|
|
694
|
+
if (e.key === "Escape") { e.preventDefault(); closeCommentEditor(); }
|
|
695
|
+
if (e.key === "Enter" && (e.ctrlKey || e.metaKey)) { e.preventDefault(); saveComment(); }
|
|
696
|
+
}}
|
|
697
|
+
></textarea>
|
|
698
|
+
<div class="sv-grid-comment-actions">
|
|
699
|
+
<button type="button" class="sv-grid-comment-remove" onclick={removeComment}>Remove</button>
|
|
700
|
+
<span class="sv-grid-comment-spacer"></span>
|
|
701
|
+
<button type="button" class="sv-grid-comment-cancel" onclick={closeCommentEditor}>Cancel</button>
|
|
702
|
+
<button type="button" class="sv-grid-comment-save" onclick={saveComment}>Save</button>
|
|
703
|
+
</div>
|
|
704
|
+
</div>
|
|
705
|
+
{/if}
|