@coreui/vue-pro
Version:
UI Components Library for Vue.js
438 lines (401 loc) • 14.1 kB
text/typescript
import { defineComponent, ref, watch, onMounted, h, PropType, VNode } from 'vue'
import {
calculateClickValue,
calculateLabelPosition,
calculateMoveValue,
calculateTooltipPosition,
getLabelValue,
getNearestValueIndex,
getThumbSize,
updateGradient,
updateValue,
validateValue,
} from './utils'
import type { Label, ThumbSize } from './types'
import { isRTL } from '../../utils'
const CRangeSlider = defineComponent({
name: 'CRangeSlider',
props: {
/**
* Enable or disable clickable labels in the Vue Range Slider.
* When set to `true`, users can click on labels to adjust the slider's value directly, enhancing interactivity and user experience.
*/
clickableLabels: {
type: Boolean,
default: true,
},
/**
* Control the interactive state of the Vue Range Slider with the `disabled` prop.
* Setting it to `true` will disable all slider functionalities, preventing user interaction and visually indicating a non-interactive state.
*/
disabled: {
type: Boolean,
default: false,
},
/**
* Define the minimum distance between slider handles using the `distance` prop in the Vue Range Slider.
* This ensures that the handles maintain a specified separation, preventing overlap and maintaining clear value distinctions.
*/
distance: {
type: Number,
default: 0,
},
/**
* Add descriptive labels to your Vue Range Slider by providing an array of `labels`.
* These labels enhance the slider's usability by clearly indicating key values and providing contextual information to users.
*/
labels: {
type: Array as PropType<Label[]>,
default: () => [],
},
/**
* Specify the maximum value for the Vue Range Slider with the `max` prop.
* This determines the upper limit of the slider's range, enabling precise control over the highest selectable value.
*/
max: {
type: Number,
default: 100,
},
/**
* Set the minimum value for the Vue Range Slider using the `min` prop.
* This defines the lower bound of the slider's range, allowing you to control the starting point of user selection.
*/
min: {
type: Number,
default: 0,
},
/**
* The default name for a value passed using v-model.
*/
modelValue: [Number, Array] as PropType<number | number[]>,
/**
* Assign a `name` to the Vue Range Slider for form integration.
* Whether using a single string or an array of strings, this prop ensures that the slider's values are correctly identified when submitting forms.
*/
name: {
type: [String, Array] as PropType<string | string[]>,
default: '',
},
/**
* Control the granularity of the Vue Range Slider by setting the `step` prop.
* This defines the increment intervals between selectable values, allowing for precise adjustments based on your application's requirements.
*/
step: {
type: Number,
default: 1,
},
/**
* Toggle the visibility of tooltips in the Vue Range Slider with the `tooltips` prop.
* When enabled, tooltips display the current value of the slider handles, providing real-time feedback to users.
*/
tooltips: {
type: Boolean,
default: true,
},
/**
* Customize the display format of tooltips in the Vue Range Slider using the `tooltipsFormat` function.
* This allows you to format the tooltip values according to your specific requirements, enhancing the clarity and presentation of information.
*/
tooltipsFormat: {
type: Function as PropType<(value: number) => string | VNode>,
default: null,
},
/**
* 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.
*/
track: {
type: [Boolean, String],
default: 'fill',
validator: (value: boolean | string) => {
return typeof value === 'boolean' || value === 'fill'
},
},
/**
* Set the current value(s) of the Vue Range Slider using the `value` prop.
* 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.
*/
value: {
type: [Number, Array] as PropType<number | number[]>,
default: () => [0],
},
/**
* Orient the Vue Range Slider vertically by setting the `vertical` prop to `true`.
* This changes the slider's layout from horizontal to vertical, providing a different aesthetic and fitting various UI designs.
*/
vertical: {
type: Boolean,
default: false,
},
},
emits: [
'change',
/**
* Emit the new value whenever there’s a change event.
*/
'update:modelValue',
],
setup(props, { emit }) {
const rangeSliderRef = ref<HTMLDivElement | null>(null)
const inputsRef = ref<HTMLInputElement[]>([])
const labelsContainerRef = ref<HTMLDivElement | null>(null)
const labelsRef = ref<HTMLDivElement[]>([])
const trackRef = ref<HTMLDivElement | null>(null)
const currentValue = ref<number[]>(
props.modelValue
? Array.isArray(props.modelValue)
? props.modelValue
: [props.modelValue]
: Array.isArray(props.value)
? props.value
: [props.value],
)
const isDragging = ref(false)
const _isRTL = ref(false)
const dragIndex = ref(0)
const thumbSize = ref<ThumbSize | null>()
watch(
() => props.value,
(newVal) => {
currentValue.value = Array.isArray(newVal) ? newVal : [newVal]
},
)
watch(
() => props.modelValue,
(newVal) => {
if (newVal !== undefined) {
currentValue.value = Array.isArray(newVal) ? newVal : [newVal]
}
},
)
// Adjust labels container size based on labels
onMounted(() => {
const maxSize = Math.max(
...labelsRef.value.map((label) =>
props.vertical ? label.offsetWidth : label.offsetHeight,
),
)
if (labelsContainerRef.value) {
labelsContainerRef.value.style[props.vertical ? 'width' : 'height'] = `${maxSize}px`
}
if (rangeSliderRef.value) {
_isRTL.value = isRTL(rangeSliderRef.value)
thumbSize.value = getThumbSize(rangeSliderRef.value, props.vertical)
}
})
watch(isDragging, (newVal) => {
if (newVal) {
globalThis.addEventListener('mousemove', handleMouseMove)
globalThis.addEventListener('mouseup', handleMouseUp)
} else {
globalThis.removeEventListener('mousemove', handleMouseMove)
globalThis.removeEventListener('mouseup', handleMouseUp)
}
})
const updateNearestValue = (value: number) => {
const nearestIndex = getNearestValueIndex(value, currentValue.value)
const newCurrentValue = [...currentValue.value]
newCurrentValue[nearestIndex] = validateValue(
value,
currentValue.value,
props.distance,
nearestIndex,
)
setTimeout(() => {
if (inputsRef.value[nearestIndex]) {
inputsRef.value[nearestIndex].focus()
}
}, 0)
currentValue.value = newCurrentValue
emit('change', newCurrentValue)
emit('update:modelValue', newCurrentValue)
}
const handleInputChange = (event: Event, index: number) => {
if (props.disabled) return
const target = event.target as HTMLInputElement
const value = Number(target.value)
const newCurrentValue = updateValue(value, currentValue.value, props.distance, index)
currentValue.value = newCurrentValue
emit('change', newCurrentValue)
emit('update:modelValue', newCurrentValue)
}
const handleInputsContainerMouseDown = (event: MouseEvent) => {
if (!trackRef.value || event.button !== 0 || props.disabled) return
const target = event.target as HTMLDivElement | HTMLInputElement
if (!(target instanceof HTMLInputElement) && target !== trackRef.value) {
return
}
const clickValue = calculateClickValue(
event,
trackRef.value,
props.min,
props.max,
props.step,
props.vertical,
_isRTL.value,
)
const index = getNearestValueIndex(clickValue, currentValue.value)
isDragging.value = true
dragIndex.value = index
updateNearestValue(clickValue)
}
const handleLabelClick = (event: MouseEvent, value: number) => {
if (!props.clickableLabels || props.disabled || event.button !== 0) return
updateNearestValue(value)
}
const handleMouseMove = (event: MouseEvent) => {
if (!isDragging.value || !trackRef.value || props.disabled) return
const moveValue = calculateMoveValue(
event,
trackRef.value,
props.min,
props.max,
props.step,
props.vertical,
_isRTL.value,
)
const newCurrentValue = updateValue(
moveValue,
currentValue.value,
props.distance,
dragIndex.value,
)
currentValue.value = newCurrentValue
emit('change', newCurrentValue)
emit('update:modelValue', newCurrentValue)
}
const handleMouseUp = () => {
isDragging.value = false
}
return () =>
h(
'div',
{
class: [
'range-slider',
{
'range-slider-vertical': props.vertical,
disabled: props.disabled,
},
],
ref: rangeSliderRef,
},
[
h(
'div',
{
class: 'range-slider-inputs-container',
onMousedown: handleInputsContainerMouseDown,
},
[
currentValue.value.map((value, index) => [
h('input', {
class: 'range-slider-input',
type: 'range',
min: props.min,
max: props.max,
step: props.step,
value: value,
name: Array.isArray(props.name)
? props.name[index]
: `${props.name || ''}-${index}`,
role: 'slider',
'aria-valuemin': props.min,
'aria-valuemax': props.max,
'aria-valuenow': value,
'aria-orientation': props.vertical ? 'vertical' : 'horizontal',
disabled: props.disabled,
onInput: (e: Event) => handleInputChange(e, index),
ref: (el) => {
inputsRef.value[index] = el as HTMLInputElement
},
}),
props.tooltips &&
h(
'div',
{
class: 'range-slider-tooltip',
...(thumbSize.value && {
style: calculateTooltipPosition(
props.min,
props.max,
value,
thumbSize.value,
props.vertical,
_isRTL.value,
),
}),
},
[
h('div', { class: 'range-slider-tooltip-inner' }, [
props.tooltipsFormat ? props.tooltipsFormat(value) : value,
]),
h('div', { class: 'range-slider-tooltip-arrow' }),
],
),
]),
h('div', {
class: 'range-slider-track',
...(props.track && {
style: updateGradient(
props.min,
props.max,
currentValue.value,
props.vertical,
_isRTL.value,
),
}),
ref: trackRef,
}),
],
),
Array.isArray(props.labels) &&
props.labels.length > 0 &&
h(
'div',
{
class: 'range-slider-labels-container',
ref: labelsContainerRef,
},
props.labels.map((label, index) => {
const labelPosition = calculateLabelPosition(
props.min,
props.max,
props.labels,
label,
index,
)
const labelValue = getLabelValue(props.min, props.max, props.labels, label, index)
const labelStyle = {
...(props.vertical
? { bottom: labelPosition }
: _isRTL.value
? { right: labelPosition }
: { left: labelPosition }),
...(typeof label === 'object' && 'style' in label ? label.style : {}),
}
return h(
'div',
{
class: [
'range-slider-label',
{
clickable: props.clickableLabels,
},
typeof label === 'object' && 'className' in label ? label.className : '',
],
style: labelStyle,
onMousedown: (event) => handleLabelClick(event, labelValue),
key: index,
ref: (el) => {
labelsRef.value[index] = el as HTMLDivElement
},
},
typeof label === 'object' && 'label' in label ? label.label : label,
)
}),
),
],
)
},
})
export { CRangeSlider }