UNPKG

range-slider-element

Version:

A cross browser customizable and accessible <range-slider> web component

310 lines (309 loc) 9.39 kB
const A = { value: "valuenow", min: "valuemin", max: "valuemax" }; function E(l = "") { const t = String(l).split(".")[1]; return t ? t.length : 0; } function h(l, t, e) { const i = A[t]; i && l.setAttribute(`aria-${i}`, e); } const x = ["min", "max", "step", "value", "disabled", "value-precision"], w = { min: "min", max: "max", step: "step", value: "value", disabled: "disabled", "value-precision": "valuePrecision" }, m = { stepUp: ["ArrowUp", "ArrowRight"], stepDown: ["ArrowDown", "ArrowLeft"] }, f = document.createElement("template"); f.innerHTML = ` <div data-track></div> <div data-track-fill></div> <div data-runnable-track> <div data-thumb></div> </div> `; class u extends HTMLElement { /** * Registers the custom element with the global or provided custom element registry. * * @param {string} [tagName='range-slider'] - The tag name to register the element under. * @param {CustomElementRegistry} [registry=window.customElements] - Optional custom element registry. * @returns {typeof RangeSliderElement | undefined} - Returns the class constructor if successfully defined, otherwise undefined. * @example * RangeSliderElement.define(); * RangeSliderElement.define('my-slider', customElements); */ static define(t = "range-slider", e = customElements) { if (!e.get(t)) return e.define(t, u), u; } static observedAttributes = x; static formAssociated = !0; #a; #l; #t = []; #n = []; #i = 0; #r = !1; /** * Creates a new instance of the RangeSliderElement. * * @constructor */ constructor() { super(), this.#a = this.attachInternals(), this.addEventListener("focusin", this.#f), this.addEventListener("pointerdown", this.#b), this.addEventListener("keydown", this.#v); } get min() { return this.hasAttribute("min") ? Number(this.getAttribute("min")) : 0; } get max() { return this.hasAttribute("max") ? Number(this.getAttribute("max")) : 100; } get step() { return this.hasAttribute("step") ? Number(this.getAttribute("step")) : 1; } get value() { return this.#t.join(","); } get disabled() { return this.hasAttribute("disabled"); } get valuePrecision() { return this.getAttribute("value-precision") || ""; } get #s() { return this.getAttribute("orientation") === "vertical"; } get #d() { return !!(this.#s || this.getAttribute("dir") === "rtl"); } get #c() { return this.#e.length > 1; } get #e() { return this.querySelectorAll("[data-runnable-track] [data-thumb]"); } get #m() { return this.querySelector("[data-track-fill]"); } get #h() { return this.#s ? this.offsetHeight : this.offsetWidth; } set min(t) { this.setAttribute("min", t); for (const e of this.#e) h(e, "min", t); } set max(t) { this.setAttribute("max", t); for (const e of this.#e) h(e, "max", t); } set step(t) { this.setAttribute("step", t); } set value(t) { const e = String(t).split(",").map((i) => Number(i.trim())).filter((i) => !Number.isNaN(i)); this.#t = [], this.#n = [], e.forEach((i, s) => { this.#u(s, i); }); } set disabled(t) { this.toggleAttribute("disabled", !!t), this.#x(); } set valuePrecision(t) { this.setAttribute("value-precision", t); } /** * Form data support * The following properties and methods aren't strictly required, * but browser-level form controls provide them. Providing them helps * ensure consistency with browser-provided controls. */ get form() { return this.#a.form; } get name() { return this.getAttribute("name"); } get type() { return this.localName; } get validity() { return this.#a.validity; } get validationMessage() { return this.#a.validationMessage; } get willValidate() { return this.#a.willValidate; } checkValidity() { return this.#a.checkValidity(); } reportValidity() { return this.#a.reportValidity(); } formDisabledCallback(t) { this.#r = t, this.#x(); } connectedCallback() { this.firstChild || this.appendChild(f.content.cloneNode(!0)), !this.disabled && !this.#r && this.setAttribute("tabindex", "-1"), this.#e.forEach((t, e) => { t.dataset.thumb = e, t.setAttribute("role", "slider"), h(t, "min", this.min), h(t, "max", this.max), !this.disabled && !this.#r && t.setAttribute("tabindex", 0); }), this.value = this.getAttribute("value") || this.#w(); } disconnectedCallback() { this.removeEventListener("focusin", this.#f), this.removeEventListener("pointerdown", this.#b), this.removeEventListener("keydown", this.#v); } attributeChangedCallback(t, e, i) { if (e === i) return; if (t === "disabled") { this.disabled = i !== null; return; } const s = w[t]; s && (this[s] = i); } #f = (t) => { t.target.dataset.thumb !== void 0 && (this.#i = Number(t.target.dataset.thumb)); }; #b = (t) => { if (!(this.disabled || this.#r)) if (this.setPointerCapture(t.pointerId), this.addEventListener("pointermove", this.#p), window.addEventListener("pointerup", this.#o), window.addEventListener("pointercancel", this.#o), this.#l = this.value, t.target.dataset.thumb !== void 0) this.#i = Number(t.target.dataset.thumb); else { const { offsetX: e, offsetY: i } = t; this.#i = this.#k(this.#s ? i : e), this.#g(this.#s ? i : e); } }; #p = (t) => { t.target === this && (t.preventDefault(), this.#g(this.#s ? t.offsetY : t.offsetX)); }; #o = (t) => { this.releasePointerCapture(t.pointerId), this.removeEventListener("pointermove", this.#p), window.removeEventListener("pointerup", this.#o), window.removeEventListener("pointercancel", this.#o), this.#l !== this.value && this.dispatchEvent(new Event("change", { bubbles: !0 })); }; #v = (t) => { const i = Object.keys(m).find((s) => m[s].includes(t.code) && s); document.activeElement !== this.#e[this.#i] && this.#e[this.#i].focus({ focusVisible: !1 }), i && (t.preventDefault(), this[i]()); }; /** * * @param {number} offset */ #g = (t) => { const i = Math.min(Math.max(t, 0), this.#h) / this.#h, s = this.#E(this.#d ? 1 - i : i); this.#u(this.#i, s, ["input"]); }; #w() { return this.max < this.min ? this.min : this.min + (this.max - this.min) / 2; } /** * * @param {number} value * @returns */ #A(t) { return 100 * (t - this.min) / (this.max - this.min); } /** * Fit the percentage complete between the range [min,max] * by remapping from [0, 1] to [min, min+(max-min)]. * * @param {number} percent * @returns */ #E(t) { return this.min + t * (this.max - this.min); } /** * * @param {number} offset * @returns */ #k(t) { let e; const s = Math.min(Math.max(t, 0), this.#h) / this.#h, n = this.#E(this.#d ? 1 - s : s), a = this.#t.findIndex((r) => n - r < 0); if (a === 0) e = a; else if (a === -1) e = this.#t.length - 1; else { const r = this.#t[a - 1], d = this.#t[a]; Math.abs(r - n) < Math.abs(d - n) ? e = a - 1 : e = a; } return e; } /** * * @param {number} index * @param {number} value * @param {string[]} dispatchEvents */ #u(t, e, i = []) { const s = this.#t[t], n = Number(this.valuePrecision) || E(this.step) || 0, a = Math.min(this.min, this.max), r = Math.max(this.min, this.max), d = this.#t[t - 1] ?? a, b = this.#t[t + 1] ?? r, p = Math.min(Math.max(e, d), b) - this.min, v = Math.round(p / this.step) * this.step, c = this.min + v, o = Number( n ? c.toFixed(n) : Math.round(c) ); if (s !== o) { this.#t[t] = o, this.#n[t] = this.#A(o), this.#a.setFormValue(this.#t.join(",")), this.#T(t, o), this.#V(); for (const g of i) this.dispatchEvent(new Event(g, { bubbles: !0 })); } } /** * * @param {number} index * @param {number} value */ #T(t, e) { this.#e[t] && (this.#e[t].style.setProperty( `inset-${this.#s ? "block" : "inline"}-${this.#s ? "end" : "start"}`, `${this.#A(e)}%` ), h(this.#e[t], "value", e)); } #V() { if (!this.#m) return; const t = this.#c ? `${this.#n[0]}%` : 0, i = `clamp(var(--thumb-size) / 2, ${this.#c ? `${100 - this.#n[this.#n.length - 1]}%` : `${100 - this.#n[0]}%`}, 100% - var(--thumb-size) / 2)`; this.#m.style.setProperty( `inset-${this.#s ? "block" : "inline"}`, this.#s ? `${i} ${t}` : `${t} ${i}` ); } #x() { if (this.disabled || this.#r) { this.removeAttribute("tabindex"); for (const t of this.#e) t.removeAttribute("tabindex"); } else { this.setAttribute("tabindex", "-1"); for (const t of this.#e) t.setAttribute("tabindex", "0"); } } /** * Increments the value * @param {number} amount - The amount to increment by. */ stepUp(t = this.step) { const e = this.#t[this.#i] + t; this.#u(this.#i, e, ["change"]); } /** * Decrements the value * @param {number} amount - The amount to decrement by. */ stepDown(t = this.step) { const e = this.#t[this.#i] - t; this.#u(this.#i, e, ["change"]); } } new URL(import.meta.url).searchParams.has("define", "false") || (window.RangeSliderElement = u.define()); export { u as default };