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 Beat, {BeatEvents} from "@/Beat";
import ISubscriber from "@/Subscriber"; import ISubscriber from "@/Subscriber";
import BeatUnitView from "@/ui/BeatUnit/BeatUnitView"; import BeatUnitView from "@/ui/BeatUnit/BeatUnitView";
import "./Beat.css"; import "./Beat.css";
import {ISubscription} from "@/Publisher";
import Ref from "@/Ref";
export type BeatUINodeOptions = UINodeOptions & { export type BeatUINodeOptions = UINodeOptions & {
beat: Beat, beat: Beat,
@@ -19,37 +21,31 @@ const EventTypeSubscriptions = [
type EventTypeSubscriptions = FlatArray<typeof EventTypeSubscriptions, 1>; type EventTypeSubscriptions = FlatArray<typeof EventTypeSubscriptions, 1>;
export default class BeatView extends UINode implements ISubscriber<EventTypeSubscriptions> { export default class BeatView extends UINode implements ISubscriber<EventTypeSubscriptions> {
private beat: Beat; private beat!: Beat;
private title!: HTMLHeadingElement; private title = new Ref<HTMLHeadingElement | null>(null);
private beatUnitViews: BeatUnitView[] = []; private beatUnitViews: BeatUnitView[] = [];
private beatUnitViewBlock: HTMLElement | null = null; private beatUnitViewBlock: HTMLElement | null = null;
private lastHoveredBeatUnitView: BeatUnitView | null = null; private lastHoveredBeatUnitView: BeatUnitView | null = null;
private sub: ISubscription | null = null;
static deselectingUnits = false; static deselectingUnits = false;
static selectingUnits = false; static selectingUnits = false;
constructor(options: BeatUINodeOptions) { constructor(options: BeatUINodeOptions) {
super(options); super(options);
this.beat = options.beat; this.setBeat(options.beat);
this.setupBindings();
} }
private onBeatViewHover(beatView: BeatUnitView) { setBeat(beat: Beat): void {
this.lastHoveredBeatUnitView = beatView; this.beat = beat;
if (BeatView.selectingUnits) { this.sub?.unbind();
this.lastHoveredBeatUnitView.turnOn(); this.sub = this.beat.addSubscriber(this, EventTypeSubscriptions);
} else if (BeatView.deselectingUnits) { this.redraw();
this.lastHoveredBeatUnitView.turnOff();
}
}
private setupBindings() {
this.beat.addSubscriber(this, EventTypeSubscriptions);
} }
notify(publisher: unknown, event: EventTypeSubscriptions): void { notify(publisher: unknown, event: EventTypeSubscriptions): void {
switch (event) { switch (event) {
case BeatEvents.NewName: case BeatEvents.NewName:
this.title.innerText = this.beat.getName(); this.title.val!.innerText = this.beat.getName();
break; break;
case BeatEvents.NewTimeSig: case BeatEvents.NewTimeSig:
case BeatEvents.NewBarCount: case BeatEvents.NewBarCount:
@@ -73,12 +69,12 @@ export default class BeatView extends UINode implements ISubscriber<EventTypeSub
} else { } else {
view = new BeatUnitView({beatUnit}); view = new BeatUnitView({beatUnit});
this.beatUnitViews.push(view); this.beatUnitViews.push(view);
}
view.onHover(() => this.onBeatViewHover(view)); view.onHover(() => this.onBeatViewHover(view));
view.onMouseDown((event: MouseEvent) => this.onBeatUnitClick(event.button, i)); view.onMouseDown((event: MouseEvent) => this.onBeatUnitClick(event.button, i));
} }
} }
} }
}
private onBeatUnitClick(button: number, index: number) { private onBeatUnitClick(button: number, index: number) {
if (button === 0) { if (button === 0) {
@@ -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 { private buildBeatUnitViewBlock(): void {
const beatUnitNodes: HTMLElement[] = []; const beatUnitNodes: HTMLElement[] = [];
for (let i = 0; i < this.beatUnitViews.length; i++) { 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) { if (this.beatUnitViewBlock) {
this.beatUnitViewBlock.replaceChildren(...beatUnitNodes); this.beatUnitViewBlock.replaceChildren(...beatUnitNodes);
} else { } else {
this.beatUnitViewBlock = UINode.make("div", { this.beatUnitViewBlock = h("div", {
classes: ["beat-unit-block"], classes: ["beat-unit-block"],
}, [ }, [
...beatUnitNodes ...beatUnitNodes
@@ -118,7 +123,7 @@ export default class BeatView extends UINode implements ISubscriber<EventTypeSub
let spacersInserted = false; let spacersInserted = false;
while (!spacersInserted) { while (!spacersInserted) {
i += barLength; i += barLength;
const newSpacer = UINode.make("div", {classes: ["beat-spacer"]}); const newSpacer = h("div", {classes: ["beat-spacer"]});
const leftNeighbour = this.beatUnitViewBlock.children.item(i); const leftNeighbour = this.beatUnitViewBlock.children.item(i);
if (leftNeighbour) { if (leftNeighbour) {
leftNeighbour.insertAdjacentElement("afterend", newSpacer); leftNeighbour.insertAdjacentElement("afterend", newSpacer);
@@ -140,21 +145,21 @@ export default class BeatView extends UINode implements ISubscriber<EventTypeSub
} }
build(): HTMLElement { build(): HTMLElement {
this.title = UINode.make("h3", {
innerText: this.beat.getName(),
classes: ["beat-title"],
});
this.setupBeatUnits(); this.setupBeatUnits();
if (!this.beatUnitViewBlock) { if (!this.beatUnitViewBlock) {
throw new Error("Beat unit block setup failed!"); throw new Error("Beat unit block setup failed!");
} }
return UINode.make("div", { return h("div", {
classes: ["beat"], classes: ["beat"],
}, [ }, [
UINode.make("div", { h("div", {
classes: ["beat-main"], classes: ["beat-main"],
}, [ }, [
this.title, h("h3", {
innerText: this.beat.getName(),
saveTo: this.title,
classes: ["beat-title"],
}),
this.beatUnitViewBlock, 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 BeatGroup, {BeatGroupEvents} from "@/BeatGroup";
import BeatView from "@/ui/Beat/BeatView"; import BeatView from "@/ui/Beat/BeatView";
import "./BeatGroup.css"; import "./BeatGroup.css";
@@ -42,7 +42,7 @@ export default class BeatGroupView extends UINode implements ISubscriber<EventTy
private setupBeatViews(): void { private setupBeatViews(): void {
this.beatViews = []; this.beatViews = [];
for (let i = 0; i < this.beatGroup.getBeatCount(); i++) { 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") { if (this.currentOrientation === "vertical") {
this.reverseDisplayOrder(); this.reverseDisplayOrder();
@@ -66,14 +66,15 @@ export default class BeatGroupView extends UINode implements ISubscriber<EventTy
this.subscription.unbind(); this.subscription.unbind();
this.beatGroup = newBeatGroup; this.beatGroup = newBeatGroup;
this.subscription = this.beatGroup.addSubscriber(this, BeatGroupEvents.BeatListChanged); this.subscription = this.beatGroup.addSubscriber(this, BeatGroupEvents.BeatListChanged);
this.beatViews.forEach((beatView, i) => beatView.setBeat(this.beatGroup.getBeatByIndex(i)));
this.redraw(); this.redraw();
} }
build(): HTMLDivElement { build(): HTMLDivElement {
return UINode.make("div", { return h("div", {
classes: ["beat-group"], classes: ["beat-group"],
},[ },[
...this.beatViews.map(bv => bv.render()) ...this.beatViews
]); ]);
} }
} }

View File

@@ -1,5 +1,5 @@
import "./BeatGroupSettings.css"; import "./BeatGroupSettings.css";
import UINode, {UINodeOptions} from "@/ui/UINode"; import UINode, {h, UINodeOptions} from "@/ui/UINode";
import NumberInputView from "@/ui/Widgets/NumberInput/NumberInputView"; import NumberInputView from "@/ui/Widgets/NumberInput/NumberInputView";
import ISubscriber from "@/Subscriber"; import ISubscriber from "@/Subscriber";
import BeatGroup, {BeatGroupEvents} from "@/BeatGroup"; import BeatGroup, {BeatGroupEvents} from "@/BeatGroup";
@@ -82,9 +82,9 @@ export default class BeatGroupSettingsView extends UINode implements ISubscriber
} }
} }
if (!this.beatSettingsContainer) { if (!this.beatSettingsContainer) {
this.beatSettingsContainer = UINode.make("div", {}, this.beatSettingsViews.map(view => view.render())); this.beatSettingsContainer = h("div", {}, this.beatSettingsViews);
} else { } 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), onInput: (isChecked: boolean) => this.beatGroup.setIsUsingAutoBeatLength(isChecked),
}); });
this.remakeBeatSettingsViews(); this.remakeBeatSettingsViews();
return UINode.make("div", { return h("div", {
classes: ["beat-group-settings"], classes: ["beat-group-settings"],
}, [ }, [
UINode.make("div", { h("div", {
classes: ["beat-group-settings-options"], classes: ["beat-group-settings-options"],
}, [ }, [
UINode.make("div", { h("div", {
classes: ["beat-group-settings-boxes", "beat-group-settings-option"], classes: ["beat-group-settings-boxes", "beat-group-settings-option"],
}, [ }, [
this.timeSigUpInput, this.timeSigUpInput,
]), ]),
UINode.make("div", { h("div", {
classes: ["beat-group-settings-bar-count", "beat-group-settings-option"] classes: ["beat-group-settings-bar-count", "beat-group-settings-option"]
, ,
}, [ }, [
this.barCountInput, this.barCountInput,
]), ]),
UINode.make("div", { h("div", {
classes: ["beat-group-settings-bar-count", "beat-group-settings-option"], classes: ["beat-group-settings-bar-count", "beat-group-settings-option"],
}, [ }, [
this.autoBeatLengthCheckbox, this.autoBeatLengthCheckbox,
@@ -132,7 +132,7 @@ export default class BeatGroupSettingsView extends UINode implements ISubscriber
new ActionButtonView({ new ActionButtonView({
label: "New Track", label: "New Track",
onClick: () => this.beatGroup.addBeat(), onClick: () => this.beatGroup.addBeat(),
}).render(), }),
this.beatSettingsContainer, this.beatSettingsContainer,
]), ]),
]); ]);

View File

@@ -1,6 +1,6 @@
import "./BeatSettings.css"; import "./BeatSettings.css";
import Beat, {BeatEvents} from "@/Beat"; import Beat, {BeatEvents} from "@/Beat";
import UINode, {UINodeOptions} from "@/ui/UINode"; import UINode, {h, UINodeOptions} from "@/ui/UINode";
import ISubscriber from "@/Subscriber"; import ISubscriber from "@/Subscriber";
import {ISubscription} from "@/Publisher"; import {ISubscription} from "@/Publisher";
import NumberInputView from "@/ui/Widgets/NumberInput/NumberInputView"; import NumberInputView from "@/ui/Widgets/NumberInput/NumberInputView";
@@ -90,25 +90,25 @@ export default class BeatSettingsView extends UINode implements ISubscriber<Even
value: this.beat.isLooping(), value: this.beat.isLooping(),
onInput: (isChecked: boolean) => this.beat.setLooping(isChecked), onInput: (isChecked: boolean) => this.beat.setLooping(isChecked),
}); });
this.loopLengthSection = UINode.make("div", { this.loopLengthSection = h("div", {
classes: ["loop-settings-option"], classes: ["loop-settings-option"],
}, [ }, [
this.loopLengthInput.render(), this.loopLengthInput,
]); ]);
if (this.beat.isLooping()) { if (this.beat.isLooping()) {
this.loopLengthSection.classList.remove("hide"); this.loopLengthSection.classList.remove("hide");
} else { } else {
this.loopLengthSection.classList.add("hide"); this.loopLengthSection.classList.add("hide");
} }
return UINode.make("div", { return h("div", {
classes: ["beat-settings"], classes: ["beat-settings"],
}, [ }, [
UINode.make("div", { h("div", {
classes: ["beat-settings-title-container"] classes: ["beat-settings-title-container"]
}, [ }, [
this.title, this.title,
]), ]),
UINode.make("div", { h("div", {
classes: ["beat-settings-lower"], classes: ["beat-settings-lower"],
}, [ }, [
this.bakeButton, this.bakeButton,
@@ -117,8 +117,8 @@ export default class BeatSettingsView extends UINode implements ISubscriber<Even
type: "secondary", type: "secondary",
alt: "Delete Track", alt: "Delete Track",
onClick: () => this.beat.delete(), onClick: () => this.beat.delete(),
}).render(), }),
UINode.make("div", { h("div", {
classes: ["loop-settings"], classes: ["loop-settings"],
}, [ }, [
this.loopCheckbox, this.loopCheckbox,

View File

@@ -1,6 +1,6 @@
import BeatUnit, {BeatUnitEvent, BeatUnitType} from "@/BeatUnit"; import BeatUnit, {BeatUnitEvent, BeatUnitType} from "@/BeatUnit";
import ISubscriber from "@/Subscriber"; import ISubscriber from "@/Subscriber";
import UINode, {UINodeOptions} from "@/ui/UINode"; import UINode, {h, UINodeOptions} from "@/ui/UINode";
import {IPublisher, ISubscription, Publisher} from "@/Publisher"; import {IPublisher, ISubscription, Publisher} from "@/Publisher";
import "./BeatUnit.css"; import "./BeatUnit.css";
@@ -113,7 +113,7 @@ export default class BeatUnitView extends UINode implements ISubscriber<EventTyp
if (this.beatUnit.isOn()) { if (this.beatUnit.isOn()) {
classes.push("beat-unit-on"); classes.push("beat-unit-on");
} }
return UINode.make("div", { return h("div", {
classes: classes, classes: classes,
oncontextmenu: () => false, 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 BeatGroupView from "@/ui/BeatGroup/BeatGroupView";
import BeatGroup from "@/BeatGroup"; import BeatGroup from "@/BeatGroup";
import "./Root.css"; import "./Root.css";
import BeatGroupSettingsView from "@/ui/BeatGroupSettings/BeatGroupSettingsView"; import BeatGroupSettingsView from "@/ui/BeatGroupSettings/BeatGroupSettingsView";
import IconView from "@/ui/Widgets/Icon/IconView"; import IconView from "@/ui/Widgets/Icon/IconView";
import StageTitleBarView from "@/ui/StageTitleBar/StageTitleBarView"; import StageTitleBarView from "@/ui/StageTitleBar/StageTitleBarView";
import Ref from "@/Ref";
export type RootUINodeOptions = UINodeOptions & { export type RootUINodeOptions = UINodeOptions & {
title: string, title: string,
@@ -16,9 +17,11 @@ export default class RootView extends UINode {
private title: string; private title: string;
private beatGroupView: BeatGroupView; private beatGroupView: BeatGroupView;
private focusedBeatGroup: BeatGroup; private focusedBeatGroup: BeatGroup;
private beatGroupSettingsView!: BeatGroupSettingsView; private beatGroupSettingsView: BeatGroupSettingsView;
private currentOrientation: "horizontal" | "vertical"; private currentOrientation: "horizontal" | "vertical";
private stageTitleBarView: StageTitleBarView; private stageTitleBarView: StageTitleBarView;
private showHideSidebarButton: Ref<HTMLDivElement | null> = new Ref<HTMLDivElement | null>(null);
private sidebarActive = true;
constructor(options: RootUINodeOptions) { constructor(options: RootUINodeOptions) {
super(options); super(options);
@@ -65,6 +68,8 @@ export default class RootView extends UINode {
} }
toggleSidebar(): void { toggleSidebar(): void {
this.sidebarActive = !this.sidebarActive;
this.showHideSidebarButton.val!.title = this.sidebarText();
this.getNode().classList.toggle("sidebar-visible"); this.getNode().classList.toggle("sidebar-visible");
} }
@@ -86,38 +91,47 @@ export default class RootView extends UINode {
this.beatGroupView.setOrientation(orientation); this.beatGroupView.setOrientation(orientation);
} }
private sidebarText(): string {
return `${this.sidebarActive ? "Hide" : "Show"} sidebar`;
}
private buildSidebarStrip(): HTMLElement { private buildSidebarStrip(): HTMLElement {
return UINode.make("div", { return h("div", {
classes: ["root-sidebar-toggle"], classes: ["root-sidebar-toggle"],
}, [ }, [
UINode.make("div", { h("div", {
classes: ["root-quick-access-button"], classes: ["root-quick-access-button"],
title: this.sidebarText(),
saveTo: this.showHideSidebarButton,
onclick: () => this.toggleSidebar(), onclick: () => this.toggleSidebar(),
}, [ }, [
new IconView({ new IconView({
iconName: "list", iconName: "list",
color: "var(--color-ui-neutral-dark)" color: "var(--color-ui-neutral-dark)"
}).render() })
]), ]),
UINode.make("div", { h("div", {
classes: ["root-quick-access-button"], classes: ["root-quick-access-button"],
title: "Change orientation",
onclick: () => this.toggleOrientation(), onclick: () => this.toggleOrientation(),
}, [ }, [
new IconView({ new IconView({
iconName: "arrowClockwise", iconName: "arrowClockwise",
color: "var(--color-ui-neutral-dark)" color: "var(--color-ui-neutral-dark)"
}).render(), }),
]), ]),
UINode.make("div", { h("div", {
classes: ["root-quick-access-button"], classes: ["root-quick-access-button"],
title: "Bake all tracks",
onclick: () => this.focusedBeatGroup.bakeLoops(), onclick: () => this.focusedBeatGroup.bakeLoops(),
}, [ }, [
new IconView({ new IconView({
iconName: "snowflake", iconName: "snowflake",
color: "var(--color-ui-neutral-dark)" color: "var(--color-ui-neutral-dark)"
}).render(), }),
]), ]),
UINode.make("div", { h("div", {
classes: ["root-quick-access-button"], classes: ["root-quick-access-button"],
title: "Reset all", title: "Reset all",
onclick: () => this.setMainBeatGroup(RootView.defaultMainBeatGroup()), onclick: () => this.setMainBeatGroup(RootView.defaultMainBeatGroup()),
@@ -125,17 +139,17 @@ export default class RootView extends UINode {
new IconView({ new IconView({
iconName: "trash", iconName: "trash",
color: "var(--color-ui-neutral-dark)" color: "var(--color-ui-neutral-dark)"
}).render() })
]), ]),
]); ]);
} }
private buildSidebar(): HTMLElement { private buildSidebar(): HTMLElement {
return ( return (
UINode.make("div", {classes: ["root-sidebar"]}, [ h("div", {classes: ["root-sidebar"]}, [
UINode.make("div", {classes: ["root-settings"]}, [ h("div", {classes: ["root-settings"]}, [
UINode.make("h1", {classes: ["root-title"], innerText: this.title}), h("h1", {classes: ["root-title"], innerText: this.title}),
this.beatGroupSettingsView.render(), this.beatGroupSettingsView,
]), ]),
this.buildSidebarStrip(), this.buildSidebarStrip(),
]) ])
@@ -144,12 +158,12 @@ export default class RootView extends UINode {
build(): HTMLElement { build(): HTMLElement {
return ( return (
UINode.make("div", {classes: ["root", "sidebar-visible"]}, [ h("div", {classes: ["root", "sidebar-visible"]}, [
this.buildSidebar(), this.buildSidebar(),
UINode.make("div", {classes: ["root-beat-stage-container"]}, [ h("div", {classes: ["root-beat-stage-container"]}, [
this.stageTitleBarView.render(), this.stageTitleBarView,
UINode.make("div", {classes: ["root-beat-stage"]}, [ h("div", {classes: ["root-beat-stage"]}, [
this.beatGroupView.render(), this.beatGroupView,
]) ])
]) ])
]) ])

View File

@@ -1,5 +1,5 @@
import "./StageTitleBar.css"; import "./StageTitleBar.css";
import UINode, {UINodeOptions} from "@/ui/UINode"; import UINode, {h, UINodeOptions} from "@/ui/UINode";
import {ISubscription} from "@/Publisher"; import {ISubscription} from "@/Publisher";
import BeatGroup, {BeatGroupEvents} from "@/BeatGroup"; import BeatGroup, {BeatGroupEvents} from "@/BeatGroup";
import ISubscriber from "@/Subscriber"; import ISubscriber from "@/Subscriber";
@@ -42,9 +42,9 @@ export default class StageTitleBarView extends UINode implements ISubscriber<Eve
} }
protected build(): HTMLElement { protected build(): HTMLElement {
return UINode.make("div", {classes: ["stage-title-bar"]}, [ return h("div", {classes: ["stage-title-bar"]}, [
UINode.make("div", {classes: ["stage-title-bar-preamble"], innerText: "Currently editing:"}), h("div", {classes: ["stage-title-bar-preamble"], innerText: "Currently editing:"}),
UINode.make("h2", {}, [this.title]), h("h2", {}, [this.title]),
]); ]);
} }
} }

View File

@@ -1,3 +1,5 @@
import Ref from "@/Ref";
export type UINodeOptions = { export type UINodeOptions = {
}; };
@@ -7,7 +9,7 @@ type IRenderAttributes<
K extends keyof HTMLElementTagNameMap[T] K extends keyof HTMLElementTagNameMap[T]
> = Partial<Record<K, HTMLElementTagNameMap[T][K]> & { > = Partial<Record<K, HTMLElementTagNameMap[T][K]> & {
classes: string[], classes: string[],
subs: HTMLElement[], saveTo: Ref<HTMLElementTagNameMap[T] | null>,
}>; }>;
export default abstract class UINode { export default abstract class UINode {
@@ -45,45 +47,51 @@ export default abstract class UINode {
} }
protected abstract build(): HTMLElement; protected abstract build(): HTMLElement;
}
static make< export function h<
T extends keyof HTMLElementTagNameMap, T extends keyof HTMLElementTagNameMap,
K extends keyof HTMLElementTagNameMap[T]>( K extends keyof HTMLElementTagNameMap[T]>(
type: T, type: T,
attributes: IRenderAttributes<T, K>, attributes: IRenderAttributes<T, K>,
subNodes?: (Node | UINode)[], subNodes?: (Node | UINode | Ref<any>)[],
): HTMLElementTagNameMap[T] { ): HTMLElementTagNameMap[T] {
const element = document.createElement(type); const element = document.createElement(type);
if (attributes) { if (attributes) {
for (const key in attributes) { for (const key in attributes) {
if (key === "classes") { if (key === "classes") {
element.classList.add(...attributes[key]!); element.classList.add(...attributes[key]!);
} else if (key === "saveTo") {
attributes.saveTo!.val = element;
} else { } else {
element[key as keyof HTMLElementTagNameMap[T]] = (attributes as any)[key]; element[key as keyof HTMLElementTagNameMap[T]] = (attributes as any)[key];
} }
} }
} }
if (subNodes) { if (subNodes) {
for (const subElement of subNodes) { for (let i = 0; i < subNodes.length; i++) {
if (subElement instanceof UINode) { const subNode = subNodes[i];
element.append(subElement.render()); 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 { } else {
element.append(subElement); element.append(subNode);
} }
} }
} }
return element; return element;
} }
static q(text: string): Text { export function q(text: string): Text {
return document.createTextNode(text); return document.createTextNode(text);
} }
static frag(subs?: Node[]): DocumentFragment { export function frag(subs?: Node[]): DocumentFragment {
const frag = document.createDocumentFragment(); const frag = document.createDocumentFragment();
if (subs) { if (subs) {
frag.append(...subs); frag.append(...subs);
} }
return frag; return frag;
}
} }

View File

@@ -1,5 +1,5 @@
import "./ActionButton.css"; import "./ActionButton.css";
import UINode, {UINodeOptions} from "@/ui/UINode"; import UINode, {h, UINodeOptions} from "@/ui/UINode";
import IconView, {IconName} from "@/ui/Widgets/Icon/IconView"; import IconView, {IconName} from "@/ui/Widgets/Icon/IconView";
export type ActionButtonUINodeOptions = UINodeOptions & { export type ActionButtonUINodeOptions = UINodeOptions & {
@@ -48,14 +48,14 @@ export default class ActionButtonView extends UINode {
} }
protected build(): HTMLButtonElement { protected build(): HTMLButtonElement {
this.buttonElement = UINode.make("button", { this.buttonElement = h("button", {
classes: ["action-button", `action-button-${this.type}`], classes: ["action-button", `action-button-${this.type}`],
onclick: (event: MouseEvent) => this.disabled || this.onClick(event) onclick: (event: MouseEvent) => this.disabled || this.onClick(event)
}, [ }, [
this.icon !== null ? new IconView({ this.icon !== null ? new IconView({
iconName: this.icon, iconName: this.icon,
color: "var(--color-p-light)", color: "var(--color-p-light)",
}).render() : UINode.make("span", { }) : h("span", {
innerText: this.label ?? "" innerText: this.label ?? ""
}), }),
]); ]);

View File

@@ -1,5 +1,6 @@
import "./BoolBox.css"; import "./BoolBox.css";
import UINode, {UINodeOptions} from "@/ui/UINode"; import UINode, {h, UINodeOptions} from "@/ui/UINode";
import Ref from "@/Ref";
export type BoolBoxUINodeOptions = UINodeOptions & { export type BoolBoxUINodeOptions = UINodeOptions & {
label?: string, label?: string,
@@ -36,9 +37,9 @@ export default class BoolBoxView extends UINode {
} }
build(): HTMLDivElement { build(): HTMLDivElement {
this.labelElement = UINode.make("label", { this.labelElement = h("label", {
classes: ["bool-box-label"], classes: ["bool-box-label"],
innerText: this.label ?? "", innerText: this.label,
onclick: () => { onclick: () => {
this.onInput(!this.checkboxElement.checked); this.onInput(!this.checkboxElement.checked);
}, },
@@ -46,14 +47,14 @@ export default class BoolBoxView extends UINode {
if (this.label !== null) { if (this.label !== null) {
this.labelElement.classList.add("visible"); this.labelElement.classList.add("visible");
} }
this.checkboxElement = UINode.make("input", { this.checkboxElement = h("input", {
type: "checkbox", type: "checkbox",
classes: ["bool-box-checkbox"], classes: ["bool-box-checkbox"],
onclick: (event: Event) => { onclick: (event: Event) => {
this.onInput((event.target as HTMLInputElement).checked); this.onInput((event.target as HTMLInputElement).checked);
}, },
}); });
return UINode.make("div", { return h("div", {
classes: ["bool-box"], classes: ["bool-box"],
},[ },[
this.labelElement, this.labelElement,

View File

@@ -1,4 +1,4 @@
import UINode, {UINodeOptions} from "@/ui/UINode"; import UINode, {h, UINodeOptions} from "@/ui/UINode";
import "./EditableTextFieldView.css"; import "./EditableTextFieldView.css";
export type EditableTextFieldViewOptions = UINodeOptions & { export type EditableTextFieldViewOptions = UINodeOptions & {
@@ -31,7 +31,7 @@ export default class EditableTextFieldView extends UINode {
} }
build(): HTMLSpanElement { build(): HTMLSpanElement {
this.titleInput = UINode.make("input", { this.titleInput = h("input", {
value: this.text, value: this.text,
classes: ["editable-text-field-view"], classes: ["editable-text-field-view"],
type: "text", type: "text",
@@ -58,7 +58,7 @@ export default class EditableTextFieldView extends UINode {
} }
}, },
}); });
this.titleDisplay = UINode.make("div", { this.titleDisplay = h("div", {
innerText: this.text, innerText: this.text,
classes: ["editable-text-field-view"], classes: ["editable-text-field-view"],
onclick: () => { onclick: () => {

View File

@@ -1,9 +1,10 @@
import UINode, {UINodeOptions} from "@/ui/UINode"; import UINode, {h, UINodeOptions} from "@/ui/UINode";
import "./Icon.css"; import "./Icon.css";
import List from "./svgs/list.svg"; import List from "./svgs/list.svg";
import ArrowClockwise from "./svgs/arrow-clockwise.svg"; import ArrowClockwise from "./svgs/arrow-clockwise.svg";
import Trash from "./svgs/trash.svg"; import Trash from "./svgs/trash.svg";
import Snowflake from "./svgs/snowflake.svg"; import Snowflake from "./svgs/snowflake.svg";
import Ref from "@/Ref";
const IconUrlMap = { const IconUrlMap = {
arrowClockwise: ArrowClockwise, arrowClockwise: ArrowClockwise,
@@ -30,7 +31,7 @@ export default class IconView extends UINode {
} }
build(): HTMLSpanElement { build(): HTMLSpanElement {
const icon = UINode.make("div", { const icon = h("div", {
classes: ["icon-view"], classes: ["icon-view"],
}); });
const colorString = this.color ? `--icon-bg:${this.color}` : ""; 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 "./NumberInput.css";
import Ref from "@/Ref";
type NumberInputUINodeOptionsBase = UINodeOptions & { type NumberInputUINodeOptionsBase = UINodeOptions & {
label?: string, label?: string,
@@ -26,8 +27,8 @@ type NumberInputUINodeOptionsGetSet = NumberInputUINodeOptionsBase & {
export type NumberInputUINodeOptions = NumberInputUINodeOptionsGetSet | NumberInputUINodeOptionsIncDecInput; export type NumberInputUINodeOptions = NumberInputUINodeOptionsGetSet | NumberInputUINodeOptionsIncDecInput;
export default class NumberInputView extends UINode { export default class NumberInputView extends UINode {
private labelElement!: HTMLLabelElement; private labelElement: Ref<HTMLLabelElement | null> = new Ref<HTMLLabelElement | null>(null);
private inputElement!: HTMLInputElement; private inputElement: Ref<HTMLInputElement | null> = new Ref<HTMLInputElement | null>(null);
private labelPosition: "top" | "left"; private labelPosition: "top" | "left";
private value: number; private value: number;
private label: string | null; private label: string | null;
@@ -52,40 +53,57 @@ export default class NumberInputView extends UINode {
setLabel(newLabel: string | null): void { setLabel(newLabel: string | null): void {
if (newLabel !== null) { if (newLabel !== null) {
this.label = newLabel; this.label = newLabel;
this.labelElement.innerText = newLabel; this.labelElement.val!.innerText = newLabel;
this.labelElement.classList.add("visible"); this.labelElement.val!.classList.add("visible");
} else { } else {
this.label = newLabel; this.label = newLabel;
this.labelElement.innerText = ""; this.labelElement.val!.innerText = "";
this.labelElement.classList.remove("visible"); this.labelElement.val!.classList.remove("visible");
} }
} }
disable(): void { disable(): void {
this.node?.classList.add("disabled"); this.node?.classList.add("disabled");
this.inputElement.disabled = true; this.inputElement.val!.disabled = true;
} }
enable(): void { enable(): void {
this.node?.classList.remove("disabled"); this.node?.classList.remove("disabled");
this.inputElement.disabled = false; this.inputElement.val!.disabled = false;
} }
setValue(value: number): void { setValue(value: number): void {
this.value = value; this.value = value;
this.inputElement.valueAsNumber = value; this.inputElement.val!.valueAsNumber = value;
} }
build(): HTMLDivElement { build(): HTMLDivElement {
this.labelElement = UINode.make("label", { const labelClasses = ["number-input-label", this.labelPosition];
classes: ["number-input-label", this.labelPosition],
innerText: this.label ?? "",
});
if (this.label !== null) { if (this.label !== null) {
this.labelElement.classList.add("visible"); labelClasses.push("visible");
} }
this.inputElement = UINode.make("input", { return h("div", {
classes: ["number-input"],
}, [
h("label", {
classes: labelClasses,
saveTo: this.labelElement,
innerText: this.label ?? "",
}),
h("button", {
innerText: "-",
classes: ["number-input-button", "number-input-dec"],
onclick: () => {
if (this.onDecrement) {
this.onDecrement();
} else if (this.setter && this.getter) {
this.setter(this.getter() - 1);
}
},
}),
h("input", {
type: "number", type: "number",
saveTo: this.inputElement,
classes: ["number-input-input"], classes: ["number-input-input"],
valueAsNumber: this.value, valueAsNumber: this.value,
onblur: (event: Event) => { onblur: (event: Event) => {
@@ -98,24 +116,8 @@ export default class NumberInputView extends UINode {
} }
} }
}, },
});
return UINode.make("div", {
classes: ["number-input"],
}, [
this.labelElement,
UINode.make("button", {
innerText: "-",
classes: ["number-input-button", "number-input-dec"],
onclick: () => {
if (this.onDecrement) {
this.onDecrement();
} else if (this.setter && this.getter) {
this.setter(this.getter() - 1);
}
},
}), }),
this.inputElement, h("button", {
UINode.make("button", {
innerText: "+", innerText: "+",
classes: ["number-input-button", "number-input-inc"], classes: ["number-input-button", "number-input-inc"],
onclick: () => { onclick: () => {