import { queryOne, queryAll } from '@ecl/dom-utils';
/**
* @param {HTMLElement} element DOM element for component instantiation and scope
* @param {Object} options
* @param {String} options.rangeInputSelector Selector for the range input
* @param {String} options.currentValueSelector Selector for the current value area
* @param {String} options.bubbleSelector Selector for the value bubble
* @param {Boolean} options.attachChangeListener Whether or not to bind change events on range
* @param {Boolean} options.attachHoverListener Whether or not to bind hover events
*/
export class Range {
/**
* @static
* Shorthand for instance creation and initialisation.
*
* @param {HTMLElement} root DOM element for component instantiation and scope
*
* @return {Range} An instance of Range.
*/
static autoInit(root, { RANGE: defaultOptions = {} } = {}) {
const range = new Range(root, defaultOptions);
range.init();
root.ECLRange = range;
return range;
}
constructor(
element,
{
rangeInputSelector = '[data-ecl-range-input]',
currentValueSelector = '[data-ecl-range-value-current]',
bubbleSelector = '[data-ecl-range-bubble]',
attachChangeListener = true,
attachHoverListener = true,
} = {},
) {
// Check element
if (!element || element.nodeType !== Node.ELEMENT_NODE) {
throw new TypeError(
'DOM element should be given to initialize this widget.',
);
}
this.element = element;
// Options
this.rangeInputSelector = rangeInputSelector;
this.currentValueSelector = currentValueSelector;
this.bubbleSelector = bubbleSelector;
this.attachChangeListener = attachChangeListener;
this.attachHoverListener = attachHoverListener;
// Private variables
this.rangeInput = null;
this.currentValue = null;
this.bubble = null;
this.direction = 'ltr';
// Bind `this` for use in callbacks
this.placeBubble = this.placeBubble.bind(this);
this.handleChange = this.handleChange.bind(this);
this.handleHoverOn = this.handleHoverOn.bind(this);
this.handleHoverOff = this.handleHoverOff.bind(this);
}
/**
* Initialise component.
*/
init() {
if (!ECL) {
throw new TypeError('Called init but ECL is not present');
}
ECL.components = ECL.components || new Map();
this.rangeInput = queryOne(this.rangeInputSelector, this.element);
this.currentValue = queryAll(this.currentValueSelector, this.element);
this.bubble = queryOne(this.bubbleSelector, this.element);
if (this.rangeInput && this.currentValue) {
// Display default value
this.currentValue.forEach((element) => {
element.innerHTML = this.rangeInput.value;
});
// Bind change and hover event on range
if (this.attachChangeListener) {
this.rangeInput.addEventListener('input', this.handleChange);
}
if (this.attachHoverListener) {
this.rangeInput.addEventListener('mouseover', this.handleHoverOn);
this.rangeInput.addEventListener('mouseout', this.handleHoverOff);
}
}
// RTL
this.direction = getComputedStyle(this.element).direction;
// Set ecl initialized attribute
this.element.setAttribute('data-ecl-auto-initialized', 'true');
ECL.components.set(this.element, this);
}
/**
* Destroy component.
*/
destroy() {
if (this.rangeInput && this.currentValue) {
if (this.attachChangeListener) {
this.rangeInput.removeEventListener('input', this.handleChange);
}
if (this.attachHoverListener) {
this.rangeInput.removeEventListener('mouseover', this.handleHoverOn);
this.rangeInput.removeEventListener('mouseout', this.handleHoverOff);
}
}
if (this.element) {
this.element.removeAttribute('data-ecl-auto-initialized');
ECL.components.delete(this.element);
}
}
/**
* Place value bubble
*/
placeBubble() {
// Quite complex calculus here
// see https://stackoverflow.com/questions/46448994/get-the-offset-position-of-an-html5-range-slider-handle
// Fixed values
const halfThumbWidth = 8; // 1rem / 2
const halfLabelWidth = this.bubble.offsetWidth / 2;
// Get range input width
const rect = this.rangeInput.getBoundingClientRect();
const center = rect.width / 2;
// Get position from center
const percentOfRange =
(this.rangeInput.value - this.rangeInput.min) /
(this.rangeInput.max - this.rangeInput.min);
const valuePxPosition = percentOfRange * rect.width;
const distFromCenter = valuePxPosition - center;
const percentDistFromCenter = distFromCenter / center;
// Calculate bubble position
const offset = percentDistFromCenter * halfThumbWidth;
let pos = 0;
if (this.direction === 'rtl') {
pos = rect.right - valuePxPosition - halfLabelWidth + offset;
} else {
pos = rect.left + valuePxPosition - halfLabelWidth - offset;
}
this.bubble.style.left = `${pos}px`;
}
/**
* Handle mouse hover
*/
handleHoverOn() {
// Display value bubble
this.bubble.classList.add('ecl-range__bubble--visible');
this.placeBubble();
}
handleHoverOff() {
// Hide value bubble
this.bubble.classList.remove('ecl-range__bubble--visible');
}
/**
* Display value when changed
*/
handleChange() {
// Update value
this.currentValue.forEach((element) => {
element.innerHTML = this.rangeInput.value;
});
this.placeBubble();
}
}
export default Range;