feat: stuff i'm tired

This commit is contained in:
Daniel Ledda
2022-03-27 00:28:51 +01:00
parent 8056a5dc89
commit fcec98d7e5
18 changed files with 400 additions and 154 deletions

View File

@@ -1,7 +1,6 @@
import Beat, {BeatEvents, BeatInitOptions} from "@/Beat";
import {IPublisher, Publisher} from "@/Publisher";
import ISubscriber from "@/Subscriber";
import BeatLike from "@/BeatLike";
import {greatestCommonDivisor, isPosInt} from "@/utils";
type BeatGroupInitOptions = {
@@ -11,6 +10,7 @@ type BeatGroupInitOptions = {
beats?: BeatInitOptions[],
loopLength?: number,
useAutoBeatLength?: boolean,
name?: string,
};
export const enum BeatGroupEvents {
@@ -20,6 +20,9 @@ export const enum BeatGroupEvents {
TimeSigUpChanged="bge-3",
AutoBeatSettingsChanged="bge-4",
LockingChanged="bge-5",
GlobalLoopLengthChanged="bge-5",
GlobalDisplayTypeChanged="bge-6",
NameChanged="bge-7",
}
type EventTypeSubscriptions =
@@ -28,19 +31,25 @@ type EventTypeSubscriptions =
| BeatEvents.WantsRemoval
| BeatEvents.Baked;
type EventTypePublications = BeatGroupEvents | BeatEvents;
export default class BeatGroup implements IPublisher<EventTypePublications>, BeatLike, ISubscriber<EventTypeSubscriptions> {
export default class BeatGroup implements IPublisher<BeatGroupEvents>, ISubscriber<EventTypeSubscriptions> {
private static globalCounter = 0;
private beats: Beat[] = [];
private publisher: Publisher<EventTypePublications, BeatGroup> = new Publisher<EventTypePublications, BeatGroup>(this);
private publisher: Publisher<BeatGroupEvents, BeatGroup> = new Publisher<BeatGroupEvents, BeatGroup>(this);
private barCount: number;
private timeSigUp: number;
private globalLoopLength: number;
private globalIsLooping: boolean;
private useAutoBeatLength: boolean;
private barSettingsLocked = false;
private name: string;
constructor(options?: BeatGroupInitOptions) {
BeatGroup.globalCounter++;
if (options?.name) {
this.name = options.name;
} else {
this.name = `Pattern ${BeatGroup.globalCounter}`;
}
if (options?.beats) {
for (const beatOptions of options.beats) {
this.addBeat(beatOptions);
@@ -68,7 +77,7 @@ export default class BeatGroup implements IPublisher<EventTypePublications>, Bea
}
}
addSubscriber(subscriber: ISubscriber<EventTypePublications>, eventType: SubscriptionEvent<EventTypePublications>): { unbind: () => void } {
addSubscriber(subscriber: ISubscriber<BeatGroupEvents>, eventType: BeatGroupEvents | BeatGroupEvents[]): { unbind: () => void } {
return this.publisher.addSubscriber(subscriber, eventType);
}
@@ -103,7 +112,7 @@ export default class BeatGroup implements IPublisher<EventTypePublications>, Bea
for (const beat of this.beats) {
beat.setLoopLength(loopLength);
}
this.publisher.notifySubs(BeatEvents.LoopLengthChanged);
this.publisher.notifySubs(BeatGroupEvents.GlobalLoopLengthChanged);
}
getLoopLength(): number {
@@ -115,7 +124,7 @@ export default class BeatGroup implements IPublisher<EventTypePublications>, Bea
for (const beat of this.beats) {
beat.setLooping(isLooping);
}
this.publisher.notifySubs(BeatEvents.DisplayTypeChanged);
this.publisher.notifySubs(BeatGroupEvents.GlobalDisplayTypeChanged);
}
isLooping(): boolean {
@@ -238,6 +247,7 @@ export default class BeatGroup implements IPublisher<EventTypePublications>, Bea
const beat = this.getBeatByKey(beatKey);
this.beats.splice(this.beats.indexOf(beat), 1);
this.autoBeatLength();
console.log("removing");
this.publisher.notifySubs(BeatGroupEvents.BeatListChanged);
}
@@ -284,4 +294,13 @@ export default class BeatGroup implements IPublisher<EventTypePublications>, Bea
bakeLoops(): void {
this.beats.forEach(beat => beat.bakeLoops());
}
setName(newName: string): void {
this.name = newName;
this.publisher.notifySubs(BeatGroupEvents.NameChanged);
}
getName(): string {
return this.name;
}
}

View File

@@ -6,6 +6,7 @@ const appNode = document.querySelector("#app");
if (appNode) {
try {
const appRoot = new RootView({
orientation: "vertical",
title: "Drum Slayer",
});
// eslint-disable-next-line @typescript-eslint/ban-ts-comment

View File

@@ -3,10 +3,12 @@ import BeatGroup, {BeatGroupEvents} from "@/BeatGroup";
import BeatView from "@/ui/Beat/BeatView";
import "./BeatGroup.css";
import ISubscriber from "@/Subscriber";
import {ISubscription} from "@/Publisher";
export type BeatGroupUINodeOptions = UINodeOptions & {
title: string,
beatGroup: BeatGroup,
orientation?: "horizontal" | "vertical",
};
const EventTypeSubscriptions = [
@@ -18,31 +20,56 @@ export default class BeatGroupView extends UINode implements ISubscriber<EventTy
private title: string;
private beatGroup: BeatGroup;
private beatViews: BeatView[] = [];
private currentOrientation: "vertical" | "horizontal";
private subscription: ISubscription;
constructor(options: BeatGroupUINodeOptions) {
super(options);
this.beatGroup = options.beatGroup;
this.title = options.title;
this.beatGroup.addSubscriber(this, BeatGroupEvents.BeatListChanged);
this.currentOrientation = options.orientation ?? "horizontal";
this.subscription = this.beatGroup.addSubscriber(this, EventTypeSubscriptions);
this.setupBeatViews();
}
notify(publisher: unknown, event: EventTypeSubscriptions): void {
if (event === BeatGroupEvents.BeatListChanged) {
this.setupBeatViews();
this.redraw();
}
}
setBeatGroup(newBeatGroup: BeatGroup): void {
this.beatGroup = newBeatGroup;
this.beatGroup.addSubscriber(this, BeatGroupEvents.BeatListChanged);
this.redraw();
}
build(): HTMLDivElement {
private setupBeatViews(): void {
this.beatViews = [];
for (let i = 0; i < this.beatGroup.getBeatCount(); i++) {
this.beatViews.push(new BeatView({beat: this.beatGroup.getBeatByIndex(i)}));
}
if (this.currentOrientation === "vertical") {
this.reverseDisplayOrder();
}
}
setOrientation(orientation: "vertical" | "horizontal"): void {
if (this.currentOrientation !== orientation) {
this.reverseDisplayOrder();
this.currentOrientation = orientation;
}
}
private reverseDisplayOrder(): void {
this.beatViews = this.beatViews.reverse();
this.getNode().classList.toggle("vertical");
this.redraw();
}
setBeatGroup(newBeatGroup: BeatGroup): void {
this.subscription.unbind();
this.beatGroup = newBeatGroup;
this.subscription = this.beatGroup.addSubscriber(this, BeatGroupEvents.BeatListChanged);
this.redraw();
}
build(): HTMLDivElement {
return UINode.make("div", {
classes: ["beat-group"],
},[

View File

@@ -1,9 +1,8 @@
import "./BeatGroupSettings.css";
import UINode, {UINodeOptions} from "@/ui/UINode";
import NumberInputView from "@/ui/Widgets/NumberInput/NumberInputView";
import ISubscriber, {SubscriptionEvent} from "@/Subscriber";
import ISubscriber from "@/Subscriber";
import BeatGroup, {BeatGroupEvents} from "@/BeatGroup";
import {BeatEvents} from "@/Beat";
import BoolBoxView from "@/ui/Widgets/BoolBox/BoolBoxView";
import BeatSettingsView from "@/ui/BeatSettings/BeatSettingsView";
import ActionButtonView from "@/ui/Widgets/ActionButton/ActionButtonView";
@@ -15,13 +14,14 @@ export type BeatGroupSettingsUINodeOptions = UINodeOptions & {
const EventTypeSubscriptions = [
BeatGroupEvents.BarCountChanged,
BeatGroupEvents.TimeSigUpChanged,
BeatEvents.DisplayTypeChanged,
BeatGroupEvents.GlobalDisplayTypeChanged,
BeatGroupEvents.BeatListChanged,
BeatGroupEvents.LockingChanged,
BeatGroupEvents.AutoBeatSettingsChanged,
];
type EventTypeSubscriptions = FlatArray<typeof EventTypeSubscriptions, 1>;
export default class BeatGroupSettingsView extends UINode implements ISubscriber<typeof EventTypeSubscriptions> {
export default class BeatGroupSettingsView extends UINode implements ISubscriber<EventTypeSubscriptions> {
private beatGroup: BeatGroup;
private barCountInput!: NumberInputView;
private timeSigUpInput!: NumberInputView;
@@ -45,7 +45,7 @@ export default class BeatGroupSettingsView extends UINode implements ISubscriber
this.beatGroup.addSubscriber(this, EventTypeSubscriptions);
}
notify(publisher: unknown, event: SubscriptionEvent<typeof EventTypeSubscriptions>): void {
notify(publisher: unknown, event: EventTypeSubscriptions): void {
switch(event) {
case BeatGroupEvents.BarCountChanged:
this.barCountInput.setValue(this.beatGroup.getBarCount());
@@ -66,7 +66,7 @@ export default class BeatGroupSettingsView extends UINode implements ISubscriber
case BeatGroupEvents.AutoBeatSettingsChanged:
this.autoBeatLengthCheckbox.setValue(this.beatGroup.autoBeatLengthOn());
break;
case BeatEvents.DisplayTypeChanged:
case BeatGroupEvents.GlobalDisplayTypeChanged:
break;
}
}
@@ -82,9 +82,7 @@ export default class BeatGroupSettingsView extends UINode implements ISubscriber
}
}
if (!this.beatSettingsContainer) {
this.beatSettingsContainer = UINode.make("div", {
subs: this.beatSettingsViews.map(view => view.render())
});
this.beatSettingsContainer = UINode.make("div", {}, this.beatSettingsViews.map(view => view.render()));
} else {
this.beatSettingsContainer.replaceChildren(...this.beatSettingsViews.map(view => view.render()));
}
@@ -118,18 +116,18 @@ export default class BeatGroupSettingsView extends UINode implements ISubscriber
UINode.make("div", {
classes: ["beat-group-settings-boxes", "beat-group-settings-option"],
}, [
this.timeSigUpInput.render(),
this.timeSigUpInput,
]),
UINode.make("div", {
classes: ["beat-group-settings-bar-count", "beat-group-settings-option"]
,
}, [
this.barCountInput.render(),
this.barCountInput,
]),
UINode.make("div", {
classes: ["beat-group-settings-bar-count", "beat-group-settings-option"],
}, [
this.autoBeatLengthCheckbox.render(),
this.autoBeatLengthCheckbox,
]),
new ActionButtonView({
label: "New Track",

View File

@@ -2,12 +2,16 @@
}
.beat-settings-title-input {
width: 100%;
.beat-settings-title-container {
}
.beat-settings-title-container input {
min-width: 100%;
height: 2em;
}
.beat-settings-title {
.beat-settings-title-container > div {
width: 100%;
font-weight: bold;
padding: 0.5em;
@@ -15,7 +19,7 @@
cursor: pointer;
}
.beat-settings-title:hover {
.beat-settings-title-container > div:hover {
background-color: var(--color-ui-neutral-dark-hover);
}

View File

@@ -1,11 +1,12 @@
import "./BeatSettings.css";
import Beat, {BeatEvents} from "@/Beat";
import UINode, {UINodeOptions} from "@/ui/UINode";
import ISubscriber, {SubscriptionEvent} from "@/Subscriber";
import ISubscriber from "@/Subscriber";
import {ISubscription} from "@/Publisher";
import NumberInputView from "@/ui/Widgets/NumberInput/NumberInputView";
import BoolBoxView from "@/ui/Widgets/BoolBox/BoolBoxView";
import ActionButtonView from "@/ui/Widgets/ActionButton/ActionButtonView";
import EditableTextFieldView from "@/ui/Widgets/EditableTextFIeld/EditableTextFieldView";
export type BeatSettingsViewUINodeOptions = UINodeOptions & {
beat: Beat,
@@ -16,16 +17,16 @@ const EventTypeSubscriptions = [
BeatEvents.LoopLengthChanged,
BeatEvents.DisplayTypeChanged,
];
type EventTypeSubscriptions = FlatArray<typeof EventTypeSubscriptions, 1>;
export default class BeatSettingsView extends UINode implements ISubscriber<typeof EventTypeSubscriptions> {
export default class BeatSettingsView extends UINode implements ISubscriber<EventTypeSubscriptions> {
private beat: Beat;
private loopLengthInput!: NumberInputView;
private bakeButton!: ActionButtonView;
private loopCheckbox!: BoolBoxView;
private loopLengthSection!: HTMLDivElement;
private sub!: ISubscription;
private titleInput!: HTMLInputElement;
private titleDisplay!: HTMLSpanElement;
private title!: EditableTextFieldView;
private editingTitle: boolean;
constructor(options: BeatSettingsViewUINodeOptions) {
@@ -36,7 +37,7 @@ export default class BeatSettingsView extends UINode implements ISubscriber<type
}
private setupBindings() {
this.sub = this.beat.addSubscriber(this, "all");
this.sub = this.beat.addSubscriber(this, EventTypeSubscriptions);
}
setBeat(beat: Beat): void {
@@ -46,11 +47,10 @@ export default class BeatSettingsView extends UINode implements ISubscriber<type
EventTypeSubscriptions.forEach(eventType => this.notify(null, eventType));
}
notify(publisher: unknown, event: SubscriptionEvent<typeof EventTypeSubscriptions>): void {
notify(publisher: unknown, event: EventTypeSubscriptions): void {
switch(event) {
case BeatEvents.NewName:
this.titleInput.value = this.beat.getName();
this.titleDisplay.innerText = this.beat.getName();
this.title.setText(this.beat.getName());
break;
case BeatEvents.LoopLengthChanged:
this.loopLengthInput.setValue(this.beat.getLoopLength());
@@ -68,27 +68,9 @@ export default class BeatSettingsView extends UINode implements ISubscriber<type
}
build(): HTMLElement {
this.titleInput = UINode.make("input", {
value: this.beat.getName(),
classes: ["beat-settings-title-input"],
type: "text",
oninput: (event: Event) => {
this.beat.setName((event.target as HTMLInputElement).value);
},
onblur: () => this.titleInput.replaceWith(this.titleDisplay),
onkeyup: (event: KeyboardEvent) => {
if (event.key === "Enter") {
(event.target as HTMLInputElement).blur();
}
}
});
this.titleDisplay = UINode.make("div", {
innerText: this.beat.getName(),
classes: ["beat-settings-title"],
onclick: () => {
this.titleDisplay.replaceWith(this.titleInput);
this.titleInput.focus();
}
this.title = new EditableTextFieldView({
initialText: this.beat.getName(),
setter: (newText) => this.beat.setName(newText),
});
this.bakeButton = new ActionButtonView({
icon: "snowflake",
@@ -121,11 +103,15 @@ export default class BeatSettingsView extends UINode implements ISubscriber<type
return UINode.make("div", {
classes: ["beat-settings"],
}, [
this.titleDisplay,
UINode.make("div", {
classes: ["beat-settings-title-container"]
}, [
this.title,
]),
UINode.make("div", {
classes: ["beat-settings-lower"],
}, [
this.bakeButton.render(),
this.bakeButton,
new ActionButtonView({
icon: "trash",
type: "secondary",
@@ -135,7 +121,7 @@ export default class BeatSettingsView extends UINode implements ISubscriber<type
UINode.make("div", {
classes: ["loop-settings"],
}, [
this.loopCheckbox.render(),
this.loopCheckbox,
]),
this.loopLengthSection,
]),

View File

@@ -33,23 +33,23 @@
background-color: var(--color-ui-accent-hover);
}
.beat-unit.beat-unit-on.beat-unit-ghost {
.beat-unit.beat-unit-on.beat-unit-accent {
border-color: var(--color-ui-neutral-light);
background-color: var(--color-ui-accent);
}
.beat-unit.beat-unit-on.beat-unit-ghost:hover {
.beat-unit.beat-unit-on.beat-unit-accent:hover {
border-color: var(--color-ui-neutral-light);
background-color: var(--color-ui-accent-hover);
}
.beat-unit.beat-unit-on.beat-unit-accent {
.beat-unit.beat-unit-on.beat-unit-ghost {
border-color: var(--color-ui-accent);
background-color: var(--color-ui-accent);
opacity: 60%;
}
.beat-unit.beat-unit-on.beat-unit-accent:hover {
.beat-unit.beat-unit-on.beat-unit-ghost:hover {
border-color: var(--color-ui-accent-hover);
background-color: var(--color-ui-accent-hover);
}

View File

@@ -20,7 +20,8 @@ export default class BeatUnitView extends UINode implements ISubscriber<EventTyp
private subscription: ISubscription | null = null;
private publisher: IPublisher<BeatUnitEvent> = new Publisher<BeatUnitEvent, BeatUnitView>(this);
private touchTimeout: ReturnType<typeof setTimeout> | null = null;
private mouseDownListeners: ((ev: MouseEvent) => void)[] = [];
private hoverListeners: ((ev: MouseEvent) => void)[] = [];
constructor(options: BeatUnitUINodeOptions) {
super(options);
@@ -35,26 +36,37 @@ export default class BeatUnitView extends UINode implements ISubscriber<EventTyp
this.notify(this.publisher, BeatUnitEvent.TypeChange);
}
private handleMouseDown(ev: MouseEvent): void {
if (ev.button === 1) {
this.beatUnit.rotateType();
}
}
private handleTouchStart(ev: TouchEvent): void {
this.touchTimeout = this.touchTimeout || setTimeout(() => {
this.beatUnit.rotateType();
this.touchTimeout = null;
}, 400);
}
private handleTouchEnd(ev: TouchEvent): void {
if (this.touchTimeout) {
clearTimeout(this.touchTimeout);
this.touchTimeout = null;
}
}
private setupBindings() {
this.subscription?.unbind();
this.subscription = this.beatUnit.addSubscriber(this, EventTypeSubscriptions);
this.onMouseDown((ev: MouseEvent) => {
if (ev.button === 1) {
this.beatUnit.rotateType();
}
});
this.getNode().addEventListener("touchstart", () => {
this.touchTimeout = setTimeout(() => {
this.beatUnit.rotateType();
this.touchTimeout = null;
}, 400);
});
this.getNode().addEventListener("touchend", () => {
if (this.touchTimeout) {
clearTimeout(this.touchTimeout);
this.touchTimeout = null;
}
});
this.mouseDownListeners.forEach(listener => this.getNode().removeEventListener("mousedown", listener));
this.hoverListeners.forEach(listener => this.getNode().removeEventListener("mouseover", listener));
this.redraw();
this.mouseDownListeners.forEach(listener => this.getNode().addEventListener("mousedown", listener));
this.hoverListeners.forEach(listener => this.getNode().addEventListener("mouseover", listener));
this.getNode().addEventListener("mousedown", (ev) => this.handleMouseDown(ev));
this.getNode().addEventListener("touchstart", (ev) => this.handleTouchStart(ev));
this.getNode().addEventListener("touchend", (ev) => this.handleTouchEnd(ev));
}
toggle(): void {
@@ -108,14 +120,12 @@ export default class BeatUnitView extends UINode implements ISubscriber<EventTyp
}
onHover(cb: () => void): void {
this.getNode().addEventListener("mouseover", cb);
this.hoverListeners.push(cb);
this.setupBindings();
}
onMouseDown(cb: (ev: MouseEvent) => void): void {
this.getNode().addEventListener("mousedown", cb);
}
onMouseUp(cb: (ev: MouseEvent) => void): void {
this.getNode().addEventListener("mouseup", cb);
this.mouseDownListeners.push(cb);
this.setupBindings();
}
}

View File

@@ -88,6 +88,7 @@
}
.root-beat-stage {
position: relative;
padding: 2em;
max-height: 100vh;
margin: auto;
@@ -96,6 +97,8 @@
}
.vertical-mode .root-beat-stage {
margin: 5em auto auto;
padding-left: 3em;
height: 100vh;
}

View File

@@ -4,25 +4,43 @@ 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";
export type RootUINodeOptions = UINodeOptions & {
title: string,
mainBeatGroup?: BeatGroup,
orientation?: "horizontal" | "vertical",
};
export default class RootView extends UINode {
private title: string;
private beatGroupView: BeatGroupView;
private mainBeatGroup: BeatGroup;
private focusedBeatGroup: BeatGroup;
private beatGroupSettingsView!: BeatGroupSettingsView;
private currentOrientation: "horizontal" | "vertical";
private stageTitleBarView: StageTitleBarView;
constructor(options: RootUINodeOptions) {
super(options);
this.mainBeatGroup = options.mainBeatGroup ?? RootView.defaultMainBeatGroup();
this.beatGroupView = new BeatGroupView({title: options.title, beatGroup: this.mainBeatGroup});
this.beatGroupSettingsView = new BeatGroupSettingsView({beatGroup: this.mainBeatGroup});
this.currentOrientation = options.orientation ?? "horizontal";
this.focusedBeatGroup = options.mainBeatGroup ?? RootView.defaultMainBeatGroup();
this.beatGroupView = new BeatGroupView({
title: options.title,
beatGroup: this.focusedBeatGroup,
orientation: this.currentOrientation,
});
this.stageTitleBarView = new StageTitleBarView({beatGroup: this.focusedBeatGroup});
this.beatGroupSettingsView = new BeatGroupSettingsView({beatGroup: this.focusedBeatGroup});
this.title = options.title;
this.setOrientation(this.currentOrientation);
this.openSidebarForDesktop();
}
private openSidebarForDesktop() {
const mediaQueryList = window.matchMedia("screen and (max-width: 900px)");
if (mediaQueryList.matches) {
this.toggleSidebar();
}
}
static defaultMainBeatGroup(): BeatGroup {
@@ -39,12 +57,33 @@ export default class RootView extends UINode {
return mainBeatGroup;
}
setMainBeatGroup(beatGroup: BeatGroup): void {
this.focusedBeatGroup = beatGroup;
this.beatGroupSettingsView.setBeatGroup(this.focusedBeatGroup);
this.beatGroupView.setBeatGroup(this.focusedBeatGroup);
this.stageTitleBarView.setBeatGroup(this.focusedBeatGroup);
}
toggleSidebar(): void {
this.getNode().classList.toggle("sidebar-visible");
}
toggleOrientation(): void {
this.getNode().classList.toggle("vertical-mode");
if (this.currentOrientation === "vertical") {
this.setOrientation("horizontal");
} else {
this.setOrientation("vertical");
}
}
setOrientation(orientation: "horizontal" | "vertical"): void {
this.currentOrientation = orientation;
if (orientation === "vertical") {
this.getNode().classList.add("vertical-mode");
} else {
this.getNode().classList.remove("vertical-mode");
}
this.beatGroupView.setOrientation(orientation);
}
private buildSidebarStrip(): HTMLElement {
@@ -71,7 +110,7 @@ export default class RootView extends UINode {
]),
UINode.make("div", {
classes: ["root-quick-access-button"],
onclick: () => this.mainBeatGroup.bakeLoops(),
onclick: () => this.focusedBeatGroup.bakeLoops(),
}, [
new IconView({
iconName: "snowflake",
@@ -81,11 +120,7 @@ export default class RootView extends UINode {
UINode.make("div", {
classes: ["root-quick-access-button"],
title: "Reset all",
onclick: () => {
this.mainBeatGroup = RootView.defaultMainBeatGroup();
this.beatGroupSettingsView.setBeatGroup(this.mainBeatGroup);
this.beatGroupView.setBeatGroup(this.mainBeatGroup);
},
onclick: () => this.setMainBeatGroup(RootView.defaultMainBeatGroup()),
}, [
new IconView({
iconName: "trash",
@@ -99,11 +134,10 @@ export default class RootView extends UINode {
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(),
]),
this.buildSidebarStrip(),
UINode.make("h1", {classes: ["root-title"], innerText: this.title}),
this.beatGroupSettingsView.render(),
]),
this.buildSidebarStrip(),
])
);
}
@@ -113,6 +147,7 @@ export default class RootView extends UINode {
UINode.make("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(),
])

View File

@@ -0,0 +1,25 @@
.stage-title-bar {
position: absolute;
background-color: var(--color-bg-light);
padding: 15px;
border-radius: 0 0 5px 5px;
color: var(--color-title-light);
left: 50%;
transform: translateX(-50%);
display: flex;
flex-direction: column;
align-items: center;
}
.stage-title-bar-preamble {
margin-bottom: 4px;
font-size: 12px;
}
.stage-title-bar * {
flex: 1;
}
.stage-title-bar h2 {
margin: 0;
}

View File

@@ -0,0 +1,50 @@
import "./StageTitleBar.css";
import UINode, {UINodeOptions} from "@/ui/UINode";
import {ISubscription} from "@/Publisher";
import BeatGroup, {BeatGroupEvents} from "@/BeatGroup";
import ISubscriber from "@/Subscriber";
import EditableTextFieldView from "@/ui/Widgets/EditableTextFIeld/EditableTextFieldView";
export type StageTitleBarViewOptions = UINodeOptions & {
beatGroup: BeatGroup,
};
const EventTypeSubscription = [BeatGroupEvents.NameChanged];
type EventTypeSubscription = FlatArray<typeof EventTypeSubscription, 1>;
export default class StageTitleBarView extends UINode implements ISubscriber<EventTypeSubscription> {
private sub: ISubscription;
private beatGroup: BeatGroup;
private title: EditableTextFieldView;
constructor(options: StageTitleBarViewOptions) {
super(options);
this.beatGroup = options.beatGroup;
this.sub = options.beatGroup.addSubscriber(this, EventTypeSubscription);
this.title = new EditableTextFieldView({
initialText: this.beatGroup.getName(),
setter: (text) => this.beatGroup.setName(text),
noEmpty: true,
});
}
notify(publisher: unknown, event: EventTypeSubscription): void {
if (event === BeatGroupEvents.NameChanged) {
this.title.setText(this.beatGroup.getName());
}
}
setBeatGroup(beatGroup: BeatGroup): void {
this.sub.unbind();
this.beatGroup = beatGroup;
this.sub = beatGroup.addSubscriber(this, EventTypeSubscription);
this.notify(this, BeatGroupEvents.NameChanged);
}
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]),
]);
}
}

View File

@@ -13,7 +13,7 @@ type IRenderAttributes<
export default abstract class UINode {
protected node: HTMLElement | null = null;
constructor(options: UINodeOptions) {}
constructor(options: UINodeOptions) { /* dummy */ }
render(): HTMLElement {
if (!this.node) {
@@ -51,7 +51,7 @@ export default abstract class UINode {
K extends keyof HTMLElementTagNameMap[T]>(
type: T,
attributes: IRenderAttributes<T, K>,
subElements?: HTMLElement[],
subNodes?: (Node | UINode)[],
): HTMLElementTagNameMap[T] {
const element = document.createElement(type);
if (attributes) {
@@ -63,8 +63,14 @@ export default abstract class UINode {
}
}
}
if (subElements) {
element.append(...subElements);
if (subNodes) {
for (const subElement of subNodes) {
if (subElement instanceof UINode) {
element.append(subElement.render());
} else {
element.append(subElement);
}
}
}
return element;
}

View File

@@ -50,16 +50,15 @@ export default class ActionButtonView extends UINode {
protected build(): HTMLButtonElement {
this.buttonElement = UINode.make("button", {
classes: ["action-button", `action-button-${this.type}`],
onclick: (event: MouseEvent) => this.disabled || this.onClick(event),
subs: [
this.icon !== null ? new IconView({
iconName: this.icon,
color: "var(--color-p-light)",
}).render() : UINode.make("span", {
innerText: this.label ?? ""
}),
],
});
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", {
innerText: this.label ?? ""
}),
]);
if (this.alt) {
this.buttonElement.title = this.alt;
}

View File

@@ -55,10 +55,9 @@ export default class BoolBoxView extends UINode {
});
return UINode.make("div", {
classes: ["bool-box"],
subs: [
this.labelElement,
this.checkboxElement,
],
});
},[
this.labelElement,
this.checkboxElement,
]);
}
}

View File

@@ -0,0 +1,14 @@
input.editable-text-field-view {
width: fit-content;
max-width: 200px;
}
div.editable-text-field-view {
width: 100%;
transition: background-color 200ms;
cursor: pointer;
}
div.editable-text-field-view:hover {
background-color: var(--color-ui-neutral-dark-hover);
}

View File

@@ -0,0 +1,71 @@
import UINode, {UINodeOptions} from "@/ui/UINode";
import "./EditableTextFieldView.css";
export type EditableTextFieldViewOptions = UINodeOptions & {
initialText?: string,
setter?: (newString: string) => void,
noEmpty?: boolean,
};
export default class EditableTextFieldView extends UINode {
private text: string;
private titleInput!: HTMLInputElement;
private setter: (newString: string) => void;
private titleDisplay!: HTMLElement;
private noEmpty: boolean;
private lastNonEmptyInput = "";
constructor(options: EditableTextFieldViewOptions) {
super(options);
this.setter = options.setter ?? (() => {/* dummy */});
this.text = options.initialText ?? "";
this.noEmpty = options.noEmpty ?? false;
}
setText(newText: string): void {
if (newText !== "" || !this.noEmpty) {
this.text = newText;
this.titleInput.value = this.text;
this.titleDisplay.innerText = this.text;
}
}
build(): HTMLSpanElement {
this.titleInput = UINode.make("input", {
value: this.text,
classes: ["editable-text-field-view"],
type: "text",
oninput: (event: Event) => {
const input = (event.target as HTMLInputElement).value;
if (input === "") {
if (!this.noEmpty) {
this.setter(input);
}
} else {
this.setter(input);
this.lastNonEmptyInput = input;
}
},
onblur: (event: FocusEvent) => {
if ((event.target as HTMLInputElement).value === "") {
this.setText(this.lastNonEmptyInput);
}
this.titleInput.replaceWith(this.titleDisplay);
},
onkeyup: (event: KeyboardEvent) => {
if (event.key === "Enter") {
(event.target as HTMLInputElement).blur();
}
},
});
this.titleDisplay = UINode.make("div", {
innerText: this.text,
classes: ["editable-text-field-view"],
onclick: () => {
this.titleDisplay.replaceWith(this.titleInput);
this.titleInput.focus();
},
});
return this.titleDisplay;
}
}

View File

@@ -101,32 +101,31 @@ export default class NumberInputView extends UINode {
});
return UINode.make("div", {
classes: ["number-input"],
subs: [
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,
UINode.make("button", {
innerText: "+",
classes: ["number-input-button", "number-input-inc"],
onclick: () => {
if (this.onIncrement) {
this.onIncrement();
} else if (this.setter && this.getter) {
this.setter(this.getter() + 1);
}
},
}),
],
});
}, [
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,
UINode.make("button", {
innerText: "+",
classes: ["number-input-button", "number-input-inc"],
onclick: () => {
if (this.onIncrement) {
this.onIncrement();
} else if (this.setter && this.getter) {
this.setter(this.getter() + 1);
}
},
}),
]);
}
}