feat: improved event semantics, finally think i have a permanent solution to urls and file paths and dev servers....

This commit is contained in:
Daniel Ledda
2022-03-26 21:57:19 +01:00
parent 625b917949
commit 8056a5dc89
15 changed files with 308 additions and 263 deletions

View File

@@ -1,3 +1,4 @@
{ {
"development": true "development": true,
"baseUrl": "/drum-slayer"
} }

View File

@@ -7,9 +7,9 @@
<title>Drum Slayer</title> <title>Drum Slayer</title>
<link rel='icon' type='image/png' href='./favicon.png'> <link rel='icon' type='image/png' href='./favicon.png'>
<link rel='stylesheet' href='static/main.css'> <link rel='stylesheet' href='./static/main.css'>
<script defer src='static/bundle.js'></script> <script defer src='./static/bundle.js'></script>
</head> </head>
<body> <body>
<div id="app"></div> <div id="app"></div>

View File

@@ -1,4 +1,4 @@
import BeatUnit, {BeatUnitType} from "@/BeatUnit"; import BeatUnit from "@/BeatUnit";
import {IPublisher, Publisher} from "@/Publisher"; import {IPublisher, Publisher} from "@/Publisher";
import ISubscriber from "@/Subscriber"; import ISubscriber from "@/Subscriber";
import BeatLike from "@/BeatLike"; import BeatLike from "@/BeatLike";
@@ -16,13 +16,13 @@ export type BeatInitOptions = {
}; };
export const enum BeatEvents { export const enum BeatEvents {
NewTimeSig="BE0", NewTimeSig="be-0",
NewBarCount="BE1", NewBarCount="be-1",
NewName="BE2", NewName="be-2",
DisplayTypeChanged="BE3", DisplayTypeChanged="be-3",
LoopLengthChanged="BE4", LoopLengthChanged="be-4",
WantsRemoval="BE5", WantsRemoval="be-5",
Baked="BE6", Baked="be-6",
} }
export default class Beat implements IPublisher<BeatEvents>, BeatLike { export default class Beat implements IPublisher<BeatEvents>, BeatLike {
@@ -60,7 +60,7 @@ export default class Beat implements IPublisher<BeatEvents>, BeatLike {
this.publisher.notifySubs(BeatEvents.DisplayTypeChanged); this.publisher.notifySubs(BeatEvents.DisplayTypeChanged);
} }
addSubscriber(subscriber: ISubscriber, eventType: BeatEvents | BeatEvents[] | "all"): { unbind: () => void } { addSubscriber(subscriber: ISubscriber<BeatEvents>, eventType: BeatEvents | BeatEvents[]): { unbind: () => void } {
return this.publisher.addSubscriber(subscriber, eventType); return this.publisher.addSubscriber(subscriber, eventType);
} }

View File

@@ -14,17 +14,25 @@ type BeatGroupInitOptions = {
}; };
export const enum BeatGroupEvents { export const enum BeatGroupEvents {
BeatOrderChanged="BGE0", BeatOrderChanged="bge-0",
BeatListChanged="BGE1", BeatListChanged="bge-1",
BarCountChanged="BGE2", BarCountChanged="bge-2",
TimeSigUpChanged="BGE3", TimeSigUpChanged="bge-3",
AutoBeatSettingsChanged="BGE4", AutoBeatSettingsChanged="bge-4",
LockingChanged="BGE5", LockingChanged="bge-5",
} }
export default class BeatGroup implements IPublisher<BeatGroupEvents | BeatEvents>, BeatLike, ISubscriber { type EventTypeSubscriptions =
| BeatEvents.LoopLengthChanged
| BeatEvents.DisplayTypeChanged
| BeatEvents.WantsRemoval
| BeatEvents.Baked;
type EventTypePublications = BeatGroupEvents | BeatEvents;
export default class BeatGroup implements IPublisher<EventTypePublications>, BeatLike, ISubscriber<EventTypeSubscriptions> {
private beats: Beat[] = []; private beats: Beat[] = [];
private publisher: Publisher<BeatGroupEvents | BeatEvents, BeatGroup> = new Publisher<BeatGroupEvents | BeatEvents, BeatGroup>(this); private publisher: Publisher<EventTypePublications, BeatGroup> = new Publisher<EventTypePublications, BeatGroup>(this);
private barCount: number; private barCount: number;
private timeSigUp: number; private timeSigUp: number;
private globalLoopLength: number; private globalLoopLength: number;
@@ -45,7 +53,7 @@ export default class BeatGroup implements IPublisher<BeatGroupEvents | BeatEvent
this.useAutoBeatLength = options?.useAutoBeatLength ?? false; this.useAutoBeatLength = options?.useAutoBeatLength ?? false;
} }
notify<T extends string | number>(publisher: IPublisher<T>, event: "all" | T[] | T): void { notify(publisher: unknown, event: EventTypeSubscriptions): void {
switch (event) { switch (event) {
case BeatEvents.LoopLengthChanged: case BeatEvents.LoopLengthChanged:
case BeatEvents.DisplayTypeChanged: case BeatEvents.DisplayTypeChanged:
@@ -60,7 +68,7 @@ export default class BeatGroup implements IPublisher<BeatGroupEvents | BeatEvent
} }
} }
addSubscriber(subscriber: ISubscriber, eventType: "all" | BeatGroupEvents | BeatEvents | (BeatGroupEvents | BeatEvents)[]): { unbind: () => void } { addSubscriber(subscriber: ISubscriber<EventTypePublications>, eventType: SubscriptionEvent<EventTypePublications>): { unbind: () => void } {
return this.publisher.addSubscriber(subscriber, eventType); return this.publisher.addSubscriber(subscriber, eventType);
} }

