@coreui/react-pro 5.8.0 → 5.9.1
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 +1 -1
- package/dist/cjs/components/date-range-picker/CDateRangePicker.js +23 -15
- package/dist/cjs/components/date-range-picker/CDateRangePicker.js.map +1 -1
- package/dist/cjs/components/index.d.ts +2 -0
- package/dist/cjs/components/multi-select/CMultiSelect.js +0 -3
- package/dist/cjs/components/multi-select/CMultiSelect.js.map +1 -1
- package/dist/cjs/components/picker/CPicker.d.ts +6 -0
- package/dist/cjs/components/picker/CPicker.js +14 -6
- package/dist/cjs/components/picker/CPicker.js.map +1 -1
- package/dist/cjs/components/range-slider/CRangeSlider.d.ts +79 -0
- package/dist/cjs/components/range-slider/CRangeSlider.js +146 -0
- package/dist/cjs/components/range-slider/CRangeSlider.js.map +1 -0
- package/dist/cjs/components/range-slider/index.d.ts +2 -0
- package/dist/cjs/components/range-slider/types.d.ts +11 -0
- package/dist/cjs/components/range-slider/utils.d.ts +39 -0
- package/dist/cjs/components/range-slider/utils.js +163 -0
- package/dist/cjs/components/range-slider/utils.js.map +1 -0
- package/dist/cjs/components/time-picker/CTimePicker.js +18 -10
- package/dist/cjs/components/time-picker/CTimePicker.js.map +1 -1
- package/dist/cjs/index.js +4 -0
- package/dist/cjs/index.js.map +1 -1
- package/dist/cjs/packages/coreui-react-pro/node_modules/tslib/tslib.es6.js.map +1 -1
- package/dist/esm/components/date-range-picker/CDateRangePicker.js +23 -15
- package/dist/esm/components/date-range-picker/CDateRangePicker.js.map +1 -1
- package/dist/esm/components/index.d.ts +2 -0
- package/dist/esm/components/multi-select/CMultiSelect.js +1 -4
- package/dist/esm/components/multi-select/CMultiSelect.js.map +1 -1
- package/dist/esm/components/picker/CPicker.d.ts +6 -0
- package/dist/esm/components/picker/CPicker.js +14 -6
- package/dist/esm/components/picker/CPicker.js.map +1 -1
- package/dist/esm/components/range-slider/CRangeSlider.d.ts +79 -0
- package/dist/esm/components/range-slider/CRangeSlider.js +144 -0
- package/dist/esm/components/range-slider/CRangeSlider.js.map +1 -0
- package/dist/esm/components/range-slider/index.d.ts +2 -0
- package/dist/esm/components/range-slider/types.d.ts +11 -0
- package/dist/esm/components/range-slider/utils.d.ts +39 -0
- package/dist/esm/components/range-slider/utils.js +148 -0
- package/dist/esm/components/range-slider/utils.js.map +1 -0
- package/dist/esm/components/time-picker/CTimePicker.js +18 -10
- package/dist/esm/components/time-picker/CTimePicker.js.map +1 -1
- package/dist/esm/index.js +2 -0
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/packages/coreui-react-pro/node_modules/tslib/tslib.es6.js.map +1 -1
- package/package.json +5 -5
- package/src/components/date-range-picker/CDateRangePicker.tsx +18 -1
- package/src/components/index.ts +2 -0
- package/src/components/multi-select/CMultiSelect.tsx +0 -4
- package/src/components/picker/CPicker.tsx +25 -4
- package/src/components/range-slider/CRangeSlider.tsx +377 -0
- package/src/components/range-slider/index.ts +3 -0
- package/src/components/range-slider/types.ts +16 -0
- package/src/components/range-slider/utils.ts +243 -0
- package/src/components/time-picker/CTimePicker.tsx +18 -1
|
@@ -4,6 +4,8 @@ import PropTypes from 'prop-types'
|
|
|
4
4
|
import classNames from 'classnames'
|
|
5
5
|
import { Placement } from '@popperjs/core'
|
|
6
6
|
|
|
7
|
+
import { CConditionalPortal } from '../conditional-portal'
|
|
8
|
+
|
|
7
9
|
import { useForkedRef, usePopper } from '../../hooks'
|
|
8
10
|
import { isRTL } from '../../utils'
|
|
9
11
|
|
|
@@ -36,6 +38,12 @@ export interface CPickerProps extends HTMLAttributes<HTMLDivElement> {
|
|
|
36
38
|
* Callback fired when the component requests to be shown.
|
|
37
39
|
*/
|
|
38
40
|
onShow?: () => void
|
|
41
|
+
/**
|
|
42
|
+
* Generates dropdown menu using createPortal.
|
|
43
|
+
*
|
|
44
|
+
* @since 5.9.0
|
|
45
|
+
*/
|
|
46
|
+
portal?: boolean
|
|
39
47
|
/**
|
|
40
48
|
* The content of toggler.
|
|
41
49
|
*/
|
|
@@ -58,6 +66,7 @@ export const CPicker = forwardRef<HTMLDivElement | HTMLLIElement, CPickerProps>(
|
|
|
58
66
|
footerContent,
|
|
59
67
|
onHide,
|
|
60
68
|
onShow,
|
|
69
|
+
portal = true,
|
|
61
70
|
toggler,
|
|
62
71
|
visible,
|
|
63
72
|
},
|
|
@@ -127,6 +136,10 @@ export const CPicker = forwardRef<HTMLDivElement | HTMLLIElement, CPickerProps>(
|
|
|
127
136
|
return
|
|
128
137
|
}
|
|
129
138
|
|
|
139
|
+
if (dropdownRef.current && dropdownRef.current.contains(event.target as HTMLElement)) {
|
|
140
|
+
return
|
|
141
|
+
}
|
|
142
|
+
|
|
130
143
|
setVisible(false)
|
|
131
144
|
}
|
|
132
145
|
|
|
@@ -152,10 +165,17 @@ export const CPicker = forwardRef<HTMLDivElement | HTMLLIElement, CPickerProps>(
|
|
|
152
165
|
React.cloneElement(toggler as React.ReactElement<any>, {
|
|
153
166
|
ref: togglerRef,
|
|
154
167
|
})}
|
|
155
|
-
<
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
168
|
+
<CConditionalPortal portal={portal}>
|
|
169
|
+
<div
|
|
170
|
+
className={classNames(dropdownClassNames, {
|
|
171
|
+
show: portal && _visible,
|
|
172
|
+
})}
|
|
173
|
+
ref={dropdownRef}
|
|
174
|
+
>
|
|
175
|
+
{children}
|
|
176
|
+
{footer && footerContent}
|
|
177
|
+
</div>
|
|
178
|
+
</CConditionalPortal>
|
|
159
179
|
</div>
|
|
160
180
|
)
|
|
161
181
|
}
|
|
@@ -175,6 +195,7 @@ CPicker.propTypes = {
|
|
|
175
195
|
footerContent: PropTypes.node,
|
|
176
196
|
onHide: PropTypes.func,
|
|
177
197
|
onShow: PropTypes.func,
|
|
198
|
+
portal: PropTypes.bool,
|
|
178
199
|
toggler: PropTypes.node,
|
|
179
200
|
visible: PropTypes.bool,
|
|
180
201
|
}
|
|
@@ -0,0 +1,377 @@
|
|
|
1
|
+
import React, {
|
|
2
|
+
ChangeEvent,
|
|
3
|
+
forwardRef,
|
|
4
|
+
HTMLAttributes,
|
|
5
|
+
ReactNode,
|
|
6
|
+
useEffect,
|
|
7
|
+
useRef,
|
|
8
|
+
useState,
|
|
9
|
+
} from 'react'
|
|
10
|
+
import PropTypes from 'prop-types'
|
|
11
|
+
import classNames from 'classnames'
|
|
12
|
+
|
|
13
|
+
import { useForkedRef } from '../../hooks'
|
|
14
|
+
import { isRTL } from '../../utils'
|
|
15
|
+
|
|
16
|
+
import { ThumbSize, type Label } from './types'
|
|
17
|
+
import {
|
|
18
|
+
calculateClickValue,
|
|
19
|
+
calculateLabelPosition,
|
|
20
|
+
calculateMoveValue,
|
|
21
|
+
calculateTooltipPosition,
|
|
22
|
+
getLabelValue,
|
|
23
|
+
getNearestValueIndex,
|
|
24
|
+
getThumbSize,
|
|
25
|
+
updateGradient,
|
|
26
|
+
updateValue,
|
|
27
|
+
validateValue,
|
|
28
|
+
} from './utils'
|
|
29
|
+
|
|
30
|
+
export interface CRangeSliderProps extends Omit<HTMLAttributes<HTMLDivElement>, 'onChange'> {
|
|
31
|
+
/**
|
|
32
|
+
* Customize the styling of your React Range Slider by adding a custom `className`.
|
|
33
|
+
* This allows you to apply additional CSS classes for enhanced design flexibility and integration with your existing stylesheets.
|
|
34
|
+
*/
|
|
35
|
+
className?: string
|
|
36
|
+
/**
|
|
37
|
+
* Enable or disable clickable labels in the React Range Slider.
|
|
38
|
+
* When set to `true`, users can click on labels to adjust the slider's value directly, enhancing interactivity and user experience.
|
|
39
|
+
*/
|
|
40
|
+
clickableLabels?: boolean
|
|
41
|
+
/**
|
|
42
|
+
* Control the interactive state of the React Range Slider with the `disabled` prop.
|
|
43
|
+
* Setting it to `true` will disable all slider functionalities, preventing user interaction and visually indicating a non-interactive state.
|
|
44
|
+
*/
|
|
45
|
+
disabled?: boolean
|
|
46
|
+
/**
|
|
47
|
+
* Define the minimum distance between slider handles using the `distance` prop in the React Range Slider.
|
|
48
|
+
* This ensures that the handles maintain a specified separation, preventing overlap and maintaining clear value distinctions.
|
|
49
|
+
*/
|
|
50
|
+
distance?: number
|
|
51
|
+
/**
|
|
52
|
+
* Add descriptive labels to your React Range Slider by providing an array of `labels`.
|
|
53
|
+
* These labels enhance the slider's usability by clearly indicating key values and providing contextual information to users.
|
|
54
|
+
*/
|
|
55
|
+
labels?: Label[]
|
|
56
|
+
/**
|
|
57
|
+
* Specify the maximum value for the React Range Slider with the `max` prop.
|
|
58
|
+
* This determines the upper limit of the slider's range, enabling precise control over the highest selectable value.
|
|
59
|
+
*/
|
|
60
|
+
max?: number
|
|
61
|
+
/**
|
|
62
|
+
* Set the minimum value for the React Range Slider using the `min` prop.
|
|
63
|
+
* This defines the lower bound of the slider's range, allowing you to control the starting point of user selection.
|
|
64
|
+
*/
|
|
65
|
+
min?: number
|
|
66
|
+
/**
|
|
67
|
+
* Assign a `name` to the React Range Slider for form integration.
|
|
68
|
+
* Whether using a single string or an array of strings, this prop ensures that the slider's values are correctly identified when submitting forms.
|
|
69
|
+
*/
|
|
70
|
+
name?: string | string[]
|
|
71
|
+
/**
|
|
72
|
+
* Control the granularity of the React Range Slider by setting the `step` prop.
|
|
73
|
+
* This defines the increment intervals between selectable values, allowing for precise adjustments based on your application's requirements.
|
|
74
|
+
*/
|
|
75
|
+
step?: number
|
|
76
|
+
/**
|
|
77
|
+
* Handle value changes in the React Range Slider by utilizing the `onChange` callback.
|
|
78
|
+
* This function is triggered whenever the slider's value updates, enabling you to manage state and respond to user interactions effectively.
|
|
79
|
+
*/
|
|
80
|
+
onChange?: (value: number[]) => void
|
|
81
|
+
/**
|
|
82
|
+
* Toggle the visibility of tooltips in the React Range Slider with the `tooltips` prop.
|
|
83
|
+
* When enabled, tooltips display the current value of the slider handles, providing real-time feedback to users.
|
|
84
|
+
*/
|
|
85
|
+
tooltips?: boolean
|
|
86
|
+
/**
|
|
87
|
+
* Customize the display format of tooltips in the React Range Slider using the `tooltipsFormat` function.
|
|
88
|
+
* This allows you to format the tooltip values according to your specific requirements, enhancing the clarity and presentation of information.
|
|
89
|
+
*/
|
|
90
|
+
tooltipsFormat?: (value: number) => ReactNode
|
|
91
|
+
/**
|
|
92
|
+
* Controls the visual representation of the slider's track. When set to `'fill'`, the track is dynamically filled based on the slider's value(s). Setting it to `false` disables the filled track.
|
|
93
|
+
*/
|
|
94
|
+
track?: 'fill' | boolean
|
|
95
|
+
/**
|
|
96
|
+
* Set the current value(s) of the React Range Slider using the `value` prop.
|
|
97
|
+
* Whether you're using a single value or an array for multi-handle sliders, this prop controls the slider's position and ensures it reflects the desired state.
|
|
98
|
+
*/
|
|
99
|
+
value?: number | number[]
|
|
100
|
+
/**
|
|
101
|
+
* Orient the React Range Slider vertically by setting the `vertical` prop to `true`.
|
|
102
|
+
* This changes the slider's layout from horizontal to vertical, providing a different aesthetic and fitting various UI designs.
|
|
103
|
+
*/
|
|
104
|
+
vertical?: boolean
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export const CRangeSlider = forwardRef<HTMLDivElement, CRangeSliderProps>(
|
|
108
|
+
(
|
|
109
|
+
{
|
|
110
|
+
className,
|
|
111
|
+
clickableLabels = true,
|
|
112
|
+
disabled = false,
|
|
113
|
+
distance = 0,
|
|
114
|
+
labels,
|
|
115
|
+
min = 0,
|
|
116
|
+
max = 100,
|
|
117
|
+
name,
|
|
118
|
+
step = 1,
|
|
119
|
+
value = [],
|
|
120
|
+
onChange,
|
|
121
|
+
tooltips = true,
|
|
122
|
+
tooltipsFormat,
|
|
123
|
+
track = 'fill',
|
|
124
|
+
vertical = false,
|
|
125
|
+
...rest
|
|
126
|
+
},
|
|
127
|
+
ref,
|
|
128
|
+
) => {
|
|
129
|
+
const rangeSliderRef = useRef<HTMLDivElement>(null)
|
|
130
|
+
const forkedRef = useForkedRef(ref, rangeSliderRef)
|
|
131
|
+
const inputsRef = useRef<HTMLInputElement[]>([])
|
|
132
|
+
const labelsContainerRef = useRef<HTMLDivElement>(null)
|
|
133
|
+
const labelsRef = useRef<HTMLDivElement[]>([])
|
|
134
|
+
const trackRef = useRef<HTMLDivElement>(null)
|
|
135
|
+
|
|
136
|
+
const [currentValue, setCurrentValue] = useState<number[]>(
|
|
137
|
+
Array.isArray(value) ? value : [value],
|
|
138
|
+
)
|
|
139
|
+
const [isDragging, setIsDragging] = useState(false)
|
|
140
|
+
const [dragIndex, setDragIndex] = useState(0)
|
|
141
|
+
const [thumbSize, setThumbSize] = useState<ThumbSize | null>()
|
|
142
|
+
|
|
143
|
+
useEffect(() => {
|
|
144
|
+
setCurrentValue(Array.isArray(value) ? value : [value])
|
|
145
|
+
}, [value])
|
|
146
|
+
|
|
147
|
+
useEffect(() => {
|
|
148
|
+
if (rangeSliderRef.current) {
|
|
149
|
+
setThumbSize(getThumbSize(rangeSliderRef.current, vertical))
|
|
150
|
+
}
|
|
151
|
+
}, [rangeSliderRef])
|
|
152
|
+
|
|
153
|
+
useEffect(() => {
|
|
154
|
+
const maxSize = Math.max(
|
|
155
|
+
...labelsRef.current.map((label) => (vertical ? label.offsetWidth : label.offsetHeight)),
|
|
156
|
+
)
|
|
157
|
+
|
|
158
|
+
if (labelsContainerRef.current) {
|
|
159
|
+
labelsContainerRef.current.style[vertical ? 'width' : 'height'] = `${maxSize}px`
|
|
160
|
+
}
|
|
161
|
+
}, [labelsRef])
|
|
162
|
+
|
|
163
|
+
useEffect(() => {
|
|
164
|
+
if (isDragging) {
|
|
165
|
+
document.addEventListener('mousemove', handleMouseMove)
|
|
166
|
+
document.addEventListener('mouseup', handleMouseUp)
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
return () => {
|
|
170
|
+
document.removeEventListener('mousemove', handleMouseMove)
|
|
171
|
+
document.removeEventListener('mouseup', handleMouseUp)
|
|
172
|
+
}
|
|
173
|
+
}, [currentValue])
|
|
174
|
+
|
|
175
|
+
const updateNearestValue = (value: number) => {
|
|
176
|
+
const nearestIndex = getNearestValueIndex(value, currentValue)
|
|
177
|
+
const newCurrentValue = [...currentValue]
|
|
178
|
+
newCurrentValue[nearestIndex] = validateValue(value, currentValue, distance, nearestIndex)
|
|
179
|
+
|
|
180
|
+
setTimeout(() => {
|
|
181
|
+
inputsRef.current[nearestIndex].focus()
|
|
182
|
+
})
|
|
183
|
+
|
|
184
|
+
setCurrentValue(newCurrentValue)
|
|
185
|
+
|
|
186
|
+
if (onChange) {
|
|
187
|
+
onChange(newCurrentValue)
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
const handleInputChange = (event: ChangeEvent<HTMLInputElement>, index: number) => {
|
|
192
|
+
setIsDragging(false)
|
|
193
|
+
|
|
194
|
+
const target = event.target
|
|
195
|
+
const value = Number(target.value)
|
|
196
|
+
|
|
197
|
+
const newCurrentValue = updateValue(value, currentValue, distance, index)
|
|
198
|
+
|
|
199
|
+
setCurrentValue(newCurrentValue)
|
|
200
|
+
|
|
201
|
+
// Trigger change event if needed
|
|
202
|
+
if (onChange) {
|
|
203
|
+
onChange(newCurrentValue)
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
const handleInputsContainerMouseDown = (event: React.MouseEvent<HTMLDivElement>) => {
|
|
208
|
+
if (trackRef.current === null) {
|
|
209
|
+
return
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
const clickValue = calculateClickValue(
|
|
213
|
+
event,
|
|
214
|
+
trackRef.current,
|
|
215
|
+
min,
|
|
216
|
+
max,
|
|
217
|
+
step,
|
|
218
|
+
vertical,
|
|
219
|
+
isRTL(rangeSliderRef.current),
|
|
220
|
+
)
|
|
221
|
+
|
|
222
|
+
const index = getNearestValueIndex(clickValue, currentValue)
|
|
223
|
+
|
|
224
|
+
setIsDragging(true)
|
|
225
|
+
setDragIndex(index)
|
|
226
|
+
updateNearestValue(clickValue)
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
const handleLabelClick = (value: number) => {
|
|
230
|
+
if (!clickableLabels || disabled) {
|
|
231
|
+
return
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
updateNearestValue(value)
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
const handleMouseMove = (event: MouseEvent) => {
|
|
238
|
+
if (!isDragging || trackRef.current === null) {
|
|
239
|
+
return
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
const moveValue = calculateMoveValue(
|
|
243
|
+
event,
|
|
244
|
+
trackRef.current,
|
|
245
|
+
min,
|
|
246
|
+
max,
|
|
247
|
+
step,
|
|
248
|
+
vertical,
|
|
249
|
+
isRTL(rangeSliderRef.current),
|
|
250
|
+
)
|
|
251
|
+
|
|
252
|
+
const newCurrentValue = updateValue(moveValue, currentValue, distance, dragIndex)
|
|
253
|
+
|
|
254
|
+
setCurrentValue(newCurrentValue)
|
|
255
|
+
|
|
256
|
+
if (onChange) {
|
|
257
|
+
onChange(newCurrentValue)
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
const handleMouseUp = () => {
|
|
262
|
+
setIsDragging(false)
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
return (
|
|
266
|
+
<div
|
|
267
|
+
className={classNames('range-slider', className, {
|
|
268
|
+
'range-slider-vertical': vertical,
|
|
269
|
+
disabled,
|
|
270
|
+
})}
|
|
271
|
+
{...rest}
|
|
272
|
+
ref={forkedRef}
|
|
273
|
+
>
|
|
274
|
+
<div className="range-slider-inputs-container" onMouseDown={handleInputsContainerMouseDown}>
|
|
275
|
+
{currentValue.map((value, index) => (
|
|
276
|
+
<React.Fragment key={index}>
|
|
277
|
+
<input
|
|
278
|
+
className="range-slider-input"
|
|
279
|
+
type="range"
|
|
280
|
+
min={min}
|
|
281
|
+
max={max}
|
|
282
|
+
step={step}
|
|
283
|
+
value={value}
|
|
284
|
+
name={Array.isArray(name) ? name[index] : `${name || ''}-${index}}`}
|
|
285
|
+
role="slider"
|
|
286
|
+
aria-valuemin={min}
|
|
287
|
+
aria-valuemax={max}
|
|
288
|
+
aria-valuenow={value}
|
|
289
|
+
aria-orientation={vertical ? 'vertical' : 'horizontal'}
|
|
290
|
+
disabled={disabled}
|
|
291
|
+
onChange={(event) => handleInputChange(event, index)}
|
|
292
|
+
ref={(el: HTMLInputElement) => {
|
|
293
|
+
inputsRef.current[index] = el
|
|
294
|
+
}}
|
|
295
|
+
/>
|
|
296
|
+
{tooltips && (
|
|
297
|
+
<div
|
|
298
|
+
className="range-slider-tooltip"
|
|
299
|
+
{...(thumbSize && {
|
|
300
|
+
style: calculateTooltipPosition(
|
|
301
|
+
min,
|
|
302
|
+
max,
|
|
303
|
+
value,
|
|
304
|
+
thumbSize,
|
|
305
|
+
vertical,
|
|
306
|
+
isRTL(rangeSliderRef.current),
|
|
307
|
+
),
|
|
308
|
+
})}
|
|
309
|
+
>
|
|
310
|
+
<div className="range-slider-tooltip-inner">
|
|
311
|
+
{tooltipsFormat ? tooltipsFormat(value) : value}
|
|
312
|
+
</div>
|
|
313
|
+
<div className="range-slider-tooltip-arrow"></div>
|
|
314
|
+
</div>
|
|
315
|
+
)}
|
|
316
|
+
</React.Fragment>
|
|
317
|
+
))}
|
|
318
|
+
<div
|
|
319
|
+
className="range-slider-track"
|
|
320
|
+
{...(track && { style: updateGradient(min, max, currentValue, vertical) })}
|
|
321
|
+
ref={trackRef}
|
|
322
|
+
></div>
|
|
323
|
+
</div>
|
|
324
|
+
{labels && (
|
|
325
|
+
<div className="range-slider-labels-container" ref={labelsContainerRef}>
|
|
326
|
+
{Array.isArray(labels) &&
|
|
327
|
+
labels.map((label: Label, index) => {
|
|
328
|
+
const labelPosition = calculateLabelPosition(min, max, labels, label, index)
|
|
329
|
+
const labelValue = getLabelValue(min, max, labels, label, index)
|
|
330
|
+
const labelStyle = Object.assign(
|
|
331
|
+
vertical ? { bottom: labelPosition } : { left: labelPosition },
|
|
332
|
+
typeof label === 'object' && 'style' in label && label.style,
|
|
333
|
+
)
|
|
334
|
+
return (
|
|
335
|
+
<div
|
|
336
|
+
className={classNames(
|
|
337
|
+
'range-slider-label',
|
|
338
|
+
{
|
|
339
|
+
clickable: clickableLabels,
|
|
340
|
+
},
|
|
341
|
+
typeof label === 'object' && 'className' in label && label.className,
|
|
342
|
+
)}
|
|
343
|
+
style={labelStyle}
|
|
344
|
+
onMouseDown={() => handleLabelClick(labelValue)}
|
|
345
|
+
key={index}
|
|
346
|
+
ref={(el: HTMLDivElement) => {
|
|
347
|
+
labelsRef.current[index] = el
|
|
348
|
+
}}
|
|
349
|
+
>
|
|
350
|
+
{typeof label === 'object' && 'label' in label ? label.label : label}
|
|
351
|
+
</div>
|
|
352
|
+
)
|
|
353
|
+
})}
|
|
354
|
+
</div>
|
|
355
|
+
)}
|
|
356
|
+
</div>
|
|
357
|
+
)
|
|
358
|
+
},
|
|
359
|
+
)
|
|
360
|
+
|
|
361
|
+
CRangeSlider.propTypes = {
|
|
362
|
+
clickableLabels: PropTypes.bool,
|
|
363
|
+
disabled: PropTypes.bool,
|
|
364
|
+
distance: PropTypes.number,
|
|
365
|
+
labels: PropTypes.any,
|
|
366
|
+
max: PropTypes.number,
|
|
367
|
+
min: PropTypes.number,
|
|
368
|
+
name: PropTypes.oneOfType([PropTypes.array, PropTypes.string]),
|
|
369
|
+
step: PropTypes.number,
|
|
370
|
+
tooltips: PropTypes.bool,
|
|
371
|
+
tooltipsFormat: PropTypes.func,
|
|
372
|
+
track: PropTypes.oneOfType([PropTypes.bool, PropTypes.oneOf<'fill'>(['fill'])]),
|
|
373
|
+
value: PropTypes.oneOfType([PropTypes.array, PropTypes.number]),
|
|
374
|
+
vertical: PropTypes.bool,
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
CRangeSlider.displayName = 'CRangeSlider'
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { CSSProperties, ReactElement } from 'react'
|
|
2
|
+
|
|
3
|
+
export type Label =
|
|
4
|
+
| {
|
|
5
|
+
label: number | ReactElement | string
|
|
6
|
+
value: number
|
|
7
|
+
className?: string | string[]
|
|
8
|
+
style?: CSSProperties
|
|
9
|
+
}
|
|
10
|
+
| ReactElement
|
|
11
|
+
| string
|
|
12
|
+
|
|
13
|
+
export type ThumbSize = {
|
|
14
|
+
value: number
|
|
15
|
+
unit: string | null
|
|
16
|
+
}
|
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
import React from 'react'
|
|
2
|
+
import type { Label, ThumbSize } from './types'
|
|
3
|
+
|
|
4
|
+
export const calculateClickValue = (
|
|
5
|
+
event: React.MouseEvent,
|
|
6
|
+
container: HTMLDivElement,
|
|
7
|
+
min: number,
|
|
8
|
+
max: number,
|
|
9
|
+
step: number,
|
|
10
|
+
vertical: boolean,
|
|
11
|
+
rtl: boolean,
|
|
12
|
+
) => {
|
|
13
|
+
const clickPosition = getClickPosition(event, container, vertical, rtl)
|
|
14
|
+
const value = min + clickPosition * (max - min)
|
|
15
|
+
return roundToStep(value, step)
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export const calculateMoveValue = (
|
|
19
|
+
event: MouseEvent,
|
|
20
|
+
container: HTMLDivElement,
|
|
21
|
+
min: number,
|
|
22
|
+
max: number,
|
|
23
|
+
step: number,
|
|
24
|
+
vertical: boolean,
|
|
25
|
+
rtl: boolean,
|
|
26
|
+
) => {
|
|
27
|
+
const rect = container.getBoundingClientRect()
|
|
28
|
+
const position = vertical
|
|
29
|
+
? calculateVerticalPosition(event.clientY, rect)
|
|
30
|
+
: calculateHorizontalPosition(event.clientX, rect, rtl)
|
|
31
|
+
|
|
32
|
+
if (typeof position === 'string') {
|
|
33
|
+
return position === 'max' ? max : min
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const value = min + position * (max - min)
|
|
37
|
+
|
|
38
|
+
return roundToStep(value, step)
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export const calculateVerticalPosition = (mouseY: number, rect: DOMRect) => {
|
|
42
|
+
if (mouseY < rect.top) {
|
|
43
|
+
return 'max'
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if (mouseY > rect.bottom) {
|
|
47
|
+
return 'min'
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
return Math.min(Math.max((rect.bottom - mouseY) / rect.height, 0), 1)
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export const calculateHorizontalPosition = (mouseX: number, rect: DOMRect, rtl: boolean) => {
|
|
54
|
+
if (mouseX < rect.left) {
|
|
55
|
+
return rtl ? 'max' : 'min'
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
if (mouseX > rect.right) {
|
|
59
|
+
return rtl ? 'min' : 'max'
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const relativeX = rtl ? rect.right - mouseX : mouseX - rect.left
|
|
63
|
+
return Math.min(Math.max(relativeX / rect.width, 0), 1)
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export const calculateLabelPosition = (
|
|
67
|
+
min: number,
|
|
68
|
+
max: number,
|
|
69
|
+
labels: Label[],
|
|
70
|
+
label: Label,
|
|
71
|
+
index: number,
|
|
72
|
+
) => {
|
|
73
|
+
if (typeof label === 'object' && 'value' in label) {
|
|
74
|
+
return `${((label.value - min) / (max - min)) * 100}%`
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
return `${(index / (labels.length - 1)) * 100}%`
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export const calculateTooltipPosition = (
|
|
81
|
+
min: number,
|
|
82
|
+
max: number,
|
|
83
|
+
value: number,
|
|
84
|
+
thumbSize: ThumbSize,
|
|
85
|
+
vertical: boolean,
|
|
86
|
+
rtl: boolean,
|
|
87
|
+
) => {
|
|
88
|
+
const percent = (value - min) / (max - min)
|
|
89
|
+
const margin =
|
|
90
|
+
percent > 0.5
|
|
91
|
+
? `-${(percent - 0.5) * thumbSize.value}${thumbSize.unit}`
|
|
92
|
+
: `${(0.5 - percent) * thumbSize.value}${thumbSize.unit}`
|
|
93
|
+
|
|
94
|
+
if (vertical) {
|
|
95
|
+
return {
|
|
96
|
+
bottom: `${percent * 100}%`,
|
|
97
|
+
marginBottom: margin,
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
return rtl
|
|
102
|
+
? { right: `${percent * 100}%`, marginRight: margin }
|
|
103
|
+
: { left: `${percent * 100}%`, marginLeft: margin }
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export const getClickPosition = (
|
|
107
|
+
event: React.MouseEvent,
|
|
108
|
+
container: HTMLDivElement,
|
|
109
|
+
vertical: boolean,
|
|
110
|
+
rtl: boolean,
|
|
111
|
+
) => {
|
|
112
|
+
// MouseEvent doesn't have `offsetX`/`offsetY`, so we need to use `event.nativeEvent` instead
|
|
113
|
+
const { offsetX, offsetY } = event.nativeEvent
|
|
114
|
+
const { offsetWidth, offsetHeight } = container
|
|
115
|
+
|
|
116
|
+
if (vertical) {
|
|
117
|
+
return 1 - offsetY / offsetHeight
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
return rtl ? 1 - offsetX / offsetWidth : offsetX / offsetWidth
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
export const getLabelValue = (
|
|
124
|
+
min: number,
|
|
125
|
+
max: number,
|
|
126
|
+
labels: Label[],
|
|
127
|
+
label: Label,
|
|
128
|
+
index: number,
|
|
129
|
+
) =>
|
|
130
|
+
typeof label === 'object' && 'value' in label
|
|
131
|
+
? label.value
|
|
132
|
+
: min + (index / (labels.length - 1)) * (max - min)
|
|
133
|
+
|
|
134
|
+
export const getNearestValueIndex = (value: number, values: number[]) => {
|
|
135
|
+
const valuesLength = values.length
|
|
136
|
+
|
|
137
|
+
if (value < values[0]) {
|
|
138
|
+
return 0
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
if (value > values[valuesLength - 1]) {
|
|
142
|
+
return valuesLength - 1
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
const distances = values.map((v) => Math.abs(v - value))
|
|
146
|
+
const min = Math.min(...distances)
|
|
147
|
+
const firstIndex = distances.indexOf(min)
|
|
148
|
+
|
|
149
|
+
return value < values[firstIndex] ? firstIndex : distances.lastIndexOf(min)
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
export const getThumbSize = (element: HTMLDivElement, vertical: boolean): ThumbSize | null => {
|
|
153
|
+
const value = globalThis
|
|
154
|
+
.getComputedStyle(element, null)
|
|
155
|
+
.getPropertyValue(
|
|
156
|
+
vertical ? '--cui-range-slider-thumb-height' : '--cui-range-slider-thumb-width',
|
|
157
|
+
)
|
|
158
|
+
|
|
159
|
+
const regex = /^(\d+\.?\d*)([%a-z]*)$/i
|
|
160
|
+
const match = value.match(regex)
|
|
161
|
+
|
|
162
|
+
if (match) {
|
|
163
|
+
return {
|
|
164
|
+
value: Number.parseFloat(match[1]),
|
|
165
|
+
unit: match[2] || null,
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
return null
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
export const roundToStep = (number: number, step: number) => {
|
|
173
|
+
const _step = step === 0 ? 1 : step
|
|
174
|
+
return Math.round(number / _step) * _step
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
export const updateGradient = (min: number, max: number, values: number[], vertical: boolean) => {
|
|
178
|
+
const minVal = Math.min(...values)
|
|
179
|
+
const maxVal = Math.max(...values)
|
|
180
|
+
|
|
181
|
+
const from = ((minVal - min) / (max - min)) * 100
|
|
182
|
+
const to = ((maxVal - min) / (max - min)) * 100
|
|
183
|
+
|
|
184
|
+
const direction = vertical ? 'to top' : 'to right'
|
|
185
|
+
|
|
186
|
+
return {
|
|
187
|
+
backgroundImage:
|
|
188
|
+
values.length === 1
|
|
189
|
+
? `linear-gradient(
|
|
190
|
+
${direction},
|
|
191
|
+
var(--cui-range-slider-track-in-range-bg) 0%,
|
|
192
|
+
var(--cui-range-slider-track-in-range-bg) ${to}%,
|
|
193
|
+
transparent ${to}%,
|
|
194
|
+
transparent 100%
|
|
195
|
+
)`
|
|
196
|
+
: `linear-gradient(
|
|
197
|
+
${direction},
|
|
198
|
+
transparent 0%,
|
|
199
|
+
transparent ${from}%,
|
|
200
|
+
var(--cui-range-slider-track-in-range-bg) ${from}%,
|
|
201
|
+
var(--cui-range-slider-track-in-range-bg) ${to}%,
|
|
202
|
+
transparent ${to}%,
|
|
203
|
+
transparent 100%
|
|
204
|
+
)`,
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
export const updateValue = (value: number, values: number[], distance: number, index: number) => {
|
|
209
|
+
const newValue = [...values]
|
|
210
|
+
newValue[index] = validateValue(value, values, distance, index)
|
|
211
|
+
return newValue
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
export const validateValue = (value: number, values: number[], distance: number, index: number) => {
|
|
215
|
+
// If there's only one value, return it as is
|
|
216
|
+
if (values.length === 1) {
|
|
217
|
+
return value
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// Determine previous and next values if they exist
|
|
221
|
+
const prevValue: number | undefined = index > 0 ? values[index - 1] : undefined
|
|
222
|
+
const nextValue: number | undefined = index < values.length - 1 ? values[index + 1] : undefined
|
|
223
|
+
|
|
224
|
+
// If it's the first element, ensure it's not too close to the next value
|
|
225
|
+
if (index === 0 && nextValue !== undefined) {
|
|
226
|
+
return Math.min(value, nextValue - distance)
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// If it's the last element, ensure it's not too close to the previous value
|
|
230
|
+
if (index === values.length - 1 && prevValue !== undefined) {
|
|
231
|
+
return Math.max(value, prevValue + distance)
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// For middle elements, ensure the value is within the allowed distance from both neighbors
|
|
235
|
+
if (prevValue !== undefined && nextValue !== undefined) {
|
|
236
|
+
const minVal = prevValue + distance
|
|
237
|
+
const maxVal = nextValue - distance
|
|
238
|
+
return Math.min(Math.max(value, minVal), maxVal)
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// Fallback: If for some reason prevValue or nextValue is undefined, return the original value
|
|
242
|
+
return value
|
|
243
|
+
}
|