diff --git a/src/Ref.ts b/src/Ref.ts new file mode 100644 index 0000000..094e997 --- /dev/null +++ b/src/Ref.ts @@ -0,0 +1,39 @@ +export default class Ref { + private watchers: Array<(newVal: T) => void> | null = null; + private value: T; + private asString?: string; + private isString: boolean; + + constructor(val: T) { + this.value = val; + this.isString = typeof val === "string"; + } + + watch(callback: (newVal: T) => void): void { + if (this.watchers === null) { + this.watchers = []; + } + this.watchers.push(callback); + } + + get val(): T { + return this.value; + } + + set val(val: T) { + this.watchers?.forEach(watcher => watcher(val)); + this.value = val; + } + + toString(): string { + if (!this.asString) { + if (this.isString) { + return this.val as unknown as string; + } else { + this.asString = this.val?.toString() ?? "null"; + } + } + return this.asString; + } +} + diff --git a/src/ui/Beat/BeatView.ts b/src/ui/Beat/BeatView.ts index 88b3046..015c872 100644 --- a/src/ui/Beat/BeatView.ts +++ b/src/ui/Beat/BeatView.ts @@ -1,8 +1,10 @@ -import UINode, {UINodeOptions} from "@/ui/UINode"; +import UINode, {h, UINodeOptions} from "@/ui/UINode"; import Beat, {BeatEvents} from "@/Beat"; import ISubscriber from "@/Subscriber"; import BeatUnitView from "@/ui/BeatUnit/BeatUnitView"; import "./Beat.css"; +import {ISubscription} from "@/Publisher"; +import Ref from "@/Ref"; export type BeatUINodeOptions = UINodeOptions & { beat: Beat, @@ -19,37 +21,31 @@ const EventTypeSubscriptions = [ type EventTypeSubscriptions = FlatArray; export default class BeatView extends UINode implements ISubscriber { - private beat: Beat; - private title!: HTMLHeadingElement; + private beat!: Beat; + private title = new Ref(null); private beatUnitViews: BeatUnitView[] = []; private beatUnitViewBlock: HTMLElement | null = null; private lastHoveredBeatUnitView: BeatUnitView | null = null; + private sub: ISubscription | null = null; static deselectingUnits = false; static selectingUnits = false; constructor(options: BeatUINodeOptions) { super(options); - this.beat = options.beat; - this.setupBindings(); + this.setBeat(options.beat); } - private onBeatViewHover(beatView: BeatUnitView) { - this.lastHoveredBeatUnitView = beatView; - if (BeatView.selectingUnits) { - this.lastHoveredBeatUnitView.turnOn(); - } else if (BeatView.deselectingUnits) { - this.lastHoveredBeatUnitView.turnOff(); - } - } - - private setupBindings() { - this.beat.addSubscriber(this, EventTypeSubscriptions); + setBeat(beat: Beat): void { + this.beat = beat; + this.sub?.unbind(); + this.sub = this.beat.addSubscriber(this, EventTypeSubscriptions); + this.redraw(); } notify(publisher: unknown, event: EventTypeSubscriptions): void { switch (event) { case BeatEvents.NewName: - this.title.innerText = this.beat.getName(); + this.title.val!.innerText = this.beat.getName(); break; case BeatEvents.NewTimeSig: case BeatEvents.NewBarCount: @@ -73,9 +69,9 @@ export default class BeatView extends UINode implements ISubscriber this.onBeatViewHover(view)); + view.onMouseDown((event: MouseEvent) => this.onBeatUnitClick(event.button, i)); } - view.onHover(() => this.onBeatViewHover(view)); - view.onMouseDown((event: MouseEvent) => this.onBeatUnitClick(event.button, i)); } } } @@ -90,6 +86,15 @@ export default class BeatView extends UINode implements ISubscriber beatView.setBeat(this.beatGroup.getBeatByIndex(i))); this.redraw(); } build(): HTMLDivElement { - return UINode.make("div", { + return h("div", { classes: ["beat-group"], },[ - ...this.beatViews.map(bv => bv.render()) + ...this.beatViews ]); } } \ No newline at end of file diff --git a/src/ui/BeatGroupSettings/BeatGroupSettingsView.ts b/src/ui/BeatGroupSettings/BeatGroupSettingsView.ts index 93de966..da55d6f 100644 --- a/src/ui/BeatGroupSettings/BeatGroupSettingsView.ts +++ b/src/ui/BeatGroupSettings/BeatGroupSettingsView.ts @@ -1,5 +1,5 @@ import "./BeatGroupSettings.css"; -import UINode, {UINodeOptions} from "@/ui/UINode"; +import UINode, {h, UINodeOptions} from "@/ui/UINode"; import NumberInputView from "@/ui/Widgets/NumberInput/NumberInputView"; import ISubscriber from "@/Subscriber"; import BeatGroup, {BeatGroupEvents} from "@/BeatGroup"; @@ -82,9 +82,9 @@ export default class BeatGroupSettingsView extends UINode implements ISubscriber } } if (!this.beatSettingsContainer) { - this.beatSettingsContainer = UINode.make("div", {}, this.beatSettingsViews.map(view => view.render())); + this.beatSettingsContainer = h("div", {}, this.beatSettingsViews); } else { - this.beatSettingsContainer.replaceChildren(...this.beatSettingsViews.map(view => view.render())); + this.beatSettingsContainer.replaceChildren(...this.beatSettingsViews.reverse().map(view => view.render())); } } @@ -107,24 +107,24 @@ export default class BeatGroupSettingsView extends UINode implements ISubscriber onInput: (isChecked: boolean) => this.beatGroup.setIsUsingAutoBeatLength(isChecked), }); this.remakeBeatSettingsViews(); - return UINode.make("div", { + return h("div", { classes: ["beat-group-settings"], }, [ - UINode.make("div", { + h("div", { classes: ["beat-group-settings-options"], }, [ - UINode.make("div", { + h("div", { classes: ["beat-group-settings-boxes", "beat-group-settings-option"], }, [ this.timeSigUpInput, ]), - UINode.make("div", { + h("div", { classes: ["beat-group-settings-bar-count", "beat-group-settings-option"] , }, [ this.barCountInput, ]), - UINode.make("div", { + h("div", { classes: ["beat-group-settings-bar-count", "beat-group-settings-option"], }, [ this.autoBeatLengthCheckbox, @@ -132,7 +132,7 @@ export default class BeatGroupSettingsView extends UINode implements ISubscriber new ActionButtonView({ label: "New Track", onClick: () => this.beatGroup.addBeat(), - }).render(), + }), this.beatSettingsContainer, ]), ]); diff --git a/src/ui/BeatSettings/BeatSettingsView.ts b/src/ui/BeatSettings/BeatSettingsView.ts index 0eb59b2..056e104 100644 --- a/src/ui/BeatSettings/BeatSettingsView.ts +++ b/src/ui/BeatSettings/BeatSettingsView.ts @@ -1,6 +1,6 @@ import "./BeatSettings.css"; import Beat, {BeatEvents} from "@/Beat"; -import UINode, {UINodeOptions} from "@/ui/UINode"; +import UINode, {h, UINodeOptions} from "@/ui/UINode"; import ISubscriber from "@/Subscriber"; import {ISubscription} from "@/Publisher"; import NumberInputView from "@/ui/Widgets/NumberInput/NumberInputView"; @@ -90,25 +90,25 @@ export default class BeatSettingsView extends UINode implements ISubscriber this.beat.setLooping(isChecked), }); - this.loopLengthSection = UINode.make("div", { + this.loopLengthSection = h("div", { classes: ["loop-settings-option"], }, [ - this.loopLengthInput.render(), + this.loopLengthInput, ]); if (this.beat.isLooping()) { this.loopLengthSection.classList.remove("hide"); } else { this.loopLengthSection.classList.add("hide"); } - return UINode.make("div", { + return h("div", { classes: ["beat-settings"], }, [ - UINode.make("div", { + h("div", { classes: ["beat-settings-title-container"] }, [ this.title, ]), - UINode.make("div", { + h("div", { classes: ["beat-settings-lower"], }, [ this.bakeButton, @@ -117,8 +117,8 @@ export default class BeatSettingsView extends UINode implements ISubscriber this.beat.delete(), - }).render(), - UINode.make("div", { + }), + h("div", { classes: ["loop-settings"], }, [ this.loopCheckbox, diff --git a/src/ui/BeatUnit/BeatUnitView.ts b/src/ui/BeatUnit/BeatUnitView.ts index cdfd3ee..8d40416 100644 --- a/src/ui/BeatUnit/BeatUnitView.ts +++ b/src/ui/BeatUnit/BeatUnitView.ts @@ -1,6 +1,6 @@ import BeatUnit, {BeatUnitEvent, BeatUnitType} from "@/BeatUnit"; import ISubscriber from "@/Subscriber"; -import UINode, {UINodeOptions} from "@/ui/UINode"; +import UINode, {h, UINodeOptions} from "@/ui/UINode"; import {IPublisher, ISubscription, Publisher} from "@/Publisher"; import "./BeatUnit.css"; @@ -113,7 +113,7 @@ export default class BeatUnitView extends UINode implements ISubscriber false, }); diff --git a/src/ui/Root/RootView.ts b/src/ui/Root/RootView.ts index b51026f..f6e4677 100644 --- a/src/ui/Root/RootView.ts +++ b/src/ui/Root/RootView.ts @@ -1,10 +1,11 @@ -import UINode, {UINodeOptions} from "@/ui/UINode"; +import UINode, {h, UINodeOptions} from "@/ui/UINode"; import BeatGroupView from "@/ui/BeatGroup/BeatGroupView"; import BeatGroup from "@/BeatGroup"; import "./Root.css"; import BeatGroupSettingsView from "@/ui/BeatGroupSettings/BeatGroupSettingsView"; import IconView from "@/ui/Widgets/Icon/IconView"; import StageTitleBarView from "@/ui/StageTitleBar/StageTitleBarView"; +import Ref from "@/Ref"; export type RootUINodeOptions = UINodeOptions & { title: string, @@ -16,9 +17,11 @@ export default class RootView extends UINode { private title: string; private beatGroupView: BeatGroupView; private focusedBeatGroup: BeatGroup; - private beatGroupSettingsView!: BeatGroupSettingsView; + private beatGroupSettingsView: BeatGroupSettingsView; private currentOrientation: "horizontal" | "vertical"; private stageTitleBarView: StageTitleBarView; + private showHideSidebarButton: Ref = new Ref(null); + private sidebarActive = true; constructor(options: RootUINodeOptions) { super(options); @@ -65,6 +68,8 @@ export default class RootView extends UINode { } toggleSidebar(): void { + this.sidebarActive = !this.sidebarActive; + this.showHideSidebarButton.val!.title = this.sidebarText(); this.getNode().classList.toggle("sidebar-visible"); } @@ -86,38 +91,47 @@ export default class RootView extends UINode { this.beatGroupView.setOrientation(orientation); } + private sidebarText(): string { + return `${this.sidebarActive ? "Hide" : "Show"} sidebar`; + } + + private buildSidebarStrip(): HTMLElement { - return UINode.make("div", { + return h("div", { classes: ["root-sidebar-toggle"], }, [ - UINode.make("div", { + h("div", { classes: ["root-quick-access-button"], + title: this.sidebarText(), + saveTo: this.showHideSidebarButton, onclick: () => this.toggleSidebar(), }, [ new IconView({ iconName: "list", color: "var(--color-ui-neutral-dark)" - }).render() + }) ]), - UINode.make("div", { + h("div", { classes: ["root-quick-access-button"], + title: "Change orientation", onclick: () => this.toggleOrientation(), }, [ new IconView({ iconName: "arrowClockwise", color: "var(--color-ui-neutral-dark)" - }).render(), + }), ]), - UINode.make("div", { + h("div", { classes: ["root-quick-access-button"], + title: "Bake all tracks", onclick: () => this.focusedBeatGroup.bakeLoops(), }, [ new IconView({ iconName: "snowflake", color: "var(--color-ui-neutral-dark)" - }).render(), + }), ]), - UINode.make("div", { + h("div", { classes: ["root-quick-access-button"], title: "Reset all", onclick: () => this.setMainBeatGroup(RootView.defaultMainBeatGroup()), @@ -125,17 +139,17 @@ export default class RootView extends UINode { new IconView({ iconName: "trash", color: "var(--color-ui-neutral-dark)" - }).render() + }) ]), ]); } private buildSidebar(): HTMLElement { return ( - UINode.make("div", {classes: ["root-sidebar"]}, [ - UINode.make("div", {classes: ["root-settings"]}, [ - UINode.make("h1", {classes: ["root-title"], innerText: this.title}), - this.beatGroupSettingsView.render(), + h("div", {classes: ["root-sidebar"]}, [ + h("div", {classes: ["root-settings"]}, [ + h("h1", {classes: ["root-title"], innerText: this.title}), + this.beatGroupSettingsView, ]), this.buildSidebarStrip(), ]) @@ -144,12 +158,12 @@ export default class RootView extends UINode { build(): HTMLElement { return ( - UINode.make("div", {classes: ["root", "sidebar-visible"]}, [ + h("div", {classes: ["root", "sidebar-visible"]}, [ this.buildSidebar(), - UINode.make("div", {classes: ["root-beat-stage-container"]}, [ - this.stageTitleBarView.render(), - UINode.make("div", {classes: ["root-beat-stage"]}, [ - this.beatGroupView.render(), + h("div", {classes: ["root-beat-stage-container"]}, [ + this.stageTitleBarView, + h("div", {classes: ["root-beat-stage"]}, [ + this.beatGroupView, ]) ]) ]) diff --git a/src/ui/StageTitleBar/StageTitleBarView.ts b/src/ui/StageTitleBar/StageTitleBarView.ts index 7874256..d8e59ea 100644 --- a/src/ui/StageTitleBar/StageTitleBarView.ts +++ b/src/ui/StageTitleBar/StageTitleBarView.ts @@ -1,5 +1,5 @@ import "./StageTitleBar.css"; -import UINode, {UINodeOptions} from "@/ui/UINode"; +import UINode, {h, UINodeOptions} from "@/ui/UINode"; import {ISubscription} from "@/Publisher"; import BeatGroup, {BeatGroupEvents} from "@/BeatGroup"; import ISubscriber from "@/Subscriber"; @@ -42,9 +42,9 @@ export default class StageTitleBarView extends UINode implements ISubscriber = Partial & { classes: string[], - subs: HTMLElement[], + saveTo: Ref, }>; export default abstract class UINode { @@ -45,45 +47,51 @@ export default abstract class UINode { } protected abstract build(): HTMLElement; +} - static make< - T extends keyof HTMLElementTagNameMap, - K extends keyof HTMLElementTagNameMap[T]>( - type: T, - attributes: IRenderAttributes, - subNodes?: (Node | UINode)[], - ): HTMLElementTagNameMap[T] { - const element = document.createElement(type); - if (attributes) { - for (const key in attributes) { - if (key === "classes") { - element.classList.add(...attributes[key]!); - } else { - element[key as keyof HTMLElementTagNameMap[T]] = (attributes as any)[key]; - } +export function h< + T extends keyof HTMLElementTagNameMap, + K extends keyof HTMLElementTagNameMap[T]>( + type: T, + attributes: IRenderAttributes, + 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 { + element[key as keyof HTMLElementTagNameMap[T]] = (attributes as any)[key]; } } - if (subNodes) { - for (const subElement of subNodes) { - if (subElement instanceof UINode) { - element.append(subElement.render()); - } else { - element.append(subElement); - } + } + 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); } } - return element; } + return element; +} - static q(text: string): Text { - return document.createTextNode(text); - } +export function q(text: string): Text { + return document.createTextNode(text); +} - static frag(subs?: Node[]): DocumentFragment { - const frag = document.createDocumentFragment(); - if (subs) { - frag.append(...subs); - } - return frag; +export function frag(subs?: Node[]): DocumentFragment { + const frag = document.createDocumentFragment(); + if (subs) { + frag.append(...subs); } + return frag; } \ No newline at end of file diff --git a/src/ui/Widgets/ActionButton/ActionButtonView.ts b/src/ui/Widgets/ActionButton/ActionButtonView.ts index 3f139dc..50e3a83 100644 --- a/src/ui/Widgets/ActionButton/ActionButtonView.ts +++ b/src/ui/Widgets/ActionButton/ActionButtonView.ts @@ -1,5 +1,5 @@ import "./ActionButton.css"; -import UINode, {UINodeOptions} from "@/ui/UINode"; +import UINode, {h, UINodeOptions} from "@/ui/UINode"; import IconView, {IconName} from "@/ui/Widgets/Icon/IconView"; export type ActionButtonUINodeOptions = UINodeOptions & { @@ -48,14 +48,14 @@ export default class ActionButtonView extends UINode { } protected build(): HTMLButtonElement { - this.buttonElement = UINode.make("button", { + this.buttonElement = h("button", { classes: ["action-button", `action-button-${this.type}`], onclick: (event: MouseEvent) => this.disabled || this.onClick(event) }, [ this.icon !== null ? new IconView({ iconName: this.icon, color: "var(--color-p-light)", - }).render() : UINode.make("span", { + }) : h("span", { innerText: this.label ?? "" }), ]); diff --git a/src/ui/Widgets/BoolBox/BoolBoxView.ts b/src/ui/Widgets/BoolBox/BoolBoxView.ts index f9bea54..daba0ad 100644 --- a/src/ui/Widgets/BoolBox/BoolBoxView.ts +++ b/src/ui/Widgets/BoolBox/BoolBoxView.ts @@ -1,5 +1,6 @@ import "./BoolBox.css"; -import UINode, {UINodeOptions} from "@/ui/UINode"; +import UINode, {h, UINodeOptions} from "@/ui/UINode"; +import Ref from "@/Ref"; export type BoolBoxUINodeOptions = UINodeOptions & { label?: string, @@ -36,9 +37,9 @@ export default class BoolBoxView extends UINode { } build(): HTMLDivElement { - this.labelElement = UINode.make("label", { + this.labelElement = h("label", { classes: ["bool-box-label"], - innerText: this.label ?? "", + innerText: this.label, onclick: () => { this.onInput(!this.checkboxElement.checked); }, @@ -46,14 +47,14 @@ export default class BoolBoxView extends UINode { if (this.label !== null) { this.labelElement.classList.add("visible"); } - this.checkboxElement = UINode.make("input", { + this.checkboxElement = h("input", { type: "checkbox", classes: ["bool-box-checkbox"], onclick: (event: Event) => { this.onInput((event.target as HTMLInputElement).checked); }, }); - return UINode.make("div", { + return h("div", { classes: ["bool-box"], },[ this.labelElement, diff --git a/src/ui/Widgets/EditableTextFIeld/EditableTextFieldView.ts b/src/ui/Widgets/EditableTextFIeld/EditableTextFieldView.ts index 91f697c..0065bb2 100644 --- a/src/ui/Widgets/EditableTextFIeld/EditableTextFieldView.ts +++ b/src/ui/Widgets/EditableTextFIeld/EditableTextFieldView.ts @@ -1,4 +1,4 @@ -import UINode, {UINodeOptions} from "@/ui/UINode"; +import UINode, {h, UINodeOptions} from "@/ui/UINode"; import "./EditableTextFieldView.css"; export type EditableTextFieldViewOptions = UINodeOptions & { @@ -31,7 +31,7 @@ export default class EditableTextFieldView extends UINode { } build(): HTMLSpanElement { - this.titleInput = UINode.make("input", { + this.titleInput = h("input", { value: this.text, classes: ["editable-text-field-view"], type: "text", @@ -58,7 +58,7 @@ export default class EditableTextFieldView extends UINode { } }, }); - this.titleDisplay = UINode.make("div", { + this.titleDisplay = h("div", { innerText: this.text, classes: ["editable-text-field-view"], onclick: () => { diff --git a/src/ui/Widgets/Icon/IconView.ts b/src/ui/Widgets/Icon/IconView.ts index f4fd6af..0e9395b 100644 --- a/src/ui/Widgets/Icon/IconView.ts +++ b/src/ui/Widgets/Icon/IconView.ts @@ -1,9 +1,10 @@ -import UINode, {UINodeOptions} from "@/ui/UINode"; +import UINode, {h, UINodeOptions} from "@/ui/UINode"; import "./Icon.css"; import List from "./svgs/list.svg"; import ArrowClockwise from "./svgs/arrow-clockwise.svg"; import Trash from "./svgs/trash.svg"; import Snowflake from "./svgs/snowflake.svg"; +import Ref from "@/Ref"; const IconUrlMap = { arrowClockwise: ArrowClockwise, @@ -30,7 +31,7 @@ export default class IconView extends UINode { } build(): HTMLSpanElement { - const icon = UINode.make("div", { + const icon = h("div", { classes: ["icon-view"], }); const colorString = this.color ? `--icon-bg:${this.color}` : ""; diff --git a/src/ui/Widgets/NumberInput/NumberInputView.ts b/src/ui/Widgets/NumberInput/NumberInputView.ts index 037666a..e534dfe 100644 --- a/src/ui/Widgets/NumberInput/NumberInputView.ts +++ b/src/ui/Widgets/NumberInput/NumberInputView.ts @@ -1,5 +1,6 @@ -import UINode, { UINodeOptions } from "@/ui/UINode"; +import UINode, {h, UINodeOptions} from "@/ui/UINode"; import "./NumberInput.css"; +import Ref from "@/Ref"; type NumberInputUINodeOptionsBase = UINodeOptions & { label?: string, @@ -26,8 +27,8 @@ type NumberInputUINodeOptionsGetSet = NumberInputUINodeOptionsBase & { export type NumberInputUINodeOptions = NumberInputUINodeOptionsGetSet | NumberInputUINodeOptionsIncDecInput; export default class NumberInputView extends UINode { - private labelElement!: HTMLLabelElement; - private inputElement!: HTMLInputElement; + private labelElement: Ref = new Ref(null); + private inputElement: Ref = new Ref(null); private labelPosition: "top" | "left"; private value: number; private label: string | null; @@ -52,58 +53,44 @@ export default class NumberInputView extends UINode { setLabel(newLabel: string | null): void { if (newLabel !== null) { this.label = newLabel; - this.labelElement.innerText = newLabel; - this.labelElement.classList.add("visible"); + this.labelElement.val!.innerText = newLabel; + this.labelElement.val!.classList.add("visible"); } else { this.label = newLabel; - this.labelElement.innerText = ""; - this.labelElement.classList.remove("visible"); + this.labelElement.val!.innerText = ""; + this.labelElement.val!.classList.remove("visible"); } } disable(): void { this.node?.classList.add("disabled"); - this.inputElement.disabled = true; + this.inputElement.val!.disabled = true; } enable(): void { this.node?.classList.remove("disabled"); - this.inputElement.disabled = false; + this.inputElement.val!.disabled = false; } setValue(value: number): void { this.value = value; - this.inputElement.valueAsNumber = value; + this.inputElement.val!.valueAsNumber = value; } build(): HTMLDivElement { - this.labelElement = UINode.make("label", { - classes: ["number-input-label", this.labelPosition], - innerText: this.label ?? "", - }); + const labelClasses = ["number-input-label", this.labelPosition]; if (this.label !== null) { - this.labelElement.classList.add("visible"); + labelClasses.push("visible"); } - this.inputElement = UINode.make("input", { - type: "number", - classes: ["number-input-input"], - valueAsNumber: this.value, - onblur: (event: Event) => { - const input = (event.target as HTMLInputElement).valueAsNumber; - if (!isNaN(input)) { - if (this.onNewInput) { - this.onNewInput(input); - } else if (this.setter) { - this.setter(input); - } - } - }, - }); - return UINode.make("div", { + return h("div", { classes: ["number-input"], }, [ - this.labelElement, - UINode.make("button", { + h("label", { + classes: labelClasses, + saveTo: this.labelElement, + innerText: this.label ?? "", + }), + h("button", { innerText: "-", classes: ["number-input-button", "number-input-dec"], onclick: () => { @@ -114,8 +101,23 @@ export default class NumberInputView extends UINode { } }, }), - this.inputElement, - UINode.make("button", { + h("input", { + type: "number", + saveTo: this.inputElement, + classes: ["number-input-input"], + valueAsNumber: this.value, + onblur: (event: Event) => { + const input = (event.target as HTMLInputElement).valueAsNumber; + if (!isNaN(input)) { + if (this.onNewInput) { + this.onNewInput(input); + } else if (this.setter) { + this.setter(input); + } + } + }, + }), + h("button", { innerText: "+", classes: ["number-input-button", "number-input-inc"], onclick: () => {