View File

@@ -1,28 +1,27 @@
import {IPublisher, ISubscription, Publisher} from "./Publisher"; import {IPublisher, Publisher} from "./Publisher";
import ISubscriber from "./Subscriber"; import ISubscriber from "./Subscriber";
export enum BeatUnitType { export const enum BeatUnitType {
Normal, Normal="but-0",
GhostNote, GhostNote="but-1",
Accent, Accent="but-2",
}
export const enum BeatUnitEvent {
Toggle="bue-0",
On="bue-1",
Off="bue-2",
TypeChange="bue-3",
} }
export const enum BeatUnitEvents { export default class BeatUnit implements IPublisher<BeatUnitEvent> {
Toggle,
On,
Off,
TypeChange,
}
export default class BeatUnit implements IPublisher<BeatUnitEvents> {
private static readonly TypeRotation = [ private static readonly TypeRotation = [
BeatUnitType.Normal, BeatUnitType.Normal,
BeatUnitType.GhostNote, BeatUnitType.GhostNote,
BeatUnitType.Accent, BeatUnitType.Accent,
] as const; ] as const;
private publisher: Publisher<BeatUnitEvents, BeatUnit> = new Publisher<BeatUnitEvents, BeatUnit>(this); private publisher: Publisher<BeatUnitEvent, BeatUnit> = new Publisher<BeatUnitEvent, BeatUnit>(this);
private on = false; private on = false;
private typeIndex = 0; private typeIndex = 0;
@@ -31,28 +30,28 @@ export default class BeatUnit implements IPublisher<BeatUnitEvents> {
this.setType(type); this.setType(type);
} }
addSubscriber(subscriber: ISubscriber, eventType: "all" | BeatUnitEvents | BeatUnitEvents[]): ISubscription { addSubscriber(subscriber: ISubscriber<BeatUnitEvent>, eventType: BeatUnitEvent[]): { unbind: () => void } {
return this.publisher.addSubscriber(subscriber, eventType); return this.publisher.addSubscriber(subscriber, eventType);
} }
toggle(): void { toggle(): void {
this.on = !this.on; this.on = !this.on;
this.publisher.notifySubs(BeatUnitEvents.Toggle); this.publisher.notifySubs(BeatUnitEvent.Toggle);
if (this.on) { if (this.on) {
this.publisher.notifySubs(BeatUnitEvents.On); this.publisher.notifySubs(BeatUnitEvent.On);
} else { } else {
this.publisher.notifySubs(BeatUnitEvents.Off); this.publisher.notifySubs(BeatUnitEvent.Off);
} }
} }
setOn(on: boolean): void { setOn(on: boolean): void {
this.on = on; this.on = on;
this.publisher.notifySubs(this.on ? BeatUnitEvents.On : BeatUnitEvents.Off); this.publisher.notifySubs(this.on ? BeatUnitEvent.On : BeatUnitEvent.Off);
} }
setType(type: BeatUnitType): void { setType(type: BeatUnitType): void {
this.typeIndex = BeatUnit.TypeRotation.indexOf(type); this.typeIndex = BeatUnit.TypeRotation.indexOf(type);
this.publisher.notifySubs(BeatUnitEvents.TypeChange); this.publisher.notifySubs(BeatUnitEvent.TypeChange);
} }
getType(): BeatUnitType { getType(): BeatUnitType {
@@ -65,7 +64,7 @@ export default class BeatUnit implements IPublisher<BeatUnitEvents> {
} else { } else {
this.typeIndex += 1; this.typeIndex += 1;
} }
this.publisher.notifySubs(BeatUnitEvents.TypeChange); this.publisher.notifySubs(BeatUnitEvent.TypeChange);
} }
isOn(): boolean { isOn(): boolean {

View File

@@ -1,10 +1,10 @@
import ISubscriber from "./Subscriber"; import ISubscriber, {LEvent} from "./Subscriber";
class Subscription<T extends (string | number), P> implements ISubscription { class Subscription<EventType extends LEvent, PublisherType> implements ISubscription {
private subscriber: ISubscriber; private subscriber: ISubscriber<EventType>;
private readonly eventTypes: T[]; private readonly eventTypes: EventType[];
private publisher: Publisher<T, P>; private publisher: Publisher<EventType, PublisherType>;
constructor(publisher: Publisher<T, P>, subscriber: ISubscriber, eventTypes: T[]) { constructor(publisher: Publisher<EventType, PublisherType>, subscriber: ISubscriber<EventType>, eventTypes: EventType[]) {
this.subscriber = subscriber; this.subscriber = subscriber;
this.publisher = publisher; this.publisher = publisher;
this.eventTypes = eventTypes; this.eventTypes = eventTypes;
@@ -14,32 +14,36 @@ class Subscription<T extends (string | number), P> implements ISubscription {
this.publisher.unbind(this); this.publisher.unbind(this);
} }
getEventTypes(): T[] { getEventTypes(): EventType[] {
return this.eventTypes; return this.eventTypes;
} }
getSubscriber(): ISubscriber { getSubscriber(): ISubscriber<EventType> {
return this.subscriber; return this.subscriber;
} }
} }
interface EventSubscriberRecord<T extends LEvent> {
get<K extends T>(key: K): ISubscriber<K>[];
set<K extends T>(key: K, subscribers: ISubscriber<K>[]): EventSubscriberRecord<T>;
}
export class Publisher<T extends (string | number), P> implements IPublisher<T> {
private subscribers: Map<T | "all", ISubscriber[]>;
private parent: P;
constructor(parent: P) { export class Publisher<EventType extends LEvent, PublisherType> implements IPublisher<EventType> {
private subscribers: EventSubscriberRecord<EventType>;
private parent: PublisherType;
constructor(parent: PublisherType) {
this.parent = parent; this.parent = parent;
this.subscribers = new Map(); this.subscribers = new Map();
this.subscribers.set("all", []);
} }
addSubscriber(subscriber: ISubscriber, eventType: (T | "all") | T[]): ISubscription { addSubscriber(subscriber: ISubscriber<EventType>, subscribeTo: EventType | EventType[]): ISubscription {
let eventTypes: (T | "all")[] = []; let eventTypes: EventType[] = [];
if (!Array.isArray(eventType)) { if (!Array.isArray(subscribeTo)) {
eventTypes.push(eventType); eventTypes.push(subscribeTo);
} else { } else {
eventTypes = eventType as (T | "all")[]; eventTypes = subscribeTo;
} }
for (const key of eventTypes) { for (const key of eventTypes) {
this.getSubscribers(key).push(subscriber); this.getSubscribers(key).push(subscriber);
@@ -47,17 +51,17 @@ export class Publisher<T extends (string | number), P> implements IPublisher<T>
return new Subscription(this, subscriber, eventTypes); return new Subscription(this, subscriber, eventTypes);
} }
unbind(subscription: Subscription<T, P>): void { unbind(subscription: Subscription<EventType, PublisherType>): void {
for (const key of subscription.getEventTypes()) { for (const key of subscription.getEventTypes()) {
const subs = this.getSubscribers(key); const subs = this.getSubscribers(key);
subs.splice(subs.indexOf(subscription.getSubscriber()), 1); subs.splice(subs.indexOf(subscription.getSubscriber()), 1);
} }
} }
private getSubscribers(key: T | "all"): ISubscriber[] { private getSubscribers<K extends EventType>(key: K): ISubscriber<K>[] {
const subscribersList = this.subscribers.get(key); const subscribersList = this.subscribers.get(key);
if (subscribersList === undefined) { if (subscribersList === undefined) {
const newList: ISubscriber[] = []; const newList: ISubscriber<K>[] = [];
this.subscribers.set(key, newList); this.subscribers.set(key, newList);
return newList; return newList;
} else { } else {
@@ -65,18 +69,15 @@ export class Publisher<T extends (string | number), P> implements IPublisher<T>
} }
} }
notifySubs(eventType: T): void { notifySubs<K extends EventType>(eventType: K): void {
for (const sub of this.getSubscribers(eventType)) { for (const sub of this.getSubscribers(eventType)) {
sub.notify(this.parent, eventType); sub.notify(this.parent, eventType);
} }
for (const sub of this.getSubscribers("all")) {
sub.notify(this.parent, eventType);
}
} }
} }
export interface IPublisher<T extends string | number> { export interface IPublisher<T extends LEvent> {
addSubscriber(subscriber: ISubscriber, eventType: (T | "all") | T[]): {unbind: () => void}; addSubscriber(subscriber: ISubscriber<T>, subscribeTo: T | T[]): {unbind: () => void};
} }
export interface ISubscription { export interface ISubscription {

View File

@@ -1,3 +1,4 @@
export default interface ISubscriber { export type LEvent = string;
notify<T extends string | number>(publisher: unknown, event: T | "all" | T[]): void; export default interface ISubscriber<T extends LEvent> {
notify(publisher: unknown, event: T): void;
} }

View File

@@ -1,6 +1,5 @@
import UINode, {UINodeOptions} from "@/ui/UINode"; import UINode, {UINodeOptions} from "@/ui/UINode";
import Beat, {BeatEvents} from "@/Beat"; import Beat, {BeatEvents} from "@/Beat";
import {IPublisher} from "@/Publisher";
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";
@@ -9,7 +8,17 @@ export type BeatUINodeOptions = UINodeOptions & {
beat: Beat, beat: Beat,
}; };
export default class BeatView extends UINode implements ISubscriber { const EventTypeSubscriptions = [
BeatEvents.NewName,
BeatEvents.NewTimeSig,
BeatEvents.NewBarCount,
BeatEvents.DisplayTypeChanged,
BeatEvents.LoopLengthChanged,
];
type EventTypeSubscriptions = FlatArray<typeof EventTypeSubscriptions, 1>;
export default class BeatView extends UINode implements ISubscriber<EventTypeSubscriptions> {
private beat: Beat; private beat: Beat;
private title!: HTMLHeadingElement; private title!: HTMLHeadingElement;
private beatUnitViews: BeatUnitView[] = []; private beatUnitViews: BeatUnitView[] = [];
@@ -34,20 +43,20 @@ export default class BeatView extends UINode implements ISubscriber {
} }
private setupBindings() { private setupBindings() {
this.beat.addSubscriber(this, "all"); this.beat.addSubscriber(this, EventTypeSubscriptions);
} }
notify<T extends string | number>(publisher: IPublisher<T>, event: "all" | T[] | T): void { notify(publisher: unknown, event: EventTypeSubscriptions): void {
if (event === BeatEvents.NewName) { switch (event) {
case BeatEvents.NewName:
this.title.innerText = this.beat.getName(); this.title.innerText = this.beat.getName();
} else if (event === BeatEvents.NewTimeSig) { break;
this.setupBeatUnits(); case BeatEvents.NewTimeSig:
} else if (event === BeatEvents.NewBarCount) { case BeatEvents.NewBarCount:
this.setupBeatUnits(); case BeatEvents.DisplayTypeChanged:
} else if (event === BeatEvents.DisplayTypeChanged) { case BeatEvents.LoopLengthChanged:
this.setupBeatUnits();
} else if (event === BeatEvents.LoopLengthChanged) {
this.setupBeatUnits(); this.setupBeatUnits();
break;
} }
} }
@@ -91,8 +100,9 @@ export default class BeatView extends UINode implements ISubscriber {
} else { } else {
this.beatUnitViewBlock = UINode.make("div", { this.beatUnitViewBlock = UINode.make("div", {
classes: ["beat-unit-block"], classes: ["beat-unit-block"],
subs: [...beatUnitNodes], }, [
}); ...beatUnitNodes
]);
} }
} }
@@ -140,16 +150,14 @@ export default class BeatView extends UINode implements ISubscriber {
} }
return UINode.make("div", { return UINode.make("div", {
classes: ["beat"], classes: ["beat"],
subs: [ }, [
UINode.make("div", { UINode.make("div", {
classes: ["beat-main"], classes: ["beat-main"],
subs: [ }, [
this.title, this.title,
this.beatUnitViewBlock, this.beatUnitViewBlock,
] ]),
}), ]);
],
});
} }
} }

