UNPKG

melt

Version:

The next generation of Melt UI. Built for Svelte 5.

146 lines (145 loc) 5.05 kB
import { dataAttr, styleAttr } from "../utils/attribute"; import { extract } from "../utils/extract"; import { clamp } from "../utils/number"; import { useEventListener } from "runed"; import { Synced } from "../Synced.svelte"; import { createBuilderMetadata } from "../utils/identifiers"; import { isHtmlElement } from "../utils/is"; const { createIds, dataAttrs } = createBuilderMetadata("slider", ["root", "thumb"]); export class Slider { /* Props */ #props; min = $derived(extract(this.#props.min, 0)); max = $derived(extract(this.#props.max, 100)); orientation = $derived(extract(this.#props.orientation, "horizontal")); step = $derived(extract(this.#props.step, 1)); /* State */ #value; ids = createIds(); #mouseDown = false; #dragging = false; #mouseDownAt = null; constructor(props = {}) { this.#props = props; this.#value = new Synced({ value: props.value, onChange: props.onValueChange, defaultValue: 0, }); } /** The value of the slider. */ get value() { return this.#value.current; } set value(value) { this.#value.current = clamp(this.min, value, this.max); } get #percentage() { const v = (this.value - this.min) / (this.max - this.min); return this.orientation === "vertical" ? 1 - v : v; } #commit(e) { this.#dragging = typeof this.#mouseDownAt === "number" && e.timeStamp - this.#mouseDownAt > 50; const el = document.getElementById(this.ids.root); if (!isHtmlElement(el)) return; e.preventDefault(); const elRect = el.getBoundingClientRect(); let percentage; if (this.orientation === "vertical") { percentage = 1 - clamp(0, e.clientY - elRect.top, elRect.height) / elRect.height; } else { percentage = clamp(0, e.clientX - elRect.left, elRect.width) / elRect.width; } this.value = getValueFixedToStep(this.min + percentage * (this.max - this.min), this.step); } get #sharedProps() { return { "data-dragging": dataAttr(this.#dragging), "data-value": dataAttr(this.value), "data-orientation": dataAttr(this.orientation), }; } /** * The root of the slider. * Any cursor interaction along this element will change the slider's values. **/ get root() { useEventListener(() => window, "pointermove", (e) => { if (!this.#mouseDown) return; this.#commit(e); }); useEventListener(() => window, "pointerup", () => { this.#mouseDown = false; this.#dragging = false; }); return { "aria-valuenow": this.value, "aria-valuemin": this.min, "aria-valuemax": this.max, "aria-orientation": this.orientation, style: styleAttr({ "--percentage": `${this.#percentage * 100}%`, "--percentage-inv": `${(1 - this.#percentage) * 100}%`, "touch-action": this.orientation === "vertical" ? "pan-x" : "pan-y", }), tabindex: 0, role: "slider", [dataAttrs.root]: "", id: this.ids.root, onpointerdown: (e) => { this.#mouseDown = true; this.#mouseDownAt = e.timeStamp; this.#commit(e); document.getElementById(this.ids.thumb)?.focus(); }, onkeydown: (e) => { switch (e.key) { case "ArrowDown": case "ArrowLeft": { if (e.metaKey) this.value = this.min; else this.value -= this.step; break; } case "ArrowUp": case "ArrowRight": { if (e.metaKey) this.value = this.max; else this.value += this.step; break; } case "Home": { this.value = this.min; break; } case "End": { this.value = this.max; break; } default: { return; } } e.preventDefault(); }, ...this.#sharedProps, }; } /** The slider's thumb, positioned at the end of the range. */ get thumb() { return { [dataAttrs.thumb]: "", id: this.ids.thumb, tabindex: 0, ...this.#sharedProps, }; } } function getValueFixedToStep(value, step) { return Math.round(value / step) * step; }