import { applySwitcheroo } from "../designApplication"; import { markEventAsHandled } from "../eventHandling"; import { ComponentBase, ComponentState } from "./componentBase"; export type SliderState = ComponentState & { _type_: "Slider-builtin"; minimum?: number; maximum?: number; value?: number; step?: number; is_sensitive?: boolean; show_values?: boolean; ticks?: (number | string | [number, string])[] | boolean; }; export class SliderComponent extends ComponentBase { declare state: Required; private innerElement: HTMLElement; private minValueElement: HTMLElement; private maxValueElement: HTMLElement; createElement(): HTMLElement { // Create the HTML structure let element = document.createElement("div"); element.classList.add("rio-slider"); element.innerHTML = `
`; // Expose the elements this.innerElement = element.querySelector( ".rio-slider-inner" ) as HTMLElement; this.minValueElement = element.querySelector( ".rio-slider-min-value" ) as HTMLElement; this.maxValueElement = element.querySelector( ".rio-slider-max-value" ) as HTMLElement; // Subscribe to events this.addDragHandler({ element: element, onStart: this.onDragStart.bind(this), onMove: this.onDragMove.bind(this), onEnd: this.onDragEnd.bind(this), }); return element; } private setValueFromPointerEvent(event: PointerEvent): [number, number] { // If the slider is disabled, do nothing if (!this.state.is_sensitive) { return [this.state.value, this.state.value]; } // Calculate the selected value from the event coordinates let rect = this.innerElement.getBoundingClientRect(); let fraction = (event.clientX - rect.left) / rect.width; fraction = Math.max(0, Math.min(1, fraction)); let valueRange = this.state.maximum - this.state.minimum; let value = fraction * valueRange + this.state.minimum; // Enforce the step size // // Converting to a value, and back to fractions may seem convoluted, but // this ensures that the step size is enforced correctly. Careless math // can lead to floating point errors that cause the reported value to be // off by a little (e.g. 7.99999999 instead of 8). if (this.state.step !== 0) { let stepIndex = Math.round(value / this.state.step); value = stepIndex * this.state.step; fraction = (value - this.state.minimum) / valueRange; } // Move the knob this.innerElement.style.setProperty( "--rio-slider-fraction", `${fraction * 100}%` ); // Return the new value and fraction let newValue = fraction * valueRange + this.state.minimum; return [fraction, newValue]; } private onDragStart(event: PointerEvent): boolean { markEventAsHandled(event); this.setValueFromPointerEvent(event); return true; } private onDragMove(event: PointerEvent): void { markEventAsHandled(event); // Make future transitions instant to avoid lag this.element.style.setProperty( "--rio-slider-position-transition-time", "0s" ); this.setValueFromPointerEvent(event); } private onDragEnd(event: PointerEvent): void { markEventAsHandled(event); // Revert to the default transition time this.element.style.removeProperty( "--rio-slider-position-transition-time" ); // Get the new value let [fraction, value] = this.setValueFromPointerEvent(event); // Update state and notify the backend of the new value this.setStateAndNotifyBackend({ value: value, }); } updateElement( deltaState: SliderState, latentComponents: Set ): void { super.updateElement(deltaState, latentComponents); if ( deltaState.minimum !== undefined || deltaState.maximum !== undefined || deltaState.step !== undefined || deltaState.value !== undefined ) { // The server can send invalid values due to reconciliation. Fix // them. let value = deltaState.value ?? this.state.value; let step = deltaState.step ?? this.state.step; let minimum = deltaState.minimum ?? this.state.minimum; let maximum = deltaState.maximum ?? this.state.maximum; // Bring the value into a valid range value = Math.max(minimum, Math.min(maximum, value)); // Apply the step size if (step !== 0) { value = Math.round(value / step) * step; } // Update the CSS let fraction = (value - minimum) / (maximum - minimum); this.innerElement.style.setProperty( "--rio-slider-fraction", `${fraction * 100}%` ); this.minValueElement.textContent = minimum.toFixed(2); this.maxValueElement.textContent = maximum.toFixed(2); } if (deltaState.is_sensitive === true) { applySwitcheroo(this.element, "keep"); } else if (deltaState.is_sensitive === false) { applySwitcheroo(this.element, "disabled"); } if (deltaState.show_values === true) { this.minValueElement.style.display = "block"; this.maxValueElement.style.display = "block"; } else if (deltaState.show_values === false) { this.minValueElement.style.display = "none"; this.maxValueElement.style.display = "none"; } } }