View File

@@ -3,14 +3,18 @@ import BeatGroup, {BeatGroupEvents} from "@/BeatGroup";
import BeatView from "@/ui/Beat/BeatView"; import BeatView from "@/ui/Beat/BeatView";
import "./BeatGroup.css"; import "./BeatGroup.css";
import ISubscriber from "@/Subscriber"; import ISubscriber from "@/Subscriber";
import {IPublisher} from "@/Publisher";
export type BeatGroupUINodeOptions = UINodeOptions & { export type BeatGroupUINodeOptions = UINodeOptions & {
title: string, title: string,
beatGroup: BeatGroup, beatGroup: BeatGroup,
}; };
export default class BeatGroupView extends UINode implements ISubscriber { const EventTypeSubscriptions = [
BeatGroupEvents.BeatListChanged
];
type EventTypeSubscriptions = FlatArray<typeof EventTypeSubscriptions, 1>;
export default class BeatGroupView extends UINode implements ISubscriber<EventTypeSubscriptions> {
private title: string; private title: string;
private beatGroup: BeatGroup; private beatGroup: BeatGroup;
private beatViews: BeatView[] = []; private beatViews: BeatView[] = [];
@@ -22,7 +26,7 @@ export default class BeatGroupView extends UINode implements ISubscriber {
this.beatGroup.addSubscriber(this, BeatGroupEvents.BeatListChanged); this.beatGroup.addSubscriber(this, BeatGroupEvents.BeatListChanged);
} }
notify<T extends string | number>(publisher: IPublisher<T>, event: "all" | T[] | T): void { notify(publisher: unknown, event: EventTypeSubscriptions): void {
if (event === BeatGroupEvents.BeatListChanged) { if (event === BeatGroupEvents.BeatListChanged) {
this.redraw(); this.redraw();
} }
@@ -41,9 +45,8 @@ export default class BeatGroupView extends UINode implements ISubscriber {
} }
return UINode.make("div", { return UINode.make("div", {
classes: ["beat-group"], classes: ["beat-group"],
subs: [ },[
...this.beatViews.map(bv => bv.render()) ...this.beatViews.map(bv => bv.render())
], ]);
});
} }
} }

