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;
+}