diff --git a/src/Publisher.ts b/src/Publisher.ts index 6a0c6cb..8ccb347 100644 --- a/src/Publisher.ts +++ b/src/Publisher.ts @@ -1,17 +1,21 @@ import ISubscriber, {LEvent} from "./Subscriber"; -class Subscription implements ISubscription { +class PublisherSubscription implements ISubscription { private subscriber: ISubscriber; private readonly eventTypes: EventType[]; - private publisher: Publisher; - constructor(publisher: Publisher, subscriber: ISubscriber, eventTypes: EventType[]) { + private unbindCallback?: () => void; + + constructor(subscriber: ISubscriber, eventTypes: EventType[]) { this.subscriber = subscriber; - this.publisher = publisher; this.eventTypes = eventTypes; } + setUnbind(unbind: () => void): void { + this.unbindCallback = unbind; + } + unbind(): void { - this.publisher.unbind(this); + this.unbindCallback?.(); } getEventTypes(): EventType[] { @@ -48,10 +52,12 @@ export class Publisher implements IPubl for (const key of eventTypes) { this.getSubscribers(key).push(subscriber); } - return new Subscription(this, subscriber, eventTypes); + const sub = new PublisherSubscription(subscriber, eventTypes); + sub.setUnbind(() => this.unbind(sub)); + return sub; } - unbind(subscription: Subscription): void { + private unbind(subscription: PublisherSubscription): void { for (const key of subscription.getEventTypes()) { const subs = this.getSubscribers(key); subs.splice(subs.indexOf(subscription.getSubscriber()), 1); diff --git a/src/Ref.ts b/src/Ref.ts index 094e997..3c91d58 100644 --- a/src/Ref.ts +++ b/src/Ref.ts @@ -1,4 +1,22 @@ -export default class Ref { +import {ISubscription} from "@/Publisher"; + +class RefSubscription implements ISubscription { + private unbindCallback?: () => void; + + constructor(unbindCallback: () => void) { + this.unbindCallback = unbindCallback; + } + + unbind(): void { + this.unbindCallback?.(); + } +} + +interface Stringable { + toString(): string; +} + +export default class Ref { private watchers: Array<(newVal: T) => void> | null = null; private value: T; private asString?: string; @@ -9,11 +27,22 @@ export default class Ref void): void { + watch(watcher: (newVal: T) => void): ISubscription { if (this.watchers === null) { this.watchers = []; } - this.watchers.push(callback); + this.watchers.push(watcher); + return new RefSubscription(() => this.unbind(watcher)); + } + + private unbind(watcher: (newVal: T) => void): void { + if (!this.watchers) { + return; + } + const index = this.watchers.indexOf(watcher); + if (index !== -1) { + this.watchers.splice(index, 1); + } } get val(): T { diff --git a/src/ui/Beat/BeatView.ts b/src/ui/Beat/BeatView.ts index 015c872..741888a 100644 --- a/src/ui/Beat/BeatView.ts +++ b/src/ui/Beat/BeatView.ts @@ -35,11 +35,15 @@ export default class BeatView extends UINode implements ISubscriber beatUnitView.setUnit(null)); } private onBeatUnitClick(button: number, index: number) { diff --git a/src/ui/BeatGroup/BeatGroupView.ts b/src/ui/BeatGroup/BeatGroupView.ts index 49d5eed..b58641b 100644 --- a/src/ui/BeatGroup/BeatGroupView.ts +++ b/src/ui/BeatGroup/BeatGroupView.ts @@ -40,11 +40,18 @@ export default class BeatGroupView extends UINode implements ISubscriber beatView.setBeat(null)); + if (this.currentOrientation === "horizontal") { this.reverseDisplayOrder(); } } @@ -57,16 +64,16 @@ export default class BeatGroupView extends UINode implements ISubscriber beatView.setBeat(this.beatGroup.getBeatByIndex(i))); + this.setupBeatViews(); this.redraw(); } diff --git a/src/ui/BeatUnit/BeatUnitView.ts b/src/ui/BeatUnit/BeatUnitView.ts index 8d40416..21f03bf 100644 --- a/src/ui/BeatUnit/BeatUnitView.ts +++ b/src/ui/BeatUnit/BeatUnitView.ts @@ -29,11 +29,28 @@ export default class BeatUnitView extends UINode implements ISubscriber this.getNode().removeEventListener("mousedown", listener)); + this.hoverListeners.forEach(listener => this.getNode().removeEventListener("mouseover", listener)); + this.redraw(); + this.mouseDownListeners.forEach(listener => this.getNode().addEventListener("mousedown", listener)); + this.hoverListeners.forEach(listener => this.getNode().addEventListener("mouseover", listener)); + this.getNode().addEventListener("mousedown", (ev) => this.handleMouseDown(ev)); + this.getNode().addEventListener("touchstart", (ev) => this.handleTouchStart(ev)); + this.getNode().addEventListener("touchend", (ev) => this.handleTouchEnd(ev)); } private handleMouseDown(ev: MouseEvent): void { @@ -56,19 +73,6 @@ export default class BeatUnitView extends UINode implements ISubscriber this.getNode().removeEventListener("mousedown", listener)); - this.hoverListeners.forEach(listener => this.getNode().removeEventListener("mouseover", listener)); - this.redraw(); - this.mouseDownListeners.forEach(listener => this.getNode().addEventListener("mousedown", listener)); - this.hoverListeners.forEach(listener => this.getNode().addEventListener("mouseover", listener)); - this.getNode().addEventListener("mousedown", (ev) => this.handleMouseDown(ev)); - this.getNode().addEventListener("touchstart", (ev) => this.handleTouchStart(ev)); - this.getNode().addEventListener("touchend", (ev) => this.handleTouchEnd(ev)); - } - toggle(): void { this.beatUnit.toggle(); } diff --git a/src/ui/UINode.ts b/src/ui/UINode.ts index 2f35b8a..319bc07 100644 --- a/src/ui/UINode.ts +++ b/src/ui/UINode.ts @@ -1,4 +1,5 @@ import Ref from "@/Ref"; +import {ISubscription} from "@/Publisher"; export type UINodeOptions = { @@ -52,38 +53,41 @@ export function h< T extends keyof HTMLElementTagNameMap>( type: T, attributes: IRenderAttributes, - subNodes?: (Node | UINode | Ref)[], + subNodes?: (Node | UINode | Ref)[], ): HTMLElementTagNameMap[T] { const element = document.createElement(type); if (attributes) { for (const key in attributes) { - if (key === "classes") { - element.classList.add(...attributes[key]!); - } else if (key === "saveTo") { - attributes.saveTo!.val = element; - } else { + if (!Object.prototype.hasOwnProperty.call(attributes, key)) { + continue; + } + if (key === "classes" && attributes.classes) { + element.classList.add(...attributes.classes); + } else if (key === "saveTo" && attributes.saveTo) { + attributes.saveTo.val = element; + } else if (Object.prototype.hasOwnProperty.call(attributes, key)) { const attribute = (attributes as any)[key]; - if (attribute instanceof Ref) { - element[key as keyof HTMLElementTagNameMap[T]] = attribute.val; - attribute.watch((newVal) => element[key as keyof HTMLElementTagNameMap[T]] = newVal); - } else { - element[key as keyof HTMLElementTagNameMap[T]] = attribute; + if (attribute) { + if (attribute instanceof Ref) { + if (element.hasAttribute(key)) { + element.setAttribute(key, attribute.val); + attribute.watch((newVal) => { + if (element.hasAttribute(key)) { + element.setAttribute(key, newVal); + } + }); + } + } else { + if (element.hasAttribute(key)) { + element.setAttribute(key, attribute); + } + } } } } } if (subNodes) { - for (let i = 0; i < subNodes.length; i++) { - const subNode = subNodes[i]; - if (subNode instanceof UINode) { - element.append(subNode.render()); - } else if (subNode instanceof Ref) { - subNode.watch((newVal) => element.childNodes.item(i).replaceWith(newVal.toString())); - element.append(q(subNode.val.toString())); - } else { - element.append(subNode); - } - } + attachSubs(element, subNodes); } return element; } @@ -95,7 +99,31 @@ export function q(text: string): Text { export function frag(subs?: Node[]): DocumentFragment { const frag = document.createDocumentFragment(); if (subs) { - frag.append(...subs); + attachSubs(frag, subs); } return frag; +} + +function nodeRefWatcher(newVal: T extends Ref ? U : never, textNode: Text, sub: ISubscription): void { + if (!textNode.parentNode) { + sub.unbind(); + textNode.remove(); + } else { + textNode.replaceWith(newVal?.toString() ?? "null"); + } +} + +function attachSubs(node: Element | DocumentFragment, subNodes: (Node | UINode | Ref)[]): void { + for (let i = 0; i < subNodes.length; i++) { + const subNode = subNodes[i]; + if (subNode instanceof UINode) { + node.append(subNode.render()); + } else if (subNode instanceof Ref) { + const textNode = q(subNode.val.toString()); + const sub = subNode.watch((newVal) => nodeRefWatcher(newVal, textNode, sub)); + node.append(textNode); + } else { + node.append(subNode); + } + } } \ No newline at end of file diff --git a/src/ui/Widgets/BoolBox/BoolBoxView.ts b/src/ui/Widgets/BoolBox/BoolBoxView.ts index daba0ad..050c630 100644 --- a/src/ui/Widgets/BoolBox/BoolBoxView.ts +++ b/src/ui/Widgets/BoolBox/BoolBoxView.ts @@ -39,12 +39,12 @@ export default class BoolBoxView extends UINode { build(): HTMLDivElement { this.labelElement = h("label", { classes: ["bool-box-label"], - innerText: this.label, onclick: () => { this.onInput(!this.checkboxElement.checked); }, }); if (this.label !== null) { + this.labelElement.innerText = this.label; this.labelElement.classList.add("visible"); } this.checkboxElement = h("input", {