UNPKG

@coreui/vue-pro

Version:

UI Components Library for Vue.js

438 lines (401 loc) 14.1 kB
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 }