View File

@@ -1,9 +1,8 @@
import "./BeatGroupSettings.css"; import "./BeatGroupSettings.css";
import UINode, {UINodeOptions} from "@/ui/UINode"; import UINode, {UINodeOptions} from "@/ui/UINode";
import NumberInputView from "@/ui/Widgets/NumberInput/NumberInputView"; import NumberInputView from "@/ui/Widgets/NumberInput/NumberInputView";
import ISubscriber from "@/Subscriber"; import ISubscriber, {SubscriptionEvent} from "@/Subscriber";
import BeatGroup, {BeatGroupEvents} from "@/BeatGroup"; import BeatGroup, {BeatGroupEvents} from "@/BeatGroup";
import {IPublisher} from "@/Publisher";
import {BeatEvents} from "@/Beat"; import {BeatEvents} from "@/Beat";
import BoolBoxView from "@/ui/Widgets/BoolBox/BoolBoxView"; import BoolBoxView from "@/ui/Widgets/BoolBox/BoolBoxView";
import BeatSettingsView from "@/ui/BeatSettings/BeatSettingsView"; import BeatSettingsView from "@/ui/BeatSettings/BeatSettingsView";
@@ -13,21 +12,22 @@ export type BeatGroupSettingsUINodeOptions = UINodeOptions & {
beatGroup: BeatGroup, beatGroup: BeatGroup,
}; };
export default class BeatGroupSettingsView extends UINode implements ISubscriber { const EventTypeSubscriptions = [
BeatGroupEvents.BarCountChanged,
BeatGroupEvents.TimeSigUpChanged,
BeatEvents.DisplayTypeChanged,
BeatGroupEvents.BeatListChanged,
BeatGroupEvents.LockingChanged,
BeatGroupEvents.AutoBeatSettingsChanged,
];
export default class BeatGroupSettingsView extends UINode implements ISubscriber<typeof EventTypeSubscriptions> {
private beatGroup: BeatGroup; private beatGroup: BeatGroup;
private barCountInput!: NumberInputView; private barCountInput!: NumberInputView;
private timeSigUpInput!: NumberInputView; private timeSigUpInput!: NumberInputView;
private autoBeatLengthCheckbox!: BoolBoxView; private autoBeatLengthCheckbox!: BoolBoxView;
private beatSettingsViews: BeatSettingsView[] = []; private beatSettingsViews: BeatSettingsView[] = [];
private beatSettingsContainer!: HTMLDivElement; private beatSettingsContainer!: HTMLDivElement;
private static readonly EventTypeSubscriptions = [
BeatGroupEvents.BarCountChanged,
BeatGroupEvents.TimeSigUpChanged,
BeatEvents.DisplayTypeChanged,
BeatGroupEvents.BeatListChanged,
BeatGroupEvents.LockingChanged,
BeatGroupEvents.AutoBeatSettingsChanged,
];
constructor(options: BeatGroupSettingsUINodeOptions) { constructor(options: BeatGroupSettingsUINodeOptions) {
super(options); super(options);
@@ -38,14 +38,14 @@ export default class BeatGroupSettingsView extends UINode implements ISubscriber
setBeatGroup(newBeatGroup: BeatGroup): void { setBeatGroup(newBeatGroup: BeatGroup): void {
this.beatGroup = newBeatGroup; this.beatGroup = newBeatGroup;
this.setupBindings(); this.setupBindings();
BeatGroupSettingsView.EventTypeSubscriptions.forEach(eventType => this.notify(null, eventType)); EventTypeSubscriptions.forEach(eventType => this.notify(null, eventType));
} }
setupBindings(): void { setupBindings(): void {
this.beatGroup.addSubscriber(this, BeatGroupSettingsView.EventTypeSubscriptions); this.beatGroup.addSubscriber(this, EventTypeSubscriptions);
} }
notify<T extends string | number>(publisher: IPublisher<T> | null, event: "all" | T[] | T): void { notify(publisher: unknown, event: SubscriptionEvent<typeof EventTypeSubscriptions>): void {
switch(event) { switch(event) {
case BeatGroupEvents.BarCountChanged: case BeatGroupEvents.BarCountChanged:
this.barCountInput.setValue(this.beatGroup.getBarCount()); this.barCountInput.setValue(this.beatGroup.getBarCount());
@@ -66,6 +66,8 @@ export default class BeatGroupSettingsView extends UINode implements ISubscriber
case BeatGroupEvents.AutoBeatSettingsChanged: case BeatGroupEvents.AutoBeatSettingsChanged:
this.autoBeatLengthCheckbox.setValue(this.beatGroup.autoBeatLengthOn()); this.autoBeatLengthCheckbox.setValue(this.beatGroup.autoBeatLengthOn());
break; break;
case BeatEvents.DisplayTypeChanged:
break;
} }
} }
@@ -109,36 +111,32 @@ export default class BeatGroupSettingsView extends UINode implements ISubscriber
this.remakeBeatSettingsViews(); this.remakeBeatSettingsViews();
return UINode.make("div", { return UINode.make("div", {
classes: ["beat-group-settings"], classes: ["beat-group-settings"],
subs: [ }, [
UINode.make("div", {
classes: ["beat-group-settings-options"],
}, [
UINode.make("div", { UINode.make("div", {
classes: ["beat-group-settings-options"], classes: ["beat-group-settings-boxes", "beat-group-settings-option"],
subs: [ }, [
UINode.make("div", { this.timeSigUpInput.render(),
classes: ["beat-group-settings-boxes", "beat-group-settings-option"], ]),
subs: [ UINode.make("div", {
this.timeSigUpInput.render(), classes: ["beat-group-settings-bar-count", "beat-group-settings-option"]
], ,
}), }, [
UINode.make("div", { this.barCountInput.render(),
classes: ["beat-group-settings-bar-count", "beat-group-settings-option"], ]),
subs: [ UINode.make("div", {
this.barCountInput.render(), classes: ["beat-group-settings-bar-count", "beat-group-settings-option"],
], }, [
}), this.autoBeatLengthCheckbox.render(),
UINode.make("div", { ]),
classes: ["beat-group-settings-bar-count", "beat-group-settings-option"], new ActionButtonView({
subs: [ label: "New Track",
this.autoBeatLengthCheckbox.render(), onClick: () => this.beatGroup.addBeat(),
], }).render(),
}), this.beatSettingsContainer,
new ActionButtonView({ ]),
label: "New Track", ]);
onClick: () => this.beatGroup.addBeat(),
}).render(),
this.beatSettingsContainer,
],
}),
],
});
} }
} }

