feat: added refs, refactored UINode for more concise build functions

This commit is contained in:
Daniel Ledda
2022-04-02 23:54:51 +02:00
parent fcec98d7e5
commit e77b89ebee
14 changed files with 227 additions and 156 deletions

39
src/Ref.ts Normal file
View File

@@ -0,0 +1,39 @@
export default class Ref<T extends { toString(): string } | string | null = string> {
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;
}
}

View File

@@ -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<typeof EventTypeSubscriptions, 1>;
export default class BeatView extends UINode implements ISubscriber<EventTypeSubscriptions> {
private beat: Beat;
private title!: HTMLHeadingElement;
private beat!: Beat;
private title = new Ref<HTMLHeadingElement | null>(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<EventTypeSub
} else {
view = new BeatUnitView({beatUnit});
this.beatUnitViews.push(view);
view.onHover(() => 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<EventTypeSub
}
}
private onBeatViewHover(beatView: BeatUnitView) {
this.lastHoveredBeatUnitView = beatView;
if (BeatView.selectingUnits) {
this.lastHoveredBeatUnitView.turnOn();
} else if (BeatView.deselectingUnits) {
this.lastHoveredBeatUnitView.turnOff();
}
}
private buildBeatUnitViewBlock(): void {
const beatUnitNodes: HTMLElement[] = [];
for (let i = 0; i < this.beatUnitViews.length; i++) {
@@ -98,7 +103,7 @@ export default class BeatView extends UINode implements ISubscriber<EventTypeSub
if (this.beatUnitViewBlock) {
this.beatUnitViewBlock.replaceChildren(...beatUnitNodes);
} else {
this.beatUnitViewBlock = UINode.make("div", {
this.beatUnitViewBlock = h("div", {
classes: ["beat-unit-block"],
}, [
...beatUnitNodes
@@ -118,7 +123,7 @@ export default class BeatView extends UINode implements ISubscriber<EventTypeSub
let spacersInserted = false;
while (!spacersInserted) {
i += barLength;
const newSpacer = UINode.make("div", {classes: ["beat-spacer"]});
const newSpacer = h("div", {classes: ["beat-spacer"]});
const leftNeighbour = this.beatUnitViewBlock.children.item(i);
if (leftNeighbour) {
leftNeighbour.insertAdjacentElement("afterend", newSpacer);
@@ -140,21 +145,21 @@ export default class BeatView extends UINode implements ISubscriber<EventTypeSub
}
build(): HTMLElement {
this.title = UINode.make("h3", {
innerText: this.beat.getName(),
classes: ["beat-title"],
});
this.setupBeatUnits();
if (!this.beatUnitViewBlock) {
throw new Error("Beat unit block setup failed!");
}
return UINode.make("div", {
return h("div", {
classes: ["beat"],
}, [
UINode.make("div", {
h("div", {
classes: ["beat-main"],
}, [
this.title,
h("h3", {
innerText: this.beat.getName(),
saveTo: this.title,
classes: ["beat-title"],
}),
this.beatUnitViewBlock,
]),
]);

View File

@@ -1,4 +1,4 @@
import UINode, {UINodeOptions} from "@/ui/UINode";
import UINode, {h, UINodeOptions} from "@/ui/UINode";
import BeatGroup, {BeatGroupEvents} from "@/BeatGroup";
import BeatView from "@/ui/Beat/BeatView";
import "./BeatGroup.css";
@@ -42,7 +42,7 @@ export default class BeatGroupView extends UINode implements ISubscriber<EventTy
private setupBeatViews(): void {
this.beatViews = [];
for (let i = 0; i < this.beatGroup.getBeatCount(); i++) {
this.beatViews.push(new BeatView({beat: this.beatGroup.getBeatByIndex(i)}));
this.beatViews.unshift(new BeatView({beat: this.beatGroup.getBeatByIndex(i)}));
}
if (this.currentOrientation === "vertical") {
this.reverseDisplayOrder();
@@ -66,14 +66,15 @@ export default class BeatGroupView extends UINode implements ISubscriber<EventTy
this.subscription.unbind();
this.beatGroup = newBeatGroup;
this.subscription = this.beatGroup.addSubscriber(this, BeatGroupEvents.BeatListChanged);
this.beatViews.forEach((beatView, i) => 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
]);
}
}

View File

@@ -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,
]),
]);

View File

@@ -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<Even
value: this.beat.isLooping(),
onInput: (isChecked: boolean) => 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<Even
type: "secondary",
alt: "Delete Track",
onClick: () => this.beat.delete(),
}).render(),
UINode.make("div", {
}),
h("div", {
classes: ["loop-settings"],
}, [
this.loopCheckbox,

View File

@@ -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<EventTyp
if (this.beatUnit.isOn()) {
classes.push("beat-unit-on");
}
return UINode.make("div", {
return h("div", {
classes: classes,
oncontextmenu: () => false,
});

View File

@@ -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<HTMLDivElement | null> = new Ref<HTMLDivElement | null>(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,
])
])
])

View File

@@ -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<Eve
}
protected build(): HTMLElement {
return UINode.make("div", {classes: ["stage-title-bar"]}, [
UINode.make("div", {classes: ["stage-title-bar-preamble"], innerText: "Currently editing:"}),
UINode.make("h2", {}, [this.title]),
return h("div", {classes: ["stage-title-bar"]}, [
h("div", {classes: ["stage-title-bar-preamble"], innerText: "Currently editing:"}),
h("h2", {}, [this.title]),
]);
}
}

View File

@@ -1,3 +1,5 @@
import Ref from "@/Ref";
export type UINodeOptions = {
};
@@ -7,7 +9,7 @@ type IRenderAttributes<
K extends keyof HTMLElementTagNameMap[T]
> = Partial<Record<K, HTMLElementTagNameMap[T][K]> & {
classes: string[],
subs: HTMLElement[],
saveTo: Ref<HTMLElementTagNameMap[T] | null>,
}>;
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<T, K>,
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];
}
}
}
if (subNodes) {
for (const subElement of subNodes) {
if (subElement instanceof UINode) {
element.append(subElement.render());
} else {
element.append(subElement);
}
}
}
return element;
}
static 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 h<
T extends keyof HTMLElementTagNameMap,
K extends keyof HTMLElementTagNameMap[T]>(
type: T,
attributes: IRenderAttributes<T, K>,
subNodes?: (Node | UINode | Ref<any>)[],
): 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 (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;
}
export function q(text: string): Text {
return document.createTextNode(text);
}
export function frag(subs?: Node[]): DocumentFragment {
const frag = document.createDocumentFragment();
if (subs) {
frag.append(...subs);
}
return frag;
}

View File

@@ -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 ?? ""
}),
]);

View File

@@ -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,

View File

@@ -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: () => {

View File

@@ -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}` : "";

View File

@@ -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<HTMLLabelElement | null> = new Ref<HTMLLabelElement | null>(null);
private inputElement: Ref<HTMLInputElement | null> = new Ref<HTMLInputElement | null>(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: () => {