range-slider-element
Version:
A cross browser customizable and accessible <range-slider> web component
310 lines (309 loc) • 9.39 kB
JavaScript
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
};