UNPKG

@coreui/coreui-pro

Version:

The most popular front-end framework for developing responsive, mobile-first projects on the web rewritten by the CoreUI Team

557 lines (533 loc) 20.9 kB
/*! * CoreUI range-slider.js v5.14.2 (https://coreui.io) * Copyright 2025 The CoreUI Team (https://github.com/orgs/coreui/people) * Licensed under MIT (https://github.com/coreui/coreui/blob/main/LICENSE) */ (function (global, factory) { typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory(require('./base-component.js'), require('./dom/event-handler.js'), require('./dom/manipulator.js'), require('./dom/selector-engine.js'), require('./util/index.js')) : typeof define === 'function' && define.amd ? define(['./base-component', './dom/event-handler', './dom/manipulator', './dom/selector-engine', './util/index'], factory) : (global = typeof globalThis !== 'undefined' ? globalThis : global || self, global.RangeSlider = factory(global.BaseComponent, global.EventHandler, global.Manipulator, global.SelectorEngine, global.Index)); })(this, (function (BaseComponent, EventHandler, Manipulator, SelectorEngine, index_js) { 'use strict'; /** * -------------------------------------------------------------------------- * CoreUI PRO range-slider.js * License (https://coreui.io/pro/license/) * -------------------------------------------------------------------------- */ /** * Constants */ const NAME = 'range-slider'; const DATA_KEY = 'coreui.range-slider'; const EVENT_KEY = `.${DATA_KEY}`; const DATA_API_KEY = '.data-api'; const EVENT_CHANGE = `change${EVENT_KEY}`; const EVENT_INPUT = 'input'; const EVENT_LOAD_DATA_API = `load${EVENT_KEY}${DATA_API_KEY}`; const EVENT_MOUSEDOWN = `mousedown${EVENT_KEY}`; const EVENT_MOUSEMOVE = `mousemove${EVENT_KEY}`; const EVENT_MOUSEUP = `mouseup${EVENT_KEY}`; const EVENT_RESIZE = `resize${EVENT_KEY}`; const CLASS_NAME_CLICKABLE = 'clickable'; const CLASS_NAME_DISABLED = 'disabled'; const CLASS_NAME_RANGE_SLIDER = 'range-slider'; const CLASS_NAME_RANGE_SLIDER_INPUT = 'range-slider-input'; const CLASS_NAME_RANGE_SLIDER_INPUTS_CONTAINER = 'range-slider-inputs-container'; const CLASS_NAME_RANGE_SLIDER_LABEL = 'range-slider-label'; const CLASS_NAME_RANGE_SLIDER_LABELS_CONTAINER = 'range-slider-labels-container'; const CLASS_NAME_RANGE_SLIDER_TOOLTIP = 'range-slider-tooltip'; const CLASS_NAME_RANGE_SLIDER_TOOLTIP_ARROW = 'range-slider-tooltip-arrow'; const CLASS_NAME_RANGE_SLIDER_TOOLTIP_INNER = 'range-slider-tooltip-inner'; const CLASS_NAME_RANGE_SLIDER_TRACK = 'range-slider-track'; const CLASS_NAME_RANGE_SLIDER_VERTICAL = 'range-slider-vertical'; const SELECTOR_DATA_TOGGLE = '[data-coreui-toggle="range-slider"]'; const SELECTOR_RANGE_SLIDER_INPUT = '.range-slider-input'; const SELECTOR_RANGE_SLIDER_INPUTS_CONTAINER = '.range-slider-inputs-container'; const SELECTOR_RANGE_SLIDER_LABEL = '.range-slider-label'; const SELECTOR_RANGE_SLIDER_LABELS_CONTAINER = '.range-slider-labels-container'; const Default = { clickableLabels: true, disabled: false, distance: 0, labels: false, max: 100, min: 0, name: null, step: 1, tooltips: true, tooltipsFormat: null, track: 'fill', value: 0, vertical: false }; const DefaultType = { clickableLabels: 'boolean', disabled: 'boolean', distance: 'number', labels: '(array|boolean|string)', max: 'number', min: 'number', name: '(array|string|null)', step: '(number|string)', tooltips: 'boolean', tooltipsFormat: '(function|null)', track: '(boolean|string)', value: '(array|number)', vertical: 'boolean' }; /** * Class definition */ class RangeSlider extends BaseComponent { constructor(element, config) { super(element); this._config = this._getConfig(config); this._currentValue = this._config.value; this._dragIndex = 0; this._inputs = []; this._isDragging = false; this._sliderTrack = null; this._thumbSize = null; this._tooltips = []; this._initializeRangeSlider(); } // Getters static get Default() { return Default; } static get DefaultType() { return DefaultType; } static get NAME() { return NAME; } // Public update(config) { this._config = this._getConfig(config); this._currentValue = this._config.value; this._element.innerHTML = ''; this._initializeRangeSlider(); } // Private _addEventListeners() { if (this._config.disabled) { return; } EventHandler.on(this._element, EVENT_INPUT, SELECTOR_RANGE_SLIDER_INPUT, event => { const { target } = event; this._isDragging = false; const children = SelectorEngine.children(target.parentElement, SELECTOR_RANGE_SLIDER_INPUT); const index = Array.from(children).indexOf(target); this._updateValue(target.value, index); }); EventHandler.on(this._element, EVENT_MOUSEDOWN, SELECTOR_RANGE_SLIDER_LABEL, event => { if (!this._config.clickableLabels || event.button !== 0) { return; } const value = Manipulator.getDataAttribute(event.target, 'value'); this._updateNearestValue(value); }); EventHandler.on(this._element, EVENT_MOUSEDOWN, SELECTOR_RANGE_SLIDER_INPUTS_CONTAINER, event => { if (event.button !== 0) { return; } if (!(event.target instanceof HTMLInputElement) && !event.target.className.includes(CLASS_NAME_RANGE_SLIDER_TRACK)) { return; } this._isDragging = true; const clickValue = this._calculateClickValue(event); this._dragIndex = this._getNearestValueIndex(clickValue); this._updateNearestValue(clickValue); }); EventHandler.on(document.documentElement, EVENT_MOUSEUP, () => { this._isDragging = false; }); EventHandler.on(document.documentElement, EVENT_MOUSEMOVE, event => { if (!this._isDragging) { return; } const moveValue = this._calculateMoveValue(event); this._updateValue(moveValue, this._dragIndex); }); EventHandler.on(window, EVENT_RESIZE, () => { this._updateLabelsContainerSize(); }); } _initializeRangeSlider() { this._element.classList.add(CLASS_NAME_RANGE_SLIDER); if (this._config.vertical) { this._element.classList.add(CLASS_NAME_RANGE_SLIDER_VERTICAL); } if (this._config.disabled) { this._element.classList.add(CLASS_NAME_DISABLED); } this._sliderTrack = this._createSliderTrack(); this._createInputs(); this._createLabels(); this._updateLabelsContainerSize(); this._createTooltips(); this._updateGradient(); this._addEventListeners(); } _createSliderTrack() { const sliderTrackElement = this._createElement('div', CLASS_NAME_RANGE_SLIDER_TRACK); return sliderTrackElement; } _createInputs() { const container = this._createElement('div', CLASS_NAME_RANGE_SLIDER_INPUTS_CONTAINER); for (const [index, value] of this._currentValue.entries()) { const inputElement = this._createInput(index, value); container.append(inputElement); this._inputs[index] = inputElement; } container.append(this._sliderTrack); this._element.append(container); } _createInput(index, value) { const inputElement = this._createElement('input', CLASS_NAME_RANGE_SLIDER_INPUT); inputElement.type = 'range'; inputElement.min = this._config.min; inputElement.max = this._config.max; inputElement.step = this._config.step; inputElement.value = value; inputElement.name = Array.isArray(this._config.name) ? `${this._config.name[index]}` : `${this._config.name || ''}-${index}`; inputElement.disabled = this._config.disabled; // Accessibility attributes inputElement.setAttribute('role', 'slider'); inputElement.setAttribute('aria-valuemin', this._config.min); inputElement.setAttribute('aria-valuemax', this._config.max); inputElement.setAttribute('aria-valuenow', value); inputElement.setAttribute('aria-orientation', this._config.vertical ? 'vertical' : 'horizontal'); return inputElement; } _createLabels() { const { clickableLabels, disabled, labels, min, max, vertical } = this._config; if (!labels || !Array.isArray(labels) || labels.length === 0) { return; } const labelsContainer = this._createElement('div', CLASS_NAME_RANGE_SLIDER_LABELS_CONTAINER); for (const [index, label] of this._config.labels.entries()) { const labelElement = this._createElement('div', CLASS_NAME_RANGE_SLIDER_LABEL); if (clickableLabels && !disabled) { labelElement.classList.add(CLASS_NAME_CLICKABLE); } if (label.class) { const classNames = Array.isArray(label.class) ? label.class : [label.class]; labelElement.classList.add(...classNames); } if (label.style && typeof label.style === 'object') { Object.assign(labelElement.style, label.style); } // Calculate percentage based on index const percentage = labels.length === 1 ? 0 : index / (labels.length - 1) * 100; // Determine label value const labelValue = typeof label === 'object' ? label.value : min + percentage / 100 * (max - min); // Set data-coreui-value attribute Manipulator.setDataAttribute(labelElement, 'value', labelValue); // Set label content labelElement.textContent = typeof label === 'object' ? label.label : label; // Calculate and set position const position = this._calculateLabelPosition(label, index, percentage); if (vertical) { labelElement.style.bottom = position; } else { labelElement.style[index_js.isRTL() ? 'right' : 'left'] = position; } labelsContainer.append(labelElement); } this._element.append(labelsContainer); } _calculateLabelPosition(label, index) { // Check if label is an object with a specific value if (typeof label === 'object' && label.value !== undefined) { return `${(label.value - this._config.min) / (this._config.max - this._config.min) * 100}%`; } // Calculate position based on index when label is not an object return `${index / (this._config.labels.length - 1) * 100}%`; } _updateLabelsContainerSize() { const labelsContainer = SelectorEngine.findOne(SELECTOR_RANGE_SLIDER_LABELS_CONTAINER, this._element); if (!this._config.labels || !labelsContainer) { return; } const labels = SelectorEngine.find(SELECTOR_RANGE_SLIDER_LABEL, this._element); if (labels.length === 0) { return; } const maxSize = Math.max(...labels.map(label => this._config.vertical ? label.offsetWidth : label.offsetHeight)); labelsContainer.style[this._config.vertical ? 'width' : 'height'] = `${maxSize}px`; } _createTooltips() { if (!this._config.tooltips) { return; } const inputs = SelectorEngine.find(SELECTOR_RANGE_SLIDER_INPUT, this._element); this._thumbSize = this._getThumbSize(); for (const input of inputs) { const tooltipElement = this._createElement('div', CLASS_NAME_RANGE_SLIDER_TOOLTIP); const tooltipInnerElement = this._createElement('div', CLASS_NAME_RANGE_SLIDER_TOOLTIP_INNER); const tooltipArrowElement = this._createElement('div', CLASS_NAME_RANGE_SLIDER_TOOLTIP_ARROW); tooltipInnerElement.innerHTML = this._config.tooltipsFormat ? this._config.tooltipsFormat(input.value) : input.value; tooltipElement.append(tooltipInnerElement, tooltipArrowElement); input.parentNode.insertBefore(tooltipElement, input.nextSibling); this._positionTooltip(tooltipElement, input); this._tooltips.push(tooltipElement); } } _getThumbSize() { const value = window.getComputedStyle(this._element, null).getPropertyValue(this._config.vertical ? '--cui-range-slider-thumb-height' : '--cui-range-slider-thumb-width'); const regex = /^(\d+\.?\d*)([%a-z]*)$/i; const match = value.match(regex); if (match) { return { value: Number.parseFloat(match[1]), unit: match[2] || null }; } return '1rem'; } _positionTooltip(tooltip, input) { const thumbSize = this._thumbSize; const percent = (input.value - this._config.min) / (this._config.max - this._config.min); const margin = percent > 0.5 ? `-${(percent - 0.5) * thumbSize.value}${thumbSize.unit}` : `${(0.5 - percent) * thumbSize.value}${thumbSize.unit}`; if (this._config.vertical) { Object.assign(tooltip.style, { bottom: `${percent * 100}%`, marginBottom: margin }); return; } Object.assign(tooltip.style, { insetInlineStart: `${percent * 100}%`, marginInlineStart: margin }); } _updateTooltip(index, value) { if (!this._config.tooltips) { return; } if (this._tooltips[index]) { this._tooltips[index].children[0].innerHTML = this._config.tooltipsFormat ? this._config.tooltipsFormat(value) : value; const input = SelectorEngine.find(SELECTOR_RANGE_SLIDER_INPUT, this._element)[index]; this._positionTooltip(this._tooltips[index], input); } } _calculateClickValue(event) { const clickPosition = this._getClickPosition(event); const value = this._config.min + clickPosition * (this._config.max - this._config.min); return this._roundToStep(value, this._config.step); } _calculateMoveValue(event) { const trackRect = this._sliderTrack.getBoundingClientRect(); const position = this._config.vertical ? this._calculateVerticalPosition(event.clientY, trackRect) : this._calculateHorizontalPosition(event.clientX, trackRect); if (typeof position === 'string') { return position === 'max' ? this._config.max : this._config.min; } const value = this._config.min + position * (this._config.max - this._config.min); return this._roundToStep(value, this._config.step); } _calculateVerticalPosition(mouseY, rect) { if (mouseY < rect.top) { return 'max'; } if (mouseY > rect.bottom) { return 'min'; } return Math.min(Math.max((rect.bottom - mouseY) / rect.height, 0), 1); } _calculateHorizontalPosition(mouseX, rect) { if (mouseX < rect.left) { return index_js.isRTL() ? 'max' : 'min'; } if (mouseX > rect.right) { return index_js.isRTL() ? 'min' : 'max'; } const relativeX = index_js.isRTL() ? rect.right - mouseX : mouseX - rect.left; return Math.min(Math.max(relativeX / rect.width, 0), 1); } _createElement(tag, className) { const element = document.createElement(tag); element.classList.add(className); return element; } _getClickPosition(event) { const { offsetX, offsetY } = event; const { offsetWidth, offsetHeight } = this._sliderTrack; if (this._config.vertical) { return 1 - offsetY / offsetHeight; } return index_js.isRTL() ? 1 - offsetX / offsetWidth : offsetX / offsetWidth; } _getNearestValueIndex(value) { const values = this._currentValue; const valuesLength = values.length; if (value < values[0]) { return 0; } if (value > values[valuesLength - 1]) { return valuesLength - 1; } const distances = values.map(v => Math.abs(v - value)); const min = Math.min(...distances); const firstIndex = distances.indexOf(min); return value < values[firstIndex] ? firstIndex : distances.lastIndexOf(min); } _updateGradient() { if (!this._config.track) { return; } const [min, max] = [Math.min(...this._currentValue), Math.max(...this._currentValue)]; const from = (min - this._config.min) / (this._config.max - this._config.min) * 100; const to = (max - this._config.min) / (this._config.max - this._config.min) * 100; const direction = this._config.vertical ? 'to top' : index_js.isRTL() ? 'to left' : 'to right'; if (this._currentValue.length === 1) { this._sliderTrack.style.backgroundImage = `linear-gradient( ${direction}, var(--cui-range-slider-track-in-range-bg) 0%, var(--cui-range-slider-track-in-range-bg) ${to}%, transparent ${to}%, transparent 100% )`; return; } this._sliderTrack.style.backgroundImage = `linear-gradient( ${direction}, transparent 0%, transparent ${from}%, var(--cui-range-slider-track-in-range-bg) ${from}%, var(--cui-range-slider-track-in-range-bg) ${to}%, transparent ${to}%, transparent 100% )`; } _updateNearestValue(value) { const nearestIndex = this._getNearestValueIndex(value); this._updateValue(value, nearestIndex); } _updateValue(value, index) { const _value = this._validateValue(value, index); this._currentValue[index] = _value; this._updateInput(index, _value); this._updateGradient(); this._updateTooltip(index, _value); EventHandler.trigger(this._element, EVENT_CHANGE, { value: this._currentValue }); } _updateInput(index, value) { const input = this._inputs[index]; input.value = value; input.setAttribute('aria-valuenow', value); setTimeout(() => { input.focus(); }); } _validateValue(value, index) { const { distance } = this._config; const { length } = this._currentValue; if (length === 1) { return value; } const prevValue = index > 0 ? this._currentValue[index - 1] : undefined; const nextValue = index < length - 1 ? this._currentValue[index + 1] : undefined; if (index === 0 && nextValue !== undefined) { return Math.min(value, nextValue - distance); } if (index === length - 1 && prevValue !== undefined) { return Math.max(value, prevValue + distance); } if (prevValue !== undefined && nextValue !== undefined) { const minVal = prevValue + distance; const maxVal = nextValue - distance; return Math.min(Math.max(value, minVal), maxVal); } return value; } _roundToStep(number, step) { const _step = step === 0 ? 1 : step; return Math.round(number / _step) * _step; } _configAfterMerge(config) { if (typeof config.labels === 'string') { config.labels = config.labels.split(/,\s*/); } if (typeof config.name === 'string') { config.name = config.name.split(/,\s*/); } if (typeof config.value === 'number') { config.value = [config.value]; } if (typeof config.value === 'string') { config.value = config.value.split(/,\s*/).map(Number); } return config; } _getConfig(config) { const dataAttributes = Manipulator.getDataAttributes(this._element); config = { ...dataAttributes, ...(typeof config === 'object' && config ? config : {}) }; config = this._mergeConfigObj(config); config = this._configAfterMerge(config); this._typeCheckConfig(config); return config; } // Static static rangeSliderInterface(element, config) { const data = RangeSlider.getOrCreateInstance(element, config); if (typeof config === 'string') { if (typeof data[config] === 'undefined') { throw new TypeError(`No method named "${config}"`); } data[config](); } } static jQueryInterface(config) { return this.each(function () { const data = RangeSlider.getOrCreateInstance(this); if (typeof config !== 'string') { return; } if (data[config] === undefined || config.startsWith('_') || config === 'constructor') { throw new TypeError(`No method named "${config}"`); } data[config](this); }); } } /** * Data API implementation */ EventHandler.on(window, EVENT_LOAD_DATA_API, () => { const ratings = SelectorEngine.find(SELECTOR_DATA_TOGGLE); for (let i = 0, len = ratings.length; i < len; i++) { RangeSlider.rangeSliderInterface(ratings[i]); } }); /** * jQuery */ index_js.defineJQueryPlugin(RangeSlider); return RangeSlider; })); //# sourceMappingURL=range-slider.js.map