2024-06-03 23:07:10 +02:00
|
|
|
/**
|
|
|
|
* Slider Logic
|
|
|
|
*
|
|
|
|
* Provides functionality for <input type="dualrange">
|
|
|
|
*
|
|
|
|
* Example usage:
|
|
|
|
*
|
|
|
|
* <input type="dualrange" min="0" max="100" valuemin="0" valuemax="100">
|
|
|
|
*/
|
|
|
|
|
2024-06-03 23:43:51 +02:00
|
|
|
import { lerp } from './utils/lerp';
|
2024-06-03 23:07:10 +02:00
|
|
|
import { $$ } from './utils/dom';
|
|
|
|
|
2024-06-03 23:43:51 +02:00
|
|
|
// Make a given slider head draggable.
|
2024-06-03 23:07:10 +02:00
|
|
|
function setupDrag(el: HTMLDivElement, dataEl: HTMLInputElement, valueProperty: string, limitProperty: string) {
|
|
|
|
const parent = el.parentElement;
|
|
|
|
|
|
|
|
if (!parent) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2024-06-03 23:43:51 +02:00
|
|
|
// Initialize variables and constants.
|
|
|
|
const inputEvent = new InputEvent('input');
|
2024-06-03 23:07:10 +02:00
|
|
|
let minPos = 0;
|
|
|
|
let maxPos = 0;
|
2024-06-03 23:43:51 +02:00
|
|
|
let cachedMin = 0;
|
|
|
|
let cachedMax = 0;
|
|
|
|
let cachedValue = 0;
|
|
|
|
let cachedLimit = 0;
|
2024-06-03 23:07:10 +02:00
|
|
|
let curValue = 0;
|
|
|
|
let dragging = false;
|
|
|
|
|
2024-06-03 23:43:51 +02:00
|
|
|
// Clamps the slider head value to not cross over the other slider head's value.
|
2024-06-03 23:07:10 +02:00
|
|
|
function clampValue(value: number): number {
|
2024-06-03 23:43:51 +02:00
|
|
|
if (cachedValue >= cachedLimit && value < cachedLimit) {
|
|
|
|
return cachedLimit;
|
2024-06-03 23:07:10 +02:00
|
|
|
}
|
2024-06-03 23:43:51 +02:00
|
|
|
else if (cachedValue < cachedLimit && value >= cachedLimit) {
|
|
|
|
return cachedLimit - 1; // Offset by 1 to ensure stored value is less than limit.
|
2024-06-03 23:07:10 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
return value;
|
|
|
|
}
|
|
|
|
|
2024-06-03 23:43:51 +02:00
|
|
|
// Utility accessor to get the minimum value of dualrange.
|
2024-06-03 23:07:10 +02:00
|
|
|
function getMin(): number {
|
|
|
|
return Number(dataEl.getAttribute('min') || '0');
|
|
|
|
}
|
|
|
|
|
2024-06-03 23:43:51 +02:00
|
|
|
// Utility accessor to get the maximum value of dualrange.
|
2024-06-03 23:07:10 +02:00
|
|
|
function getMax(): number {
|
|
|
|
return Number(dataEl.getAttribute('max') || '0');
|
|
|
|
}
|
|
|
|
|
2024-06-03 23:43:51 +02:00
|
|
|
// Initializes cached variables. Should be used
|
|
|
|
// when the pointer event begins.
|
|
|
|
function initVars() {
|
|
|
|
if (!parent) { return; }
|
|
|
|
|
|
|
|
const rect = parent.getBoundingClientRect();
|
|
|
|
|
|
|
|
minPos = rect.x;
|
|
|
|
maxPos = rect.x + rect.width - el.clientWidth;
|
|
|
|
cachedMin = getMin();
|
|
|
|
cachedMax = getMax();
|
|
|
|
cachedValue = Number(dataEl.getAttribute(valueProperty) || '0');
|
|
|
|
cachedLimit = Number(dataEl.getAttribute(limitProperty) || '0');
|
|
|
|
curValue = Number(dataEl.getAttribute(valueProperty) || '0');
|
|
|
|
}
|
|
|
|
|
|
|
|
// Called during pointer movement.
|
2024-06-03 23:07:10 +02:00
|
|
|
function dragMove(e: PointerEvent) {
|
|
|
|
if (!dragging) { return; }
|
|
|
|
|
|
|
|
e.preventDefault();
|
|
|
|
|
|
|
|
let desiredPos = e.clientX;
|
|
|
|
|
2024-06-03 23:43:51 +02:00
|
|
|
// `lerp` cleverly clamps the value between min and max,
|
|
|
|
// so no need for any explicit checks for that here, only
|
|
|
|
// the crossover check is required.
|
2024-06-03 23:07:10 +02:00
|
|
|
curValue = clampValue(
|
|
|
|
lerp(
|
|
|
|
(desiredPos - minPos) / (maxPos - minPos),
|
2024-06-03 23:43:51 +02:00
|
|
|
cachedMin,
|
|
|
|
cachedMax
|
2024-06-03 23:07:10 +02:00
|
|
|
)
|
|
|
|
);
|
|
|
|
|
2024-06-03 23:43:51 +02:00
|
|
|
// Same here, lerp clamps the value so it doesn't get out
|
|
|
|
// of the slider boundary.
|
|
|
|
desiredPos = lerp(curValue / cachedMax, minPos, maxPos);
|
2024-06-03 23:07:10 +02:00
|
|
|
|
|
|
|
el.style.left = `${desiredPos}px`;
|
|
|
|
|
|
|
|
dataEl.setAttribute(valueProperty, curValue.toString());
|
2024-06-03 23:43:51 +02:00
|
|
|
dataEl.dispatchEvent(inputEvent);
|
2024-06-03 23:07:10 +02:00
|
|
|
}
|
|
|
|
|
2024-06-03 23:43:51 +02:00
|
|
|
// Called when the pointer is let go of.
|
2024-06-03 23:07:10 +02:00
|
|
|
function dragEnd(e: PointerEvent) {
|
|
|
|
if (!dragging) { return; }
|
|
|
|
|
|
|
|
e.preventDefault();
|
|
|
|
|
|
|
|
dataEl.setAttribute(valueProperty, curValue.toString());
|
2024-06-03 23:43:51 +02:00
|
|
|
dataEl.dispatchEvent(inputEvent);
|
2024-06-03 23:07:10 +02:00
|
|
|
|
|
|
|
dragging = false;
|
|
|
|
}
|
|
|
|
|
2024-06-03 23:43:51 +02:00
|
|
|
// Called when the slider head is clicked or tapped.
|
2024-06-03 23:07:10 +02:00
|
|
|
function dragBegin(e: PointerEvent) {
|
|
|
|
if (!parent) { return; }
|
|
|
|
|
|
|
|
e.preventDefault();
|
|
|
|
initVars();
|
|
|
|
|
|
|
|
dragging = true;
|
|
|
|
}
|
|
|
|
|
2024-06-03 23:43:51 +02:00
|
|
|
// Set the initial variables and position;
|
2024-06-03 23:07:10 +02:00
|
|
|
initVars();
|
|
|
|
el.style.left = `${lerp(curValue / getMax(), minPos, maxPos)}px`;
|
|
|
|
|
|
|
|
// Attach event listeners for dragging the head.
|
|
|
|
el.addEventListener('pointerdown', dragBegin);
|
|
|
|
window.addEventListener('pointerup', dragEnd);
|
|
|
|
window.addEventListener('pointermove', dragMove);
|
|
|
|
}
|
|
|
|
|
2024-06-03 23:43:51 +02:00
|
|
|
// Sets up the slider input element.
|
|
|
|
// Creates `div` elements for presentation, hides the
|
|
|
|
// original `input` element. The logic is that
|
|
|
|
// we use divs for presentation, and input for data storage.
|
2024-06-03 23:07:10 +02:00
|
|
|
function setupSlider(el: HTMLInputElement) {
|
|
|
|
const parent = el.parentElement;
|
|
|
|
|
|
|
|
if (!parent) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Create a bunch of divs for presentation.
|
|
|
|
const sliderContainer: HTMLDivElement = document.createElement('div');
|
|
|
|
const minHead: HTMLDivElement = document.createElement('div');
|
|
|
|
const maxHead: HTMLDivElement = document.createElement('div');
|
|
|
|
const body: HTMLDivElement = document.createElement('div');
|
|
|
|
|
|
|
|
// Hide the real input, and add CSS classes to our divs.
|
|
|
|
el.classList.add('hidden');
|
|
|
|
sliderContainer.classList.add('slider');
|
|
|
|
minHead.classList.add('slider__head');
|
|
|
|
minHead.classList.add('slider__head--min');
|
|
|
|
maxHead.classList.add('slider__head');
|
|
|
|
maxHead.classList.add('slider__head--max');
|
|
|
|
body.classList.add('slider__body');
|
|
|
|
|
|
|
|
// Insert divs into other divs and subsequently into the document.
|
|
|
|
sliderContainer.appendChild(body);
|
|
|
|
sliderContainer.appendChild(minHead);
|
|
|
|
sliderContainer.appendChild(maxHead);
|
|
|
|
parent.insertBefore(sliderContainer, el);
|
|
|
|
|
|
|
|
// Setup drag events on head elements.
|
|
|
|
setupDrag(minHead, el, 'valuemin', 'valuemax');
|
|
|
|
setupDrag(maxHead, el, 'valuemax', 'valuemin');
|
|
|
|
}
|
|
|
|
|
2024-06-03 23:43:51 +02:00
|
|
|
// Sets up all sliders currently on the page.
|
2024-06-03 23:07:10 +02:00
|
|
|
function setupSliders() {
|
|
|
|
$$<HTMLInputElement>('input[type="dualrange"]').forEach(el => {
|
|
|
|
setupSlider(el);
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
export { setupSliders };
|