feat: stuff i'm tired
This commit is contained in:
@@ -1,7 +1,6 @@
|
|||||||
import Beat, {BeatEvents, BeatInitOptions} from "@/Beat";
|
import Beat, {BeatEvents, BeatInitOptions} from "@/Beat";
|
||||||
import {IPublisher, Publisher} from "@/Publisher";
|
import {IPublisher, Publisher} from "@/Publisher";
|
||||||
import ISubscriber from "@/Subscriber";
|
import ISubscriber from "@/Subscriber";
|
||||||
import BeatLike from "@/BeatLike";
|
|
||||||
import {greatestCommonDivisor, isPosInt} from "@/utils";
|
import {greatestCommonDivisor, isPosInt} from "@/utils";
|
||||||
|
|
||||||
type BeatGroupInitOptions = {
|
type BeatGroupInitOptions = {
|
||||||
@@ -11,6 +10,7 @@ type BeatGroupInitOptions = {
|
|||||||
beats?: BeatInitOptions[],
|
beats?: BeatInitOptions[],
|
||||||
loopLength?: number,
|
loopLength?: number,
|
||||||
useAutoBeatLength?: boolean,
|
useAutoBeatLength?: boolean,
|
||||||
|
name?: string,
|
||||||
};
|
};
|
||||||
|
|
||||||
export const enum BeatGroupEvents {
|
export const enum BeatGroupEvents {
|
||||||
@@ -20,6 +20,9 @@ export const enum BeatGroupEvents {
|
|||||||
TimeSigUpChanged="bge-3",
|
TimeSigUpChanged="bge-3",
|
||||||
AutoBeatSettingsChanged="bge-4",
|
AutoBeatSettingsChanged="bge-4",
|
||||||
LockingChanged="bge-5",
|
LockingChanged="bge-5",
|
||||||
|
GlobalLoopLengthChanged="bge-5",
|
||||||
|
GlobalDisplayTypeChanged="bge-6",
|
||||||
|
NameChanged="bge-7",
|
||||||
}
|
}
|
||||||
|
|
||||||
type EventTypeSubscriptions =
|
type EventTypeSubscriptions =
|
||||||
@@ -28,19 +31,25 @@ type EventTypeSubscriptions =
|
|||||||
| BeatEvents.WantsRemoval
|
| BeatEvents.WantsRemoval
|
||||||
| BeatEvents.Baked;
|
| BeatEvents.Baked;
|
||||||
|
|
||||||
type EventTypePublications = BeatGroupEvents | BeatEvents;
|
export default class BeatGroup implements IPublisher<BeatGroupEvents>, ISubscriber<EventTypeSubscriptions> {
|
||||||
|
private static globalCounter = 0;
|
||||||
export default class BeatGroup implements IPublisher<EventTypePublications>, BeatLike, ISubscriber<EventTypeSubscriptions> {
|
|
||||||
private beats: Beat[] = [];
|
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 barCount: number;
|
||||||
private timeSigUp: number;
|
private timeSigUp: number;
|
||||||
private globalLoopLength: number;
|
private globalLoopLength: number;
|
||||||
private globalIsLooping: boolean;
|
private globalIsLooping: boolean;
|
||||||
private useAutoBeatLength: boolean;
|
private useAutoBeatLength: boolean;
|
||||||
private barSettingsLocked = false;
|
private barSettingsLocked = false;
|
||||||
|
private name: string;
|
||||||
|
|
||||||
constructor(options?: BeatGroupInitOptions) {
|
constructor(options?: BeatGroupInitOptions) {
|
||||||
|
BeatGroup.globalCounter++;
|
||||||
|
if (options?.name) {
|
||||||
|
this.name = options.name;
|
||||||
|
} else {
|
||||||
|
this.name = `Pattern ${BeatGroup.globalCounter}`;
|
||||||
|
}
|
||||||
if (options?.beats) {
|
if (options?.beats) {
|
||||||
for (const beatOptions of options.beats) {
|
for (const beatOptions of options.beats) {
|
||||||
this.addBeat(beatOptions);
|
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);
|
return this.publisher.addSubscriber(subscriber, eventType);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -103,7 +112,7 @@ export default class BeatGroup implements IPublisher<EventTypePublications>, Bea
|
|||||||
for (const beat of this.beats) {
|
for (const beat of this.beats) {
|
||||||
beat.setLoopLength(loopLength);
|
beat.setLoopLength(loopLength);
|
||||||
}
|
}
|
||||||
this.publisher.notifySubs(BeatEvents.LoopLengthChanged);
|
this.publisher.notifySubs(BeatGroupEvents.GlobalLoopLengthChanged);
|
||||||
}
|
}
|
||||||
|
|
||||||
getLoopLength(): number {
|
getLoopLength(): number {
|
||||||
@@ -115,7 +124,7 @@ export default class BeatGroup implements IPublisher<EventTypePublications>, Bea
|
|||||||
for (const beat of this.beats) {
|
for (const beat of this.beats) {
|
||||||
beat.setLooping(isLooping);
|
beat.setLooping(isLooping);
|
||||||
}
|
}
|
||||||
this.publisher.notifySubs(BeatEvents.DisplayTypeChanged);
|
this.publisher.notifySubs(BeatGroupEvents.GlobalDisplayTypeChanged);
|
||||||
}
|
}
|
||||||
|
|
||||||
isLooping(): boolean {
|
isLooping(): boolean {
|
||||||
@@ -238,6 +247,7 @@ export default class BeatGroup implements IPublisher<EventTypePublications>, Bea
|
|||||||
const beat = this.getBeatByKey(beatKey);
|
const beat = this.getBeatByKey(beatKey);
|
||||||
this.beats.splice(this.beats.indexOf(beat), 1);
|
this.beats.splice(this.beats.indexOf(beat), 1);
|
||||||
this.autoBeatLength();
|
this.autoBeatLength();
|
||||||
|
console.log("removing");
|
||||||
this.publisher.notifySubs(BeatGroupEvents.BeatListChanged);
|
this.publisher.notifySubs(BeatGroupEvents.BeatListChanged);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -284,4 +294,13 @@ export default class BeatGroup implements IPublisher<EventTypePublications>, Bea
|
|||||||
bakeLoops(): void {
|
bakeLoops(): void {
|
||||||
this.beats.forEach(beat => beat.bakeLoops());
|
this.beats.forEach(beat => beat.bakeLoops());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setName(newName: string): void {
|
||||||
|
this.name = newName;
|
||||||
|
this.publisher.notifySubs(BeatGroupEvents.NameChanged);
|
||||||
|
}
|
||||||
|
|
||||||
|
getName(): string {
|
||||||
|
return this.name;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ const appNode = document.querySelector("#app");
|
|||||||
if (appNode) {
|
if (appNode) {
|
||||||
try {
|
try {
|
||||||
const appRoot = new RootView({
|
const appRoot = new RootView({
|
||||||
|
orientation: "vertical",
|
||||||
title: "Drum Slayer",
|
title: "Drum Slayer",
|
||||||
});
|
});
|
||||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||||
|
|||||||
@@ -3,10 +3,12 @@ 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 {ISubscription} from "@/Publisher";
|
||||||
|
|
||||||
export type BeatGroupUINodeOptions = UINodeOptions & {
|
export type BeatGroupUINodeOptions = UINodeOptions & {
|
||||||
title: string,
|
title: string,
|
||||||
beatGroup: BeatGroup,
|
beatGroup: BeatGroup,
|
||||||
|
orientation?: "horizontal" | "vertical",
|
||||||
};
|
};
|
||||||
|
|
||||||
const EventTypeSubscriptions = [
|
const EventTypeSubscriptions = [
|
||||||
@@ -18,31 +20,56 @@ export default class BeatGroupView extends UINode implements ISubscriber<EventTy
|
|||||||
private title: string;
|
private title: string;
|
||||||
private beatGroup: BeatGroup;
|
private beatGroup: BeatGroup;
|
||||||
private beatViews: BeatView[] = [];
|
private beatViews: BeatView[] = [];
|
||||||
|
private currentOrientation: "vertical" | "horizontal";
|
||||||
|
private subscription: ISubscription;
|
||||||
|
|
||||||
constructor(options: BeatGroupUINodeOptions) {
|
constructor(options: BeatGroupUINodeOptions) {
|
||||||
super(options);
|
super(options);
|
||||||
this.beatGroup = options.beatGroup;
|
this.beatGroup = options.beatGroup;
|
||||||
this.title = options.title;
|
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 {
|
notify(publisher: unknown, event: EventTypeSubscriptions): void {
|
||||||
if (event === BeatGroupEvents.BeatListChanged) {
|
if (event === BeatGroupEvents.BeatListChanged) {
|
||||||
|
this.setupBeatViews();
|
||||||
this.redraw();
|
this.redraw();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
setBeatGroup(newBeatGroup: BeatGroup): void {
|
private setupBeatViews(): void {
|
||||||
this.beatGroup = newBeatGroup;
|
|
||||||
this.beatGroup.addSubscriber(this, BeatGroupEvents.BeatListChanged);
|
|
||||||
this.redraw();
|
|
||||||
}
|
|
||||||
|
|
||||||
build(): HTMLDivElement {
|
|
||||||
this.beatViews = [];
|
this.beatViews = [];
|
||||||
for (let i = 0; i < this.beatGroup.getBeatCount(); i++) {
|
for (let i = 0; i < this.beatGroup.getBeatCount(); i++) {
|
||||||
this.beatViews.push(new BeatView({beat: this.beatGroup.getBeatByIndex(i)}));
|
this.beatViews.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", {
|
return UINode.make("div", {
|
||||||
classes: ["beat-group"],
|
classes: ["beat-group"],
|
||||||
},[
|
},[
|
||||||
|
|||||||
@@ -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, {SubscriptionEvent} from "@/Subscriber";
|
import ISubscriber from "@/Subscriber";
|
||||||
import BeatGroup, {BeatGroupEvents} from "@/BeatGroup";
|
import BeatGroup, {BeatGroupEvents} from "@/BeatGroup";
|
||||||
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";
|
||||||
import ActionButtonView from "@/ui/Widgets/ActionButton/ActionButtonView";
|
import ActionButtonView from "@/ui/Widgets/ActionButton/ActionButtonView";
|
||||||
@@ -15,13 +14,14 @@ export type BeatGroupSettingsUINodeOptions = UINodeOptions & {
|
|||||||
const EventTypeSubscriptions = [
|
const EventTypeSubscriptions = [
|
||||||
BeatGroupEvents.BarCountChanged,
|
BeatGroupEvents.BarCountChanged,
|
||||||
BeatGroupEvents.TimeSigUpChanged,
|
BeatGroupEvents.TimeSigUpChanged,
|
||||||
BeatEvents.DisplayTypeChanged,
|
BeatGroupEvents.GlobalDisplayTypeChanged,
|
||||||
BeatGroupEvents.BeatListChanged,
|
BeatGroupEvents.BeatListChanged,
|
||||||
BeatGroupEvents.LockingChanged,
|
BeatGroupEvents.LockingChanged,
|
||||||
BeatGroupEvents.AutoBeatSettingsChanged,
|
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 beatGroup: BeatGroup;
|
||||||
private barCountInput!: NumberInputView;
|
private barCountInput!: NumberInputView;
|
||||||
private timeSigUpInput!: NumberInputView;
|
private timeSigUpInput!: NumberInputView;
|
||||||
@@ -45,7 +45,7 @@ export default class BeatGroupSettingsView extends UINode implements ISubscriber
|
|||||||
this.beatGroup.addSubscriber(this, EventTypeSubscriptions);
|
this.beatGroup.addSubscriber(this, EventTypeSubscriptions);
|
||||||
}
|
}
|
||||||
|
|
||||||
notify(publisher: unknown, event: SubscriptionEvent<typeof EventTypeSubscriptions>): void {
|
notify(publisher: unknown, event: 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,7 +66,7 @@ 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:
|
case BeatGroupEvents.GlobalDisplayTypeChanged:
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -82,9 +82,7 @@ export default class BeatGroupSettingsView extends UINode implements ISubscriber
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (!this.beatSettingsContainer) {
|
if (!this.beatSettingsContainer) {
|
||||||
this.beatSettingsContainer = UINode.make("div", {
|
this.beatSettingsContainer = UINode.make("div", {}, this.beatSettingsViews.map(view => view.render()));
|
||||||
subs: this.beatSettingsViews.map(view => view.render())
|
|
||||||
});
|
|
||||||
} else {
|
} else {
|
||||||
this.beatSettingsContainer.replaceChildren(...this.beatSettingsViews.map(view => view.render()));
|
this.beatSettingsContainer.replaceChildren(...this.beatSettingsViews.map(view => view.render()));
|
||||||
}
|
}
|
||||||
@@ -118,18 +116,18 @@ export default class BeatGroupSettingsView extends UINode implements ISubscriber
|
|||||||
UINode.make("div", {
|
UINode.make("div", {
|
||||||
classes: ["beat-group-settings-boxes", "beat-group-settings-option"],
|
classes: ["beat-group-settings-boxes", "beat-group-settings-option"],
|
||||||
}, [
|
}, [
|
||||||
this.timeSigUpInput.render(),
|
this.timeSigUpInput,
|
||||||
]),
|
]),
|
||||||
UINode.make("div", {
|
UINode.make("div", {
|
||||||
classes: ["beat-group-settings-bar-count", "beat-group-settings-option"]
|
classes: ["beat-group-settings-bar-count", "beat-group-settings-option"]
|
||||||
,
|
,
|
||||||
}, [
|
}, [
|
||||||
this.barCountInput.render(),
|
this.barCountInput,
|
||||||
]),
|
]),
|
||||||
UINode.make("div", {
|
UINode.make("div", {
|
||||||
classes: ["beat-group-settings-bar-count", "beat-group-settings-option"],
|
classes: ["beat-group-settings-bar-count", "beat-group-settings-option"],
|
||||||
}, [
|
}, [
|
||||||
this.autoBeatLengthCheckbox.render(),
|
this.autoBeatLengthCheckbox,
|
||||||
]),
|
]),
|
||||||
new ActionButtonView({
|
new ActionButtonView({
|
||||||
label: "New Track",
|
label: "New Track",
|
||||||
|
|||||||
@@ -2,12 +2,16 @@
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.beat-settings-title-input {
|
.beat-settings-title-container {
|
||||||
width: 100%;
|
|
||||||
|
}
|
||||||
|
|
||||||
|
.beat-settings-title-container input {
|
||||||
|
min-width: 100%;
|
||||||
height: 2em;
|
height: 2em;
|
||||||
}
|
}
|
||||||
|
|
||||||
.beat-settings-title {
|
.beat-settings-title-container > div {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
padding: 0.5em;
|
padding: 0.5em;
|
||||||
@@ -15,7 +19,7 @@
|
|||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
.beat-settings-title:hover {
|
.beat-settings-title-container > div:hover {
|
||||||
background-color: var(--color-ui-neutral-dark-hover);
|
background-color: var(--color-ui-neutral-dark-hover);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,11 +1,12 @@
|
|||||||
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, {SubscriptionEvent} from "@/Subscriber";
|
import ISubscriber from "@/Subscriber";
|
||||||
import {ISubscription} from "@/Publisher";
|
import {ISubscription} from "@/Publisher";
|
||||||
import NumberInputView from "@/ui/Widgets/NumberInput/NumberInputView";
|
import NumberInputView from "@/ui/Widgets/NumberInput/NumberInputView";
|
||||||
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";
|
||||||
|
import EditableTextFieldView from "@/ui/Widgets/EditableTextFIeld/EditableTextFieldView";
|
||||||
|
|
||||||
export type BeatSettingsViewUINodeOptions = UINodeOptions & {
|
export type BeatSettingsViewUINodeOptions = UINodeOptions & {
|
||||||
beat: Beat,
|
beat: Beat,
|
||||||
@@ -16,16 +17,16 @@ const EventTypeSubscriptions = [
|
|||||||
BeatEvents.LoopLengthChanged,
|
BeatEvents.LoopLengthChanged,
|
||||||
BeatEvents.DisplayTypeChanged,
|
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 beat: Beat;
|
||||||
private loopLengthInput!: NumberInputView;
|
private loopLengthInput!: NumberInputView;
|
||||||
private bakeButton!: ActionButtonView;
|
private bakeButton!: ActionButtonView;
|
||||||
private loopCheckbox!: BoolBoxView;
|
private loopCheckbox!: BoolBoxView;
|
||||||
private loopLengthSection!: HTMLDivElement;
|
private loopLengthSection!: HTMLDivElement;
|
||||||
private sub!: ISubscription;
|
private sub!: ISubscription;
|
||||||
private titleInput!: HTMLInputElement;
|
private title!: EditableTextFieldView;
|
||||||
private titleDisplay!: HTMLSpanElement;
|
|
||||||
private editingTitle: boolean;
|
private editingTitle: boolean;
|
||||||
|
|
||||||
constructor(options: BeatSettingsViewUINodeOptions) {
|
constructor(options: BeatSettingsViewUINodeOptions) {
|
||||||
@@ -36,7 +37,7 @@ export default class BeatSettingsView extends UINode implements ISubscriber<type
|
|||||||
}
|
}
|
||||||
|
|
||||||
private setupBindings() {
|
private setupBindings() {
|
||||||
this.sub = this.beat.addSubscriber(this, "all");
|
this.sub = this.beat.addSubscriber(this, EventTypeSubscriptions);
|
||||||
}
|
}
|
||||||
|
|
||||||
setBeat(beat: Beat): void {
|
setBeat(beat: Beat): void {
|
||||||
@@ -46,11 +47,10 @@ export default class BeatSettingsView extends UINode implements ISubscriber<type
|
|||||||
EventTypeSubscriptions.forEach(eventType => this.notify(null, eventType));
|
EventTypeSubscriptions.forEach(eventType => this.notify(null, eventType));
|
||||||
}
|
}
|
||||||
|
|
||||||
notify(publisher: unknown, event: SubscriptionEvent<typeof EventTypeSubscriptions>): void {
|
notify(publisher: unknown, event: EventTypeSubscriptions): void {
|
||||||
switch(event) {
|
switch(event) {
|
||||||
case BeatEvents.NewName:
|
case BeatEvents.NewName:
|
||||||
this.titleInput.value = this.beat.getName();
|
this.title.setText(this.beat.getName());
|
||||||
this.titleDisplay.innerText = this.beat.getName();
|
|
||||||
break;
|
break;
|
||||||
case BeatEvents.LoopLengthChanged:
|
case BeatEvents.LoopLengthChanged:
|
||||||
this.loopLengthInput.setValue(this.beat.getLoopLength());
|
this.loopLengthInput.setValue(this.beat.getLoopLength());
|
||||||
@@ -68,27 +68,9 @@ export default class BeatSettingsView extends UINode implements ISubscriber<type
|
|||||||
}
|
}
|
||||||
|
|
||||||
build(): HTMLElement {
|
build(): HTMLElement {
|
||||||
this.titleInput = UINode.make("input", {
|
this.title = new EditableTextFieldView({
|
||||||
value: this.beat.getName(),
|
initialText: this.beat.getName(),
|
||||||
classes: ["beat-settings-title-input"],
|
setter: (newText) => this.beat.setName(newText),
|
||||||
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.bakeButton = new ActionButtonView({
|
this.bakeButton = new ActionButtonView({
|
||||||
icon: "snowflake",
|
icon: "snowflake",
|
||||||
@@ -121,11 +103,15 @@ export default class BeatSettingsView extends UINode implements ISubscriber<type
|
|||||||
return UINode.make("div", {
|
return UINode.make("div", {
|
||||||
classes: ["beat-settings"],
|
classes: ["beat-settings"],
|
||||||
}, [
|
}, [
|
||||||
this.titleDisplay,
|
UINode.make("div", {
|
||||||
|
classes: ["beat-settings-title-container"]
|
||||||
|
}, [
|
||||||
|
this.title,
|
||||||
|
]),
|
||||||
UINode.make("div", {
|
UINode.make("div", {
|
||||||
classes: ["beat-settings-lower"],
|
classes: ["beat-settings-lower"],
|
||||||
}, [
|
}, [
|
||||||
this.bakeButton.render(),
|
this.bakeButton,
|
||||||
new ActionButtonView({
|
new ActionButtonView({
|
||||||
icon: "trash",
|
icon: "trash",
|
||||||
type: "secondary",
|
type: "secondary",
|
||||||
@@ -135,7 +121,7 @@ export default class BeatSettingsView extends UINode implements ISubscriber<type
|
|||||||
UINode.make("div", {
|
UINode.make("div", {
|
||||||
classes: ["loop-settings"],
|
classes: ["loop-settings"],
|
||||||
}, [
|
}, [
|
||||||
this.loopCheckbox.render(),
|
this.loopCheckbox,
|
||||||
]),
|
]),
|
||||||
this.loopLengthSection,
|
this.loopLengthSection,
|
||||||
]),
|
]),
|
||||||
|
|||||||
@@ -33,23 +33,23 @@
|
|||||||
background-color: var(--color-ui-accent-hover);
|
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);
|
border-color: var(--color-ui-neutral-light);
|
||||||
background-color: var(--color-ui-accent);
|
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);
|
border-color: var(--color-ui-neutral-light);
|
||||||
background-color: var(--color-ui-accent-hover);
|
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);
|
border-color: var(--color-ui-accent);
|
||||||
background-color: var(--color-ui-accent);
|
background-color: var(--color-ui-accent);
|
||||||
opacity: 60%;
|
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);
|
border-color: var(--color-ui-accent-hover);
|
||||||
background-color: var(--color-ui-accent-hover);
|
background-color: var(--color-ui-accent-hover);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,7 +20,8 @@ export default class BeatUnitView extends UINode implements ISubscriber<EventTyp
|
|||||||
private subscription: ISubscription | null = null;
|
private subscription: ISubscription | null = null;
|
||||||
private publisher: IPublisher<BeatUnitEvent> = new Publisher<BeatUnitEvent, 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;
|
||||||
|
private mouseDownListeners: ((ev: MouseEvent) => void)[] = [];
|
||||||
|
private hoverListeners: ((ev: MouseEvent) => void)[] = [];
|
||||||
|
|
||||||
constructor(options: BeatUnitUINodeOptions) {
|
constructor(options: BeatUnitUINodeOptions) {
|
||||||
super(options);
|
super(options);
|
||||||
@@ -35,26 +36,37 @@ export default class BeatUnitView extends UINode implements ISubscriber<EventTyp
|
|||||||
this.notify(this.publisher, BeatUnitEvent.TypeChange);
|
this.notify(this.publisher, BeatUnitEvent.TypeChange);
|
||||||
}
|
}
|
||||||
|
|
||||||
private setupBindings() {
|
private handleMouseDown(ev: MouseEvent): void {
|
||||||
this.subscription?.unbind();
|
|
||||||
this.subscription = this.beatUnit.addSubscriber(this, EventTypeSubscriptions);
|
|
||||||
this.onMouseDown((ev: MouseEvent) => {
|
|
||||||
if (ev.button === 1) {
|
if (ev.button === 1) {
|
||||||
this.beatUnit.rotateType();
|
this.beatUnit.rotateType();
|
||||||
}
|
}
|
||||||
});
|
}
|
||||||
this.getNode().addEventListener("touchstart", () => {
|
|
||||||
this.touchTimeout = setTimeout(() => {
|
private handleTouchStart(ev: TouchEvent): void {
|
||||||
|
this.touchTimeout = this.touchTimeout || setTimeout(() => {
|
||||||
this.beatUnit.rotateType();
|
this.beatUnit.rotateType();
|
||||||
this.touchTimeout = null;
|
this.touchTimeout = null;
|
||||||
}, 400);
|
}, 400);
|
||||||
});
|
}
|
||||||
this.getNode().addEventListener("touchend", () => {
|
|
||||||
|
private handleTouchEnd(ev: TouchEvent): void {
|
||||||
if (this.touchTimeout) {
|
if (this.touchTimeout) {
|
||||||
clearTimeout(this.touchTimeout);
|
clearTimeout(this.touchTimeout);
|
||||||
this.touchTimeout = null;
|
this.touchTimeout = null;
|
||||||
}
|
}
|
||||||
});
|
}
|
||||||
|
|
||||||
|
private setupBindings() {
|
||||||
|
this.subscription?.unbind();
|
||||||
|
this.subscription = this.beatUnit.addSubscriber(this, EventTypeSubscriptions);
|
||||||
|
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 {
|
toggle(): void {
|
||||||
@@ -108,14 +120,12 @@ export default class BeatUnitView extends UINode implements ISubscriber<EventTyp
|
|||||||
}
|
}
|
||||||
|
|
||||||
onHover(cb: () => void): void {
|
onHover(cb: () => void): void {
|
||||||
this.getNode().addEventListener("mouseover", cb);
|
this.hoverListeners.push(cb);
|
||||||
|
this.setupBindings();
|
||||||
}
|
}
|
||||||
|
|
||||||
onMouseDown(cb: (ev: MouseEvent) => void): void {
|
onMouseDown(cb: (ev: MouseEvent) => void): void {
|
||||||
this.getNode().addEventListener("mousedown", cb);
|
this.mouseDownListeners.push(cb);
|
||||||
}
|
this.setupBindings();
|
||||||
|
|
||||||
onMouseUp(cb: (ev: MouseEvent) => void): void {
|
|
||||||
this.getNode().addEventListener("mouseup", cb);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -88,6 +88,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.root-beat-stage {
|
.root-beat-stage {
|
||||||
|
position: relative;
|
||||||
padding: 2em;
|
padding: 2em;
|
||||||
max-height: 100vh;
|
max-height: 100vh;
|
||||||
margin: auto;
|
margin: auto;
|
||||||
@@ -96,6 +97,8 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.vertical-mode .root-beat-stage {
|
.vertical-mode .root-beat-stage {
|
||||||
|
margin: 5em auto auto;
|
||||||
|
padding-left: 3em;
|
||||||
height: 100vh;
|
height: 100vh;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -4,25 +4,43 @@ import BeatGroup from "@/BeatGroup";
|
|||||||
import "./Root.css";
|
import "./Root.css";
|
||||||
import BeatGroupSettingsView from "@/ui/BeatGroupSettings/BeatGroupSettingsView";
|
import BeatGroupSettingsView from "@/ui/BeatGroupSettings/BeatGroupSettingsView";
|
||||||
import IconView from "@/ui/Widgets/Icon/IconView";
|
import IconView from "@/ui/Widgets/Icon/IconView";
|
||||||
|
import StageTitleBarView from "@/ui/StageTitleBar/StageTitleBarView";
|
||||||
|
|
||||||
export type RootUINodeOptions = UINodeOptions & {
|
export type RootUINodeOptions = UINodeOptions & {
|
||||||
title: string,
|
title: string,
|
||||||
mainBeatGroup?: BeatGroup,
|
mainBeatGroup?: BeatGroup,
|
||||||
|
orientation?: "horizontal" | "vertical",
|
||||||
};
|
};
|
||||||
|
|
||||||
export default class RootView extends UINode {
|
export default class RootView extends UINode {
|
||||||
private title: string;
|
private title: string;
|
||||||
private beatGroupView: BeatGroupView;
|
private beatGroupView: BeatGroupView;
|
||||||
private mainBeatGroup: BeatGroup;
|
private focusedBeatGroup: BeatGroup;
|
||||||
private beatGroupSettingsView!: BeatGroupSettingsView;
|
private beatGroupSettingsView!: BeatGroupSettingsView;
|
||||||
|
private currentOrientation: "horizontal" | "vertical";
|
||||||
|
private stageTitleBarView: StageTitleBarView;
|
||||||
|
|
||||||
constructor(options: RootUINodeOptions) {
|
constructor(options: RootUINodeOptions) {
|
||||||
super(options);
|
super(options);
|
||||||
this.mainBeatGroup = options.mainBeatGroup ?? RootView.defaultMainBeatGroup();
|
this.currentOrientation = options.orientation ?? "horizontal";
|
||||||
this.beatGroupView = new BeatGroupView({title: options.title, beatGroup: this.mainBeatGroup});
|
this.focusedBeatGroup = options.mainBeatGroup ?? RootView.defaultMainBeatGroup();
|
||||||
this.beatGroupSettingsView = new BeatGroupSettingsView({beatGroup: this.mainBeatGroup});
|
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.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 {
|
static defaultMainBeatGroup(): BeatGroup {
|
||||||
@@ -39,12 +57,33 @@ export default class RootView extends UINode {
|
|||||||
return mainBeatGroup;
|
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 {
|
toggleSidebar(): void {
|
||||||
this.getNode().classList.toggle("sidebar-visible");
|
this.getNode().classList.toggle("sidebar-visible");
|
||||||
}
|
}
|
||||||
|
|
||||||
toggleOrientation(): void {
|
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 {
|
private buildSidebarStrip(): HTMLElement {
|
||||||
@@ -71,7 +110,7 @@ export default class RootView extends UINode {
|
|||||||
]),
|
]),
|
||||||
UINode.make("div", {
|
UINode.make("div", {
|
||||||
classes: ["root-quick-access-button"],
|
classes: ["root-quick-access-button"],
|
||||||
onclick: () => this.mainBeatGroup.bakeLoops(),
|
onclick: () => this.focusedBeatGroup.bakeLoops(),
|
||||||
}, [
|
}, [
|
||||||
new IconView({
|
new IconView({
|
||||||
iconName: "snowflake",
|
iconName: "snowflake",
|
||||||
@@ -81,11 +120,7 @@ export default class RootView extends UINode {
|
|||||||
UINode.make("div", {
|
UINode.make("div", {
|
||||||
classes: ["root-quick-access-button"],
|
classes: ["root-quick-access-button"],
|
||||||
title: "Reset all",
|
title: "Reset all",
|
||||||
onclick: () => {
|
onclick: () => this.setMainBeatGroup(RootView.defaultMainBeatGroup()),
|
||||||
this.mainBeatGroup = RootView.defaultMainBeatGroup();
|
|
||||||
this.beatGroupSettingsView.setBeatGroup(this.mainBeatGroup);
|
|
||||||
this.beatGroupView.setBeatGroup(this.mainBeatGroup);
|
|
||||||
},
|
|
||||||
}, [
|
}, [
|
||||||
new IconView({
|
new IconView({
|
||||||
iconName: "trash",
|
iconName: "trash",
|
||||||
@@ -99,11 +134,10 @@ export default class RootView extends UINode {
|
|||||||
return (
|
return (
|
||||||
UINode.make("div", {classes: ["root-sidebar"]}, [
|
UINode.make("div", {classes: ["root-sidebar"]}, [
|
||||||
UINode.make("div", {classes: ["root-settings"]}, [
|
UINode.make("div", {classes: ["root-settings"]}, [
|
||||||
UINode.make("h1", {classes: ["root-title"], innerText: this.title}, [
|
UINode.make("h1", {classes: ["root-title"], innerText: this.title}),
|
||||||
this.beatGroupSettingsView.render(),
|
this.beatGroupSettingsView.render(),
|
||||||
]),
|
]),
|
||||||
this.buildSidebarStrip(),
|
this.buildSidebarStrip(),
|
||||||
]),
|
|
||||||
])
|
])
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -113,6 +147,7 @@ export default class RootView extends UINode {
|
|||||||
UINode.make("div", {classes: ["root", "sidebar-visible"]}, [
|
UINode.make("div", {classes: ["root", "sidebar-visible"]}, [
|
||||||
this.buildSidebar(),
|
this.buildSidebar(),
|
||||||
UINode.make("div", {classes: ["root-beat-stage-container"]}, [
|
UINode.make("div", {classes: ["root-beat-stage-container"]}, [
|
||||||
|
this.stageTitleBarView.render(),
|
||||||
UINode.make("div", {classes: ["root-beat-stage"]}, [
|
UINode.make("div", {classes: ["root-beat-stage"]}, [
|
||||||
this.beatGroupView.render(),
|
this.beatGroupView.render(),
|
||||||
])
|
])
|
||||||
|
|||||||
25
src/ui/StageTitleBar/StageTitleBar.css
Normal file
25
src/ui/StageTitleBar/StageTitleBar.css
Normal 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;
|
||||||
|
}
|
||||||
50
src/ui/StageTitleBar/StageTitleBarView.ts
Normal file
50
src/ui/StageTitleBar/StageTitleBarView.ts
Normal 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]),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -13,7 +13,7 @@ type IRenderAttributes<
|
|||||||
export default abstract class UINode {
|
export default abstract class UINode {
|
||||||
protected node: HTMLElement | null = null;
|
protected node: HTMLElement | null = null;
|
||||||
|
|
||||||
constructor(options: UINodeOptions) {}
|
constructor(options: UINodeOptions) { /* dummy */ }
|
||||||
|
|
||||||
render(): HTMLElement {
|
render(): HTMLElement {
|
||||||
if (!this.node) {
|
if (!this.node) {
|
||||||
@@ -51,7 +51,7 @@ export default abstract class UINode {
|
|||||||
K extends keyof HTMLElementTagNameMap[T]>(
|
K extends keyof HTMLElementTagNameMap[T]>(
|
||||||
type: T,
|
type: T,
|
||||||
attributes: IRenderAttributes<T, K>,
|
attributes: IRenderAttributes<T, K>,
|
||||||
subElements?: HTMLElement[],
|
subNodes?: (Node | UINode)[],
|
||||||
): HTMLElementTagNameMap[T] {
|
): HTMLElementTagNameMap[T] {
|
||||||
const element = document.createElement(type);
|
const element = document.createElement(type);
|
||||||
if (attributes) {
|
if (attributes) {
|
||||||
@@ -63,8 +63,14 @@ export default abstract class UINode {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (subElements) {
|
if (subNodes) {
|
||||||
element.append(...subElements);
|
for (const subElement of subNodes) {
|
||||||
|
if (subElement instanceof UINode) {
|
||||||
|
element.append(subElement.render());
|
||||||
|
} else {
|
||||||
|
element.append(subElement);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return element;
|
return element;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -50,16 +50,15 @@ export default class ActionButtonView extends UINode {
|
|||||||
protected build(): HTMLButtonElement {
|
protected build(): HTMLButtonElement {
|
||||||
this.buttonElement = UINode.make("button", {
|
this.buttonElement = UINode.make("button", {
|
||||||
classes: ["action-button", `action-button-${this.type}`],
|
classes: ["action-button", `action-button-${this.type}`],
|
||||||
onclick: (event: MouseEvent) => this.disabled || this.onClick(event),
|
onclick: (event: MouseEvent) => this.disabled || this.onClick(event)
|
||||||
subs: [
|
}, [
|
||||||
this.icon !== null ? new IconView({
|
this.icon !== null ? new IconView({
|
||||||
iconName: this.icon,
|
iconName: this.icon,
|
||||||
color: "var(--color-p-light)",
|
color: "var(--color-p-light)",
|
||||||
}).render() : UINode.make("span", {
|
}).render() : UINode.make("span", {
|
||||||
innerText: this.label ?? ""
|
innerText: this.label ?? ""
|
||||||
}),
|
}),
|
||||||
],
|
]);
|
||||||
});
|
|
||||||
if (this.alt) {
|
if (this.alt) {
|
||||||
this.buttonElement.title = this.alt;
|
this.buttonElement.title = this.alt;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -55,10 +55,9 @@ export default class BoolBoxView extends UINode {
|
|||||||
});
|
});
|
||||||
return UINode.make("div", {
|
return UINode.make("div", {
|
||||||
classes: ["bool-box"],
|
classes: ["bool-box"],
|
||||||
subs: [
|
},[
|
||||||
this.labelElement,
|
this.labelElement,
|
||||||
this.checkboxElement,
|
this.checkboxElement,
|
||||||
],
|
]);
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
14
src/ui/Widgets/EditableTextFIeld/EditableTextFieldView.css
Normal file
14
src/ui/Widgets/EditableTextFIeld/EditableTextFieldView.css
Normal 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);
|
||||||
|
}
|
||||||
71
src/ui/Widgets/EditableTextFIeld/EditableTextFieldView.ts
Normal file
71
src/ui/Widgets/EditableTextFIeld/EditableTextFieldView.ts
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -101,7 +101,7 @@ export default class NumberInputView extends UINode {
|
|||||||
});
|
});
|
||||||
return UINode.make("div", {
|
return UINode.make("div", {
|
||||||
classes: ["number-input"],
|
classes: ["number-input"],
|
||||||
subs: [
|
}, [
|
||||||
this.labelElement,
|
this.labelElement,
|
||||||
UINode.make("button", {
|
UINode.make("button", {
|
||||||
innerText: "-",
|
innerText: "-",
|
||||||
@@ -126,7 +126,6 @@ export default class NumberInputView extends UINode {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
],
|
]);
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user