View File

@@ -1,8 +1,8 @@
import "./BeatSettings.css"; import "./BeatSettings.css";
import Beat, {BeatEvents} from "@/Beat"; import Beat, {BeatEvents} from "@/Beat";
import UINode, {UINodeOptions} from "@/ui/UINode"; import UINode, {UINodeOptions} from "@/ui/UINode";
import ISubscriber from "@/Subscriber"; import ISubscriber, {SubscriptionEvent} from "@/Subscriber";
import {IPublisher, ISubscription} from "@/Publisher"; import {ISubscription} from "@/Publisher";
import NumberInputView from "@/ui/Widgets/NumberInput/NumberInputView"; import NumberInputView from "@/ui/Widgets/NumberInput/NumberInputView";
import BoolBoxView from "@/ui/Widgets/BoolBox/BoolBoxView"; import BoolBoxView from "@/ui/Widgets/BoolBox/BoolBoxView";
import ActionButtonView from "@/ui/Widgets/ActionButton/ActionButtonView"; import ActionButtonView from "@/ui/Widgets/ActionButton/ActionButtonView";
@@ -11,7 +11,13 @@ export type BeatSettingsViewUINodeOptions = UINodeOptions & {
beat: Beat, beat: Beat,
}; };
export default class BeatSettingsView extends UINode implements ISubscriber { const EventTypeSubscriptions = [
BeatEvents.NewName,
BeatEvents.LoopLengthChanged,
BeatEvents.DisplayTypeChanged,
];
export default class BeatSettingsView extends UINode implements ISubscriber<typeof EventTypeSubscriptions> {
private beat: Beat; private beat: Beat;
private loopLengthInput!: NumberInputView; private loopLengthInput!: NumberInputView;
private bakeButton!: ActionButtonView; private bakeButton!: ActionButtonView;
@@ -37,18 +43,19 @@ export default class BeatSettingsView extends UINode implements ISubscriber {
this.sub.unbind(); this.sub.unbind();
this.beat = beat; this.beat = beat;
this.setupBindings(); this.setupBindings();
this.notify(null, BeatEvents.NewName); EventTypeSubscriptions.forEach(eventType => this.notify(null, eventType));
this.notify(null, BeatEvents.LoopLengthChanged);
this.notify(null, BeatEvents.DisplayTypeChanged);
} }
notify<T extends string | number>(publisher: IPublisher<T> | null, event: "all" | T[] | T): void { notify(publisher: unknown, event: SubscriptionEvent<typeof EventTypeSubscriptions>): void {
if (event === BeatEvents.NewName) { switch(event) {
case BeatEvents.NewName:
this.titleInput.value = this.beat.getName(); this.titleInput.value = this.beat.getName();
this.titleDisplay.innerText = this.beat.getName(); this.titleDisplay.innerText = this.beat.getName();
} else if (event === BeatEvents.LoopLengthChanged) { break;
case BeatEvents.LoopLengthChanged:
this.loopLengthInput.setValue(this.beat.getLoopLength()); this.loopLengthInput.setValue(this.beat.getLoopLength());
} else if (event === BeatEvents.DisplayTypeChanged) { break;
case BeatEvents.DisplayTypeChanged:
this.loopCheckbox.setValue(this.beat.isLooping()); this.loopCheckbox.setValue(this.beat.isLooping());
this.bakeButton.setDisabled(!this.beat.isLooping()); this.bakeButton.setDisabled(!this.beat.isLooping());
if (this.beat.isLooping()) { if (this.beat.isLooping()) {
@@ -56,6 +63,7 @@ export default class BeatSettingsView extends UINode implements ISubscriber {
} else { } else {
this.loopLengthSection.classList.add("hide"); this.loopLengthSection.classList.add("hide");
} }
break;
} }
} }
@@ -102,10 +110,9 @@ export default class BeatSettingsView extends UINode implements ISubscriber {
}); });
this.loopLengthSection = UINode.make("div", { this.loopLengthSection = UINode.make("div", {
classes: ["loop-settings-option"], classes: ["loop-settings-option"],
subs: [ }, [
this.loopLengthInput.render(), this.loopLengthInput.render(),
], ]);
});
if (this.beat.isLooping()) { if (this.beat.isLooping()) {
this.loopLengthSection.classList.remove("hide"); this.loopLengthSection.classList.remove("hide");
} else { } else {
@@ -113,28 +120,25 @@ export default class BeatSettingsView extends UINode implements ISubscriber {
} }
return UINode.make("div", { return UINode.make("div", {
classes: ["beat-settings"], classes: ["beat-settings"],
subs: [ }, [
this.titleDisplay, this.titleDisplay,
UINode.make("div", {
classes: ["beat-settings-lower"],
}, [
this.bakeButton.render(),
new ActionButtonView({
icon: "trash",
type: "secondary",
alt: "Delete Track",
onClick: () => this.beat.delete(),
}).render(),
UINode.make("div", { UINode.make("div", {
classes: ["beat-settings-lower"], classes: ["loop-settings"],
subs: [ }, [
this.bakeButton.render(), this.loopCheckbox.render(),
new ActionButtonView({ ]),
icon: "trash", this.loopLengthSection,
type: "secondary", ]),
alt: "Delete Track", ]);
onClick: () => this.beat.delete(),
}).render(),
UINode.make("div", {
classes: ["loop-settings"],
subs: [
this.loopCheckbox.render(),
]
}),
this.loopLengthSection,
]
}),
],
});
} }
} }

