class Range extends HTMLElement { static get observedAttributes() { return ["min", "max", "value", "step", "disabled"]; } constructor() { super(); this._dom = {}; this.addEventListener("mousedown", this); this.addEventListener("keydown", this); } get _valueAsNumber() { let raw = (this.hasAttribute("value") ? Number(this.getAttribute("value")) : 50); return this._constrain(raw); } get _minAsNumber() { return (this.hasAttribute("min") ? Number(this.getAttribute("min")) : 0); } get _maxAsNumber() { return (this.hasAttribute("max") ? Number(this.getAttribute("max")) : 100); } get _stepAsNumber() { return (this.hasAttribute("step") ? Number(this.getAttribute("step")) : 1); } get value() { return String(this._valueAsNumber); } get valueAsNumber() { return this._valueAsNumber; } get min() { return this.hasAttribute("min") ? this.getAttribute("min") : ""; } get max() { return this.hasAttribute("max") ? this.getAttribute("max") : ""; } get step() { return this.hasAttribute("step") ? this.getAttribute("step") : ""; } get disabled() { return this.hasAttribute("disabled"); } set _valueAsNumber(value) { this.value = String(value); } set min(min) { this.setAttribute("min", min); } set max(max) { this.setAttribute("max", max); } set value(value) { this.setAttribute("value", value); } set step(step) { this.setAttribute("step", step); } set disabled(disabled) { disabled ? this.setAttribute("disabled", "") : this.removeAttribute("disabled"); } connectedCallback() { if (this.firstChild) { return; } this.innerHTML = `
`; Array.from(this.querySelectorAll("[class^='-']")).forEach(node => { let name = node.className.substring(1); this._dom[name] = node; }); this._update(); } attributeChangedCallback(name, oldValue, newValue) { switch (name) { case "min": case "max": case "value": case "step": this._update(); break; } } handleEvent(e) { switch (e.type) { case "mousedown": if (this.disabled) { return; } document.addEventListener("mousemove", this); document.addEventListener("mouseup", this); this._setToMouse(e); break; case "mousemove": this._setToMouse(e); break; case "mouseup": document.removeEventListener("mousemove", this); document.removeEventListener("mouseup", this); this.dispatchEvent(new CustomEvent("change")); break; case "keydown": if (this.disabled) { return; } this._handleKey(e.code); this.dispatchEvent(new CustomEvent("input")); this.dispatchEvent(new CustomEvent("change")); break; } } _handleKey(code) { let min = this._minAsNumber; let max = this._maxAsNumber; let range = max - min; let step = this._stepAsNumber; switch (code) { case "ArrowLeft": case "ArrowDown": this._valueAsNumber = this._constrain(this._valueAsNumber - step); break; case "ArrowRight": case "ArrowUp": this._valueAsNumber = this._constrain(this._valueAsNumber + step); break; case "Home": this._valueAsNumber = this._constrain(min); break; case "End": this._valueAsNumber = this._constrain(max); break; case "PageUp": this._valueAsNumber = this._constrain(this._valueAsNumber + range/10); break; case "PageDown": this._valueAsNumber = this._constrain(this._valueAsNumber - range/10); break; } } _constrain(value) { const min = this._minAsNumber; const max = this._maxAsNumber; const step = this._stepAsNumber; value = Math.max(value, min); value = Math.min(value, max); value -= min; value = Math.round(value / step) * step; value += min; if (value > max) { value -= step; } return value; } _update() { let min = this._minAsNumber; let max = this._maxAsNumber; let frac = (this._valueAsNumber-min) / (max-min); this._dom.thumb.style.left = `${frac * 100}%`; this._dom.remaining.style.left = `${frac * 100}%`; this._dom.elapsed.style.width = `${frac * 100}%`; } _setToMouse(e) { let rect = this._dom.inner.getBoundingClientRect(); let x = e.clientX; x = Math.max(x, rect.left); x = Math.min(x, rect.right); let min = this._minAsNumber; let max = this._maxAsNumber; let frac = (x-rect.left) / (rect.right-rect.left); let value = this._constrain(min + frac * (max-min)); if (value == this._valueAsNumber) { return; } this._valueAsNumber = value; this.dispatchEvent(new CustomEvent("input")); } } customElements.define('x-range', Range);