import TrackUnit, { TrackUnitEvent, TrackUnitStickingType, TrackUnitType } from "@/TrackUnit"; import "./TrackUnit.css"; import { Capsule, h, IPublisher, ISubscriber, ISubscription, Publisher, Rung, RungOptions } from "@djledda/ladder"; import IconView, { IconName } from "@/ui/Widgets/Icon/IconView"; export type TrackUnitUINodeOptions = RungOptions & { trackUnit: TrackUnit, }; const EventTypeSubscriptions = [ TrackUnitEvent.On, TrackUnitEvent.Off, TrackUnitEvent.TypeChange, ]; type EventTypeSubscriptions = typeof EventTypeSubscriptions[number]; export const StickingTypeIconMap = { none: null, lf: 'lf', lh: 'lh', rf: 'rf', rh: 'rh', } as const satisfies Readonly>; const TypeClasses = [ "Ghost", "Accent" ] as const; export const TrackUnitTypeClassMap = { "Normal": [], "GhostNote": ["Ghost"], "Accent": ["Accent"], "GhostNoteAccent": ["Ghost", "Accent"], } as const satisfies Readonly>>; export default class TrackUnitView extends Rung implements ISubscriber { private trackUnit: TrackUnit; private subscription: ISubscription | null = null; private publisher: IPublisher = new Publisher(this); private rotationTimeout: ReturnType | null = null; private mouseDownListeners: ((ev: MouseEvent) => void)[] = []; private hoverListeners: ((ev: MouseEvent) => void)[] = []; private blockNextMouseUp = false; private icon: IconView = new IconView({ iconName: 'list' }); constructor(options: TrackUnitUINodeOptions) { super(options); this.trackUnit = options.trackUnit; this.setUnit(this.trackUnit); } setUnit(trackUnit: TrackUnit | null): void { if (trackUnit) { this.trackUnit = trackUnit; this.setupBindings(); this.notify(this.publisher, trackUnit.isOn() ? TrackUnitEvent.On : TrackUnitEvent.Off); this.notify(this.publisher, TrackUnitEvent.TypeChange); } else { this.subscription?.unbind(); } } private setupBindings() { this.subscription?.unbind(); this.subscription = this.trackUnit.addSubscriber(this, EventTypeSubscriptions); this.hoverListeners.forEach(listener => this.render().removeEventListener("mouseover", listener)); this.mouseDownListeners.forEach(listener => this.render().removeEventListener("mousedown", listener)); this.redraw(); this.hoverListeners.forEach(listener => this.render().addEventListener("mouseover", listener)); this.mouseDownListeners.forEach(listener => this.render().addEventListener("mousedown", listener)); this.render().addEventListener("mousedown", (ev) => this.handleMouseDown(ev)); this.render().addEventListener("mouseout", (ev) => this.handleMouseOut(ev)); this.render().addEventListener("mouseup", (ev) => this.handleMouseUp(ev)); this.render().addEventListener("touchstart", (ev) => this.handleTouchStart(ev)); this.render().addEventListener("touchend", (ev) => this.handleTouchEnd(ev)); } private handleMouseDown(ev: MouseEvent): void { if (ev.button === 1) { this.trackUnit.rotateType(); } } private handleMouseOut(ev: MouseEvent): void { if (this.rotationTimeout) { clearTimeout(this.rotationTimeout); this.rotationTimeout = null; } } private handleMouseUp(ev: MouseEvent): void { if (!this.blockNextMouseUp) { this.mouseDownListeners.forEach(listener => listener(ev)); } this.blockNextMouseUp = false; } private handleTouchStart(ev: TouchEvent): void { this.rotationTimeout = this.rotationTimeout || setTimeout(() => { this.trackUnit.rotateType(); this.rotationTimeout = null; }, 400); } private handleTouchEnd(ev: TouchEvent): void { if (this.rotationTimeout) { clearTimeout(this.rotationTimeout); this.rotationTimeout = null; } } toggle(): void { this.trackUnit.toggle(); } setStickingType(type: TrackUnitStickingType): void { if (this.trackUnit.isOn()) { this.trackUnit.setStickingType(type); } } setType(type: TrackUnitType): void { this.trackUnit.setType(type); } turnOn(): void { this.trackUnit.setOn(true); } turnOff(): void { this.trackUnit.setStickingType("none"); this.trackUnit.setOn(false); } notify(publisher: unknown, event: EventTypeSubscriptions): void { switch (event) { case TrackUnitEvent.On: this.render().classList.add("on"); break; case TrackUnitEvent.Off: this.render().classList.remove("on"); break; case TrackUnitEvent.TypeChange: this.syncTrackUnitType(); this.syncStickingType(this.trackUnit.getStickingType()); } } private syncStickingType(type: TrackUnitStickingType) { if (StickingTypeIconMap[this.trackUnit.getStickingType()]) { this.render().classList.add("icon-visible"); } else { this.render().classList.remove("icon-visible"); } const icon = StickingTypeIconMap[this.trackUnit.getStickingType()]; if (icon) { this.icon.setIcon(icon); } } private syncTrackUnitType() { for (const className of TypeClasses) { this.render().classList.remove(className); } for (const className of TrackUnitTypeClassMap[this.trackUnit.getType()]) { this.render().classList.add(className); } } static getClasses(options: { on: boolean, stickingType: TrackUnitStickingType, type: TrackUnitType, highlightable?: boolean }) { const classes = ["track-unit"]; if (options.on) { classes.push("on"); } if (StickingTypeIconMap[options.stickingType]) { classes.push("icon-visible"); } if (options.type) { classes.push(...TrackUnitTypeClassMap[options.type]); } if (options.highlightable) { classes.push("highlightable"); } return classes; } build() { const classes = TrackUnitView.getClasses({ on: this.trackUnit.isOn(), stickingType: this.trackUnit.getStickingType(), type: this.trackUnit.getType(), highlightable: true, }); return (
false}> {this.icon}
) as HTMLDivElement; } onHover(cb: () => void): void { this.hoverListeners.push(cb); this.render().addEventListener("mouseover", cb); } onMouseDown(cb: (ev: MouseEvent) => void): void { this.mouseDownListeners.push(cb); this.render().addEventListener("mousedown", cb); } }