View File

@@ -1,4 +1,4 @@
import BeatUnit, {BeatUnitEvents, 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, {UINodeOptions} from "@/ui/UINode";
import {IPublisher, ISubscription, Publisher} from "@/Publisher"; import {IPublisher, ISubscription, Publisher} from "@/Publisher";
@@ -8,10 +8,17 @@ export type BeatUnitUINodeOptions = UINodeOptions & {
beatUnit: BeatUnit, beatUnit: BeatUnit,
}; };
export default class BeatUnitView extends UINode implements ISubscriber { const EventTypeSubscriptions = [
BeatUnitEvent.On,
BeatUnitEvent.Off,
BeatUnitEvent.TypeChange,
];
type EventTypeSubscriptions = FlatArray<typeof EventTypeSubscriptions, 1>;
export default class BeatUnitView extends UINode implements ISubscriber<EventTypeSubscriptions> {
private beatUnit: BeatUnit; private beatUnit: BeatUnit;
private subscription: ISubscription | null = null; private subscription: ISubscription | null = null;
private publisher: IPublisher<BeatUnitEvents> = new Publisher<BeatUnitEvents, BeatUnitView>(this); private publisher: IPublisher<BeatUnitEvent> = new Publisher<BeatUnitEvent, BeatUnitView>(this);
private touchTimeout: ReturnType<typeof setTimeout> | null = null; private touchTimeout: ReturnType<typeof setTimeout> | null = null;
@@ -24,13 +31,13 @@ export default class BeatUnitView extends UINode implements ISubscriber {
setUnit(beatUnit: BeatUnit): void { setUnit(beatUnit: BeatUnit): void {
this.beatUnit = beatUnit; this.beatUnit = beatUnit;
this.setupBindings(); this.setupBindings();
this.notify(this.publisher, beatUnit.isOn() ? BeatUnitEvents.On : BeatUnitEvents.Off); this.notify(this.publisher, beatUnit.isOn() ? BeatUnitEvent.On : BeatUnitEvent.Off);
this.notify(this.publisher, BeatUnitEvents.TypeChange); this.notify(this.publisher, BeatUnitEvent.TypeChange);
} }
private setupBindings() { private setupBindings() {
this.subscription?.unbind(); this.subscription?.unbind();
this.subscription = this.beatUnit.addSubscriber(this, "all"); this.subscription = this.beatUnit.addSubscriber(this, EventTypeSubscriptions);
this.onMouseDown((ev: MouseEvent) => { this.onMouseDown((ev: MouseEvent) => {
if (ev.button === 1) { if (ev.button === 1) {
this.beatUnit.rotateType(); this.beatUnit.rotateType();
@@ -40,7 +47,7 @@ export default class BeatUnitView extends UINode implements ISubscriber {
this.touchTimeout = setTimeout(() => { this.touchTimeout = setTimeout(() => {
this.beatUnit.rotateType(); this.beatUnit.rotateType();
this.touchTimeout = null; this.touchTimeout = null;
}, 600); }, 400);
}); });
this.getNode().addEventListener("touchend", () => { this.getNode().addEventListener("touchend", () => {
if (this.touchTimeout) { if (this.touchTimeout) {
@@ -62,12 +69,15 @@ export default class BeatUnitView extends UINode implements ISubscriber {
this.beatUnit.setOn(false); this.beatUnit.setOn(false);
} }
notify<T extends string | number>(publisher: IPublisher<T>, event: "all" | T[] | T): void { notify(publisher: unknown, event: EventTypeSubscriptions): void {
if (event === BeatUnitEvents.On) { switch (event) {
case BeatUnitEvent.On:
this.getNode().classList.add("beat-unit-on"); this.getNode().classList.add("beat-unit-on");
} else if (event === BeatUnitEvents.Off) { break;
case BeatUnitEvent.Off:
this.getNode().classList.remove("beat-unit-on"); this.getNode().classList.remove("beat-unit-on");
} else if (event === BeatUnitEvents.TypeChange) { break;
case BeatUnitEvent.TypeChange:
switch (this.beatUnit.getType()) { switch (this.beatUnit.getType()) {
case BeatUnitType.Normal: case BeatUnitType.Normal:
this.getNode().classList.remove("beat-unit-ghost"); this.getNode().classList.remove("beat-unit-ghost");
@@ -82,6 +92,7 @@ export default class BeatUnitView extends UINode implements ISubscriber {
this.getNode().classList.add("beat-unit-accent"); this.getNode().classList.add("beat-unit-accent");
break; break;
} }
break;
} }
} }

View File

@@ -15,13 +15,13 @@ export default class RootView extends UINode {
private beatGroupView: BeatGroupView; private beatGroupView: BeatGroupView;
private mainBeatGroup: BeatGroup; private mainBeatGroup: BeatGroup;
private beatGroupSettingsView!: BeatGroupSettingsView; private beatGroupSettingsView!: BeatGroupSettingsView;
private sidebar!: HTMLDivElement;
constructor(options: RootUINodeOptions) { constructor(options: RootUINodeOptions) {
super(options); super(options);
this.mainBeatGroup = options.mainBeatGroup ?? RootView.defaultMainBeatGroup(); this.mainBeatGroup = options.mainBeatGroup ?? RootView.defaultMainBeatGroup();
this.beatGroupView = new BeatGroupView({title: options.title, beatGroup: this.mainBeatGroup}); this.beatGroupView = new BeatGroupView({title: options.title, beatGroup: this.mainBeatGroup});
this.beatGroupSettingsView = new BeatGroupSettingsView({beatGroup: this.mainBeatGroup});
this.title = options.title; this.title = options.title;
} }
@@ -47,68 +47,77 @@ export default class RootView extends UINode {
this.getNode().classList.toggle("vertical-mode"); this.getNode().classList.toggle("vertical-mode");
} }
build(): HTMLElement { private buildSidebarStrip(): HTMLElement {
this.beatGroupSettingsView = new BeatGroupSettingsView({beatGroup: this.mainBeatGroup});
const sidebarMain = UINode.make("div", {
classes: ["root-settings"],
subs: [
UINode.make("h1", {innerText: this.title, classes: ["root-title"]}),
this.beatGroupSettingsView.render(),
]
});
const sidebarStrip = UINode.make("div", {
classes: ["root-sidebar-toggle"],
subs: [
UINode.make("div", {
classes: ["root-quick-access-button"],
subs: [new IconView({iconName: "list", color: "var(--color-ui-neutral-dark)"}).render()],
onclick: () => this.toggleSidebar(),
}),
UINode.make("div", {
classes: ["root-quick-access-button"],
subs: [new IconView({iconName: "arrowClockwise", color: "var(--color-ui-neutral-dark)"}).render()],
onclick: () => this.toggleOrientation(),
}),
UINode.make("div", {
classes: ["root-quick-access-button"],
subs: [new IconView({iconName: "snowflake", color: "var(--color-ui-neutral-dark)"}).render()],
onclick: () => this.mainBeatGroup.bakeLoops(),
}),
UINode.make("div", {
classes: ["root-quick-access-button"],
title: "Reset all",
subs: [new IconView({iconName: "trash", color: "var(--color-ui-neutral-dark)"}).render()],
onclick: () => {
this.mainBeatGroup = RootView.defaultMainBeatGroup();
this.beatGroupSettingsView.setBeatGroup(this.mainBeatGroup);
this.beatGroupView.setBeatGroup(this.mainBeatGroup);
},
}),
]
});
this.sidebar = UINode.make("div", {
classes: ["root-sidebar"],
subs: [
sidebarMain,
sidebarStrip,
]
});
return UINode.make("div", { return UINode.make("div", {
classes: ["root", "sidebar-visible"], classes: ["root-sidebar-toggle"],
subs: [ }, [
this.sidebar, UINode.make("div", {
UINode.make("div", { classes: ["root-quick-access-button"],
classes: ["root-beat-stage-container"], onclick: () => this.toggleSidebar(),
subs: [ }, [
UINode.make("div", { new IconView({
classes: ["root-beat-stage"], iconName: "list",
subs: [ color: "var(--color-ui-neutral-dark)"
this.beatGroupView.render(), }).render()
], ]),
}) UINode.make("div", {
] classes: ["root-quick-access-button"],
}) onclick: () => this.toggleOrientation(),
], }, [
}); new IconView({
iconName: "arrowClockwise",
color: "var(--color-ui-neutral-dark)"
}).render(),
]),
UINode.make("div", {
classes: ["root-quick-access-button"],
onclick: () => this.mainBeatGroup.bakeLoops(),
}, [
new IconView({
iconName: "snowflake",
color: "var(--color-ui-neutral-dark)"
}).render(),
]),
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);
},
}, [
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(),
]),
this.buildSidebarStrip(),
]),
])
);
}
build(): HTMLElement {
return (
UINode.make("div", {classes: ["root", "sidebar-visible"]}, [
this.buildSidebar(),
UINode.make("div", {classes: ["root-beat-stage-container"]}, [
UINode.make("div", {classes: ["root-beat-stage"]}, [
this.beatGroupView.render(),
])
])
])
);
} }
} }

View File

@@ -50,20 +50,22 @@ export default abstract class UINode {
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>,
subElements?: HTMLElement[],
): 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 === "subs") {
element.append(...attributes.subs!);
} else { } else {
element[key as keyof HTMLElementTagNameMap[T]] = (attributes as any)[key]; element[key as keyof HTMLElementTagNameMap[T]] = (attributes as any)[key];
} }
} }
} }
if (subElements) {
element.append(...subElements);
}
return element; return element;
} }

View File

@@ -50,14 +50,14 @@ const webpackConfig = {
output: { output: {
filename: "bundle.js", filename: "bundle.js",
publicPath: "./static/", publicPath: `${config.baseUrl}/static/`,
path: path.resolve(__dirname, "./public/static"), path: path.resolve(__dirname, "./public/static"),
}, },
devServer: { devServer: {
static: { static: {
directory: path.join(__dirname, "./public"), directory: path.join(__dirname, "./public"),
publicPath: "/", publicPath: `${config.baseUrl}/`,
}, },
hot: true, hot: true,
compress: true, compress: true,