webcoreui 1.1.0 → 1.2.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 +9 -1
- package/astro.d.ts +5 -0
- package/astro.js +2 -0
- package/components/BottomNavigation/BottomNavigation.astro +1 -3
- package/components/Breadcrumb/Breadcrumb.astro +1 -3
- package/components/Carousel/Carousel.astro +79 -9
- package/components/Carousel/Carousel.svelte +40 -9
- package/components/Carousel/Carousel.tsx +46 -11
- package/components/Carousel/carousel.ts +3 -1
- package/components/Copy/Copy.astro +3 -5
- package/components/Copy/Copy.svelte +1 -1
- package/components/Copy/Copy.tsx +1 -1
- package/components/Input/input.ts +62 -62
- package/components/Modal/Modal.astro +75 -75
- package/components/Modal/modal.ts +25 -25
- package/components/OTPInput/OTPInput.astro +194 -96
- package/components/OTPInput/OTPInput.svelte +141 -26
- package/components/OTPInput/OTPInput.tsx +140 -36
- package/components/OTPInput/otpinput.module.scss +59 -85
- package/components/Pagination/Pagination.astro +3 -3
- package/components/Pagination/Pagination.svelte +4 -4
- package/components/Pagination/pagination.module.scss +3 -3
- package/components/RangeSlider/RangeSlider.astro +270 -0
- package/components/RangeSlider/RangeSlider.svelte +188 -0
- package/components/RangeSlider/RangeSlider.tsx +205 -0
- package/components/RangeSlider/rangeslider.module.scss +143 -0
- package/components/RangeSlider/rangeslider.ts +37 -0
- package/components/Sidebar/Sidebar.astro +1 -3
- package/components/Stepper/Stepper.astro +1 -3
- package/index.d.ts +23 -4
- package/index.js +2 -0
- package/package.json +109 -103
- package/react.d.ts +5 -0
- package/react.js +2 -0
- package/scss/global/breakpoints.scss +15 -0
- package/scss/setup.scss +7 -1
- package/svelte.d.ts +5 -0
- package/svelte.js +2 -0
- package/utils/DOMUtils.ts +27 -2
- package/utils/bodyFreeze.ts +1 -1
- package/utils/context.ts +2 -2
- package/utils/getBreakpoint.ts +17 -0
- package/utils/isOneOf.ts +5 -0
- package/utils/modal.ts +54 -55
- package/utils/toast.ts +1 -1
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
---
|
|
2
|
+
import type { RangeSliderProps } from './rangeslider'
|
|
3
|
+
|
|
4
|
+
import ConditionalWrapper from '../ConditionalWrapper/ConditionalWrapper.astro'
|
|
5
|
+
import Icon from '../Icon/Icon.astro'
|
|
6
|
+
|
|
7
|
+
import { classNames } from '../../utils/classNames'
|
|
8
|
+
import { interpolate } from '../../utils/interpolate'
|
|
9
|
+
|
|
10
|
+
import styles from './rangeslider.module.scss'
|
|
11
|
+
|
|
12
|
+
interface Props extends RangeSliderProps {}
|
|
13
|
+
|
|
14
|
+
const {
|
|
15
|
+
min = 0,
|
|
16
|
+
max = 100,
|
|
17
|
+
selectedMin,
|
|
18
|
+
selectedMax,
|
|
19
|
+
step = 1,
|
|
20
|
+
minGap = 5,
|
|
21
|
+
disabled,
|
|
22
|
+
color,
|
|
23
|
+
background,
|
|
24
|
+
thumb,
|
|
25
|
+
label,
|
|
26
|
+
subText,
|
|
27
|
+
minLabel,
|
|
28
|
+
maxLabel,
|
|
29
|
+
minIcon,
|
|
30
|
+
maxIcon,
|
|
31
|
+
interactiveLabels,
|
|
32
|
+
updateLabels,
|
|
33
|
+
className
|
|
34
|
+
} = Astro.props
|
|
35
|
+
|
|
36
|
+
const styleVariables = classNames([
|
|
37
|
+
color && `--w-range-slider-color: ${color};`,
|
|
38
|
+
background && `--w-range-slider-background: ${background};`,
|
|
39
|
+
thumb && `--w-range-slider-thumb: ${thumb};`
|
|
40
|
+
])
|
|
41
|
+
|
|
42
|
+
const minLabelWidth = `${String(max).length}ch`
|
|
43
|
+
const labelStyle = updateLabels ? `min-width:${minLabelWidth};` : null
|
|
44
|
+
---
|
|
45
|
+
|
|
46
|
+
<ConditionalWrapper condition={!!(label || subText)}>
|
|
47
|
+
<label slot="wrapper" class:list={[styles.label, className]}>children</label>
|
|
48
|
+
|
|
49
|
+
{label && <span>{label}</span>}
|
|
50
|
+
|
|
51
|
+
<div
|
|
52
|
+
class:list={[styles.container, !(label && subText) && className]}
|
|
53
|
+
data-id="w-range-slider"
|
|
54
|
+
data-gap={minGap}
|
|
55
|
+
data-interactive={interactiveLabels}
|
|
56
|
+
data-update-labels={updateLabels}
|
|
57
|
+
style={styleVariables}
|
|
58
|
+
>
|
|
59
|
+
<ConditionalWrapper condition={!!interactiveLabels}>
|
|
60
|
+
<button slot="wrapper" data-dir="left">children</button>
|
|
61
|
+
|
|
62
|
+
{minIcon && (
|
|
63
|
+
<Fragment>
|
|
64
|
+
{minIcon.startsWith('<svg')
|
|
65
|
+
? <Fragment set:html={minIcon} />
|
|
66
|
+
: <Icon type={minIcon} size={18} />
|
|
67
|
+
}
|
|
68
|
+
</Fragment>
|
|
69
|
+
)}
|
|
70
|
+
{minLabel && (
|
|
71
|
+
<span data-id="w-min-label" style={labelStyle}>{minLabel}</span>
|
|
72
|
+
)}
|
|
73
|
+
</ConditionalWrapper>
|
|
74
|
+
|
|
75
|
+
<div class={styles.slider}>
|
|
76
|
+
<div
|
|
77
|
+
data-id="w-range"
|
|
78
|
+
data-disabled={disabled ? 'true' : undefined}
|
|
79
|
+
class={styles.range}
|
|
80
|
+
style={`
|
|
81
|
+
left: ${interpolate(selectedMin || min, [min, max], [0, 100])}%;
|
|
82
|
+
right: ${interpolate(selectedMax || max, [min, max], [100, 0])}%;
|
|
83
|
+
`}
|
|
84
|
+
/>
|
|
85
|
+
<input
|
|
86
|
+
type="range"
|
|
87
|
+
class:list={[styles.input, styles.min]}
|
|
88
|
+
min={min}
|
|
89
|
+
max={max}
|
|
90
|
+
value={selectedMin || min}
|
|
91
|
+
step={step}
|
|
92
|
+
disabled={disabled}
|
|
93
|
+
data-min="true"
|
|
94
|
+
/>
|
|
95
|
+
<input
|
|
96
|
+
type="range"
|
|
97
|
+
min={min}
|
|
98
|
+
max={max}
|
|
99
|
+
class={styles.input}
|
|
100
|
+
value={selectedMax || max}
|
|
101
|
+
step={step}
|
|
102
|
+
disabled={disabled}
|
|
103
|
+
data-max="true"
|
|
104
|
+
/>
|
|
105
|
+
</div>
|
|
106
|
+
|
|
107
|
+
<ConditionalWrapper condition={!!interactiveLabels}>
|
|
108
|
+
<button slot="wrapper" data-dir="right">children</button>
|
|
109
|
+
|
|
110
|
+
{maxLabel && (
|
|
111
|
+
<span data-id="w-max-label" style={labelStyle}>{maxLabel}</span>
|
|
112
|
+
)}
|
|
113
|
+
{maxIcon && (
|
|
114
|
+
<Fragment>
|
|
115
|
+
{maxIcon.startsWith('<svg')
|
|
116
|
+
? <Fragment set:html={maxIcon} />
|
|
117
|
+
: <Icon type={maxIcon} size={14} />
|
|
118
|
+
}
|
|
119
|
+
</Fragment>
|
|
120
|
+
)}
|
|
121
|
+
</ConditionalWrapper>
|
|
122
|
+
</div>
|
|
123
|
+
|
|
124
|
+
{subText && <span class="muted">{subText}</span>}
|
|
125
|
+
</ConditionalWrapper>
|
|
126
|
+
|
|
127
|
+
<script>
|
|
128
|
+
import { off, on } from '../../utils/DOMUtils'
|
|
129
|
+
import { dispatch } from '../../utils/event'
|
|
130
|
+
import { interpolate } from '../../utils/interpolate'
|
|
131
|
+
|
|
132
|
+
type RangeParams = {
|
|
133
|
+
range: HTMLDivElement
|
|
134
|
+
minValue: number
|
|
135
|
+
maxValue: number
|
|
136
|
+
min: number
|
|
137
|
+
max: number
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const updateRange = ({ range, minValue, maxValue, min, max }: RangeParams) => {
|
|
141
|
+
range.style.left = `${interpolate(minValue, [min, max], [0, 100])}%`
|
|
142
|
+
range.style.right = `${interpolate(maxValue, [min, max], [100, 0])}%`
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
const updateLabels = (wrapper: HTMLDivElement, minValue: number, maxValue: number) => {
|
|
146
|
+
const minLabel = wrapper.querySelector('[data-id="w-min-label"]')
|
|
147
|
+
const maxLabel = wrapper.querySelector('[data-id="w-max-label"]')
|
|
148
|
+
|
|
149
|
+
if (minLabel instanceof HTMLElement && maxLabel instanceof HTMLElement) {
|
|
150
|
+
minLabel.innerText = minLabel.innerText.replace(/\d+(\.\d+)?/, String(minValue))
|
|
151
|
+
maxLabel.innerText = maxLabel.innerText.replace(/\d+(\.\d+)?/, String(maxValue))
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
const addEventListeners = () => {
|
|
156
|
+
on('[data-id="w-range-slider"] input', 'input', (event: Event) => {
|
|
157
|
+
const target = event.target
|
|
158
|
+
|
|
159
|
+
if (!(target instanceof HTMLInputElement)) {
|
|
160
|
+
return
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
const range = target.parentElement?.querySelector('[data-id="w-range"]')
|
|
164
|
+
const wrapper = target.parentElement?.parentElement
|
|
165
|
+
const prevInput = target.previousElementSibling
|
|
166
|
+
const nextInput = target.nextElementSibling
|
|
167
|
+
|
|
168
|
+
if (!(wrapper instanceof HTMLDivElement) || !(range instanceof HTMLDivElement)) {
|
|
169
|
+
return
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
const value = Number(target.value)
|
|
173
|
+
const min = Number(target.min)
|
|
174
|
+
const max = Number(target.max)
|
|
175
|
+
const step = Number(target.step)
|
|
176
|
+
const gap = Number(wrapper.dataset.gap)
|
|
177
|
+
const shouldUpdateLabels = !!wrapper.dataset.updateLabels
|
|
178
|
+
const prevInputValue = prevInput instanceof HTMLInputElement ? prevInput.value : 0
|
|
179
|
+
const nextInputValue = nextInput instanceof HTMLInputElement ? nextInput.value : 0
|
|
180
|
+
const minValue = target.dataset.min ? value : Number(prevInputValue)
|
|
181
|
+
const maxValue = target.dataset.max ? value : Number(nextInputValue)
|
|
182
|
+
|
|
183
|
+
let currentMin = minValue
|
|
184
|
+
let currentMax = maxValue
|
|
185
|
+
|
|
186
|
+
if (maxValue - minValue >= gap) {
|
|
187
|
+
if (shouldUpdateLabels) {
|
|
188
|
+
updateLabels(wrapper, minValue, maxValue)
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
dispatch('rangeSliderOnChange', {
|
|
192
|
+
min: minValue,
|
|
193
|
+
max: maxValue
|
|
194
|
+
})
|
|
195
|
+
} else if (target.dataset.min) {
|
|
196
|
+
currentMin = maxValue - Math.max(step, gap)
|
|
197
|
+
target.value = String(currentMin)
|
|
198
|
+
} else {
|
|
199
|
+
currentMax = minValue + Math.max(step, gap)
|
|
200
|
+
target.value = String(currentMax)
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
updateRange({
|
|
204
|
+
range,
|
|
205
|
+
minValue: currentMin,
|
|
206
|
+
maxValue: currentMax,
|
|
207
|
+
min,
|
|
208
|
+
max
|
|
209
|
+
})
|
|
210
|
+
}, true)
|
|
211
|
+
|
|
212
|
+
on('[data-id="w-range-slider"] button', 'click', (event: Event) => {
|
|
213
|
+
const target = event.currentTarget
|
|
214
|
+
|
|
215
|
+
if (!(target instanceof HTMLButtonElement)) {
|
|
216
|
+
return
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
const wrapper = target.parentElement
|
|
220
|
+
const range = wrapper?.querySelector('[data-id="w-range"]')
|
|
221
|
+
const minInput = wrapper?.querySelector('[data-min]')
|
|
222
|
+
const maxInput = wrapper?.querySelector('[data-max]')
|
|
223
|
+
|
|
224
|
+
if (!(wrapper instanceof HTMLDivElement)
|
|
225
|
+
|| !(range instanceof HTMLDivElement)
|
|
226
|
+
|| !(minInput instanceof HTMLInputElement)
|
|
227
|
+
|| !(maxInput instanceof HTMLInputElement)
|
|
228
|
+
) {
|
|
229
|
+
return
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
const dir = target.dataset.dir === 'left' ? -1 : 1
|
|
233
|
+
const step = Number(minInput.step)
|
|
234
|
+
const min = Number(minInput.min)
|
|
235
|
+
const max = Number(minInput.max)
|
|
236
|
+
const minValue = Number(minInput.value) + (dir * step)
|
|
237
|
+
const maxValue = Number(maxInput.value) + (dir * step)
|
|
238
|
+
const shouldUpdateLabels = !!wrapper.dataset.updateLabels
|
|
239
|
+
|
|
240
|
+
if (minValue < min || maxValue > max) {
|
|
241
|
+
return
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
minInput.value = String(minValue)
|
|
245
|
+
maxInput.value = String(maxValue)
|
|
246
|
+
|
|
247
|
+
updateRange({
|
|
248
|
+
range,
|
|
249
|
+
minValue,
|
|
250
|
+
maxValue,
|
|
251
|
+
min,
|
|
252
|
+
max
|
|
253
|
+
})
|
|
254
|
+
|
|
255
|
+
if (shouldUpdateLabels) {
|
|
256
|
+
updateLabels(wrapper, minValue, maxValue)
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
dispatch('rangeSliderOnChange', {
|
|
260
|
+
min: minValue,
|
|
261
|
+
max: maxValue
|
|
262
|
+
})
|
|
263
|
+
}, true)
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
off(document, 'astro:after-swap', addEventListeners)
|
|
267
|
+
on(document, 'astro:after-swap', addEventListeners)
|
|
268
|
+
|
|
269
|
+
addEventListeners()
|
|
270
|
+
</script>
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { SvelteRangeSliderProps } from './rangeslider'
|
|
3
|
+
|
|
4
|
+
import ConditionalWrapper from '../ConditionalWrapper/ConditionalWrapper.svelte'
|
|
5
|
+
|
|
6
|
+
import { classNames } from '../../utils/classNames'
|
|
7
|
+
import { interpolate } from '../../utils/interpolate'
|
|
8
|
+
|
|
9
|
+
import styles from './rangeslider.module.scss'
|
|
10
|
+
|
|
11
|
+
const {
|
|
12
|
+
min = 0,
|
|
13
|
+
max = 100,
|
|
14
|
+
selectedMin,
|
|
15
|
+
selectedMax,
|
|
16
|
+
step = 1,
|
|
17
|
+
minGap = 5,
|
|
18
|
+
disabled,
|
|
19
|
+
color,
|
|
20
|
+
background,
|
|
21
|
+
thumb,
|
|
22
|
+
label,
|
|
23
|
+
subText,
|
|
24
|
+
minLabel,
|
|
25
|
+
maxLabel,
|
|
26
|
+
minIcon,
|
|
27
|
+
maxIcon,
|
|
28
|
+
interactiveLabels,
|
|
29
|
+
updateLabels,
|
|
30
|
+
className,
|
|
31
|
+
onChange
|
|
32
|
+
}: SvelteRangeSliderProps = $props()
|
|
33
|
+
|
|
34
|
+
const styleVariables = classNames([
|
|
35
|
+
color && `--w-range-slider-color: ${color};`,
|
|
36
|
+
background && `--w-range-slider-background: ${background};`,
|
|
37
|
+
thumb && `--w-range-slider-thumb: ${thumb};`
|
|
38
|
+
])
|
|
39
|
+
|
|
40
|
+
const minLabelWidth = `${String(max).length}ch`
|
|
41
|
+
const labelStyle = updateLabels ? `min-width:${minLabelWidth};` : null
|
|
42
|
+
|
|
43
|
+
let minValue = $state(selectedMin || min)
|
|
44
|
+
let maxValue = $state(selectedMax || max)
|
|
45
|
+
let dynamicMinLabel = $state(minLabel)
|
|
46
|
+
let dynamicMaxLabel = $state(maxLabel)
|
|
47
|
+
|
|
48
|
+
const rangeLeftPercent = $derived(interpolate((minValue || min), [min, max], [0, 100]))
|
|
49
|
+
const rangeRightPercent = $derived(interpolate((maxValue || max), [min, max], [100, 0]))
|
|
50
|
+
|
|
51
|
+
const updateDynamicLabels = (minValue: number, maxValue: number) => {
|
|
52
|
+
if (dynamicMinLabel && dynamicMaxLabel) {
|
|
53
|
+
dynamicMinLabel = dynamicMinLabel.replace(/\d+(\.\d+)?/, String(minValue))
|
|
54
|
+
dynamicMaxLabel = dynamicMaxLabel.replace(/\d+(\.\d+)?/, String(maxValue))
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const handleInput = (event: Event) => {
|
|
59
|
+
const target = event.target
|
|
60
|
+
|
|
61
|
+
if (!(target instanceof HTMLInputElement)) {
|
|
62
|
+
return
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
if (maxValue - minValue >= minGap) {
|
|
66
|
+
if (updateLabels) {
|
|
67
|
+
updateDynamicLabels(minValue, maxValue)
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
onChange?.({
|
|
71
|
+
min: minValue,
|
|
72
|
+
max: maxValue
|
|
73
|
+
})
|
|
74
|
+
} else if (target.dataset.min) {
|
|
75
|
+
minValue = maxValue - Math.max(step, minGap)
|
|
76
|
+
target.value = String(minValue)
|
|
77
|
+
} else {
|
|
78
|
+
maxValue = minValue + Math.max(step, minGap)
|
|
79
|
+
target.value = String(maxValue)
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const handleClick = (event: Event, direction: 'left' | 'right') => {
|
|
84
|
+
const target = event.currentTarget
|
|
85
|
+
|
|
86
|
+
if (!(target instanceof HTMLButtonElement)) {
|
|
87
|
+
return
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const dir = direction === 'left' ? -1 : 1
|
|
91
|
+
const updatedMinValue = Number(minValue) + (dir * step)
|
|
92
|
+
const updatedMaxValue = Number(maxValue) + (dir * step)
|
|
93
|
+
|
|
94
|
+
if (updatedMinValue < min || updatedMaxValue > max) {
|
|
95
|
+
return
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
minValue = updatedMinValue
|
|
99
|
+
maxValue = updatedMaxValue
|
|
100
|
+
|
|
101
|
+
if (updateLabels) {
|
|
102
|
+
updateDynamicLabels(minValue, maxValue)
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
onChange?.({
|
|
106
|
+
min: minValue,
|
|
107
|
+
max: maxValue
|
|
108
|
+
})
|
|
109
|
+
}
|
|
110
|
+
</script>
|
|
111
|
+
|
|
112
|
+
<ConditionalWrapper
|
|
113
|
+
element="label"
|
|
114
|
+
condition={!!(label || subText)}
|
|
115
|
+
class={classNames([styles.label, className])}
|
|
116
|
+
>
|
|
117
|
+
{#if label}
|
|
118
|
+
<span>{label}</span>
|
|
119
|
+
{/if}
|
|
120
|
+
|
|
121
|
+
<div
|
|
122
|
+
class={classNames([styles.container, !(label && subText) && className])}
|
|
123
|
+
style={styleVariables}
|
|
124
|
+
>
|
|
125
|
+
<ConditionalWrapper
|
|
126
|
+
element="button"
|
|
127
|
+
condition={!!interactiveLabels}
|
|
128
|
+
onclick={(e: Event) => handleClick(e, 'left')}
|
|
129
|
+
>
|
|
130
|
+
{#if minIcon}
|
|
131
|
+
{@html minIcon}
|
|
132
|
+
{/if}
|
|
133
|
+
{#if dynamicMinLabel}
|
|
134
|
+
<span style={labelStyle}>{dynamicMinLabel}</span>
|
|
135
|
+
{/if}
|
|
136
|
+
</ConditionalWrapper>
|
|
137
|
+
|
|
138
|
+
<div class={styles.slider}>
|
|
139
|
+
<div
|
|
140
|
+
data-disabled={disabled ? 'true' : undefined}
|
|
141
|
+
class={styles.range}
|
|
142
|
+
style={`
|
|
143
|
+
left: ${rangeLeftPercent}%;
|
|
144
|
+
right: ${rangeRightPercent}%;
|
|
145
|
+
`}
|
|
146
|
+
></div>
|
|
147
|
+
<input
|
|
148
|
+
type="range"
|
|
149
|
+
class={classNames([styles.input, styles.min])}
|
|
150
|
+
min={min}
|
|
151
|
+
max={max}
|
|
152
|
+
bind:value={minValue}
|
|
153
|
+
step={step}
|
|
154
|
+
disabled={disabled}
|
|
155
|
+
oninput={handleInput}
|
|
156
|
+
data-min="true"
|
|
157
|
+
/>
|
|
158
|
+
<input
|
|
159
|
+
type="range"
|
|
160
|
+
min={min}
|
|
161
|
+
max={max}
|
|
162
|
+
class={styles.input}
|
|
163
|
+
bind:value={maxValue}
|
|
164
|
+
step={step}
|
|
165
|
+
disabled={disabled}
|
|
166
|
+
oninput={handleInput}
|
|
167
|
+
data-max="true"
|
|
168
|
+
/>
|
|
169
|
+
</div>
|
|
170
|
+
|
|
171
|
+
<ConditionalWrapper
|
|
172
|
+
element="button"
|
|
173
|
+
condition={!!interactiveLabels}
|
|
174
|
+
onclick={(e: Event) => handleClick(e, 'right')}
|
|
175
|
+
>
|
|
176
|
+
{#if maxIcon}
|
|
177
|
+
{@html maxIcon}
|
|
178
|
+
{/if}
|
|
179
|
+
{#if dynamicMaxLabel}
|
|
180
|
+
<span style={labelStyle}>{dynamicMaxLabel}</span>
|
|
181
|
+
{/if}
|
|
182
|
+
</ConditionalWrapper>
|
|
183
|
+
</div>
|
|
184
|
+
|
|
185
|
+
{#if subText}
|
|
186
|
+
<span class="muted">{subText}</span>
|
|
187
|
+
{/if}
|
|
188
|
+
</ConditionalWrapper>
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
/* eslint-disable complexity */
|
|
2
|
+
import React, { useEffect, useRef, useState } from 'react'
|
|
3
|
+
import type { ReactRangeSliderProps } from './rangeslider'
|
|
4
|
+
|
|
5
|
+
import ConditionalWrapper from '../ConditionalWrapper/ConditionalWrapper.tsx'
|
|
6
|
+
|
|
7
|
+
import { classNames } from '../../utils/classNames'
|
|
8
|
+
import { interpolate } from '../../utils/interpolate'
|
|
9
|
+
|
|
10
|
+
import styles from './rangeslider.module.scss'
|
|
11
|
+
|
|
12
|
+
const RangeSlider = ({
|
|
13
|
+
min = 0,
|
|
14
|
+
max = 100,
|
|
15
|
+
selectedMin,
|
|
16
|
+
selectedMax,
|
|
17
|
+
step = 1,
|
|
18
|
+
minGap = 5,
|
|
19
|
+
disabled,
|
|
20
|
+
color,
|
|
21
|
+
background,
|
|
22
|
+
thumb,
|
|
23
|
+
label,
|
|
24
|
+
subText,
|
|
25
|
+
minLabel,
|
|
26
|
+
maxLabel,
|
|
27
|
+
minIcon,
|
|
28
|
+
maxIcon,
|
|
29
|
+
interactiveLabels,
|
|
30
|
+
updateLabels,
|
|
31
|
+
className,
|
|
32
|
+
onChange
|
|
33
|
+
}: ReactRangeSliderProps) => {
|
|
34
|
+
const [minValue, setMinValue] = useState(selectedMin || min)
|
|
35
|
+
const [maxValue, setMaxValue] = useState(selectedMax || max)
|
|
36
|
+
const [dynamicMinLabel, setDynamicMinLabel] = useState(minLabel)
|
|
37
|
+
const [dynamicMaxLabel, setDynamicMaxLabel] = useState(maxLabel)
|
|
38
|
+
const rangeLeftPercent = useRef(interpolate(minValue || min, [min, max], [0, 100]))
|
|
39
|
+
const rangeRightPercent = useRef(interpolate(maxValue || max, [min, max], [100, 0]))
|
|
40
|
+
|
|
41
|
+
const minLabelWidth = `${String(max).length}ch`
|
|
42
|
+
const labelStyle = updateLabels ? { minWidth: minLabelWidth } as React.CSSProperties : undefined
|
|
43
|
+
|
|
44
|
+
const styleVariables = {
|
|
45
|
+
...(color && { '--w-range-slider-color': color }),
|
|
46
|
+
...(background && { '--w-range-slider-background': background }),
|
|
47
|
+
...(thumb && { '--w-range-slider-thumb': thumb })
|
|
48
|
+
} as React.CSSProperties
|
|
49
|
+
|
|
50
|
+
const updateDynamicLabels = (minValue: number, maxValue: number) => {
|
|
51
|
+
if (dynamicMinLabel && dynamicMaxLabel) {
|
|
52
|
+
setDynamicMinLabel(dynamicMinLabel.replace(/\d+(\.\d+)?/, String(minValue)))
|
|
53
|
+
setDynamicMaxLabel(dynamicMaxLabel.replace(/\d+(\.\d+)?/, String(maxValue)))
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const handleInput = (event: React.FormEvent) => {
|
|
58
|
+
const target = event.target
|
|
59
|
+
|
|
60
|
+
if (!(target instanceof HTMLInputElement)) {
|
|
61
|
+
return
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const value = Number(target.value)
|
|
65
|
+
|
|
66
|
+
let currentMin = target.dataset.min ? value : minValue
|
|
67
|
+
let currentMax = target.dataset.max ? value : maxValue
|
|
68
|
+
|
|
69
|
+
if (currentMax - currentMin >= minGap) {
|
|
70
|
+
if (updateLabels) {
|
|
71
|
+
updateDynamicLabels(currentMin, currentMax)
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
onChange?.({
|
|
75
|
+
min: currentMin,
|
|
76
|
+
max: currentMax
|
|
77
|
+
})
|
|
78
|
+
} else if (target.dataset.min) {
|
|
79
|
+
currentMin = currentMax - Math.max(step, minGap)
|
|
80
|
+
target.value = String(currentMin)
|
|
81
|
+
} else {
|
|
82
|
+
currentMax = currentMin + Math.max(step, minGap)
|
|
83
|
+
target.value = String(currentMax)
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
setMinValue(currentMin)
|
|
87
|
+
setMaxValue(currentMax)
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const handleClick = (event: React.MouseEvent, direction: 'left' | 'right') => {
|
|
91
|
+
const target = event.currentTarget
|
|
92
|
+
|
|
93
|
+
if (!(target instanceof HTMLButtonElement)) {
|
|
94
|
+
return
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const dir = direction === 'left' ? -1 : 1
|
|
98
|
+
const updatedMinValue = Number(minValue) + (dir * step)
|
|
99
|
+
const updatedMaxValue = Number(maxValue) + (dir * step)
|
|
100
|
+
|
|
101
|
+
if (updatedMinValue < min || updatedMaxValue > max) {
|
|
102
|
+
return
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
setMinValue(updatedMinValue)
|
|
106
|
+
setMaxValue(updatedMaxValue)
|
|
107
|
+
|
|
108
|
+
if (updateLabels) {
|
|
109
|
+
updateDynamicLabels(updatedMinValue, updatedMaxValue)
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
onChange?.({
|
|
113
|
+
min: updatedMinValue,
|
|
114
|
+
max: updatedMaxValue
|
|
115
|
+
})
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
useEffect(() => {
|
|
119
|
+
rangeLeftPercent.current = interpolate((minValue || min), [min, max], [0, 100])
|
|
120
|
+
rangeRightPercent.current = interpolate((maxValue || max), [min, max], [100, 0])
|
|
121
|
+
}, [minValue, maxValue])
|
|
122
|
+
|
|
123
|
+
return (
|
|
124
|
+
<ConditionalWrapper
|
|
125
|
+
condition={!!(label || subText)}
|
|
126
|
+
wrapper={children => (
|
|
127
|
+
<label className={classNames([styles.label, className])}>{children}</label>
|
|
128
|
+
)}
|
|
129
|
+
>
|
|
130
|
+
{label && <span>{label}</span>}
|
|
131
|
+
|
|
132
|
+
<div
|
|
133
|
+
className={classNames([styles.container, !(label && subText) && className])}
|
|
134
|
+
style={styleVariables}
|
|
135
|
+
>
|
|
136
|
+
<ConditionalWrapper
|
|
137
|
+
condition={!!interactiveLabels}
|
|
138
|
+
wrapper={children => (
|
|
139
|
+
<button onClick={(e: React.MouseEvent) => handleClick(e, 'left')}>{children}</button>
|
|
140
|
+
)}
|
|
141
|
+
>
|
|
142
|
+
{minIcon && (
|
|
143
|
+
<span
|
|
144
|
+
dangerouslySetInnerHTML={{ __html: minIcon }}
|
|
145
|
+
style={{ height: 18 }}
|
|
146
|
+
/>
|
|
147
|
+
)}
|
|
148
|
+
{dynamicMinLabel && <span style={labelStyle}>{dynamicMinLabel}</span>}
|
|
149
|
+
</ConditionalWrapper>
|
|
150
|
+
|
|
151
|
+
<div className={styles.slider}>
|
|
152
|
+
<div
|
|
153
|
+
data-disabled={disabled ? 'true' : undefined}
|
|
154
|
+
className={styles.range}
|
|
155
|
+
style={{
|
|
156
|
+
left: `${rangeLeftPercent.current}%`,
|
|
157
|
+
right: `${rangeRightPercent.current}%`
|
|
158
|
+
}}
|
|
159
|
+
/>
|
|
160
|
+
<input
|
|
161
|
+
type="range"
|
|
162
|
+
className={classNames([styles.input, styles.min])}
|
|
163
|
+
min={min}
|
|
164
|
+
max={max}
|
|
165
|
+
value={minValue}
|
|
166
|
+
step={step}
|
|
167
|
+
disabled={disabled}
|
|
168
|
+
onInput={handleInput}
|
|
169
|
+
data-min="true"
|
|
170
|
+
/>
|
|
171
|
+
<input
|
|
172
|
+
type="range"
|
|
173
|
+
min={min}
|
|
174
|
+
max={max}
|
|
175
|
+
className={styles.input}
|
|
176
|
+
value={maxValue}
|
|
177
|
+
step={step}
|
|
178
|
+
disabled={disabled}
|
|
179
|
+
onInput={handleInput}
|
|
180
|
+
data-max="true"
|
|
181
|
+
/>
|
|
182
|
+
</div>
|
|
183
|
+
|
|
184
|
+
<ConditionalWrapper
|
|
185
|
+
condition={!!interactiveLabels}
|
|
186
|
+
wrapper={children => (
|
|
187
|
+
<button onClick={(e: React.MouseEvent) => handleClick(e, 'right')}>{children}</button>
|
|
188
|
+
)}
|
|
189
|
+
>
|
|
190
|
+
{maxIcon && (
|
|
191
|
+
<span
|
|
192
|
+
dangerouslySetInnerHTML={{ __html: maxIcon }}
|
|
193
|
+
style={{ height: 18 }}
|
|
194
|
+
/>
|
|
195
|
+
)}
|
|
196
|
+
{dynamicMaxLabel && <span style={labelStyle}>{dynamicMaxLabel}</span>}
|
|
197
|
+
</ConditionalWrapper>
|
|
198
|
+
</div>
|
|
199
|
+
|
|
200
|
+
{subText && <span className="muted">{subText}</span>}
|
|
201
|
+
</ConditionalWrapper>
|
|
202
|
+
)
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
export default RangeSlider
|