@coreui/coreui-pro
Version:
The most popular front-end framework for developing responsive, mobile-first projects on the web rewritten by the CoreUI Team
582 lines (558 loc) • 22 kB
JavaScript
/*!
* CoreUI range-slider.js v5.23.0 (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'), require('./util/sanitizer.js')) :
typeof define === 'function' && define.amd ? define(['./base-component', './dom/event-handler', './dom/manipulator', './dom/selector-engine', './util/index', './util/sanitizer'], factory) :
(global = typeof globalThis !== 'undefined' ? globalThis : global || self, global.RangeSlider = factory(global.BaseComponent, global.EventHandler, global.Manipulator, global.SelectorEngine, global.Index, global.Sanitizer));
})(this, (function (BaseComponent, EventHandler, Manipulator, SelectorEngine, index_js, sanitizer_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 DISALLOWED_ATTRIBUTES = new Set(['sanitize', 'allowList', 'sanitizeFn']);
const EVENT_CHANGE = `change${EVENT_KEY}`;
const EVENT_INPUT = `input${EVENT_KEY}`;
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 = {
allowList: sanitizer_js.DefaultAllowlist,
clickableLabels: true,
disabled: false,
distance: 0,
labels: false,
max: 100,
min: 0,
name: null,
sanitize: true,
sanitizeFn: null,
step: 1,
tooltips: true,
tooltipsFormat: null,
track: 'fill',
value: 0,
vertical: false
};
const DefaultType = {
allowList: 'object',
clickableLabels: 'boolean',
disabled: 'boolean',
distance: 'number',
labels: '(array|boolean|string)',
max: 'number',
min: 'number',
name: '(array|string|null)',
sanitize: 'boolean',
sanitizeFn: '(null|function)',
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.trigger(this._element, EVENT_INPUT, {
value: this._currentValue
});
});
EventHandler.on(this._element, EVENT_CHANGE, SELECTOR_RANGE_SLIDER_INPUT, () => {
EventHandler.trigger(this._element, EVENT_CHANGE, {
value: this._currentValue
});
});
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.trigger(this._element, EVENT_CHANGE, {
value: this._currentValue
});
EventHandler.trigger(this._element, EVENT_INPUT, {
value: this._currentValue
});
});
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;
if (this._config.name) {
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.sanitize ? sanitizer_js.sanitizeHtml(this._config.tooltipsFormat(input.value), this._config.allowList, this._config.sanitizeFn) : 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);
}
_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.includes(',')) {
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);
for (const dataAttribute of Object.keys(dataAttributes)) {
if (DISALLOWED_ATTRIBUTES.has(dataAttribute)) {
delete dataAttributes[dataAttribute];
}
}
config = {
...dataAttributes,
...(typeof config === 'object' && config ? config : {})
};
config = this._mergeConfigObj(config, this._element);
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