@svgrid/grid 2.2.27 → 2.2.29
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 +179 -179
- package/dist/GridMenus.svelte +782 -734
- package/dist/SvAutoComplete.svelte +165 -165
- package/dist/SvCalendar.svelte +503 -503
- package/dist/SvCarousel.svelte +141 -141
- package/dist/SvCheckBox.svelte +101 -101
- 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 -250
- 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.controller.svelte.d.ts +14 -1
- package/dist/SvGrid.controller.svelte.js +112 -28
- package/dist/SvGrid.css +2704 -2723
- package/dist/SvGrid.svelte +3361 -3333
- package/dist/SvGrid.types.d.ts +20 -5
- package/dist/SvGridChart.svelte +1724 -1724
- package/dist/SvGridChartPanel.svelte +485 -485
- package/dist/SvGridChartView.svelte +70 -70
- package/dist/SvGridDropdown.svelte +696 -696
- package/dist/SvGridSelect.svelte +259 -259
- package/dist/SvGroupCell.svelte +109 -109
- package/dist/SvListBox.svelte +326 -287
- package/dist/SvListBox.svelte.d.ts +10 -1
- package/dist/SvMaskedInput.svelte +122 -122
- package/dist/SvMenu.svelte +124 -124
- package/dist/SvMenuList.svelte +146 -146
- package/dist/SvMultiSelect.svelte +281 -281
- package/dist/SvNumberInput.svelte +168 -168
- package/dist/SvOtpInput.svelte +158 -158
- package/dist/SvPasswordInput.svelte +151 -151
- package/dist/SvPhoneInput.svelte +133 -131
- package/dist/SvPopover.svelte +197 -197
- package/dist/SvRadioGroup.svelte +107 -107
- package/dist/SvRowGroupPanel.svelte +170 -170
- package/dist/SvScrollArea.svelte +61 -61
- package/dist/SvSlider.svelte +200 -200
- package/dist/SvSwitchButton.svelte +107 -107
- 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 +204 -204
- package/dist/SvTree.svelte +437 -437
- package/dist/SvTreeSelect.svelte +233 -233
- package/dist/cdn/GridMenus-Da_p7SKS.js +486 -0
- package/dist/cdn/GridMenus-Ds3OpzZT.js +490 -0
- package/dist/cdn/{SvDateTimePicker-B1sDc9Pz.js → SvDateTimePicker-B8GopjhC.js} +2 -2
- package/dist/cdn/{SvDateTimePicker-Bh0FTs1E.js → SvDateTimePicker-DB0aIEk8.js} +2 -2
- package/dist/cdn/{SvGridChart-Bgk4hDs8.js → SvGridChart-BcLCYvw9.js} +1 -1
- package/dist/cdn/{SvGridChart-BKQsO5fZ.js → SvGridChart-BhnFdLd0.js} +1 -1
- package/dist/cdn/{SvGridChartPanel-DkCc2Z7b.js → SvGridChartPanel-Cmx9DHAS.js} +1 -1
- package/dist/cdn/{SvGridChartPanel-DSkHIQDd.js → SvGridChartPanel-DvjwxLvK.js} +1 -1
- package/dist/cdn/{SvGridChartView-BrbVaELm.js → SvGridChartView-B40DTJSX.js} +1 -1
- package/dist/cdn/{SvGridChartView-i9rWvSKg.js → SvGridChartView-PwqI4s02.js} +1 -1
- package/dist/cdn/{SvGridDropdown-D08sVmAy.js → SvGridDropdown-DjALc_4f.js} +1 -1
- package/dist/cdn/{SvGridDropdown-CGxxopQ4.js → SvGridDropdown-hpnHvOGt.js} +1 -1
- package/dist/cdn/{src-cgs1wcUS.js → src-BLJfdyL0.js} +4034 -3889
- package/dist/cdn/{src-Cw8d9U8j.js → src-DYDCiC0D.js} +7053 -6908
- package/dist/cdn/svgrid.js +9 -9
- package/dist/cdn/svgrid.svelte-external.js +9 -9
- package/dist/chart-export.js +8 -8
- package/dist/column-groups.js +8 -10
- package/dist/column-id.d.ts +9 -0
- package/dist/column-id.js +25 -0
- package/dist/core.js +2 -1
- package/dist/createListbox.svelte.d.ts +15 -2
- package/dist/createListbox.svelte.js +27 -6
- package/dist/editing.d.ts +1 -0
- package/dist/editing.js +45 -10
- package/dist/filtering/excel-filters.d.ts +30 -10
- package/dist/filtering/excel-filters.js +96 -15
- package/dist/index.d.ts +2 -2
- package/dist/index.js +2 -2
- package/dist/menus.d.ts +2 -0
- package/dist/menus.js +25 -0
- package/dist/row-resize.js +40 -0
- package/dist/selection.js +1 -1
- package/package.json +10 -10
- package/src/FlexRender.svelte +96 -96
- package/src/GridFooter.svelte +179 -179
- package/src/GridMenus.svelte +782 -734
- package/src/SvAutoComplete.svelte +165 -165
- package/src/SvCalendar.svelte +503 -503
- package/src/SvCalendar.test.ts +226 -226
- package/src/SvCarousel.svelte +141 -141
- package/src/SvCheckBox.svelte +101 -101
- 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 -250
- package/src/SvDrawer.test.ts +18 -1
- 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 +3335 -3239
- package/src/SvGrid.css +2704 -2723
- package/src/SvGrid.svelte +3361 -3333
- package/src/SvGrid.types.ts +1628 -1613
- package/src/SvGridChart.svelte +1724 -1724
- package/src/SvGridChartPanel.svelte +485 -485
- package/src/SvGridChartView.svelte +70 -70
- package/src/SvGridDropdown.svelte +696 -696
- package/src/SvGridSelect.svelte +259 -259
- package/src/SvGroupCell.svelte +109 -109
- package/src/SvListBox.svelte +326 -287
- 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 +281 -281
- package/src/SvNavPane.test.ts +15 -4
- package/src/SvNumberInput.svelte +168 -168
- package/src/SvOtpInput.svelte +158 -158
- package/src/SvPasswordInput.svelte +151 -151
- package/src/SvPhoneInput.svelte +133 -131
- package/src/SvPopover.svelte +197 -197
- package/src/SvRadioGroup.svelte +107 -107
- package/src/SvRowGroupPanel.svelte +170 -170
- package/src/SvScrollArea.svelte +61 -61
- package/src/SvSlider.svelte +200 -200
- package/src/SvSwitchButton.svelte +107 -107
- 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 +204 -204
- package/src/SvTree.svelte +437 -437
- package/src/SvTreeSelect.svelte +233 -233
- 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 +895 -895
- package/src/builtin-editors.grid.test.ts +85 -85
- package/src/cell-formatting.ts +169 -169
- package/src/cell-render.ts +469 -469
- package/src/chart-export.ts +201 -201
- package/src/chart-view.svelte.ts +36 -36
- package/src/chart.ts +2296 -2296
- package/src/collaboration.test.ts +104 -104
- package/src/collaboration.ts +167 -167
- package/src/column-groups.test.ts +48 -0
- package/src/column-groups.ts +8 -10
- package/src/column-id.ts +30 -0
- package/src/core.performance.test.ts +30 -30
- package/src/core.ts +1112 -1111
- 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 +39 -7
- package/src/createMenu.svelte.ts +225 -225
- package/src/createPopoverSelect.svelte.ts +213 -213
- package/src/createSlider.svelte.ts +191 -191
- package/src/createTooltip.svelte.ts +144 -144
- package/src/createTree.svelte.ts +319 -319
- package/src/editing.test.ts +949 -859
- package/src/editing.ts +709 -675
- package/src/editor-contract.ts +157 -157
- 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/filter-operators.ts +157 -157
- package/src/filtering/excel-filters.test.ts +44 -1
- package/src/filtering/excel-filters.ts +91 -14
- package/src/flex-render.ts +3 -3
- package/src/form-field.ts +127 -127
- package/src/index.ts +715 -713
- package/src/js-scroller.svelte.ts +173 -173
- package/src/keyboard.test.ts +59 -59
- package/src/keyboard.ts +97 -97
- package/src/list-nav.test.ts +49 -49
- package/src/list-nav.ts +29 -29
- package/src/list-option.test.ts +56 -56
- package/src/list-option.ts +172 -172
- package/src/menus.test.ts +736 -700
- package/src/menus.ts +679 -652
- package/src/merge-objects.ts +48 -48
- package/src/overlays.test.ts +90 -90
- package/src/render-component.ts +28 -28
- package/src/row-resize.test.ts +100 -0
- package/src/row-resize.ts +36 -0
- 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 +600 -600
- 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 +489 -489
- 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 +524 -524
- package/src/svgrid.api-extensions.test.ts +8 -3
- package/src/svgrid.behavior.test.ts +706 -706
- package/src/svgrid.cell-dom-ids.test.ts +96 -0
- package/src/svgrid.charting.test.ts +534 -534
- package/src/svgrid.conditional-stat-scope.test.ts +111 -0
- package/src/svgrid.features.test.ts +157 -157
- package/src/svgrid.filter-menu-listbox.svelte.test.ts +308 -0
- package/src/svgrid.filter-menu-scroll.test.ts +3 -1
- package/src/svgrid.group-header-ids.test.ts +121 -0
- package/src/svgrid.new-features.wrapper.test.ts +251 -251
- package/src/svgrid.wrapper.test.ts +40 -40
- package/src/test-setup.ts +62 -62
- package/src/themes/index.ts +215 -215
- package/src/toast-store.svelte.ts +246 -246
- package/src/toast-store.test.ts +147 -147
- package/src/ui-buttons.test.ts +144 -144
- package/src/ui-inputs.test.ts +118 -118
- 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/ember.css +71 -71
- package/dist/cdn/GridMenus-BzxEYNtW.js +0 -421
- package/dist/cdn/GridMenus-Dr-nGEXn.js +0 -417
|
@@ -1,191 +1,191 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* createSlider - the HEADLESS core behind <SvSlider>, in the same spirit as
|
|
3
|
-
* `createSvGrid` for the grid: a runes-based state machine (single + dual-thumb,
|
|
4
|
-
* stepping, keyboard, drag state) that owns the value<->position math and the
|
|
5
|
-
* ARIA wiring, exposed as **prop-getters** you spread onto YOUR OWN markup.
|
|
6
|
-
*
|
|
7
|
-
* The core never touches the DOM: the styled renderer measures the track
|
|
8
|
-
* (`getBoundingClientRect`) and passes the rect + pointer position IN via
|
|
9
|
-
* `pointerDown` / `setFromPointer`. The core turns that into a stepped value.
|
|
10
|
-
*
|
|
11
|
-
* ```svelte
|
|
12
|
-
* <script lang="ts">
|
|
13
|
-
* import { createSlider } from '@svgrid/grid'
|
|
14
|
-
* let value = $state(50)
|
|
15
|
-
* const s = createSlider({ value: () => value, onChange: (v) => (value = v as number) })
|
|
16
|
-
* </script>
|
|
17
|
-
* <div bind:this={track} onpointerdown={(e) => s.pointerDown({ x: e.clientX, y: e.clientY }, track.getBoundingClientRect())}>
|
|
18
|
-
* <div {...s.thumbProps(0)} style:left={s.percentOf(s.hi) + '%'}></div>
|
|
19
|
-
* </div>
|
|
20
|
-
* ```
|
|
21
|
-
*/
|
|
22
|
-
import { editorAria } from './editor-contract'
|
|
23
|
-
|
|
24
|
-
export type SliderValue = number | [number, number]
|
|
25
|
-
export type SliderOrientation = 'horizontal' | 'vertical'
|
|
26
|
-
export type SliderThumb = 'single' | 'lo' | 'hi'
|
|
27
|
-
export type SliderPoint = { x: number; y: number }
|
|
28
|
-
|
|
29
|
-
/** Reactive inputs are passed as getters so the core tracks live prop changes. */
|
|
30
|
-
export type SliderConfig = {
|
|
31
|
-
value: () => SliderValue
|
|
32
|
-
onChange?: (value: SliderValue) => void
|
|
33
|
-
min?: () => number
|
|
34
|
-
max?: () => number
|
|
35
|
-
step?: () => number
|
|
36
|
-
range?: () => boolean
|
|
37
|
-
orientation?: () => SliderOrientation
|
|
38
|
-
disabled?: () => boolean
|
|
39
|
-
ariaLabel?: () => string | undefined
|
|
40
|
-
/** Right-to-left: mirrors horizontal pointer + Left/Right key direction. */
|
|
41
|
-
rtl?: () => boolean
|
|
42
|
-
// Editor contract (validation ARIA) - folded into thumbProps.
|
|
43
|
-
id?: () => string | undefined
|
|
44
|
-
invalid?: () => boolean | undefined
|
|
45
|
-
required?: () => boolean | undefined
|
|
46
|
-
error?: () => string | undefined
|
|
47
|
-
hint?: () => string | undefined
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
export function createSlider(config: SliderConfig) {
|
|
51
|
-
const min = () => config.min?.() ?? 0
|
|
52
|
-
const max = () => config.max?.() ?? 100
|
|
53
|
-
const stepSize = () => config.step?.() ?? 1
|
|
54
|
-
const range = () => config.range?.() ?? false
|
|
55
|
-
const isVert = () => (config.orientation?.() ?? 'horizontal') === 'vertical'
|
|
56
|
-
const disabled = () => config.disabled?.() ?? false
|
|
57
|
-
const rtl = () => config.rtl?.() ?? false
|
|
58
|
-
|
|
59
|
-
const lo = $derived(range() ? (config.value() as [number, number])[0] : min())
|
|
60
|
-
const hi = $derived(range() ? (config.value() as [number, number])[1] : (config.value() as number))
|
|
61
|
-
|
|
62
|
-
function clampStep(v: number): number {
|
|
63
|
-
const stepped = Math.round((v - min()) / stepSize()) * stepSize() + min()
|
|
64
|
-
return Math.min(max(), Math.max(min(), Number(stepped.toFixed(10))))
|
|
65
|
-
}
|
|
66
|
-
/** Value -> percentage along the track (0..100). */
|
|
67
|
-
const percentOf = (v: number) => ((v - min()) / (max() - min())) * 100
|
|
68
|
-
|
|
69
|
-
let dragging = $state<SliderThumb | null>(null)
|
|
70
|
-
|
|
71
|
-
/** Turn a pointer position (client coords) + measured track rect into a value. */
|
|
72
|
-
function valueFromPointer(pos: SliderPoint, rect: DOMRect): number {
|
|
73
|
-
const horiz = (pos.x - rect.left) / rect.width
|
|
74
|
-
const frac = isVert()
|
|
75
|
-
? 1 - (pos.y - rect.top) / rect.height
|
|
76
|
-
: rtl() ? 1 - horiz : horiz
|
|
77
|
-
return clampStep(min() + Math.min(1, Math.max(0, frac)) * (max() - min()))
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
const emitSingle = (v: number) => config.onChange?.(v)
|
|
81
|
-
const emitRange = (nlo: number, nhi: number) => config.onChange?.([Math.min(nlo, nhi), Math.max(nlo, nhi)])
|
|
82
|
-
|
|
83
|
-
function nudge(which: SliderThumb, next: number) {
|
|
84
|
-
if (which === 'single') emitSingle(next)
|
|
85
|
-
else if (which === 'lo') emitRange(next, hi)
|
|
86
|
-
else emitRange(lo, next)
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
/** Begin a drag: pick the nearest thumb (range) and emit the initial value. */
|
|
90
|
-
function pointerDown(pos: SliderPoint, rect: DOMRect) {
|
|
91
|
-
if (disabled()) return
|
|
92
|
-
const v = valueFromPointer(pos, rect)
|
|
93
|
-
if (range()) {
|
|
94
|
-
const which: SliderThumb = Math.abs(v - lo) <= Math.abs(v - hi) ? 'lo' : 'hi'
|
|
95
|
-
dragging = which
|
|
96
|
-
which === 'lo' ? emitRange(v, hi) : emitRange(lo, v)
|
|
97
|
-
} else {
|
|
98
|
-
dragging = 'single'
|
|
99
|
-
emitSingle(v)
|
|
100
|
-
}
|
|
101
|
-
}
|
|
102
|
-
/** Continue a drag: emit the value for the currently-dragged thumb. */
|
|
103
|
-
function setFromPointer(pos: SliderPoint, rect: DOMRect) {
|
|
104
|
-
if (!dragging) return
|
|
105
|
-
nudge(dragging, valueFromPointer(pos, rect))
|
|
106
|
-
}
|
|
107
|
-
function endDrag() { dragging = null }
|
|
108
|
-
|
|
109
|
-
/** Step a thumb by `delta` steps (used for custom controls). */
|
|
110
|
-
function step(which: SliderThumb, delta: number) {
|
|
111
|
-
if (disabled()) return
|
|
112
|
-
const cur = which === 'lo' ? lo : hi
|
|
113
|
-
nudge(which, clampStep(cur + delta * stepSize()))
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
function onThumbKey(e: KeyboardEvent, which: SliderThumb) {
|
|
117
|
-
if (disabled()) return
|
|
118
|
-
const cur = which === 'lo' ? lo : hi
|
|
119
|
-
// Under horizontal RTL, Left increases and Right decreases (Up/Down keep
|
|
120
|
-
// their natural up=more direction regardless of text direction).
|
|
121
|
-
const rtlH = rtl() && !isVert()
|
|
122
|
-
const inc = clampStep(cur + stepSize())
|
|
123
|
-
const dec = clampStep(cur - stepSize())
|
|
124
|
-
let next = cur
|
|
125
|
-
if (e.key === 'ArrowUp') next = inc
|
|
126
|
-
else if (e.key === 'ArrowDown') next = dec
|
|
127
|
-
else if (e.key === 'ArrowRight') next = rtlH ? dec : inc
|
|
128
|
-
else if (e.key === 'ArrowLeft') next = rtlH ? inc : dec
|
|
129
|
-
else if (e.key === 'Home') next = min()
|
|
130
|
-
else if (e.key === 'End') next = max()
|
|
131
|
-
else if (e.key === 'PageUp') next = clampStep(cur + stepSize() * 10)
|
|
132
|
-
else if (e.key === 'PageDown') next = clampStep(cur - stepSize() * 10)
|
|
133
|
-
else return
|
|
134
|
-
e.preventDefault()
|
|
135
|
-
nudge(which, next)
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
const thumbOf = (i: number): SliderThumb => (range() ? (i === 0 ? 'lo' : 'hi') : 'single')
|
|
139
|
-
|
|
140
|
-
return {
|
|
141
|
-
get value() { return config.value() },
|
|
142
|
-
get lo() { return lo },
|
|
143
|
-
get hi() { return hi },
|
|
144
|
-
/** Thumb currently being dragged, or null. */
|
|
145
|
-
get dragging() { return dragging },
|
|
146
|
-
percentOf,
|
|
147
|
-
clampStep,
|
|
148
|
-
valueFromPointer,
|
|
149
|
-
pointerDown,
|
|
150
|
-
setFromPointer,
|
|
151
|
-
endDrag,
|
|
152
|
-
step,
|
|
153
|
-
/** Spread onto the track container (static a11y/data hooks). */
|
|
154
|
-
trackProps: () => ({
|
|
155
|
-
'data-orientation': isVert() ? 'vertical' : 'horizontal',
|
|
156
|
-
}),
|
|
157
|
-
/** Spread onto the thumb at index `i` (0 = single/low, 1 = high). */
|
|
158
|
-
thumbProps: (i: number) => {
|
|
159
|
-
const which = thumbOf(i)
|
|
160
|
-
const cur = which === 'lo' ? lo : hi
|
|
161
|
-
const vmin = which === 'hi' ? lo : min()
|
|
162
|
-
const vmax = which === 'lo' ? hi : max()
|
|
163
|
-
const label = which === 'lo' ? 'Minimum' : which === 'hi' ? 'Maximum' : (config.ariaLabel?.() ?? 'Value')
|
|
164
|
-
// Validation ARIA (invalid / required / describedby) from the editor
|
|
165
|
-
// contract; keep the thumb's own value-oriented aria-label.
|
|
166
|
-
const va = editorAria({
|
|
167
|
-
id: config.id?.(),
|
|
168
|
-
invalid: config.invalid?.(),
|
|
169
|
-
required: config.required?.(),
|
|
170
|
-
error: config.error?.(),
|
|
171
|
-
hint: config.hint?.(),
|
|
172
|
-
})
|
|
173
|
-
return {
|
|
174
|
-
role: 'slider' as const,
|
|
175
|
-
tabindex: disabled() ? -1 : 0,
|
|
176
|
-
'aria-valuemin': vmin,
|
|
177
|
-
'aria-valuemax': vmax,
|
|
178
|
-
'aria-valuenow': cur,
|
|
179
|
-
'aria-label': label,
|
|
180
|
-
'aria-orientation': (isVert() ? 'vertical' : 'horizontal') as SliderOrientation,
|
|
181
|
-
'aria-disabled': disabled() || undefined,
|
|
182
|
-
'aria-invalid': va['aria-invalid'],
|
|
183
|
-
'aria-required': va['aria-required'],
|
|
184
|
-
'aria-describedby': va['aria-describedby'],
|
|
185
|
-
onkeydown: (e: KeyboardEvent) => onThumbKey(e, which),
|
|
186
|
-
}
|
|
187
|
-
},
|
|
188
|
-
}
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
export type Slider = ReturnType<typeof createSlider>
|
|
1
|
+
/**
|
|
2
|
+
* createSlider - the HEADLESS core behind <SvSlider>, in the same spirit as
|
|
3
|
+
* `createSvGrid` for the grid: a runes-based state machine (single + dual-thumb,
|
|
4
|
+
* stepping, keyboard, drag state) that owns the value<->position math and the
|
|
5
|
+
* ARIA wiring, exposed as **prop-getters** you spread onto YOUR OWN markup.
|
|
6
|
+
*
|
|
7
|
+
* The core never touches the DOM: the styled renderer measures the track
|
|
8
|
+
* (`getBoundingClientRect`) and passes the rect + pointer position IN via
|
|
9
|
+
* `pointerDown` / `setFromPointer`. The core turns that into a stepped value.
|
|
10
|
+
*
|
|
11
|
+
* ```svelte
|
|
12
|
+
* <script lang="ts">
|
|
13
|
+
* import { createSlider } from '@svgrid/grid'
|
|
14
|
+
* let value = $state(50)
|
|
15
|
+
* const s = createSlider({ value: () => value, onChange: (v) => (value = v as number) })
|
|
16
|
+
* </script>
|
|
17
|
+
* <div bind:this={track} onpointerdown={(e) => s.pointerDown({ x: e.clientX, y: e.clientY }, track.getBoundingClientRect())}>
|
|
18
|
+
* <div {...s.thumbProps(0)} style:left={s.percentOf(s.hi) + '%'}></div>
|
|
19
|
+
* </div>
|
|
20
|
+
* ```
|
|
21
|
+
*/
|
|
22
|
+
import { editorAria } from './editor-contract'
|
|
23
|
+
|
|
24
|
+
export type SliderValue = number | [number, number]
|
|
25
|
+
export type SliderOrientation = 'horizontal' | 'vertical'
|
|
26
|
+
export type SliderThumb = 'single' | 'lo' | 'hi'
|
|
27
|
+
export type SliderPoint = { x: number; y: number }
|
|
28
|
+
|
|
29
|
+
/** Reactive inputs are passed as getters so the core tracks live prop changes. */
|
|
30
|
+
export type SliderConfig = {
|
|
31
|
+
value: () => SliderValue
|
|
32
|
+
onChange?: (value: SliderValue) => void
|
|
33
|
+
min?: () => number
|
|
34
|
+
max?: () => number
|
|
35
|
+
step?: () => number
|
|
36
|
+
range?: () => boolean
|
|
37
|
+
orientation?: () => SliderOrientation
|
|
38
|
+
disabled?: () => boolean
|
|
39
|
+
ariaLabel?: () => string | undefined
|
|
40
|
+
/** Right-to-left: mirrors horizontal pointer + Left/Right key direction. */
|
|
41
|
+
rtl?: () => boolean
|
|
42
|
+
// Editor contract (validation ARIA) - folded into thumbProps.
|
|
43
|
+
id?: () => string | undefined
|
|
44
|
+
invalid?: () => boolean | undefined
|
|
45
|
+
required?: () => boolean | undefined
|
|
46
|
+
error?: () => string | undefined
|
|
47
|
+
hint?: () => string | undefined
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function createSlider(config: SliderConfig) {
|
|
51
|
+
const min = () => config.min?.() ?? 0
|
|
52
|
+
const max = () => config.max?.() ?? 100
|
|
53
|
+
const stepSize = () => config.step?.() ?? 1
|
|
54
|
+
const range = () => config.range?.() ?? false
|
|
55
|
+
const isVert = () => (config.orientation?.() ?? 'horizontal') === 'vertical'
|
|
56
|
+
const disabled = () => config.disabled?.() ?? false
|
|
57
|
+
const rtl = () => config.rtl?.() ?? false
|
|
58
|
+
|
|
59
|
+
const lo = $derived(range() ? (config.value() as [number, number])[0] : min())
|
|
60
|
+
const hi = $derived(range() ? (config.value() as [number, number])[1] : (config.value() as number))
|
|
61
|
+
|
|
62
|
+
function clampStep(v: number): number {
|
|
63
|
+
const stepped = Math.round((v - min()) / stepSize()) * stepSize() + min()
|
|
64
|
+
return Math.min(max(), Math.max(min(), Number(stepped.toFixed(10))))
|
|
65
|
+
}
|
|
66
|
+
/** Value -> percentage along the track (0..100). */
|
|
67
|
+
const percentOf = (v: number) => ((v - min()) / (max() - min())) * 100
|
|
68
|
+
|
|
69
|
+
let dragging = $state<SliderThumb | null>(null)
|
|
70
|
+
|
|
71
|
+
/** Turn a pointer position (client coords) + measured track rect into a value. */
|
|
72
|
+
function valueFromPointer(pos: SliderPoint, rect: DOMRect): number {
|
|
73
|
+
const horiz = (pos.x - rect.left) / rect.width
|
|
74
|
+
const frac = isVert()
|
|
75
|
+
? 1 - (pos.y - rect.top) / rect.height
|
|
76
|
+
: rtl() ? 1 - horiz : horiz
|
|
77
|
+
return clampStep(min() + Math.min(1, Math.max(0, frac)) * (max() - min()))
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const emitSingle = (v: number) => config.onChange?.(v)
|
|
81
|
+
const emitRange = (nlo: number, nhi: number) => config.onChange?.([Math.min(nlo, nhi), Math.max(nlo, nhi)])
|
|
82
|
+
|
|
83
|
+
function nudge(which: SliderThumb, next: number) {
|
|
84
|
+
if (which === 'single') emitSingle(next)
|
|
85
|
+
else if (which === 'lo') emitRange(next, hi)
|
|
86
|
+
else emitRange(lo, next)
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** Begin a drag: pick the nearest thumb (range) and emit the initial value. */
|
|
90
|
+
function pointerDown(pos: SliderPoint, rect: DOMRect) {
|
|
91
|
+
if (disabled()) return
|
|
92
|
+
const v = valueFromPointer(pos, rect)
|
|
93
|
+
if (range()) {
|
|
94
|
+
const which: SliderThumb = Math.abs(v - lo) <= Math.abs(v - hi) ? 'lo' : 'hi'
|
|
95
|
+
dragging = which
|
|
96
|
+
which === 'lo' ? emitRange(v, hi) : emitRange(lo, v)
|
|
97
|
+
} else {
|
|
98
|
+
dragging = 'single'
|
|
99
|
+
emitSingle(v)
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
/** Continue a drag: emit the value for the currently-dragged thumb. */
|
|
103
|
+
function setFromPointer(pos: SliderPoint, rect: DOMRect) {
|
|
104
|
+
if (!dragging) return
|
|
105
|
+
nudge(dragging, valueFromPointer(pos, rect))
|
|
106
|
+
}
|
|
107
|
+
function endDrag() { dragging = null }
|
|
108
|
+
|
|
109
|
+
/** Step a thumb by `delta` steps (used for custom controls). */
|
|
110
|
+
function step(which: SliderThumb, delta: number) {
|
|
111
|
+
if (disabled()) return
|
|
112
|
+
const cur = which === 'lo' ? lo : hi
|
|
113
|
+
nudge(which, clampStep(cur + delta * stepSize()))
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function onThumbKey(e: KeyboardEvent, which: SliderThumb) {
|
|
117
|
+
if (disabled()) return
|
|
118
|
+
const cur = which === 'lo' ? lo : hi
|
|
119
|
+
// Under horizontal RTL, Left increases and Right decreases (Up/Down keep
|
|
120
|
+
// their natural up=more direction regardless of text direction).
|
|
121
|
+
const rtlH = rtl() && !isVert()
|
|
122
|
+
const inc = clampStep(cur + stepSize())
|
|
123
|
+
const dec = clampStep(cur - stepSize())
|
|
124
|
+
let next = cur
|
|
125
|
+
if (e.key === 'ArrowUp') next = inc
|
|
126
|
+
else if (e.key === 'ArrowDown') next = dec
|
|
127
|
+
else if (e.key === 'ArrowRight') next = rtlH ? dec : inc
|
|
128
|
+
else if (e.key === 'ArrowLeft') next = rtlH ? inc : dec
|
|
129
|
+
else if (e.key === 'Home') next = min()
|
|
130
|
+
else if (e.key === 'End') next = max()
|
|
131
|
+
else if (e.key === 'PageUp') next = clampStep(cur + stepSize() * 10)
|
|
132
|
+
else if (e.key === 'PageDown') next = clampStep(cur - stepSize() * 10)
|
|
133
|
+
else return
|
|
134
|
+
e.preventDefault()
|
|
135
|
+
nudge(which, next)
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const thumbOf = (i: number): SliderThumb => (range() ? (i === 0 ? 'lo' : 'hi') : 'single')
|
|
139
|
+
|
|
140
|
+
return {
|
|
141
|
+
get value() { return config.value() },
|
|
142
|
+
get lo() { return lo },
|
|
143
|
+
get hi() { return hi },
|
|
144
|
+
/** Thumb currently being dragged, or null. */
|
|
145
|
+
get dragging() { return dragging },
|
|
146
|
+
percentOf,
|
|
147
|
+
clampStep,
|
|
148
|
+
valueFromPointer,
|
|
149
|
+
pointerDown,
|
|
150
|
+
setFromPointer,
|
|
151
|
+
endDrag,
|
|
152
|
+
step,
|
|
153
|
+
/** Spread onto the track container (static a11y/data hooks). */
|
|
154
|
+
trackProps: () => ({
|
|
155
|
+
'data-orientation': isVert() ? 'vertical' : 'horizontal',
|
|
156
|
+
}),
|
|
157
|
+
/** Spread onto the thumb at index `i` (0 = single/low, 1 = high). */
|
|
158
|
+
thumbProps: (i: number) => {
|
|
159
|
+
const which = thumbOf(i)
|
|
160
|
+
const cur = which === 'lo' ? lo : hi
|
|
161
|
+
const vmin = which === 'hi' ? lo : min()
|
|
162
|
+
const vmax = which === 'lo' ? hi : max()
|
|
163
|
+
const label = which === 'lo' ? 'Minimum' : which === 'hi' ? 'Maximum' : (config.ariaLabel?.() ?? 'Value')
|
|
164
|
+
// Validation ARIA (invalid / required / describedby) from the editor
|
|
165
|
+
// contract; keep the thumb's own value-oriented aria-label.
|
|
166
|
+
const va = editorAria({
|
|
167
|
+
id: config.id?.(),
|
|
168
|
+
invalid: config.invalid?.(),
|
|
169
|
+
required: config.required?.(),
|
|
170
|
+
error: config.error?.(),
|
|
171
|
+
hint: config.hint?.(),
|
|
172
|
+
})
|
|
173
|
+
return {
|
|
174
|
+
role: 'slider' as const,
|
|
175
|
+
tabindex: disabled() ? -1 : 0,
|
|
176
|
+
'aria-valuemin': vmin,
|
|
177
|
+
'aria-valuemax': vmax,
|
|
178
|
+
'aria-valuenow': cur,
|
|
179
|
+
'aria-label': label,
|
|
180
|
+
'aria-orientation': (isVert() ? 'vertical' : 'horizontal') as SliderOrientation,
|
|
181
|
+
'aria-disabled': disabled() || undefined,
|
|
182
|
+
'aria-invalid': va['aria-invalid'],
|
|
183
|
+
'aria-required': va['aria-required'],
|
|
184
|
+
'aria-describedby': va['aria-describedby'],
|
|
185
|
+
onkeydown: (e: KeyboardEvent) => onThumbKey(e, which),
|
|
186
|
+
}
|
|
187
|
+
},
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
export type Slider = ReturnType<typeof createSlider>
|