Convert range_slider.js to TypeScript (issue #17)
- Add ts/range_slider.ts: typed port of the custom range-slider widget. Number inputs typed as HTMLInputElement; setTargetValue coerces via String(); mouse handlers typed MouseEvent; var → const/let - Point the RangeSlider component Media and every e2e/test reference at the compiled dist/range_slider.js Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -55,7 +55,7 @@ _FILTER_GRID_CLASS = "grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4 mb-4"
|
|||||||
# range_slider.js wires RangeSlider; ts/filter_bar.ts wires the bar chrome
|
# range_slider.js wires RangeSlider; ts/filter_bar.ts wires the bar chrome
|
||||||
# (Apply/Clear, presets, search injection). Widget media (dist/search_select.js,
|
# (Apply/Clear, presets, search injection). Widget media (dist/search_select.js,
|
||||||
# date_range_picker.js) bubbles up from the contained FilterSelect / picker.
|
# date_range_picker.js) bubbles up from the contained FilterSelect / picker.
|
||||||
_RANGE_SLIDER_MEDIA = Media(js=("range_slider.js",))
|
_RANGE_SLIDER_MEDIA = Media(js=("dist/range_slider.js",))
|
||||||
_FILTER_BAR_MEDIA = Media(js=("dist/filter_bar.js",))
|
_FILTER_BAR_MEDIA = Media(js=("dist/filter_bar.js",))
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ def _bar_page(filter_json: str = "") -> str:
|
|||||||
<head>
|
<head>
|
||||||
<title>Boolean filter E2E</title>
|
<title>Boolean filter E2E</title>
|
||||||
<script src="/static/js/htmx.min.js"></script>
|
<script src="/static/js/htmx.min.js"></script>
|
||||||
<script src="/static/js/range_slider.js" type="module"></script>
|
<script src="/static/js/dist/range_slider.js" type="module"></script>
|
||||||
<script src="/static/js/dist/search_select.js" type="module"></script>
|
<script src="/static/js/dist/search_select.js" type="module"></script>
|
||||||
<script src="/static/js/dist/filter_bar.js" type="module"></script>
|
<script src="/static/js/dist/filter_bar.js" type="module"></script>
|
||||||
</head>
|
</head>
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ def _bar_page(filter_json: str = "") -> str:
|
|||||||
<head>
|
<head>
|
||||||
<title>Date filter E2E</title>
|
<title>Date filter E2E</title>
|
||||||
<script src="/static/js/htmx.min.js"></script>
|
<script src="/static/js/htmx.min.js"></script>
|
||||||
<script src="/static/js/range_slider.js" type="module"></script>
|
<script src="/static/js/dist/range_slider.js" type="module"></script>
|
||||||
<script src="/static/js/dist/search_select.js" type="module"></script>
|
<script src="/static/js/dist/search_select.js" type="module"></script>
|
||||||
<script src="/static/js/dist/filter_bar.js" type="module"></script>
|
<script src="/static/js/dist/filter_bar.js" type="module"></script>
|
||||||
</head>
|
</head>
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ def _bar_page(filter_json: str = "") -> str:
|
|||||||
<head>
|
<head>
|
||||||
<title>Date range picker E2E</title>
|
<title>Date range picker E2E</title>
|
||||||
<script src="/static/js/htmx.min.js"></script>
|
<script src="/static/js/htmx.min.js"></script>
|
||||||
<script src="/static/js/range_slider.js" type="module"></script>
|
<script src="/static/js/dist/range_slider.js" type="module"></script>
|
||||||
<script src="/static/js/dist/search_select.js" type="module"></script>
|
<script src="/static/js/dist/search_select.js" type="module"></script>
|
||||||
<script src="/static/js/date_range_picker.js" defer></script>
|
<script src="/static/js/date_range_picker.js" defer></script>
|
||||||
<script src="/static/js/dist/filter_bar.js" type="module"></script>
|
<script src="/static/js/dist/filter_bar.js" type="module"></script>
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ def _bar_page(filter_json: str = "") -> str:
|
|||||||
<head>
|
<head>
|
||||||
<title>Range Slider E2E</title>
|
<title>Range Slider E2E</title>
|
||||||
<script src="/static/js/htmx.min.js"></script>
|
<script src="/static/js/htmx.min.js"></script>
|
||||||
<script src="/static/js/range_slider.js" type="module"></script>
|
<script src="/static/js/dist/range_slider.js" type="module"></script>
|
||||||
<script src="/static/js/dist/search_select.js" type="module"></script>
|
<script src="/static/js/dist/search_select.js" type="module"></script>
|
||||||
<script src="/static/js/dist/filter_bar.js" type="module"></script>
|
<script src="/static/js/dist/filter_bar.js" type="module"></script>
|
||||||
</head>
|
</head>
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ def _bar_page(filter_json: str = "") -> str:
|
|||||||
<head>
|
<head>
|
||||||
<title>String filter E2E</title>
|
<title>String filter E2E</title>
|
||||||
<script src="/static/js/htmx.min.js"></script>
|
<script src="/static/js/htmx.min.js"></script>
|
||||||
<script src="/static/js/range_slider.js" type="module"></script>
|
<script src="/static/js/dist/range_slider.js" type="module"></script>
|
||||||
<script src="/static/js/dist/search_select.js" type="module"></script>
|
<script src="/static/js/dist/search_select.js" type="module"></script>
|
||||||
<script src="/static/js/dist/filter_bar.js" type="module"></script>
|
<script src="/static/js/dist/filter_bar.js" type="module"></script>
|
||||||
</head>
|
</head>
|
||||||
|
|||||||
@@ -1,230 +0,0 @@
|
|||||||
/**
|
|
||||||
* Range slider — custom draggable handles (no native <input type=range>).
|
|
||||||
*
|
|
||||||
* Supports two modes on each slider, toggled via the .range-mode-toggle button:
|
|
||||||
* range (default) — two handles, min ≤ max constraint
|
|
||||||
* point — single handle, sets both number inputs to the same value
|
|
||||||
*
|
|
||||||
* Handles track-fill positioning and sync between handles and the connected
|
|
||||||
* number inputs (linked via data-target attributes).
|
|
||||||
*/
|
|
||||||
import { onSwap } from "./utils.js";
|
|
||||||
|
|
||||||
(function () {
|
|
||||||
"use strict";
|
|
||||||
|
|
||||||
function initializeSlider(slider) {
|
|
||||||
var mode = slider.getAttribute("data-mode") || "range";
|
|
||||||
var trackFill = slider.querySelector(".range-track-fill");
|
|
||||||
var minHandle = slider.querySelector(".range-handle-min");
|
|
||||||
var maxHandle = slider.querySelector(".range-handle-max");
|
|
||||||
if (!minHandle || !maxHandle) return;
|
|
||||||
|
|
||||||
var minTarget = document.getElementById(
|
|
||||||
minHandle.getAttribute("data-target")
|
|
||||||
);
|
|
||||||
var maxTarget = document.getElementById(
|
|
||||||
maxHandle.getAttribute("data-target")
|
|
||||||
);
|
|
||||||
var dataMin = parseInt(slider.getAttribute("data-min"), 10);
|
|
||||||
var dataMax = parseInt(slider.getAttribute("data-max"), 10);
|
|
||||||
var step = parseInt(slider.getAttribute("data-step"), 10) || 1;
|
|
||||||
|
|
||||||
// ── Helpers ──
|
|
||||||
|
|
||||||
function valueToPercent(value) {
|
|
||||||
return ((value - dataMin) / (dataMax - dataMin)) * 100;
|
|
||||||
}
|
|
||||||
function percentToValue(percent) {
|
|
||||||
var raw = dataMin + (percent / 100) * (dataMax - dataMin);
|
|
||||||
return Math.round(raw / step) * step;
|
|
||||||
}
|
|
||||||
function clamp(value, lo, hi) {
|
|
||||||
return Math.max(lo, Math.min(hi, value));
|
|
||||||
}
|
|
||||||
|
|
||||||
function getTargetValue(target, defaultVal) {
|
|
||||||
if (!target || target.value === "") return defaultVal;
|
|
||||||
var parsed = parseInt(target.value, 10);
|
|
||||||
return isNaN(parsed) ? defaultVal : parsed;
|
|
||||||
}
|
|
||||||
function setTargetValue(target, value) {
|
|
||||||
if (target) target.value = value;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── Track fill positioning ──
|
|
||||||
|
|
||||||
function updateTrackFill() {
|
|
||||||
if (!trackFill) return;
|
|
||||||
var minVal = clamp(getTargetValue(minTarget, dataMin), dataMin, dataMax);
|
|
||||||
var maxVal = clamp(getTargetValue(maxTarget, dataMax), dataMin, dataMax);
|
|
||||||
if (mode === "point") {
|
|
||||||
trackFill.style.left = "0%";
|
|
||||||
trackFill.style.width = valueToPercent(maxVal) + "%";
|
|
||||||
} else {
|
|
||||||
var leftPct = valueToPercent(minVal);
|
|
||||||
var rightPct = valueToPercent(maxVal);
|
|
||||||
if (leftPct > rightPct) {
|
|
||||||
var tmp = leftPct;
|
|
||||||
leftPct = rightPct;
|
|
||||||
rightPct = tmp;
|
|
||||||
}
|
|
||||||
var widthPct = rightPct - leftPct;
|
|
||||||
trackFill.style.left = leftPct + "%";
|
|
||||||
trackFill.style.width = widthPct + "%";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function updateHandles() {
|
|
||||||
var minVal = clamp(getTargetValue(minTarget, dataMin), dataMin, dataMax);
|
|
||||||
var maxVal = clamp(getTargetValue(maxTarget, dataMax), dataMin, dataMax);
|
|
||||||
minHandle.style.left = valueToPercent(minVal) + "%";
|
|
||||||
maxHandle.style.left = valueToPercent(maxVal) + "%";
|
|
||||||
updateTrackFill();
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── Dragging ──
|
|
||||||
|
|
||||||
function makeDraggable(handle, isMin) {
|
|
||||||
handle.addEventListener("mousedown", function (e) {
|
|
||||||
e.preventDefault();
|
|
||||||
var rect = slider.getBoundingClientRect();
|
|
||||||
|
|
||||||
function onMove(ev) {
|
|
||||||
var pct = ((ev.clientX - rect.left) / rect.width) * 100;
|
|
||||||
var value = percentToValue(clamp(pct, 0, 100));
|
|
||||||
|
|
||||||
if (mode === "point") {
|
|
||||||
setTargetValue(minTarget, value);
|
|
||||||
setTargetValue(maxTarget, value);
|
|
||||||
if (minTarget)
|
|
||||||
minTarget.dispatchEvent(
|
|
||||||
new Event("input", { bubbles: true })
|
|
||||||
);
|
|
||||||
if (maxTarget)
|
|
||||||
maxTarget.dispatchEvent(
|
|
||||||
new Event("input", { bubbles: true })
|
|
||||||
);
|
|
||||||
} else if (isMin) {
|
|
||||||
setTargetValue(
|
|
||||||
minTarget,
|
|
||||||
clamp(value, dataMin, getTargetValue(maxTarget, dataMax))
|
|
||||||
);
|
|
||||||
if (minTarget)
|
|
||||||
minTarget.dispatchEvent(
|
|
||||||
new Event("input", { bubbles: true })
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
setTargetValue(
|
|
||||||
maxTarget,
|
|
||||||
clamp(value, getTargetValue(minTarget, dataMin), dataMax)
|
|
||||||
);
|
|
||||||
if (maxTarget)
|
|
||||||
maxTarget.dispatchEvent(
|
|
||||||
new Event("input", { bubbles: true })
|
|
||||||
);
|
|
||||||
}
|
|
||||||
updateHandles();
|
|
||||||
}
|
|
||||||
|
|
||||||
function onUp() {
|
|
||||||
document.removeEventListener("mousemove", onMove);
|
|
||||||
document.removeEventListener("mouseup", onUp);
|
|
||||||
}
|
|
||||||
document.addEventListener("mousemove", onMove);
|
|
||||||
document.addEventListener("mouseup", onUp);
|
|
||||||
onMove(e);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
makeDraggable(minHandle, true);
|
|
||||||
makeDraggable(maxHandle, false);
|
|
||||||
|
|
||||||
// ── Sync from number inputs back to handles ──
|
|
||||||
|
|
||||||
function syncFromInputs(e) {
|
|
||||||
if (mode === "point") {
|
|
||||||
var src = (e && e.target) || minTarget || maxTarget;
|
|
||||||
var val = src ? src.value : "";
|
|
||||||
setTargetValue(minTarget, val);
|
|
||||||
setTargetValue(maxTarget, val);
|
|
||||||
} else if (e && e.target) {
|
|
||||||
var minVal = getTargetValue(minTarget, dataMin);
|
|
||||||
var maxVal = getTargetValue(maxTarget, dataMax);
|
|
||||||
if (e.target === minTarget) {
|
|
||||||
if (minVal > maxVal) {
|
|
||||||
setTargetValue(maxTarget, minVal);
|
|
||||||
}
|
|
||||||
} else if (e.target === maxTarget) {
|
|
||||||
if (maxVal < minVal) {
|
|
||||||
setTargetValue(minTarget, maxVal);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
updateHandles();
|
|
||||||
}
|
|
||||||
|
|
||||||
function enforceStrictBounds(e) {
|
|
||||||
if (e && e.target) {
|
|
||||||
var val = parseInt(e.target.value, 10);
|
|
||||||
if (!isNaN(val)) {
|
|
||||||
var clamped = clamp(val, dataMin, dataMax);
|
|
||||||
if (clamped !== val) {
|
|
||||||
setTargetValue(e.target, clamped);
|
|
||||||
e.target.dispatchEvent(new Event("input", { bubbles: true }));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (minTarget) {
|
|
||||||
minTarget.addEventListener("input", syncFromInputs);
|
|
||||||
minTarget.addEventListener("change", enforceStrictBounds);
|
|
||||||
}
|
|
||||||
if (maxTarget) {
|
|
||||||
maxTarget.addEventListener("input", syncFromInputs);
|
|
||||||
maxTarget.addEventListener("change", enforceStrictBounds);
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── Mode toggle ──
|
|
||||||
|
|
||||||
var block = slider.closest(".range-slider-block");
|
|
||||||
var toggleButton =
|
|
||||||
block && block.querySelector(".range-mode-toggle");
|
|
||||||
if (toggleButton) {
|
|
||||||
toggleButton.addEventListener("click", function () {
|
|
||||||
var newMode = mode === "range" ? "point" : "range";
|
|
||||||
slider.setAttribute("data-mode", newMode);
|
|
||||||
|
|
||||||
// Swap toggle icons
|
|
||||||
var iconRange = toggleButton.querySelector(
|
|
||||||
".range-mode-icon-range"
|
|
||||||
);
|
|
||||||
var iconPoint = toggleButton.querySelector(
|
|
||||||
".range-mode-icon-point"
|
|
||||||
);
|
|
||||||
if (iconRange) iconRange.classList.toggle("hidden");
|
|
||||||
if (iconPoint) iconPoint.classList.toggle("hidden");
|
|
||||||
|
|
||||||
var dashSpan = block && block.querySelector(".range-dash");
|
|
||||||
if (newMode === "point") {
|
|
||||||
minHandle.style.display = "none";
|
|
||||||
setTargetValue(minTarget, maxTarget ? maxTarget.value : "");
|
|
||||||
if (minTarget) minTarget.classList.add("hidden");
|
|
||||||
if (dashSpan) dashSpan.classList.add("hidden");
|
|
||||||
} else {
|
|
||||||
minHandle.style.display = "";
|
|
||||||
if (minTarget) minTarget.classList.remove("hidden");
|
|
||||||
if (dashSpan) dashSpan.classList.remove("hidden");
|
|
||||||
}
|
|
||||||
mode = newMode;
|
|
||||||
updateHandles();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── Initial position ──
|
|
||||||
updateHandles();
|
|
||||||
}
|
|
||||||
|
|
||||||
onSwap(".range-slider", initializeSlider);
|
|
||||||
})();
|
|
||||||
@@ -159,7 +159,7 @@ class RealComponentMediaTest(unittest.TestCase):
|
|||||||
label="Year", input_name_prefix="year", range_min=2000, range_max=2025
|
label="Year", input_name_prefix="year", range_min=2000, range_max=2025
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
self.assertEqual(media.js, ("range_slider.js",))
|
self.assertEqual(media.js, ("dist/range_slider.js",))
|
||||||
|
|
||||||
def test_filter_bar_collects_chrome_and_widget_media(self):
|
def test_filter_bar_collects_chrome_and_widget_media(self):
|
||||||
"""A FilterBar's media merges its own chrome script with the scripts that
|
"""A FilterBar's media merges its own chrome script with the scripts that
|
||||||
@@ -171,7 +171,7 @@ class RealComponentMediaTest(unittest.TestCase):
|
|||||||
media = collect_media(FilterBar())
|
media = collect_media(FilterBar())
|
||||||
self.assertIn("dist/filter_bar.js", media.js)
|
self.assertIn("dist/filter_bar.js", media.js)
|
||||||
self.assertIn("dist/search_select.js", media.js)
|
self.assertIn("dist/search_select.js", media.js)
|
||||||
self.assertIn("range_slider.js", media.js)
|
self.assertIn("dist/range_slider.js", media.js)
|
||||||
|
|
||||||
|
|
||||||
class HtpyStyleSugarTest(unittest.TestCase):
|
class HtpyStyleSugarTest(unittest.TestCase):
|
||||||
|
|||||||
@@ -65,7 +65,7 @@ class RenderedPagesTest(TestCase):
|
|||||||
html = self.get("games:list_games").content.decode()
|
html = self.get("games:list_games").content.decode()
|
||||||
self.assertIn("js/dist/filter_bar.js", html)
|
self.assertIn("js/dist/filter_bar.js", html)
|
||||||
self.assertIn("js/dist/search_select.js", html)
|
self.assertIn("js/dist/search_select.js", html)
|
||||||
self.assertIn("js/range_slider.js", html)
|
self.assertIn("js/dist/range_slider.js", html)
|
||||||
|
|
||||||
def test_stats_page_auto_loads_datepicker(self):
|
def test_stats_page_auto_loads_datepicker(self):
|
||||||
"""YearPicker declares the datepicker UMD bundle as media; the stats
|
"""YearPicker declares the datepicker UMD bundle as media; the stats
|
||||||
|
|||||||
@@ -0,0 +1,215 @@
|
|||||||
|
/**
|
||||||
|
* Range slider — custom draggable handles (no native <input type=range>).
|
||||||
|
*
|
||||||
|
* Supports two modes on each slider, toggled via the .range-mode-toggle button:
|
||||||
|
* range (default) — two handles, min ≤ max constraint
|
||||||
|
* point — single handle, sets both number inputs to the same value
|
||||||
|
*
|
||||||
|
* Handles track-fill positioning and sync between handles and the connected
|
||||||
|
* number inputs (linked via data-target attributes).
|
||||||
|
*/
|
||||||
|
import { onSwap } from "./utils.js";
|
||||||
|
|
||||||
|
(() => {
|
||||||
|
"use strict";
|
||||||
|
|
||||||
|
function initializeSlider(sliderElement: Element) {
|
||||||
|
const slider = sliderElement as HTMLElement;
|
||||||
|
let mode = slider.getAttribute("data-mode") || "range";
|
||||||
|
const trackFill = slider.querySelector<HTMLElement>(".range-track-fill");
|
||||||
|
const minHandle = slider.querySelector<HTMLElement>(".range-handle-min");
|
||||||
|
const maxHandle = slider.querySelector<HTMLElement>(".range-handle-max");
|
||||||
|
if (!minHandle || !maxHandle) return;
|
||||||
|
|
||||||
|
const minTarget = document.getElementById(
|
||||||
|
minHandle.getAttribute("data-target") ?? ""
|
||||||
|
) as HTMLInputElement | null;
|
||||||
|
const maxTarget = document.getElementById(
|
||||||
|
maxHandle.getAttribute("data-target") ?? ""
|
||||||
|
) as HTMLInputElement | null;
|
||||||
|
const dataMin = parseInt(slider.getAttribute("data-min") ?? "", 10);
|
||||||
|
const dataMax = parseInt(slider.getAttribute("data-max") ?? "", 10);
|
||||||
|
const step = parseInt(slider.getAttribute("data-step") ?? "", 10) || 1;
|
||||||
|
|
||||||
|
// ── Helpers ──
|
||||||
|
|
||||||
|
function valueToPercent(value: number): number {
|
||||||
|
return ((value - dataMin) / (dataMax - dataMin)) * 100;
|
||||||
|
}
|
||||||
|
function percentToValue(percent: number): number {
|
||||||
|
const raw = dataMin + (percent / 100) * (dataMax - dataMin);
|
||||||
|
return Math.round(raw / step) * step;
|
||||||
|
}
|
||||||
|
function clamp(value: number, low: number, high: number): number {
|
||||||
|
return Math.max(low, Math.min(high, value));
|
||||||
|
}
|
||||||
|
|
||||||
|
function getTargetValue(target: HTMLInputElement | null, defaultValue: number): number {
|
||||||
|
if (!target || target.value === "") return defaultValue;
|
||||||
|
const parsed = parseInt(target.value, 10);
|
||||||
|
return isNaN(parsed) ? defaultValue : parsed;
|
||||||
|
}
|
||||||
|
function setTargetValue(target: HTMLInputElement | null, value: number | string): void {
|
||||||
|
if (target) target.value = String(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Track fill positioning ──
|
||||||
|
|
||||||
|
function updateTrackFill(): void {
|
||||||
|
if (!trackFill) return;
|
||||||
|
const minValue = clamp(getTargetValue(minTarget, dataMin), dataMin, dataMax);
|
||||||
|
const maxValue = clamp(getTargetValue(maxTarget, dataMax), dataMin, dataMax);
|
||||||
|
if (mode === "point") {
|
||||||
|
trackFill.style.left = "0%";
|
||||||
|
trackFill.style.width = valueToPercent(maxValue) + "%";
|
||||||
|
} else {
|
||||||
|
let leftPercent = valueToPercent(minValue);
|
||||||
|
let rightPercent = valueToPercent(maxValue);
|
||||||
|
if (leftPercent > rightPercent) {
|
||||||
|
const temp = leftPercent;
|
||||||
|
leftPercent = rightPercent;
|
||||||
|
rightPercent = temp;
|
||||||
|
}
|
||||||
|
const widthPercent = rightPercent - leftPercent;
|
||||||
|
trackFill.style.left = leftPercent + "%";
|
||||||
|
trackFill.style.width = widthPercent + "%";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateHandles(): void {
|
||||||
|
const minValue = clamp(getTargetValue(minTarget, dataMin), dataMin, dataMax);
|
||||||
|
const maxValue = clamp(getTargetValue(maxTarget, dataMax), dataMin, dataMax);
|
||||||
|
minHandle!.style.left = valueToPercent(minValue) + "%";
|
||||||
|
maxHandle!.style.left = valueToPercent(maxValue) + "%";
|
||||||
|
updateTrackFill();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Dragging ──
|
||||||
|
|
||||||
|
function makeDraggable(handle: HTMLElement, isMin: boolean): void {
|
||||||
|
handle.addEventListener("mousedown", (event) => {
|
||||||
|
event.preventDefault();
|
||||||
|
const rect = slider.getBoundingClientRect();
|
||||||
|
|
||||||
|
function onMove(moveEvent: MouseEvent): void {
|
||||||
|
const percent = ((moveEvent.clientX - rect.left) / rect.width) * 100;
|
||||||
|
const value = percentToValue(clamp(percent, 0, 100));
|
||||||
|
|
||||||
|
if (mode === "point") {
|
||||||
|
setTargetValue(minTarget, value);
|
||||||
|
setTargetValue(maxTarget, value);
|
||||||
|
if (minTarget) minTarget.dispatchEvent(new Event("input", { bubbles: true }));
|
||||||
|
if (maxTarget) maxTarget.dispatchEvent(new Event("input", { bubbles: true }));
|
||||||
|
} else if (isMin) {
|
||||||
|
setTargetValue(
|
||||||
|
minTarget,
|
||||||
|
clamp(value, dataMin, getTargetValue(maxTarget, dataMax))
|
||||||
|
);
|
||||||
|
if (minTarget) minTarget.dispatchEvent(new Event("input", { bubbles: true }));
|
||||||
|
} else {
|
||||||
|
setTargetValue(
|
||||||
|
maxTarget,
|
||||||
|
clamp(value, getTargetValue(minTarget, dataMin), dataMax)
|
||||||
|
);
|
||||||
|
if (maxTarget) maxTarget.dispatchEvent(new Event("input", { bubbles: true }));
|
||||||
|
}
|
||||||
|
updateHandles();
|
||||||
|
}
|
||||||
|
|
||||||
|
function onUp(): void {
|
||||||
|
document.removeEventListener("mousemove", onMove);
|
||||||
|
document.removeEventListener("mouseup", onUp);
|
||||||
|
}
|
||||||
|
document.addEventListener("mousemove", onMove);
|
||||||
|
document.addEventListener("mouseup", onUp);
|
||||||
|
onMove(event);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
makeDraggable(minHandle, true);
|
||||||
|
makeDraggable(maxHandle, false);
|
||||||
|
|
||||||
|
// ── Sync from number inputs back to handles ──
|
||||||
|
|
||||||
|
function syncFromInputs(event?: Event): void {
|
||||||
|
if (mode === "point") {
|
||||||
|
const source = (event?.target as HTMLInputElement | null) || minTarget || maxTarget;
|
||||||
|
const value = source ? source.value : "";
|
||||||
|
setTargetValue(minTarget, value);
|
||||||
|
setTargetValue(maxTarget, value);
|
||||||
|
} else if (event && event.target) {
|
||||||
|
const minValue = getTargetValue(minTarget, dataMin);
|
||||||
|
const maxValue = getTargetValue(maxTarget, dataMax);
|
||||||
|
if (event.target === minTarget) {
|
||||||
|
if (minValue > maxValue) {
|
||||||
|
setTargetValue(maxTarget, minValue);
|
||||||
|
}
|
||||||
|
} else if (event.target === maxTarget) {
|
||||||
|
if (maxValue < minValue) {
|
||||||
|
setTargetValue(minTarget, maxValue);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
updateHandles();
|
||||||
|
}
|
||||||
|
|
||||||
|
function enforceStrictBounds(event: Event): void {
|
||||||
|
const target = event.target as HTMLInputElement | null;
|
||||||
|
if (target) {
|
||||||
|
const value = parseInt(target.value, 10);
|
||||||
|
if (!isNaN(value)) {
|
||||||
|
const clamped = clamp(value, dataMin, dataMax);
|
||||||
|
if (clamped !== value) {
|
||||||
|
setTargetValue(target, clamped);
|
||||||
|
target.dispatchEvent(new Event("input", { bubbles: true }));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (minTarget) {
|
||||||
|
minTarget.addEventListener("input", syncFromInputs);
|
||||||
|
minTarget.addEventListener("change", enforceStrictBounds);
|
||||||
|
}
|
||||||
|
if (maxTarget) {
|
||||||
|
maxTarget.addEventListener("input", syncFromInputs);
|
||||||
|
maxTarget.addEventListener("change", enforceStrictBounds);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Mode toggle ──
|
||||||
|
|
||||||
|
const block = slider.closest(".range-slider-block");
|
||||||
|
const toggleButton = block && block.querySelector(".range-mode-toggle");
|
||||||
|
if (toggleButton) {
|
||||||
|
toggleButton.addEventListener("click", () => {
|
||||||
|
const newMode = mode === "range" ? "point" : "range";
|
||||||
|
slider.setAttribute("data-mode", newMode);
|
||||||
|
|
||||||
|
// Swap toggle icons
|
||||||
|
const iconRange = toggleButton.querySelector(".range-mode-icon-range");
|
||||||
|
const iconPoint = toggleButton.querySelector(".range-mode-icon-point");
|
||||||
|
if (iconRange) iconRange.classList.toggle("hidden");
|
||||||
|
if (iconPoint) iconPoint.classList.toggle("hidden");
|
||||||
|
|
||||||
|
const dashSpan = block && block.querySelector(".range-dash");
|
||||||
|
if (newMode === "point") {
|
||||||
|
minHandle.style.display = "none";
|
||||||
|
setTargetValue(minTarget, maxTarget ? maxTarget.value : "");
|
||||||
|
if (minTarget) minTarget.classList.add("hidden");
|
||||||
|
if (dashSpan) dashSpan.classList.add("hidden");
|
||||||
|
} else {
|
||||||
|
minHandle.style.display = "";
|
||||||
|
if (minTarget) minTarget.classList.remove("hidden");
|
||||||
|
if (dashSpan) dashSpan.classList.remove("hidden");
|
||||||
|
}
|
||||||
|
mode = newMode;
|
||||||
|
updateHandles();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Initial position ──
|
||||||
|
updateHandles();
|
||||||
|
}
|
||||||
|
|
||||||
|
onSwap(".range-slider", initializeSlider);
|
||||||
|
})();
|
||||||
Reference in New Issue
Block a user