diff --git a/assets/js/slider.ts b/assets/js/slider.ts index fee08a51..92304488 100644 --- a/assets/js/slider.ts +++ b/assets/js/slider.ts @@ -8,15 +8,10 @@ * */ +import { lerp } from './utils/lerp'; import { $$ } from './utils/dom'; -function lerp(delta: number, from: number, to: number): number { - if (delta >= 1) { return to; } - else if (delta <= 0) { return from; } - - return from + (to - from) * delta; -} - +// Make a given slider head draggable. function setupDrag(el: HTMLDivElement, dataEl: HTMLInputElement, valueProperty: string, limitProperty: string) { const parent = el.parentElement; @@ -24,11 +19,41 @@ function setupDrag(el: HTMLDivElement, dataEl: HTMLInputElement, valueProperty: return; } + // Initialize variables and constants. + const inputEvent = new InputEvent('input'); let minPos = 0; let maxPos = 0; + let cachedMin = 0; + let cachedMax = 0; + let cachedValue = 0; + let cachedLimit = 0; let curValue = 0; let dragging = false; + // Clamps the slider head value to not cross over the other slider head's value. + function clampValue(value: number): number { + if (cachedValue >= cachedLimit && value < cachedLimit) { + return cachedLimit; + } + else if (cachedValue < cachedLimit && value >= cachedLimit) { + return cachedLimit - 1; // Offset by 1 to ensure stored value is less than limit. + } + + return value; + } + + // Utility accessor to get the minimum value of dualrange. + function getMin(): number { + return Number(dataEl.getAttribute('min') || '0'); + } + + // Utility accessor to get the maximum value of dualrange. + function getMax(): number { + return Number(dataEl.getAttribute('max') || '0'); + } + + // Initializes cached variables. Should be used + // when the pointer event begins. function initVars() { if (!parent) { return; } @@ -36,32 +61,14 @@ function setupDrag(el: HTMLDivElement, dataEl: HTMLInputElement, valueProperty: 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'); } - function clampValue(value: number): number { - const storedValue = Number(dataEl.getAttribute(valueProperty) || '0'); - const limitValue = Number(dataEl.getAttribute(limitProperty) || '0'); - - if (storedValue >= limitValue && value < limitValue) { - return limitValue; - } - else if (storedValue < limitValue && value >= limitValue) { - return limitValue - 1; // Offset by 1 to ensure stored value is less than limit. - } - - return value; - } - - function getMin(): number { - return Number(dataEl.getAttribute('min') || '0'); - } - - function getMax(): number { - return Number(dataEl.getAttribute('max') || '0'); - } - - // Define functions to control the drag behavior of the slider. + // Called during pointer movement. function dragMove(e: PointerEvent) { if (!dragging) { return; } @@ -69,40 +76,40 @@ function setupDrag(el: HTMLDivElement, dataEl: HTMLInputElement, valueProperty: let desiredPos = e.clientX; - if (desiredPos > maxPos) { - desiredPos = maxPos; - } - else if (desiredPos < minPos) { - desiredPos = minPos; - } - + // `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. curValue = clampValue( lerp( (desiredPos - minPos) / (maxPos - minPos), - getMin(), - getMax() + cachedMin, + cachedMax ) ); - desiredPos = lerp(curValue / getMax(), minPos, maxPos); + // Same here, lerp clamps the value so it doesn't get out + // of the slider boundary. + desiredPos = lerp(curValue / cachedMax, minPos, maxPos); el.style.left = `${desiredPos}px`; dataEl.setAttribute(valueProperty, curValue.toString()); - dataEl.dispatchEvent(new InputEvent('input')); + dataEl.dispatchEvent(inputEvent); } + // Called when the pointer is let go of. function dragEnd(e: PointerEvent) { if (!dragging) { return; } e.preventDefault(); dataEl.setAttribute(valueProperty, curValue.toString()); - dataEl.dispatchEvent(new InputEvent('input')); + dataEl.dispatchEvent(inputEvent); dragging = false; } + // Called when the slider head is clicked or tapped. function dragBegin(e: PointerEvent) { if (!parent) { return; } @@ -112,7 +119,7 @@ function setupDrag(el: HTMLDivElement, dataEl: HTMLInputElement, valueProperty: dragging = true; } - // Set initial position; + // Set the initial variables and position; initVars(); el.style.left = `${lerp(curValue / getMax(), minPos, maxPos)}px`; @@ -122,6 +129,10 @@ function setupDrag(el: HTMLDivElement, dataEl: HTMLInputElement, valueProperty: window.addEventListener('pointermove', dragMove); } +// 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. function setupSlider(el: HTMLInputElement) { const parent = el.parentElement; @@ -155,6 +166,7 @@ function setupSlider(el: HTMLInputElement) { setupDrag(maxHead, el, 'valuemax', 'valuemin'); } +// Sets up all sliders currently on the page. function setupSliders() { $$('input[type="dualrange"]').forEach(el => { setupSlider(el); diff --git a/assets/js/utils/__tests__/lerp.spec.ts b/assets/js/utils/__tests__/lerp.spec.ts new file mode 100644 index 00000000..189f37e7 --- /dev/null +++ b/assets/js/utils/__tests__/lerp.spec.ts @@ -0,0 +1,17 @@ +import { lerp } from '../lerp'; + +describe('Linear interpolation', () => { + describe('lerp', () => { + it('should interpolate the min-max range based on a delta', () => { + expect(lerp(0.5, 0, 100)).toEqual(50); + expect(lerp(0.75, 0, 100)).toEqual(75); + }); + + it('should clamp the value between min and max', () => { + expect(lerp(-999, 0, 100)).toEqual(0); + expect(lerp(0, 0, 100)).toEqual(0); + expect(lerp(999, 0, 100)).toEqual(100); + expect(lerp(1, 0, 100)).toEqual(100); + }); + }); +}); diff --git a/assets/js/utils/lerp.ts b/assets/js/utils/lerp.ts new file mode 100644 index 00000000..41170284 --- /dev/null +++ b/assets/js/utils/lerp.ts @@ -0,0 +1,12 @@ +// Simple linear interpolation. +// Returns a value between min and max based on a delta. +// The delta is a number between 0 and 1. +// If the delta is not within the 0-1 range, this function will +// clamp the value between min and max, depending on whether +// the delta >= 1 or <= 0. +export function lerp(delta: number, from: number, to: number): number { + if (delta >= 1) { return to; } + else if (delta <= 0) { return from; } + + return from + (to - from) * delta; +}