ui/Slider.tstypescript
import { clamp } from "../math";
import type { EventsHandler } from "../Events";
import { View } from "./View";
import type { IViewParams, ViewEventsList } from "./View";
import { stringTemplate } from "../utils/shared";
interface ISliderParams extends IViewParams {
label?: string;
value?: number;
min?: number;
max?: number;
step?: number | "any";
}
type SliderEventsList = ["change"];
const SLIDER_EVENTS: SliderEventsList = ["change"];
const TEMPLATE = `<div class="og-slider">
<div class="og-slider-label">{label}</div>
<div class="og-slider-panel">
<div class="og-slider-progress"></div>
<div class="og-slider-pointer"></div>
</div>
<input type="number" inputmode="decimal"/>
</div>`;
class Slider extends View<null> {
public override events: EventsHandler<SliderEventsList> & EventsHandler<ViewEventsList>;
protected _value: number;
protected _min: number;
protected _max: number;
protected _step: number | "any";
protected _startPosX: number;
protected _resizeObserver: ResizeObserver;
protected $label: HTMLElement | null;
protected $pointer: HTMLElement | null;
protected $progress: HTMLElement | null;
protected $input: HTMLInputElement | null;
protected $panel: HTMLElement | null;
constructor(options: ISliderParams = {}) {
super({
template: stringTemplate(TEMPLATE, {
label: options.label || ""
})
});
//@ts-ignore
this.events = this.events.registerNames(SLIDER_EVENTS);
this._value = options.value || 0.0;
this._min = options.min || 0.0;
this._max = options.max || 1.0;
this._step = options.step ?? "any";
//this._step = options.step || ((this._max - this._min) / 10.0);
this._resizeObserver = new ResizeObserver(this._onResize);
this._startPosX = 0;
this.$label = null;
this.$pointer = null;
this.$progress = null;
this.$input = null;
this.$panel = null;
}
public override render(params: any): this {
super.render(params);
this.$label = this.select(".og-slider-label")!;
if (this.$label.innerHTML === "") {
this.$label.style.display = "none";
}
this.$pointer = this.select(".og-slider-pointer");
this.$progress = this.select(".og-slider-progress");
this.$panel = this.select(".og-slider-panel");
this.$input = this.select<HTMLInputElement>("input");
if (this.$input) {
this.$input.setAttribute("step", this._step.toString());
this.$input.min = this._min.toString();
this.$input.max = this._max.toString();
}
this._syncUiFromValue();
this._resizeObserver.observe(this.el!);
this._initEvents();
return this;
}
protected _onResize = () => {
this._syncUiFromValue();
};
protected _syncUiFromValue() {
if (!this.$panel || !this.$input) return;
this.$input.value = this._value.toString();
const range = this._max - this._min;
if (range === 0) {
this._setOffset(0);
return;
}
this._setOffset(((this._value - this._min) * this.$panel.clientWidth) / range);
}
public set value(val: number) {
const clamped = clamp(val, this._min, this._max);
if (clamped !== this._value) {
this._value = clamped;
this._syncUiFromValue();
this.events.dispatch(this.events.change, this._value, this);
}
}
public get value(): number {
return this._value;
}
public override afterRender(parentNode: HTMLElement) {
this._initEvents();
}
protected _initEvents() {
this.$panel?.addEventListener("mousedown", this._onMouseDown);
//@ts-ignore
this.$panel?.addEventListener("mousewheel", this._onMouseWheel);
this.$panel?.addEventListener("wheel", this._onMouseWheelFF);
this.$input?.addEventListener("input", this._onInput);
}
protected _clearEvents() {
this.$panel?.removeEventListener("mousedown", this._onMouseDown);
//@ts-ignore
this.$panel?.removeEventListener("mousewheel", this._onMouseWheel);
this.$panel?.removeEventListener("wheel", this._onMouseWheelFF);
this.$input?.removeEventListener("input", this._onInput);
}
protected _onMouseWheel = (e: WheelEvent) => {
//@ts-ignore
e = e || window.event;
e.preventDefault();
e.stopPropagation();
//@ts-ignore
this.value = this._value + (Math.sign(e.wheelDelta) * (this._max - this._min)) / 100.0;
};
protected _onMouseWheelFF = (e: WheelEvent) => {
this._onMouseWheel(e);
};
protected _onInput = (e: Event) => {
//@ts-ignore
e = e || window.event;
e.preventDefault();
e.stopPropagation();
//@ts-ignore
this.value = e.target.value;
};
protected _onMouseDown = (e: MouseEvent) => {
//@ts-ignore
e = e || window.event;
e.preventDefault();
this._startPosX = e.clientX;
this.value = this._min + (this._max - this._min) * (e.offsetX / this.$panel!.clientWidth);
document.addEventListener("mousemove", this._onMouseMove);
document.addEventListener("mouseup", this._onMouseUp);
};
protected _setOffset(x: number) {
if (x >= 0 && x <= this.$panel!.clientWidth) {
this.$pointer!.style.left = this.$progress!.style.width = `${(x * 100) / this.$panel!.clientWidth}%`;
}
}
protected _onMouseMove = (e: MouseEvent) => {
//@ts-ignore
e = e || window.event;
e.preventDefault();
e.stopPropagation();
let rect = this.$panel!.getBoundingClientRect();
let clientX = clamp(e.clientX, rect.left, rect.right);
let dx = this._startPosX - clientX;
this._startPosX = clientX;
this.value = this._value - (dx * (this._max - this._min)) / this.$panel!.clientWidth;
};
protected _onMouseUp = () => {
document.removeEventListener("mouseup", this._onMouseUp);
document.removeEventListener("mousemove", this._onMouseMove);
};
public override remove() {
this._clearEvents();
super.remove();
}
public set max(value: number) {
this._max = value;
this.events.stopPropagation();
this.value = this._value;
this._syncUiFromValue();
}
public get max(): number {
return this._max;
}
}
export { Slider };