feat: added refs, refactored UINode for more concise build functions
This commit is contained in:
39
src/Ref.ts
Normal file
39
src/Ref.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
]),
|
||||
]);
|
||||
|
||||
@@ -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
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
]),
|
||||
]);
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
|
||||
@@ -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,
|
||||
])
|
||||
])
|
||||
])
|
||||
|
||||
@@ -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]),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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];
|
||||
}
|
||||
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 (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;
|
||||
}
|
||||
@@ -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 ?? ""
|
||||
}),
|
||||
]);
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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: () => {
|
||||
|
||||
@@ -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}` : "";
|
||||
|
||||
@@ -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: () => {
|
||||
|
||||
Reference in New Issue
Block a user