/** @jsxImportSource react */ import { VDOM } from "../../ui/Widget"; import { PureContainerBase, PureContainerConfig } from "../../ui/PureContainer"; import { KeyCode } from "../../util/KeyCode"; import { Icon } from "../Icon"; import { addEventListenerWithOptions } from "../../util"; import type { RenderingContext } from "../../ui/RenderingContext"; import type { Instance } from "../../ui/Instance"; import type { CSS } from "../../ui/CSS"; import { Prop, StructuredProp } from "../../ui/Prop"; export interface WheelConfig extends PureContainerConfig { /** The selected value. */ value?: Prop; /** Array of available options. */ options?: StructuredProp; /** Number of visible options in the wheel. Default is 3. */ size?: number; } export class Wheel extends PureContainerBase { declare size: number; declare baseClass: string; declareData(...args: Record[]): void { return super.declareData(...args, { value: undefined, options: undefined, }); } render(context: RenderingContext, instance: Instance, key: string): React.ReactNode { let { data } = instance; let { value, options } = data; let index = options.findIndex((a: any) => a.id === value); if (index === -1) index = Math.floor(options.length / 2); return ( { let option = options[newIndex]; instance.set("value", option.id); }} > {options.map((o: any, i: number) => ( {o.text} ))} ); } } Wheel.prototype.baseClass = "wheel"; Wheel.prototype.size = 3; Wheel.prototype.styled = true; export interface WheelComponentProps { size: number; children: React.ReactNode[]; CSS: typeof CSS; baseClass: string; active?: boolean; className?: string; style?: React.CSSProperties; index?: number; onChange: (newIndex: number) => void; onPipeKeyDown?: (fn: (e: React.KeyboardEvent) => void) => void; onMouseDown?: () => void; focusable?: boolean; } interface WheelComponentState { wheelHeight?: number; } export class WheelComponent extends VDOM.Component { index: number; wheelEl!: HTMLDivElement; scrollEl!: HTMLDivElement; unsubscribeOnWheel!: () => void; declare scrolling?: boolean; constructor(props: WheelComponentProps) { super(props); this.state = {}; this.index = props.index || 0; this.wheelRef = (el: HTMLDivElement | null) => { if (el) this.wheelEl = el; }; this.scrollRef = (el: HTMLDivElement | null) => { if (el) this.scrollEl = el; }; this.onWheel = this.onWheel.bind(this); this.onKeyDown = this.onKeyDown.bind(this); } wheelRef: (el: HTMLDivElement | null) => void; scrollRef: (el: HTMLDivElement | null) => void; render(): React.ReactNode { let { size, children, CSS, baseClass, active, className, style, onMouseDown } = this.props; let optionClass = CSS.element(baseClass, "option"); let dummyClass = CSS.element(baseClass, "option", { dummy: true }); let tpad = [], bpad = [], padSize = 0; for (let i = 0; i < (size - 1) / 2; i++) { tpad.push({ key: -1 - i, child: children[0], cls: dummyClass }); bpad.push({ key: -100 - i, child: children[0], cls: dummyClass }); padSize++; } let displayedOptions = [ ...tpad, ...children.map((c, i) => ({ key: i, child: c, cls: optionClass, })), ...bpad, ]; if (!this.state.wheelHeight) displayedOptions = displayedOptions.slice(this.index, this.index + size); return (
{displayedOptions.map((opt) => (
{opt.child}
))}
{ e.preventDefault(); this.select(this.index - 1); }} > {Icon.render("drop-down", { className: CSS.element(baseClass, "arrow-icon") })}
{ e.preventDefault(); this.select(this.index + 1); }} > {Icon.render("drop-down", { className: CSS.element(baseClass, "arrow-icon") })}
); } componentDidMount(): void { this.unsubscribeOnWheel = addEventListenerWithOptions(this.wheelEl, "wheel", this.onWheel, { passive: false }); this.setState( { wheelHeight: this.wheelEl.offsetHeight, }, () => { if (this.state.wheelHeight !== undefined) { this.scrollEl.scrollTop = (this.index * this.state.wheelHeight) / this.props.size; } }, ); if (this.props.onPipeKeyDown) this.props.onPipeKeyDown(this.onKeyDown); } UNSAFE_componentWillReceiveProps(props: WheelComponentProps): void { this.index = props.index || 0; this.scrollTo(); } componentWillUnmount(): void { this.scrolling = false; this.unsubscribeOnWheel(); } onKeyDown(e: React.KeyboardEvent): void { switch (e.keyCode) { case KeyCode.up: e.preventDefault(); this.select(this.index - 1); break; case KeyCode.down: e.preventDefault(); this.select(this.index + 1); break; } } onWheel(e: WheelEvent): void { e.preventDefault(); let index = this.index; if (e.deltaY > 0) index++; else index--; this.select(index); } onTouchStart(e: React.TouchEvent): void { this.scrolling = false; } onTouchEnd(e: React.TouchEvent): void { let { size } = this.props; if (this.state.wheelHeight !== undefined) { let index = Math.round(this.scrollEl.scrollTop / (this.state.wheelHeight / size)); this.select(index); } } select(newIndex: number): void { let { children } = this.props; newIndex = Math.max(0, Math.min(children.length - 1, newIndex)); if (this.index !== newIndex) { this.index = newIndex; this.props.onChange(newIndex); } this.scrollTo(); } scrollTo(): void { let { size } = this.props; let callback = (): void => { if (!this.scrolling || this.state.wheelHeight === undefined) return; let x = (this.index * this.state.wheelHeight) / size; let delta = Math.round(x - this.scrollEl.scrollTop); if (delta === 0) { this.scrolling = false; return; } let sign = delta > 0 ? 1 : -1; delta = Math.abs(delta) / 10; if (delta < 1) delta = 1; this.scrollEl.scrollTop += sign * delta; requestAnimationFrame(callback); }; if (!this.scrolling) { this.scrolling = true; requestAnimationFrame(callback); } } }