refactor: loads of sensible renaming
This commit is contained in:
474
src/Beat.ts
474
src/Beat.ts
@@ -1,169 +1,305 @@
|
|||||||
import BeatUnit from "@/BeatUnit";
|
import Track, {TrackEvents, TrackInitOptions} from "@/Track";
|
||||||
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 {isPosInt} from "@/utils";
|
|
||||||
|
type BeatGroupInitOptions = {
|
||||||
export type BeatInitOptions = {
|
barCount: number;
|
||||||
timeSig?: {
|
isLooping: boolean;
|
||||||
up: number,
|
timeSigUp: number;
|
||||||
down: number,
|
tracks?: TrackInitOptions[],
|
||||||
},
|
loopLength?: number,
|
||||||
name?: string,
|
useAutoBeatLength?: boolean,
|
||||||
bars?: number,
|
name?: string,
|
||||||
isLooping?: boolean,
|
};
|
||||||
loopLength?: number,
|
|
||||||
};
|
export const enum BeatEvents {
|
||||||
|
TrackOrderChanged="be-0",
|
||||||
export const enum BeatEvents {
|
TrackListChanged="be-1",
|
||||||
NewTimeSig="be-0",
|
BarCountChanged="be-2",
|
||||||
NewBarCount="be-1",
|
TimeSigUpChanged="be-3",
|
||||||
NewName="be-2",
|
AutoBeatSettingsChanged="be-4",
|
||||||
DisplayTypeChanged="be-3",
|
LockingChanged="be-5",
|
||||||
LoopLengthChanged="be-4",
|
GlobalLoopLengthChanged="be-5",
|
||||||
WantsRemoval="be-5",
|
GlobalDisplayTypeChanged="be-6",
|
||||||
Baked="be-6",
|
NameChanged="be-7",
|
||||||
}
|
}
|
||||||
|
|
||||||
export default class Beat implements IPublisher<BeatEvents>, BeatLike {
|
type EventTypeSubscriptions =
|
||||||
private static count = 0;
|
| TrackEvents.LoopLengthChanged
|
||||||
private readonly key: string;
|
| TrackEvents.DisplayTypeChanged
|
||||||
private name: string;
|
| TrackEvents.WantsRemoval
|
||||||
private timeSigUp = 4;
|
| TrackEvents.Baked;
|
||||||
private timeSigDown = 4;
|
|
||||||
private readonly unitRecord: BeatUnit[] = [];
|
export default class Beat implements IPublisher<BeatEvents>, ISubscriber<EventTypeSubscriptions> {
|
||||||
private barCount = 1;
|
private static globalCounter = 0;
|
||||||
private publisher = new Publisher<BeatEvents, Beat>(this);
|
private tracks: Track[] = [];
|
||||||
private loopLength: number;
|
private publisher: Publisher<BeatEvents, Beat> = new Publisher<BeatEvents, Beat>(this);
|
||||||
private looping: boolean;
|
private barCount: number;
|
||||||
|
private timeSigUp: number;
|
||||||
constructor(options?: BeatInitOptions) {
|
private globalLoopLength: number;
|
||||||
this.key = `B-${Beat.count}`;
|
private globalIsLooping: boolean;
|
||||||
this.name = options?.name ?? this.key;
|
private useAutoBeatLength: boolean;
|
||||||
this.setTimeSignature({up: options?.timeSig?.up ?? 4, down: options?.timeSig?.down ?? 4});
|
private barSettingsLocked = false;
|
||||||
this.setBarCount(options?.bars ?? 4);
|
private name: string;
|
||||||
Beat.count++;
|
|
||||||
this.loopLength = options?.loopLength ?? this.timeSigUp * this.barCount;
|
constructor(options?: BeatGroupInitOptions) {
|
||||||
this.looping = options?.isLooping ?? false;
|
Beat.globalCounter++;
|
||||||
}
|
if (options?.name) {
|
||||||
|
this.name = options.name;
|
||||||
setLoopLength(loopLength: number): void {
|
} else {
|
||||||
if (!isPosInt(loopLength) || loopLength < 2) {
|
this.name = `Pattern ${Beat.globalCounter}`;
|
||||||
loopLength = this.loopLength;
|
}
|
||||||
}
|
if (options?.tracks) {
|
||||||
this.loopLength = loopLength;
|
for (const trackOptions of options.tracks) {
|
||||||
this.publisher.notifySubs(BeatEvents.LoopLengthChanged);
|
this.addTrack(trackOptions);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
setLooping(isLooping: boolean): void {
|
this.barCount = options?.barCount ?? 4;
|
||||||
this.looping = isLooping;
|
this.timeSigUp = options?.timeSigUp ?? 4;
|
||||||
this.publisher.notifySubs(BeatEvents.DisplayTypeChanged);
|
this.globalLoopLength = options?.loopLength ?? this.timeSigUp;
|
||||||
}
|
this.globalIsLooping = options?.isLooping ?? false;
|
||||||
|
this.useAutoBeatLength = options?.useAutoBeatLength ?? false;
|
||||||
addSubscriber(subscriber: ISubscriber<BeatEvents>, eventType: BeatEvents | BeatEvents[]): { unbind: () => void } {
|
}
|
||||||
return this.publisher.addSubscriber(subscriber, eventType);
|
|
||||||
}
|
notify(publisher: unknown, event: EventTypeSubscriptions): void {
|
||||||
|
switch (event) {
|
||||||
setTimeSignature(timeSig: {up?: number, down?: number}): void {
|
case TrackEvents.LoopLengthChanged:
|
||||||
if (timeSig.up && Beat.isValidTimeSigRange(timeSig.up)) {
|
case TrackEvents.DisplayTypeChanged:
|
||||||
this.timeSigUp = timeSig.up | 0;
|
this.autoBeatLength();
|
||||||
}
|
break;
|
||||||
if (timeSig.down && Beat.isValidTimeSigRange(timeSig.down)) {
|
case TrackEvents.WantsRemoval:
|
||||||
this.timeSigDown = timeSig.down | 0;
|
this.removeTrack((publisher as Track).getKey());
|
||||||
}
|
break;
|
||||||
this.updateBeatUnitLength();
|
case TrackEvents.Baked:
|
||||||
this.publisher.notifySubs(BeatEvents.NewTimeSig);
|
this.setIsUsingAutoBeatLength(false);
|
||||||
}
|
break;
|
||||||
|
}
|
||||||
setTimeSigUp(timeSigUp: number): void {
|
}
|
||||||
this.setTimeSignature({up: timeSigUp});
|
|
||||||
}
|
addSubscriber(subscriber: ISubscriber<BeatEvents>, eventType: BeatEvents | BeatEvents[]): { unbind: () => void } {
|
||||||
|
return this.publisher.addSubscriber(subscriber, eventType);
|
||||||
setTimeSigDown(timeSigUp: number): void {
|
}
|
||||||
this.setTimeSignature({down: timeSigUp});
|
|
||||||
}
|
private setBarCountInternal(barCount: number): void {
|
||||||
|
if (!isPosInt(barCount)) {
|
||||||
setBarCount(barCount: number): void {
|
barCount = this.barCount;
|
||||||
if (!isPosInt(barCount) || barCount == this.barCount) {
|
}
|
||||||
barCount = this.barCount;
|
this.barCount = barCount;
|
||||||
}
|
for (const track of this.tracks) {
|
||||||
this.barCount = barCount;
|
track.setBarCount(barCount);
|
||||||
this.updateBeatUnitLength();
|
}
|
||||||
this.publisher.notifySubs(BeatEvents.NewBarCount);
|
this.publisher.notifySubs(BeatEvents.BarCountChanged);
|
||||||
}
|
}
|
||||||
|
|
||||||
getUnitByIndex(index: number): BeatUnit | null {
|
setBarCount(barCount: number): void {
|
||||||
if (this.looping) {
|
if (!this.barSettingsLocked) {
|
||||||
index %= this.loopLength;
|
this.setBarCountInternal(barCount);
|
||||||
}
|
} else {
|
||||||
return this.unitRecord[index] ?? null;
|
this.setBarCountInternal(this.barCount);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
private updateBeatUnitLength() {
|
|
||||||
const newBarCount = this.barCount * this.timeSigUp;
|
getBarCount(): number {
|
||||||
if (newBarCount < this.unitRecord.length) {
|
return this.barCount;
|
||||||
this.unitRecord.splice(this.barCount * this.timeSigUp, this.unitRecord.length - newBarCount);
|
}
|
||||||
} else if (newBarCount > this.unitRecord.length) {
|
|
||||||
const barsToAdd = newBarCount - this.unitRecord.length;
|
setLoopLength(loopLength: number): void {
|
||||||
for (let i = 0; i < barsToAdd; i++) {
|
if (!isPosInt(loopLength)) {
|
||||||
this.unitRecord.push(new BeatUnit());
|
return;
|
||||||
}
|
}
|
||||||
}
|
this.globalLoopLength = loopLength;
|
||||||
}
|
for (const track of this.tracks) {
|
||||||
|
track.setLoopLength(loopLength);
|
||||||
getTimeSigUp(): number {
|
}
|
||||||
return this.timeSigUp;
|
this.publisher.notifySubs(BeatEvents.GlobalLoopLengthChanged);
|
||||||
}
|
}
|
||||||
|
|
||||||
getTimeSigDown(): number {
|
getLoopLength(): number {
|
||||||
return this.timeSigDown;
|
return this.globalLoopLength;
|
||||||
}
|
}
|
||||||
|
|
||||||
getBarCount(): number {
|
setLooping(isLooping: boolean): void {
|
||||||
return this.barCount;
|
this.globalIsLooping = isLooping;
|
||||||
}
|
for (const track of this.tracks) {
|
||||||
|
track.setLooping(isLooping);
|
||||||
getKey(): string {
|
}
|
||||||
return this.key;
|
this.publisher.notifySubs(BeatEvents.GlobalDisplayTypeChanged);
|
||||||
}
|
}
|
||||||
|
|
||||||
static isValidTimeSigRange(sig: number): boolean {
|
isLooping(): boolean {
|
||||||
return sig >= 2 && sig <= 32;
|
return this.globalIsLooping;
|
||||||
}
|
}
|
||||||
|
|
||||||
setName(newName: string): void {
|
private findSmallestLoopLength(): number {
|
||||||
this.name = newName;
|
const loopLengths = [this.timeSigUp];
|
||||||
this.publisher.notifySubs(BeatEvents.NewName);
|
for (const track of this.tracks) {
|
||||||
}
|
if (track.isLooping()) {
|
||||||
|
const loopLength = track.getLoopLength();
|
||||||
getName(): string {
|
if (loopLengths.indexOf(loopLength) === -1) {
|
||||||
return this.name;
|
loopLengths.push(loopLength);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
isLooping(): boolean {
|
}
|
||||||
return this.looping;
|
if (loopLengths.length === 1) {
|
||||||
}
|
loopLengths.push(1);
|
||||||
|
}
|
||||||
getLoopLength(): number {
|
return loopLengths.reduce((prev, curr) => (prev * curr) / greatestCommonDivisor(prev, curr));
|
||||||
return this.loopLength;
|
}
|
||||||
}
|
|
||||||
|
setTimeSigUp(timeSigUp: number): void {
|
||||||
delete(): void {
|
if (!Track.isValidTimeSigRange(timeSigUp)) {
|
||||||
this.publisher.notifySubs(BeatEvents.WantsRemoval);
|
timeSigUp = this.timeSigUp;
|
||||||
}
|
}
|
||||||
|
this.timeSigUp = timeSigUp;
|
||||||
bakeLoops(): void {
|
for (const track of this.tracks) {
|
||||||
if (this.isLooping()) {
|
track.setTimeSignature({up: timeSigUp});
|
||||||
this.unitRecord.forEach((unit, i) => {
|
}
|
||||||
const reprUnitAtPos = this.getUnitByIndex(i);
|
this.autoBeatLength();
|
||||||
if (reprUnitAtPos) {
|
this.publisher.notifySubs(BeatEvents.TimeSigUpChanged);
|
||||||
unit.mimic(reprUnitAtPos);
|
}
|
||||||
}
|
|
||||||
});
|
getTimeSigUp(): number {
|
||||||
this.publisher.notifySubs(BeatEvents.Baked);
|
return this.timeSigUp;
|
||||||
this.setLooping(false);
|
}
|
||||||
} else {
|
|
||||||
this.publisher.notifySubs(BeatEvents.Baked);
|
getTrackByKey(trackKey: string): Track {
|
||||||
}
|
const foundTrack = this.tracks.find(track => track.getKey() === trackKey);
|
||||||
}
|
if (typeof foundTrack === "undefined") {
|
||||||
}
|
throw new Error(`Could not find the track with key: ${trackKey}`);
|
||||||
|
}
|
||||||
|
return foundTrack;
|
||||||
|
}
|
||||||
|
|
||||||
|
getTrackByIndex(trackIndex: number): Track {
|
||||||
|
if (!this.tracks[trackIndex]) {
|
||||||
|
throw new Error(`Could not find the track with index: ${trackIndex}`);
|
||||||
|
}
|
||||||
|
return this.tracks[trackIndex];
|
||||||
|
}
|
||||||
|
|
||||||
|
getTrackCount(): number {
|
||||||
|
return this.tracks.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
getTrackKeys(): string[] {
|
||||||
|
return this.tracks.map(track => track.getKey());
|
||||||
|
}
|
||||||
|
|
||||||
|
swapTracksByIndices(trackIndex1: number, trackIndex2: number): void {
|
||||||
|
const track1 = this.getTrackByIndex(trackIndex1);
|
||||||
|
const track2 = this.getTrackByIndex(trackIndex2);
|
||||||
|
this.tracks[trackIndex1] = track2;
|
||||||
|
this.tracks[trackIndex2] = track1;
|
||||||
|
this.publisher.notifySubs(BeatEvents.TrackOrderChanged);
|
||||||
|
}
|
||||||
|
|
||||||
|
moveTrackBack(trackKey: string): void {
|
||||||
|
const index = this.tracks.indexOf(this.getTrackByKey(trackKey));
|
||||||
|
if (typeof index !== "undefined" && index > 0) {
|
||||||
|
this.swapTracksByIndices(index, index - 1);
|
||||||
|
}
|
||||||
|
this.publisher.notifySubs(BeatEvents.TrackOrderChanged);
|
||||||
|
this.publisher.notifySubs(BeatEvents.TrackListChanged);
|
||||||
|
}
|
||||||
|
|
||||||
|
moveTrackForward(trackKey: string): void {
|
||||||
|
const index = this.tracks.indexOf(this.getTrackByKey(trackKey));
|
||||||
|
if (typeof index !== "undefined" && index < this.getTrackCount()) {
|
||||||
|
this.swapTracksByIndices(index, index + 1);
|
||||||
|
}
|
||||||
|
this.publisher.notifySubs(BeatEvents.TrackOrderChanged);
|
||||||
|
this.publisher.notifySubs(BeatEvents.TrackListChanged);
|
||||||
|
}
|
||||||
|
|
||||||
|
canMoveTrackBack(trackKey: string): boolean {
|
||||||
|
return this.tracks.indexOf(this.getTrackByKey(trackKey)) > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
canMoveTrackForward(trackKey: string): boolean {
|
||||||
|
return this.tracks.indexOf(this.getTrackByKey(trackKey)) < this.tracks.length - 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
addTrack(options?: TrackInitOptions): Track {
|
||||||
|
options = {
|
||||||
|
timeSig: {
|
||||||
|
up: this.timeSigUp,
|
||||||
|
down: 4,
|
||||||
|
},
|
||||||
|
bars: this.barCount,
|
||||||
|
isLooping: this.globalIsLooping,
|
||||||
|
loopLength: this.globalLoopLength,
|
||||||
|
...options
|
||||||
|
};
|
||||||
|
const newTrack = new Track(options);
|
||||||
|
this.tracks.push(newTrack);
|
||||||
|
newTrack.addSubscriber(this, [
|
||||||
|
TrackEvents.LoopLengthChanged,
|
||||||
|
TrackEvents.WantsRemoval,
|
||||||
|
TrackEvents.DisplayTypeChanged,
|
||||||
|
TrackEvents.Baked,
|
||||||
|
]);
|
||||||
|
this.publisher.notifySubs(BeatEvents.TrackListChanged);
|
||||||
|
return newTrack;
|
||||||
|
}
|
||||||
|
|
||||||
|
removeTrack(trackKey: string): void {
|
||||||
|
const track = this.getTrackByKey(trackKey);
|
||||||
|
this.tracks.splice(this.tracks.indexOf(track), 1);
|
||||||
|
this.autoBeatLength();
|
||||||
|
this.publisher.notifySubs(BeatEvents.TrackListChanged);
|
||||||
|
}
|
||||||
|
|
||||||
|
setTrackName(trackKey: string, newName: string): void {
|
||||||
|
this.getTrackByKey(trackKey).setName(newName);
|
||||||
|
this.publisher.notifySubs(BeatEvents.TrackOrderChanged);
|
||||||
|
}
|
||||||
|
|
||||||
|
autoBeatLengthOn(): boolean {
|
||||||
|
return this.useAutoBeatLength;
|
||||||
|
}
|
||||||
|
|
||||||
|
private autoBeatLength(): void {
|
||||||
|
if (this.useAutoBeatLength) {
|
||||||
|
this.setBarCountInternal(this.findSmallestLoopLength() / this.timeSigUp);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setIsUsingAutoBeatLength(isOn: boolean): void {
|
||||||
|
this.useAutoBeatLength = isOn;
|
||||||
|
this.autoBeatLength();
|
||||||
|
if (isOn) {
|
||||||
|
this.lockBars();
|
||||||
|
} else {
|
||||||
|
this.unlockBars();
|
||||||
|
}
|
||||||
|
this.publisher.notifySubs(BeatEvents.AutoBeatSettingsChanged);
|
||||||
|
}
|
||||||
|
|
||||||
|
barsLocked(): boolean {
|
||||||
|
return this.barSettingsLocked;
|
||||||
|
}
|
||||||
|
|
||||||
|
lockBars(): void {
|
||||||
|
this.barSettingsLocked = true;
|
||||||
|
this.publisher.notifySubs(BeatEvents.LockingChanged);
|
||||||
|
}
|
||||||
|
|
||||||
|
unlockBars(): void {
|
||||||
|
this.barSettingsLocked = false;
|
||||||
|
this.publisher.notifySubs(BeatEvents.LockingChanged);
|
||||||
|
}
|
||||||
|
|
||||||
|
bakeLoops(): void {
|
||||||
|
this.tracks.forEach(track => track.bakeLoops());
|
||||||
|
}
|
||||||
|
|
||||||
|
setName(newName: string): void {
|
||||||
|
this.name = newName;
|
||||||
|
this.publisher.notifySubs(BeatEvents.NameChanged);
|
||||||
|
}
|
||||||
|
|
||||||
|
getName(): string {
|
||||||
|
return this.name;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
306
src/BeatGroup.ts
306
src/BeatGroup.ts
@@ -1,306 +0,0 @@
|
|||||||
import Beat, {BeatEvents, BeatInitOptions} from "@/Beat";
|
|
||||||
import {IPublisher, Publisher} from "@/Publisher";
|
|
||||||
import ISubscriber from "@/Subscriber";
|
|
||||||
import {greatestCommonDivisor, isPosInt} from "@/utils";
|
|
||||||
|
|
||||||
type BeatGroupInitOptions = {
|
|
||||||
barCount: number;
|
|
||||||
isLooping: boolean;
|
|
||||||
timeSigUp: number;
|
|
||||||
beats?: BeatInitOptions[],
|
|
||||||
loopLength?: number,
|
|
||||||
useAutoBeatLength?: boolean,
|
|
||||||
name?: string,
|
|
||||||
};
|
|
||||||
|
|
||||||
export const enum BeatGroupEvents {
|
|
||||||
BeatOrderChanged="bge-0",
|
|
||||||
BeatListChanged="bge-1",
|
|
||||||
BarCountChanged="bge-2",
|
|
||||||
TimeSigUpChanged="bge-3",
|
|
||||||
AutoBeatSettingsChanged="bge-4",
|
|
||||||
LockingChanged="bge-5",
|
|
||||||
GlobalLoopLengthChanged="bge-5",
|
|
||||||
GlobalDisplayTypeChanged="bge-6",
|
|
||||||
NameChanged="bge-7",
|
|
||||||
}
|
|
||||||
|
|
||||||
type EventTypeSubscriptions =
|
|
||||||
| BeatEvents.LoopLengthChanged
|
|
||||||
| BeatEvents.DisplayTypeChanged
|
|
||||||
| BeatEvents.WantsRemoval
|
|
||||||
| BeatEvents.Baked;
|
|
||||||
|
|
||||||
export default class BeatGroup implements IPublisher<BeatGroupEvents>, ISubscriber<EventTypeSubscriptions> {
|
|
||||||
private static globalCounter = 0;
|
|
||||||
private beats: Beat[] = [];
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
this.barCount = options?.barCount ?? 4;
|
|
||||||
this.timeSigUp = options?.timeSigUp ?? 4;
|
|
||||||
this.globalLoopLength = options?.loopLength ?? this.timeSigUp;
|
|
||||||
this.globalIsLooping = options?.isLooping ?? false;
|
|
||||||
this.useAutoBeatLength = options?.useAutoBeatLength ?? false;
|
|
||||||
}
|
|
||||||
|
|
||||||
notify(publisher: unknown, event: EventTypeSubscriptions): void {
|
|
||||||
switch (event) {
|
|
||||||
case BeatEvents.LoopLengthChanged:
|
|
||||||
case BeatEvents.DisplayTypeChanged:
|
|
||||||
this.autoBeatLength();
|
|
||||||
break;
|
|
||||||
case BeatEvents.WantsRemoval:
|
|
||||||
this.removeBeat((publisher as Beat).getKey());
|
|
||||||
break;
|
|
||||||
case BeatEvents.Baked:
|
|
||||||
this.setIsUsingAutoBeatLength(false);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
addSubscriber(subscriber: ISubscriber<BeatGroupEvents>, eventType: BeatGroupEvents | BeatGroupEvents[]): { unbind: () => void } {
|
|
||||||
return this.publisher.addSubscriber(subscriber, eventType);
|
|
||||||
}
|
|
||||||
|
|
||||||
private setBarCountInternal(barCount: number): void {
|
|
||||||
if (!isPosInt(barCount)) {
|
|
||||||
barCount = this.barCount;
|
|
||||||
}
|
|
||||||
this.barCount = barCount;
|
|
||||||
for (const beat of this.beats) {
|
|
||||||
beat.setBarCount(barCount);
|
|
||||||
}
|
|
||||||
this.publisher.notifySubs(BeatGroupEvents.BarCountChanged);
|
|
||||||
}
|
|
||||||
|
|
||||||
setBarCount(barCount: number): void {
|
|
||||||
if (!this.barSettingsLocked) {
|
|
||||||
this.setBarCountInternal(barCount);
|
|
||||||
} else {
|
|
||||||
this.setBarCountInternal(this.barCount);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
getBarCount(): number {
|
|
||||||
return this.barCount;
|
|
||||||
}
|
|
||||||
|
|
||||||
setLoopLength(loopLength: number): void {
|
|
||||||
if (!isPosInt(loopLength)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
this.globalLoopLength = loopLength;
|
|
||||||
for (const beat of this.beats) {
|
|
||||||
beat.setLoopLength(loopLength);
|
|
||||||
}
|
|
||||||
this.publisher.notifySubs(BeatGroupEvents.GlobalLoopLengthChanged);
|
|
||||||
}
|
|
||||||
|
|
||||||
getLoopLength(): number {
|
|
||||||
return this.globalLoopLength;
|
|
||||||
}
|
|
||||||
|
|
||||||
setLooping(isLooping: boolean): void {
|
|
||||||
this.globalIsLooping = isLooping;
|
|
||||||
for (const beat of this.beats) {
|
|
||||||
beat.setLooping(isLooping);
|
|
||||||
}
|
|
||||||
this.publisher.notifySubs(BeatGroupEvents.GlobalDisplayTypeChanged);
|
|
||||||
}
|
|
||||||
|
|
||||||
isLooping(): boolean {
|
|
||||||
return this.globalIsLooping;
|
|
||||||
}
|
|
||||||
|
|
||||||
private findSmallestLoopLength(): number {
|
|
||||||
const loopLengths = [this.timeSigUp];
|
|
||||||
for (const beat of this.beats) {
|
|
||||||
if (beat.isLooping()) {
|
|
||||||
const loopLength = beat.getLoopLength();
|
|
||||||
if (loopLengths.indexOf(loopLength) === -1) {
|
|
||||||
loopLengths.push(loopLength);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (loopLengths.length === 1) {
|
|
||||||
loopLengths.push(1);
|
|
||||||
}
|
|
||||||
return loopLengths.reduce((prev, curr) => (prev * curr) / greatestCommonDivisor(prev, curr));
|
|
||||||
}
|
|
||||||
|
|
||||||
setTimeSigUp(timeSigUp: number): void {
|
|
||||||
if (!Beat.isValidTimeSigRange(timeSigUp)) {
|
|
||||||
timeSigUp = this.timeSigUp;
|
|
||||||
}
|
|
||||||
this.timeSigUp = timeSigUp;
|
|
||||||
for (const beat of this.beats) {
|
|
||||||
beat.setTimeSignature({up: timeSigUp});
|
|
||||||
}
|
|
||||||
this.autoBeatLength();
|
|
||||||
this.publisher.notifySubs(BeatGroupEvents.TimeSigUpChanged);
|
|
||||||
}
|
|
||||||
|
|
||||||
getTimeSigUp(): number {
|
|
||||||
return this.timeSigUp;
|
|
||||||
}
|
|
||||||
|
|
||||||
getBeatByKey(beatKey: string): Beat {
|
|
||||||
const foundBeat = this.beats.find(beat => beat.getKey() === beatKey);
|
|
||||||
if (typeof foundBeat === "undefined") {
|
|
||||||
throw new Error(`Could not find the beat with key: ${beatKey}`);
|
|
||||||
}
|
|
||||||
return foundBeat;
|
|
||||||
}
|
|
||||||
|
|
||||||
getBeatByIndex(beatIndex: number): Beat {
|
|
||||||
if (!this.beats[beatIndex]) {
|
|
||||||
throw new Error(`Could not find the beat with index: ${beatIndex}`);
|
|
||||||
}
|
|
||||||
return this.beats[beatIndex];
|
|
||||||
}
|
|
||||||
|
|
||||||
getBeatCount(): number {
|
|
||||||
return this.beats.length;
|
|
||||||
}
|
|
||||||
|
|
||||||
getBeatKeys(): string[] {
|
|
||||||
return this.beats.map(beat => beat.getKey());
|
|
||||||
}
|
|
||||||
|
|
||||||
swapBeatsByIndices(beatIndex1: number, beatIndex2: number): void {
|
|
||||||
const beat1 = this.getBeatByIndex(beatIndex1);
|
|
||||||
const beat2 = this.getBeatByIndex(beatIndex2);
|
|
||||||
this.beats[beatIndex1] = beat2;
|
|
||||||
this.beats[beatIndex2] = beat1;
|
|
||||||
this.publisher.notifySubs(BeatGroupEvents.BeatOrderChanged);
|
|
||||||
}
|
|
||||||
|
|
||||||
moveBeatBack(beatKey: string): void {
|
|
||||||
const index = this.beats.indexOf(this.getBeatByKey(beatKey));
|
|
||||||
if (typeof index !== "undefined" && index > 0) {
|
|
||||||
this.swapBeatsByIndices(index, index - 1);
|
|
||||||
}
|
|
||||||
this.publisher.notifySubs(BeatGroupEvents.BeatOrderChanged);
|
|
||||||
this.publisher.notifySubs(BeatGroupEvents.BeatListChanged);
|
|
||||||
}
|
|
||||||
|
|
||||||
moveBeatForward(beatKey: string): void {
|
|
||||||
const index = this.beats.indexOf(this.getBeatByKey(beatKey));
|
|
||||||
if (typeof index !== "undefined" && index < this.getBeatCount()) {
|
|
||||||
this.swapBeatsByIndices(index, index + 1);
|
|
||||||
}
|
|
||||||
this.publisher.notifySubs(BeatGroupEvents.BeatOrderChanged);
|
|
||||||
this.publisher.notifySubs(BeatGroupEvents.BeatListChanged);
|
|
||||||
}
|
|
||||||
|
|
||||||
canMoveBeatBack(beatKey: string): boolean {
|
|
||||||
return this.beats.indexOf(this.getBeatByKey(beatKey)) > 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
canMoveBeatForward(beatKey: string): boolean {
|
|
||||||
return this.beats.indexOf(this.getBeatByKey(beatKey)) < this.beats.length - 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
addBeat(options?: BeatInitOptions): Beat {
|
|
||||||
options = {
|
|
||||||
timeSig: {
|
|
||||||
up: this.timeSigUp,
|
|
||||||
down: 4,
|
|
||||||
},
|
|
||||||
bars: this.barCount,
|
|
||||||
isLooping: this.globalIsLooping,
|
|
||||||
loopLength: this.globalLoopLength,
|
|
||||||
...options
|
|
||||||
};
|
|
||||||
const newBeat = new Beat(options);
|
|
||||||
this.beats.push(newBeat);
|
|
||||||
newBeat.addSubscriber(this, [
|
|
||||||
BeatEvents.LoopLengthChanged,
|
|
||||||
BeatEvents.WantsRemoval,
|
|
||||||
BeatEvents.DisplayTypeChanged,
|
|
||||||
BeatEvents.Baked,
|
|
||||||
]);
|
|
||||||
this.publisher.notifySubs(BeatGroupEvents.BeatListChanged);
|
|
||||||
return newBeat;
|
|
||||||
}
|
|
||||||
|
|
||||||
removeBeat(beatKey: string): void {
|
|
||||||
const beat = this.getBeatByKey(beatKey);
|
|
||||||
this.beats.splice(this.beats.indexOf(beat), 1);
|
|
||||||
this.autoBeatLength();
|
|
||||||
console.log("removing");
|
|
||||||
this.publisher.notifySubs(BeatGroupEvents.BeatListChanged);
|
|
||||||
}
|
|
||||||
|
|
||||||
setBeatName(beatKey: string, newName: string): void {
|
|
||||||
this.getBeatByKey(beatKey).setName(newName);
|
|
||||||
this.publisher.notifySubs(BeatGroupEvents.BeatOrderChanged);
|
|
||||||
}
|
|
||||||
|
|
||||||
autoBeatLengthOn(): boolean {
|
|
||||||
return this.useAutoBeatLength;
|
|
||||||
}
|
|
||||||
|
|
||||||
private autoBeatLength(): void {
|
|
||||||
if (this.useAutoBeatLength) {
|
|
||||||
this.setBarCountInternal(this.findSmallestLoopLength() / this.timeSigUp);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
setIsUsingAutoBeatLength(isOn: boolean): void {
|
|
||||||
this.useAutoBeatLength = isOn;
|
|
||||||
this.autoBeatLength();
|
|
||||||
if (isOn) {
|
|
||||||
this.lockBars();
|
|
||||||
} else {
|
|
||||||
this.unlockBars();
|
|
||||||
}
|
|
||||||
this.publisher.notifySubs(BeatGroupEvents.AutoBeatSettingsChanged);
|
|
||||||
}
|
|
||||||
|
|
||||||
barsLocked(): boolean {
|
|
||||||
return this.barSettingsLocked;
|
|
||||||
}
|
|
||||||
|
|
||||||
lockBars(): void {
|
|
||||||
this.barSettingsLocked = true;
|
|
||||||
this.publisher.notifySubs(BeatGroupEvents.LockingChanged);
|
|
||||||
}
|
|
||||||
|
|
||||||
unlockBars(): void {
|
|
||||||
this.barSettingsLocked = false;
|
|
||||||
this.publisher.notifySubs(BeatGroupEvents.LockingChanged);
|
|
||||||
}
|
|
||||||
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
import {IPublisher} from "./Publisher";
|
|
||||||
import {BeatEvents} from "./Beat";
|
|
||||||
|
|
||||||
export default interface BeatLike extends IPublisher<BeatEvents>{
|
|
||||||
setBarCount(barCount: number): void;
|
|
||||||
getBarCount(): void;
|
|
||||||
setLooping(isLooping: boolean): void;
|
|
||||||
isLooping(): boolean;
|
|
||||||
setLoopLength(loopLength: number): void;
|
|
||||||
getLoopLength(): number;
|
|
||||||
}
|
|
||||||
@@ -1,78 +0,0 @@
|
|||||||
import {IPublisher, Publisher} from "./Publisher";
|
|
||||||
import ISubscriber from "./Subscriber";
|
|
||||||
|
|
||||||
export const enum BeatUnitType {
|
|
||||||
Normal="but-0",
|
|
||||||
GhostNote="but-1",
|
|
||||||
Accent="but-2",
|
|
||||||
}
|
|
||||||
|
|
||||||
export const enum BeatUnitEvent {
|
|
||||||
Toggle="bue-0",
|
|
||||||
On="bue-1",
|
|
||||||
Off="bue-2",
|
|
||||||
TypeChange="bue-3",
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
export default class BeatUnit implements IPublisher<BeatUnitEvent> {
|
|
||||||
private static readonly TypeRotation = [
|
|
||||||
BeatUnitType.Normal,
|
|
||||||
BeatUnitType.GhostNote,
|
|
||||||
BeatUnitType.Accent,
|
|
||||||
] as const;
|
|
||||||
private publisher: Publisher<BeatUnitEvent, BeatUnit> = new Publisher<BeatUnitEvent, BeatUnit>(this);
|
|
||||||
private on = false;
|
|
||||||
private typeIndex = 0;
|
|
||||||
|
|
||||||
constructor(on = false, type = BeatUnitType.Normal) {
|
|
||||||
this.on = on;
|
|
||||||
this.setType(type);
|
|
||||||
}
|
|
||||||
|
|
||||||
addSubscriber(subscriber: ISubscriber<BeatUnitEvent>, eventType: BeatUnitEvent[]): { unbind: () => void } {
|
|
||||||
return this.publisher.addSubscriber(subscriber, eventType);
|
|
||||||
}
|
|
||||||
|
|
||||||
toggle(): void {
|
|
||||||
this.on = !this.on;
|
|
||||||
this.publisher.notifySubs(BeatUnitEvent.Toggle);
|
|
||||||
if (this.on) {
|
|
||||||
this.publisher.notifySubs(BeatUnitEvent.On);
|
|
||||||
} else {
|
|
||||||
this.publisher.notifySubs(BeatUnitEvent.Off);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
setOn(on: boolean): void {
|
|
||||||
this.on = on;
|
|
||||||
this.publisher.notifySubs(this.on ? BeatUnitEvent.On : BeatUnitEvent.Off);
|
|
||||||
}
|
|
||||||
|
|
||||||
setType(type: BeatUnitType): void {
|
|
||||||
this.typeIndex = BeatUnit.TypeRotation.indexOf(type);
|
|
||||||
this.publisher.notifySubs(BeatUnitEvent.TypeChange);
|
|
||||||
}
|
|
||||||
|
|
||||||
getType(): BeatUnitType {
|
|
||||||
return BeatUnit.TypeRotation[this.typeIndex];
|
|
||||||
}
|
|
||||||
|
|
||||||
rotateType(): void {
|
|
||||||
if (this.typeIndex === BeatUnit.TypeRotation.length - 1) {
|
|
||||||
this.typeIndex = 0;
|
|
||||||
} else {
|
|
||||||
this.typeIndex += 1;
|
|
||||||
}
|
|
||||||
this.publisher.notifySubs(BeatUnitEvent.TypeChange);
|
|
||||||
}
|
|
||||||
|
|
||||||
isOn(): boolean {
|
|
||||||
return this.on;
|
|
||||||
}
|
|
||||||
|
|
||||||
mimic(beatUnit: BeatUnit): void {
|
|
||||||
this.setOn(beatUnit.isOn());
|
|
||||||
this.setType(beatUnit.getType());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
16
src/Ref.ts
16
src/Ref.ts
@@ -1,5 +1,7 @@
|
|||||||
import {ISubscription} from "@/Publisher";
|
import {ISubscription} from "@/Publisher";
|
||||||
|
|
||||||
|
export type MaybeRef<T> = T | Ref<T>;
|
||||||
|
|
||||||
class RefSubscription implements ISubscription {
|
class RefSubscription implements ISubscription {
|
||||||
private unbindCallback?: () => void;
|
private unbindCallback?: () => void;
|
||||||
|
|
||||||
@@ -16,17 +18,27 @@ interface Stringable {
|
|||||||
toString(): string;
|
toString(): string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default class Ref<T extends { toString(): string } | string | null = Stringable> {
|
type AllowedRef = { toString(): string } | string | null;
|
||||||
|
|
||||||
|
export default class Ref<T extends AllowedRef = Stringable> {
|
||||||
private watchers: Array<(newVal: T) => void> | null = null;
|
private watchers: Array<(newVal: T) => void> | null = null;
|
||||||
private value: T;
|
private value: T;
|
||||||
private asString?: string;
|
private asString?: string;
|
||||||
private isString: boolean;
|
private isString: boolean;
|
||||||
|
|
||||||
constructor(val: T) {
|
private constructor(val: T) {
|
||||||
this.value = val;
|
this.value = val;
|
||||||
this.isString = typeof val === "string";
|
this.isString = typeof val === "string";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static new<T extends AllowedRef>(val: MaybeRef<T>): Ref<T> {
|
||||||
|
if (val instanceof Ref) {
|
||||||
|
return val;
|
||||||
|
} else {
|
||||||
|
return new Ref<T>(val);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
watch(watcher: (newVal: T) => void): ISubscription {
|
watch(watcher: (newVal: T) => void): ISubscription {
|
||||||
if (this.watchers === null) {
|
if (this.watchers === null) {
|
||||||
this.watchers = [];
|
this.watchers = [];
|
||||||
|
|||||||
10
src/Store.ts
Normal file
10
src/Store.ts
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
import Beat from "@/Beat";
|
||||||
|
|
||||||
|
export default class Store {
|
||||||
|
private beats: Beat[];
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
this.beats = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
168
src/Track.ts
Normal file
168
src/Track.ts
Normal file
@@ -0,0 +1,168 @@
|
|||||||
|
import TrackUnit from "@/TrackUnit";
|
||||||
|
import {IPublisher, Publisher} from "@/Publisher";
|
||||||
|
import ISubscriber from "@/Subscriber";
|
||||||
|
import {isPosInt} from "@/utils";
|
||||||
|
|
||||||
|
export type TrackInitOptions = {
|
||||||
|
timeSig?: {
|
||||||
|
up: number,
|
||||||
|
down: number,
|
||||||
|
},
|
||||||
|
name?: string,
|
||||||
|
bars?: number,
|
||||||
|
isLooping?: boolean,
|
||||||
|
loopLength?: number,
|
||||||
|
};
|
||||||
|
|
||||||
|
export const enum TrackEvents {
|
||||||
|
NewTimeSig="be-0",
|
||||||
|
NewBarCount="be-1",
|
||||||
|
NewName="be-2",
|
||||||
|
DisplayTypeChanged="be-3",
|
||||||
|
LoopLengthChanged="be-4",
|
||||||
|
WantsRemoval="be-5",
|
||||||
|
Baked="be-6",
|
||||||
|
}
|
||||||
|
|
||||||
|
export default class Track implements IPublisher<TrackEvents> {
|
||||||
|
private static count = 0;
|
||||||
|
private readonly key: string;
|
||||||
|
private name: string;
|
||||||
|
private timeSigUp = 4;
|
||||||
|
private timeSigDown = 4;
|
||||||
|
private readonly unitRecord: TrackUnit[] = [];
|
||||||
|
private barCount = 1;
|
||||||
|
private publisher = new Publisher<TrackEvents, Track>(this);
|
||||||
|
private loopLength: number;
|
||||||
|
private looping: boolean;
|
||||||
|
|
||||||
|
constructor(options?: TrackInitOptions) {
|
||||||
|
this.key = `B-${Track.count}`;
|
||||||
|
this.name = options?.name ?? this.key;
|
||||||
|
this.setTimeSignature({up: options?.timeSig?.up ?? 4, down: options?.timeSig?.down ?? 4});
|
||||||
|
this.setBarCount(options?.bars ?? 4);
|
||||||
|
Track.count++;
|
||||||
|
this.loopLength = options?.loopLength ?? this.timeSigUp * this.barCount;
|
||||||
|
this.looping = options?.isLooping ?? false;
|
||||||
|
}
|
||||||
|
|
||||||
|
setLoopLength(loopLength: number): void {
|
||||||
|
if (!isPosInt(loopLength) || loopLength < 2) {
|
||||||
|
loopLength = this.loopLength;
|
||||||
|
}
|
||||||
|
this.loopLength = loopLength;
|
||||||
|
this.publisher.notifySubs(TrackEvents.LoopLengthChanged);
|
||||||
|
}
|
||||||
|
|
||||||
|
setLooping(isLooping: boolean): void {
|
||||||
|
this.looping = isLooping;
|
||||||
|
this.publisher.notifySubs(TrackEvents.DisplayTypeChanged);
|
||||||
|
}
|
||||||
|
|
||||||
|
addSubscriber(subscriber: ISubscriber<TrackEvents>, eventType: TrackEvents | TrackEvents[]): { unbind: () => void } {
|
||||||
|
return this.publisher.addSubscriber(subscriber, eventType);
|
||||||
|
}
|
||||||
|
|
||||||
|
setTimeSignature(timeSig: {up?: number, down?: number}): void {
|
||||||
|
if (timeSig.up && Track.isValidTimeSigRange(timeSig.up)) {
|
||||||
|
this.timeSigUp = timeSig.up | 0;
|
||||||
|
}
|
||||||
|
if (timeSig.down && Track.isValidTimeSigRange(timeSig.down)) {
|
||||||
|
this.timeSigDown = timeSig.down | 0;
|
||||||
|
}
|
||||||
|
this.updateTrackUnitLength();
|
||||||
|
this.publisher.notifySubs(TrackEvents.NewTimeSig);
|
||||||
|
}
|
||||||
|
|
||||||
|
setTimeSigUp(timeSigUp: number): void {
|
||||||
|
this.setTimeSignature({up: timeSigUp});
|
||||||
|
}
|
||||||
|
|
||||||
|
setTimeSigDown(timeSigUp: number): void {
|
||||||
|
this.setTimeSignature({down: timeSigUp});
|
||||||
|
}
|
||||||
|
|
||||||
|
setBarCount(barCount: number): void {
|
||||||
|
if (!isPosInt(barCount) || barCount == this.barCount) {
|
||||||
|
barCount = this.barCount;
|
||||||
|
}
|
||||||
|
this.barCount = barCount;
|
||||||
|
this.updateTrackUnitLength();
|
||||||
|
this.publisher.notifySubs(TrackEvents.NewBarCount);
|
||||||
|
}
|
||||||
|
|
||||||
|
getUnitByIndex(index: number): TrackUnit | null {
|
||||||
|
if (this.looping) {
|
||||||
|
index %= this.loopLength;
|
||||||
|
}
|
||||||
|
return this.unitRecord[index] ?? null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private updateTrackUnitLength() {
|
||||||
|
const newBarCount = this.barCount * this.timeSigUp;
|
||||||
|
if (newBarCount < this.unitRecord.length) {
|
||||||
|
this.unitRecord.splice(this.barCount * this.timeSigUp, this.unitRecord.length - newBarCount);
|
||||||
|
} else if (newBarCount > this.unitRecord.length) {
|
||||||
|
const barsToAdd = newBarCount - this.unitRecord.length;
|
||||||
|
for (let i = 0; i < barsToAdd; i++) {
|
||||||
|
this.unitRecord.push(new TrackUnit());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
getTimeSigUp(): number {
|
||||||
|
return this.timeSigUp;
|
||||||
|
}
|
||||||
|
|
||||||
|
getTimeSigDown(): number {
|
||||||
|
return this.timeSigDown;
|
||||||
|
}
|
||||||
|
|
||||||
|
getBarCount(): number {
|
||||||
|
return this.barCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
getKey(): string {
|
||||||
|
return this.key;
|
||||||
|
}
|
||||||
|
|
||||||
|
static isValidTimeSigRange(sig: number): boolean {
|
||||||
|
return sig >= 2 && sig <= 32;
|
||||||
|
}
|
||||||
|
|
||||||
|
setName(newName: string): void {
|
||||||
|
this.name = newName;
|
||||||
|
this.publisher.notifySubs(TrackEvents.NewName);
|
||||||
|
}
|
||||||
|
|
||||||
|
getName(): string {
|
||||||
|
return this.name;
|
||||||
|
}
|
||||||
|
|
||||||
|
isLooping(): boolean {
|
||||||
|
return this.looping;
|
||||||
|
}
|
||||||
|
|
||||||
|
getLoopLength(): number {
|
||||||
|
return this.loopLength;
|
||||||
|
}
|
||||||
|
|
||||||
|
delete(): void {
|
||||||
|
this.publisher.notifySubs(TrackEvents.WantsRemoval);
|
||||||
|
}
|
||||||
|
|
||||||
|
bakeLoops(): void {
|
||||||
|
if (this.isLooping()) {
|
||||||
|
this.unitRecord.forEach((unit, i) => {
|
||||||
|
const reprUnitAtPos = this.getUnitByIndex(i);
|
||||||
|
if (reprUnitAtPos) {
|
||||||
|
unit.mimic(reprUnitAtPos);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
this.publisher.notifySubs(TrackEvents.Baked);
|
||||||
|
this.setLooping(false);
|
||||||
|
} else {
|
||||||
|
this.publisher.notifySubs(TrackEvents.Baked);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
78
src/TrackUnit.ts
Normal file
78
src/TrackUnit.ts
Normal file
@@ -0,0 +1,78 @@
|
|||||||
|
import {IPublisher, Publisher} from "./Publisher";
|
||||||
|
import ISubscriber from "./Subscriber";
|
||||||
|
|
||||||
|
export const enum TrackUnitType {
|
||||||
|
Normal="but-0",
|
||||||
|
GhostNote="but-1",
|
||||||
|
Accent="but-2",
|
||||||
|
}
|
||||||
|
|
||||||
|
export const enum TrackUnitEvent {
|
||||||
|
Toggle="tue-0",
|
||||||
|
On="tue-1",
|
||||||
|
Off="tue-2",
|
||||||
|
TypeChange="tue-3",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export default class TrackUnit implements IPublisher<TrackUnitEvent> {
|
||||||
|
private static readonly TypeRotation = [
|
||||||
|
TrackUnitType.Normal,
|
||||||
|
TrackUnitType.GhostNote,
|
||||||
|
TrackUnitType.Accent,
|
||||||
|
] as const;
|
||||||
|
private publisher: Publisher<TrackUnitEvent, TrackUnit> = new Publisher<TrackUnitEvent, TrackUnit>(this);
|
||||||
|
private on = false;
|
||||||
|
private typeIndex = 0;
|
||||||
|
|
||||||
|
constructor(on = false, type = TrackUnitType.Normal) {
|
||||||
|
this.on = on;
|
||||||
|
this.setType(type);
|
||||||
|
}
|
||||||
|
|
||||||
|
addSubscriber(subscriber: ISubscriber<TrackUnitEvent>, eventType: TrackUnitEvent[]): { unbind: () => void } {
|
||||||
|
return this.publisher.addSubscriber(subscriber, eventType);
|
||||||
|
}
|
||||||
|
|
||||||
|
toggle(): void {
|
||||||
|
this.on = !this.on;
|
||||||
|
this.publisher.notifySubs(TrackUnitEvent.Toggle);
|
||||||
|
if (this.on) {
|
||||||
|
this.publisher.notifySubs(TrackUnitEvent.On);
|
||||||
|
} else {
|
||||||
|
this.publisher.notifySubs(TrackUnitEvent.Off);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setOn(on: boolean): void {
|
||||||
|
this.on = on;
|
||||||
|
this.publisher.notifySubs(this.on ? TrackUnitEvent.On : TrackUnitEvent.Off);
|
||||||
|
}
|
||||||
|
|
||||||
|
setType(type: TrackUnitType): void {
|
||||||
|
this.typeIndex = TrackUnit.TypeRotation.indexOf(type);
|
||||||
|
this.publisher.notifySubs(TrackUnitEvent.TypeChange);
|
||||||
|
}
|
||||||
|
|
||||||
|
getType(): TrackUnitType {
|
||||||
|
return TrackUnit.TypeRotation[this.typeIndex];
|
||||||
|
}
|
||||||
|
|
||||||
|
rotateType(): void {
|
||||||
|
if (this.typeIndex === TrackUnit.TypeRotation.length - 1) {
|
||||||
|
this.typeIndex = 0;
|
||||||
|
} else {
|
||||||
|
this.typeIndex += 1;
|
||||||
|
}
|
||||||
|
this.publisher.notifySubs(TrackUnitEvent.TypeChange);
|
||||||
|
}
|
||||||
|
|
||||||
|
isOn(): boolean {
|
||||||
|
return this.on;
|
||||||
|
}
|
||||||
|
|
||||||
|
mimic(trackUnit: TrackUnit): void {
|
||||||
|
this.setOn(trackUnit.isOn());
|
||||||
|
this.setType(trackUnit.getType());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import {BeatUnitType} from "./BeatUnit";
|
import {TrackUnitType} from "./TrackUnit";
|
||||||
import Beat from "./Beat";
|
import Track from "./Beat";
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,65 +1,15 @@
|
|||||||
.beat > * {
|
|
||||||
padding-right: 1em;
|
|
||||||
padding-left: 1em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.vertical-mode .beat > * {
|
|
||||||
padding-right: 0;
|
|
||||||
padding-left: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.beat-unit-block {
|
|
||||||
height: 2em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.vertical-mode .beat-unit-block {
|
|
||||||
height: auto;
|
|
||||||
width: 2em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.beat-title {
|
|
||||||
width: 3em;
|
|
||||||
line-height: 32px;
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.vertical-mode .beat-title {
|
|
||||||
display: block;
|
|
||||||
width: auto;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.beat-spacer {
|
|
||||||
display: inline-block;
|
|
||||||
width: 1em;
|
|
||||||
height: 2em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.vertical-mode .beat-spacer {
|
|
||||||
display: block;
|
|
||||||
width: 2em;
|
|
||||||
height: 1em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.beat-main {
|
|
||||||
display: inline-flex;
|
|
||||||
}
|
|
||||||
|
|
||||||
.vertical-mode .beat-main {
|
|
||||||
width: 2em;
|
|
||||||
margin-right: 4px;
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
|
|
||||||
.beat-settings-container {
|
|
||||||
display: flex;
|
|
||||||
}
|
|
||||||
|
|
||||||
.beat {
|
.beat {
|
||||||
width: max-content;
|
padding: 1em;
|
||||||
margin-bottom: 4px;
|
overflow-x: scroll;
|
||||||
|
overflow-y: hidden;
|
||||||
|
display: flex;
|
||||||
|
width: inherit;
|
||||||
|
flex-direction: column;
|
||||||
}
|
}
|
||||||
|
|
||||||
.vertical-mode .beat {
|
.vertical-mode .beat {
|
||||||
display: inline-block;
|
height: inherit;
|
||||||
}
|
overflow-x: hidden;
|
||||||
|
overflow-y: scroll;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,177 +1,87 @@
|
|||||||
import UINode, {h, UINodeOptions} from "@/ui/UINode";
|
import UINode, {h, UINodeOptions} from "@/ui/UINode";
|
||||||
import Beat, {BeatEvents} from "@/Beat";
|
import Beat, {BeatEvents} from "@/Beat";
|
||||||
import ISubscriber from "@/Subscriber";
|
import TrackView from "@/ui/Track/TrackView";
|
||||||
import BeatUnitView from "@/ui/BeatUnit/BeatUnitView";
|
|
||||||
import "./Beat.css";
|
import "./Beat.css";
|
||||||
|
import ISubscriber from "@/Subscriber";
|
||||||
import {ISubscription} from "@/Publisher";
|
import {ISubscription} from "@/Publisher";
|
||||||
import Ref from "@/Ref";
|
|
||||||
|
|
||||||
export type BeatUINodeOptions = UINodeOptions & {
|
export type BeatUINodeOptions = UINodeOptions & {
|
||||||
|
title: string,
|
||||||
beat: Beat,
|
beat: Beat,
|
||||||
|
orientation?: "horizontal" | "vertical",
|
||||||
};
|
};
|
||||||
|
|
||||||
const EventTypeSubscriptions = [
|
const EventTypeSubscriptions = [
|
||||||
BeatEvents.NewName,
|
BeatEvents.TrackListChanged
|
||||||
BeatEvents.NewTimeSig,
|
|
||||||
BeatEvents.NewBarCount,
|
|
||||||
BeatEvents.DisplayTypeChanged,
|
|
||||||
BeatEvents.LoopLengthChanged,
|
|
||||||
];
|
];
|
||||||
|
|
||||||
type EventTypeSubscriptions = FlatArray<typeof EventTypeSubscriptions, 1>;
|
type EventTypeSubscriptions = FlatArray<typeof EventTypeSubscriptions, 1>;
|
||||||
|
|
||||||
export default class BeatView extends UINode implements ISubscriber<EventTypeSubscriptions> {
|
export default class BeatView extends UINode implements ISubscriber<EventTypeSubscriptions> {
|
||||||
private beat!: Beat;
|
private title: string;
|
||||||
private title = new Ref<HTMLHeadingElement | null>(null);
|
private beat: Beat;
|
||||||
private beatUnitViews: BeatUnitView[] = [];
|
private trackViews: TrackView[] = [];
|
||||||
private beatUnitViewBlock: HTMLElement | null = null;
|
private currentOrientation: "vertical" | "horizontal";
|
||||||
private lastHoveredBeatUnitView: BeatUnitView | null = null;
|
private subscription: ISubscription;
|
||||||
private sub: ISubscription | null = null;
|
|
||||||
static deselectingUnits = false;
|
|
||||||
static selectingUnits = false;
|
|
||||||
|
|
||||||
constructor(options: BeatUINodeOptions) {
|
constructor(options: BeatUINodeOptions) {
|
||||||
super(options);
|
super(options);
|
||||||
this.setBeat(options.beat);
|
this.beat = options.beat;
|
||||||
}
|
this.title = options.title;
|
||||||
|
this.currentOrientation = options.orientation ?? "horizontal";
|
||||||
setBeat(beat: Beat | null): void {
|
this.subscription = this.beat.addSubscriber(this, EventTypeSubscriptions);
|
||||||
if (beat) {
|
this.setupBeatViews();
|
||||||
this.beat = beat;
|
|
||||||
this.sub?.unbind();
|
|
||||||
this.sub = this.beat.addSubscriber(this, EventTypeSubscriptions);
|
|
||||||
this.redraw();
|
|
||||||
} else {
|
|
||||||
this.sub?.unbind();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
notify(publisher: unknown, event: EventTypeSubscriptions): void {
|
notify(publisher: unknown, event: EventTypeSubscriptions): void {
|
||||||
switch (event) {
|
if (event === BeatEvents.TrackListChanged) {
|
||||||
case BeatEvents.NewName:
|
this.setupBeatViews();
|
||||||
this.title.val!.innerText = this.beat.getName();
|
this.redraw();
|
||||||
break;
|
|
||||||
case BeatEvents.NewTimeSig:
|
|
||||||
case BeatEvents.NewBarCount:
|
|
||||||
case BeatEvents.DisplayTypeChanged:
|
|
||||||
case BeatEvents.LoopLengthChanged:
|
|
||||||
this.setupBeatUnits();
|
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private rebuildBeatUnitViews() {
|
private setupBeatViews(): void {
|
||||||
const beatUnitCount = this.beat.getBarCount() * this.beat.getTimeSigUp();
|
const newCount = this.beat.getTrackCount();
|
||||||
for (let i = 0; i < beatUnitCount; i++) {
|
for (let i = 0; i < newCount; i++) {
|
||||||
const beatUnit = this.beat.getUnitByIndex(i);
|
const beat = this.beat.getTrackByIndex(i);
|
||||||
if (beatUnit) {
|
if (beat && this.trackViews[i]) {
|
||||||
let view: BeatUnitView;
|
this.trackViews[i].setBeat(beat);
|
||||||
if (this.beatUnitViews[i]) {
|
|
||||||
view = this.beatUnitViews[i];
|
|
||||||
view.setUnit(beatUnit);
|
|
||||||
} else {
|
|
||||||
view = new BeatUnitView({beatUnit});
|
|
||||||
this.beatUnitViews.push(view);
|
|
||||||
view.onHover(() => this.onBeatViewHover(view));
|
|
||||||
view.onMouseDown((event: MouseEvent) => this.onBeatUnitClick(event.button, i));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
const deadViews = this.beatUnitViews.splice(beatUnitCount, this.beatUnitViews.length - beatUnitCount);
|
|
||||||
deadViews.forEach(beatUnitView => beatUnitView.setUnit(null));
|
|
||||||
}
|
|
||||||
|
|
||||||
private onBeatUnitClick(button: number, index: number) {
|
|
||||||
if (button === 0) {
|
|
||||||
BeatView.selectingUnits = true;
|
|
||||||
this.beat.getUnitByIndex(index)?.toggle();
|
|
||||||
} else if (button === 2) {
|
|
||||||
BeatView.deselectingUnits = true;
|
|
||||||
this.beat.getUnitByIndex(index)?.setOn(false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private onBeatViewHover(beatView: BeatUnitView) {
|
|
||||||
this.lastHoveredBeatUnitView = beatView;
|
|
||||||
if (BeatView.selectingUnits) {
|
|
||||||
this.lastHoveredBeatUnitView.turnOn();
|
|
||||||
} else if (BeatView.deselectingUnits) {
|
|
||||||
this.lastHoveredBeatUnitView.turnOff();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private buildBeatUnitViewBlock(): void {
|
|
||||||
const beatUnitNodes: HTMLElement[] = [];
|
|
||||||
for (let i = 0; i < this.beatUnitViews.length; i++) {
|
|
||||||
beatUnitNodes.push(this.beatUnitViews[i].render());
|
|
||||||
}
|
|
||||||
if (this.beatUnitViewBlock) {
|
|
||||||
this.beatUnitViewBlock.replaceChildren(...beatUnitNodes);
|
|
||||||
} else {
|
|
||||||
this.beatUnitViewBlock = h("div", {
|
|
||||||
classes: ["beat-unit-block"],
|
|
||||||
}, [
|
|
||||||
...beatUnitNodes
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private respaceBeatUnits(): void {
|
|
||||||
if (!this.beatUnitViewBlock) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
this.beatUnitViewBlock.querySelectorAll(".beat-spacer").forEach(spacer => spacer.remove());
|
|
||||||
const barLength = this.beat.getTimeSigUp();
|
|
||||||
const barCount = this.beat.getBarCount();
|
|
||||||
let bars = 0;
|
|
||||||
let i = -1;
|
|
||||||
let spacersInserted = false;
|
|
||||||
while (!spacersInserted) {
|
|
||||||
i += barLength;
|
|
||||||
const newSpacer = h("div", {classes: ["beat-spacer"]});
|
|
||||||
const leftNeighbour = this.beatUnitViewBlock.children.item(i);
|
|
||||||
if (leftNeighbour) {
|
|
||||||
leftNeighbour.insertAdjacentElement("afterend", newSpacer);
|
|
||||||
} else {
|
} else {
|
||||||
break;
|
this.trackViews.push(new TrackView({track: this.beat.getTrackByIndex(i)}));
|
||||||
}
|
|
||||||
i++;
|
|
||||||
bars++;
|
|
||||||
if (bars === barCount) {
|
|
||||||
spacersInserted = true;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
const deadTrackViews = this.trackViews.splice(newCount, this.trackViews.length - newCount);
|
||||||
|
deadTrackViews.forEach(beatView => beatView.setBeat(null));
|
||||||
|
if (this.currentOrientation === "horizontal") {
|
||||||
|
this.reverseDisplayOrder();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private setupBeatUnits(): void {
|
setOrientation(orientation: "vertical" | "horizontal"): void {
|
||||||
this.rebuildBeatUnitViews();
|
if (this.currentOrientation !== orientation) {
|
||||||
this.buildBeatUnitViewBlock();
|
this.reverseDisplayOrder();
|
||||||
this.respaceBeatUnits();
|
this.currentOrientation = orientation;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
build(): HTMLElement {
|
private reverseDisplayOrder(): void {
|
||||||
this.setupBeatUnits();
|
this.trackViews.reverse();
|
||||||
if (!this.beatUnitViewBlock) {
|
this.getNode().classList.toggle("vertical");
|
||||||
throw new Error("Beat unit block setup failed!");
|
this.redraw();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setBeatGroup(newBeatGroup: Beat): void {
|
||||||
|
this.beat = newBeatGroup;
|
||||||
|
this.subscription.unbind();
|
||||||
|
this.subscription = this.beat.addSubscriber(this, BeatEvents.TrackListChanged);
|
||||||
|
this.setupBeatViews();
|
||||||
|
this.redraw();
|
||||||
|
}
|
||||||
|
|
||||||
|
build(): HTMLDivElement {
|
||||||
return h("div", {
|
return h("div", {
|
||||||
classes: ["beat"],
|
classes: ["beat"],
|
||||||
}, [
|
},[
|
||||||
h("div", {
|
...this.trackViews
|
||||||
classes: ["beat-main"],
|
|
||||||
}, [
|
|
||||||
h("h3", {
|
|
||||||
innerText: this.beat.getName(),
|
|
||||||
saveTo: this.title,
|
|
||||||
classes: ["beat-title"],
|
|
||||||
}),
|
|
||||||
this.beatUnitViewBlock,
|
|
||||||
]),
|
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
window.addEventListener("mouseup", () => {
|
|
||||||
BeatView.selectingUnits = false;
|
|
||||||
BeatView.deselectingUnits = false;
|
|
||||||
});
|
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
.beat-group {
|
|
||||||
padding: 1em;
|
|
||||||
overflow-x: scroll;
|
|
||||||
overflow-y: hidden;
|
|
||||||
display: flex;
|
|
||||||
width: inherit;
|
|
||||||
flex-direction: column;
|
|
||||||
}
|
|
||||||
|
|
||||||
.vertical-mode .beat-group {
|
|
||||||
height: inherit;
|
|
||||||
overflow-x: hidden;
|
|
||||||
overflow-y: scroll;
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
@@ -1,87 +0,0 @@
|
|||||||
import UINode, {h, UINodeOptions} from "@/ui/UINode";
|
|
||||||
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 = [
|
|
||||||
BeatGroupEvents.BeatListChanged
|
|
||||||
];
|
|
||||||
type EventTypeSubscriptions = FlatArray<typeof EventTypeSubscriptions, 1>;
|
|
||||||
|
|
||||||
export default class BeatGroupView extends UINode implements ISubscriber<EventTypeSubscriptions> {
|
|
||||||
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.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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private setupBeatViews(): void {
|
|
||||||
const newCount = this.beatGroup.getBeatCount();
|
|
||||||
for (let i = 0; i < newCount; i++) {
|
|
||||||
const beat = this.beatGroup.getBeatByIndex(i);
|
|
||||||
if (beat && this.beatViews[i]) {
|
|
||||||
this.beatViews[i].setBeat(beat);
|
|
||||||
} else {
|
|
||||||
this.beatViews.push(new BeatView({beat: this.beatGroup.getBeatByIndex(i)}));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
const deadBeatViews = this.beatViews.splice(newCount, this.beatViews.length - newCount);
|
|
||||||
deadBeatViews.forEach(beatView => beatView.setBeat(null));
|
|
||||||
if (this.currentOrientation === "horizontal") {
|
|
||||||
this.reverseDisplayOrder();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
setOrientation(orientation: "vertical" | "horizontal"): void {
|
|
||||||
if (this.currentOrientation !== orientation) {
|
|
||||||
this.reverseDisplayOrder();
|
|
||||||
this.currentOrientation = orientation;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private reverseDisplayOrder(): void {
|
|
||||||
this.beatViews.reverse();
|
|
||||||
this.getNode().classList.toggle("vertical");
|
|
||||||
this.redraw();
|
|
||||||
}
|
|
||||||
|
|
||||||
setBeatGroup(newBeatGroup: BeatGroup): void {
|
|
||||||
this.beatGroup = newBeatGroup;
|
|
||||||
this.subscription.unbind();
|
|
||||||
this.subscription = this.beatGroup.addSubscriber(this, BeatGroupEvents.BeatListChanged);
|
|
||||||
this.setupBeatViews();
|
|
||||||
this.redraw();
|
|
||||||
}
|
|
||||||
|
|
||||||
build(): HTMLDivElement {
|
|
||||||
return h("div", {
|
|
||||||
classes: ["beat-group"],
|
|
||||||
},[
|
|
||||||
...this.beatViews
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,20 +0,0 @@
|
|||||||
.beat-group-settings {
|
|
||||||
}
|
|
||||||
|
|
||||||
.beat-group-settings-options {
|
|
||||||
padding: 1em;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
justify-content: space-evenly;
|
|
||||||
}
|
|
||||||
|
|
||||||
.beat-group-settings-option {
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.beat-group-settings-option-group {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
.beat-group-settings-option-group.visible {
|
|
||||||
display: inline-block;
|
|
||||||
}
|
|
||||||
@@ -1,140 +0,0 @@
|
|||||||
import "./BeatGroupSettings.css";
|
|
||||||
import UINode, {h, UINodeOptions} from "@/ui/UINode";
|
|
||||||
import NumberInputView from "@/ui/Widgets/NumberInput/NumberInputView";
|
|
||||||
import ISubscriber from "@/Subscriber";
|
|
||||||
import BeatGroup, {BeatGroupEvents} from "@/BeatGroup";
|
|
||||||
import BoolBoxView from "@/ui/Widgets/BoolBox/BoolBoxView";
|
|
||||||
import BeatSettingsView from "@/ui/BeatSettings/BeatSettingsView";
|
|
||||||
import ActionButtonView from "@/ui/Widgets/ActionButton/ActionButtonView";
|
|
||||||
|
|
||||||
export type BeatGroupSettingsUINodeOptions = UINodeOptions & {
|
|
||||||
beatGroup: BeatGroup,
|
|
||||||
};
|
|
||||||
|
|
||||||
const EventTypeSubscriptions = [
|
|
||||||
BeatGroupEvents.BarCountChanged,
|
|
||||||
BeatGroupEvents.TimeSigUpChanged,
|
|
||||||
BeatGroupEvents.GlobalDisplayTypeChanged,
|
|
||||||
BeatGroupEvents.BeatListChanged,
|
|
||||||
BeatGroupEvents.LockingChanged,
|
|
||||||
BeatGroupEvents.AutoBeatSettingsChanged,
|
|
||||||
];
|
|
||||||
type EventTypeSubscriptions = FlatArray<typeof EventTypeSubscriptions, 1>;
|
|
||||||
|
|
||||||
export default class BeatGroupSettingsView extends UINode implements ISubscriber<EventTypeSubscriptions> {
|
|
||||||
private beatGroup: BeatGroup;
|
|
||||||
private barCountInput!: NumberInputView;
|
|
||||||
private timeSigUpInput!: NumberInputView;
|
|
||||||
private autoBeatLengthCheckbox!: BoolBoxView;
|
|
||||||
private beatSettingsViews: BeatSettingsView[] = [];
|
|
||||||
private beatSettingsContainer!: HTMLDivElement;
|
|
||||||
|
|
||||||
constructor(options: BeatGroupSettingsUINodeOptions) {
|
|
||||||
super(options);
|
|
||||||
this.beatGroup = options.beatGroup;
|
|
||||||
this.setupBindings();
|
|
||||||
}
|
|
||||||
|
|
||||||
setBeatGroup(newBeatGroup: BeatGroup): void {
|
|
||||||
this.beatGroup = newBeatGroup;
|
|
||||||
this.setupBindings();
|
|
||||||
EventTypeSubscriptions.forEach(eventType => this.notify(null, eventType));
|
|
||||||
}
|
|
||||||
|
|
||||||
setupBindings(): void {
|
|
||||||
this.beatGroup.addSubscriber(this, EventTypeSubscriptions);
|
|
||||||
}
|
|
||||||
|
|
||||||
notify(publisher: unknown, event: EventTypeSubscriptions): void {
|
|
||||||
switch(event) {
|
|
||||||
case BeatGroupEvents.BarCountChanged:
|
|
||||||
this.barCountInput.setValue(this.beatGroup.getBarCount());
|
|
||||||
break;
|
|
||||||
case BeatGroupEvents.TimeSigUpChanged:
|
|
||||||
this.timeSigUpInput.setValue(this.beatGroup.getTimeSigUp());
|
|
||||||
break;
|
|
||||||
case BeatGroupEvents.BeatListChanged:
|
|
||||||
this.remakeBeatSettingsViews();
|
|
||||||
break;
|
|
||||||
case BeatGroupEvents.LockingChanged:
|
|
||||||
if (this.beatGroup.barsLocked()) {
|
|
||||||
this.barCountInput.disable();
|
|
||||||
} else {
|
|
||||||
this.barCountInput.enable();
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case BeatGroupEvents.AutoBeatSettingsChanged:
|
|
||||||
this.autoBeatLengthCheckbox.setValue(this.beatGroup.autoBeatLengthOn());
|
|
||||||
break;
|
|
||||||
case BeatGroupEvents.GlobalDisplayTypeChanged:
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private remakeBeatSettingsViews() {
|
|
||||||
const beatCount = this.beatGroup.getBeatCount();
|
|
||||||
this.beatSettingsViews.splice(beatCount, this.beatSettingsViews.length - beatCount);
|
|
||||||
for (let i = 0; i < beatCount; i++) {
|
|
||||||
if (this.beatSettingsViews[i]) {
|
|
||||||
this.beatSettingsViews[i].setBeat(this.beatGroup.getBeatByIndex(i));
|
|
||||||
} else {
|
|
||||||
this.beatSettingsViews.push(new BeatSettingsView({ beat: this.beatGroup.getBeatByIndex(i) }));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (!this.beatSettingsContainer) {
|
|
||||||
this.beatSettingsContainer = h("div", {}, this.beatSettingsViews);
|
|
||||||
} else {
|
|
||||||
this.beatSettingsContainer.replaceChildren(...this.beatSettingsViews.reverse().map(view => view.render()));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
build(): HTMLElement {
|
|
||||||
this.barCountInput = new NumberInputView({
|
|
||||||
label: "Bars:",
|
|
||||||
initialValue: this.beatGroup.getBarCount(),
|
|
||||||
setter: (input: number) => this.beatGroup.setBarCount(input),
|
|
||||||
getter: () => this.beatGroup.getBarCount(),
|
|
||||||
});
|
|
||||||
this.timeSigUpInput = new NumberInputView({
|
|
||||||
label: "Boxes per bar:",
|
|
||||||
initialValue: this.beatGroup.getTimeSigUp(),
|
|
||||||
setter: (input: number) => this.beatGroup.setTimeSigUp(input),
|
|
||||||
getter: () => this.beatGroup.getTimeSigUp(),
|
|
||||||
});
|
|
||||||
this.autoBeatLengthCheckbox = new BoolBoxView({
|
|
||||||
label: "Auto beat length:",
|
|
||||||
value: this.beatGroup.autoBeatLengthOn(),
|
|
||||||
onInput: (isChecked: boolean) => this.beatGroup.setIsUsingAutoBeatLength(isChecked),
|
|
||||||
});
|
|
||||||
this.remakeBeatSettingsViews();
|
|
||||||
return h("div", {
|
|
||||||
classes: ["beat-group-settings"],
|
|
||||||
}, [
|
|
||||||
h("div", {
|
|
||||||
classes: ["beat-group-settings-options"],
|
|
||||||
}, [
|
|
||||||
h("div", {
|
|
||||||
classes: ["beat-group-settings-boxes", "beat-group-settings-option"],
|
|
||||||
}, [
|
|
||||||
this.timeSigUpInput,
|
|
||||||
]),
|
|
||||||
h("div", {
|
|
||||||
classes: ["beat-group-settings-bar-count", "beat-group-settings-option"]
|
|
||||||
,
|
|
||||||
}, [
|
|
||||||
this.barCountInput,
|
|
||||||
]),
|
|
||||||
h("div", {
|
|
||||||
classes: ["beat-group-settings-bar-count", "beat-group-settings-option"],
|
|
||||||
}, [
|
|
||||||
this.autoBeatLengthCheckbox,
|
|
||||||
]),
|
|
||||||
new ActionButtonView({
|
|
||||||
label: "New Track",
|
|
||||||
onClick: () => this.beatGroup.addBeat(),
|
|
||||||
}),
|
|
||||||
this.beatSettingsContainer,
|
|
||||||
]),
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,54 +1,20 @@
|
|||||||
.beat-settings {
|
.beat-settings {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.beat-settings-title-container {
|
.beat-settings-options {
|
||||||
|
padding: 1em;
|
||||||
}
|
|
||||||
|
|
||||||
.beat-settings-title-container input {
|
|
||||||
min-width: 100%;
|
|
||||||
height: 2em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.beat-settings-title-container > div {
|
|
||||||
width: 100%;
|
|
||||||
font-weight: bold;
|
|
||||||
padding: 0.5em;
|
|
||||||
transition: background-color 200ms;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
.beat-settings-title-container > div:hover {
|
|
||||||
background-color: var(--color-ui-neutral-dark-hover);
|
|
||||||
}
|
|
||||||
|
|
||||||
.beat-settings-lower {
|
|
||||||
height: 3.5em;
|
|
||||||
display: flex;
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: space-evenly;
|
||||||
|
}
|
||||||
|
|
||||||
|
.beat-settings-option {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
align-items: center;
|
|
||||||
justify-content: space-between;
|
|
||||||
margin-bottom: 0.5em;
|
|
||||||
}
|
|
||||||
.beat-settings-lower > * {
|
|
||||||
margin-right: 0.2em;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.beat-settings-lower:last-child {
|
.beat-settings-option-group {
|
||||||
margin-right: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.beat-settings .loop-settings {
|
|
||||||
text-align: left;
|
|
||||||
flex: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.beat-settings .loop-settings-option.hide {
|
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
.beat-settings-option-group.visible {
|
||||||
.beat-settings .loop-settings-option {
|
display: inline-block;
|
||||||
flex: auto;
|
}
|
||||||
padding-right: 1em;
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,130 +1,140 @@
|
|||||||
import "./BeatSettings.css";
|
import "./BeatSettings.css";
|
||||||
import Beat, {BeatEvents} from "@/Beat";
|
|
||||||
import UINode, {h, UINodeOptions} from "@/ui/UINode";
|
import UINode, {h, UINodeOptions} from "@/ui/UINode";
|
||||||
import ISubscriber from "@/Subscriber";
|
|
||||||
import {ISubscription} from "@/Publisher";
|
|
||||||
import NumberInputView from "@/ui/Widgets/NumberInput/NumberInputView";
|
import NumberInputView from "@/ui/Widgets/NumberInput/NumberInputView";
|
||||||
|
import ISubscriber from "@/Subscriber";
|
||||||
|
import Beat, {BeatEvents} from "@/Beat";
|
||||||
import BoolBoxView from "@/ui/Widgets/BoolBox/BoolBoxView";
|
import BoolBoxView from "@/ui/Widgets/BoolBox/BoolBoxView";
|
||||||
|
import TrackSettingsView from "@/ui/TrackSettings/TrackSettingsView";
|
||||||
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 BeatSettingsUINodeOptions = UINodeOptions & {
|
||||||
beat: Beat,
|
beat: Beat,
|
||||||
};
|
};
|
||||||
|
|
||||||
const EventTypeSubscriptions = [
|
const EventTypeSubscriptions = [
|
||||||
BeatEvents.NewName,
|
BeatEvents.TimeSigUpChanged,
|
||||||
BeatEvents.LoopLengthChanged,
|
BeatEvents.BarCountChanged,
|
||||||
BeatEvents.DisplayTypeChanged,
|
BeatEvents.GlobalDisplayTypeChanged,
|
||||||
|
BeatEvents.TrackListChanged,
|
||||||
|
BeatEvents.LockingChanged,
|
||||||
|
BeatEvents.AutoBeatSettingsChanged,
|
||||||
];
|
];
|
||||||
type EventTypeSubscriptions = FlatArray<typeof EventTypeSubscriptions, 1>;
|
type EventTypeSubscriptions = FlatArray<typeof EventTypeSubscriptions, 1>;
|
||||||
|
|
||||||
export default class BeatSettingsView extends UINode implements ISubscriber<EventTypeSubscriptions> {
|
export default class BeatSettingsView extends UINode implements ISubscriber<EventTypeSubscriptions> {
|
||||||
private beat: Beat;
|
private beat: Beat;
|
||||||
private loopLengthInput!: NumberInputView;
|
private barCountInput!: NumberInputView;
|
||||||
private bakeButton!: ActionButtonView;
|
private timeSigUpInput!: NumberInputView;
|
||||||
private loopCheckbox!: BoolBoxView;
|
private autoBeatLengthCheckbox!: BoolBoxView;
|
||||||
private loopLengthSection!: HTMLDivElement;
|
private trackSettingsViews: TrackSettingsView[] = [];
|
||||||
private sub!: ISubscription;
|
private trackSettingsContainer!: HTMLDivElement;
|
||||||
private title!: EditableTextFieldView;
|
|
||||||
private editingTitle: boolean;
|
|
||||||
|
|
||||||
constructor(options: BeatSettingsViewUINodeOptions) {
|
constructor(options: BeatSettingsUINodeOptions) {
|
||||||
super(options);
|
super(options);
|
||||||
this.editingTitle = false;
|
|
||||||
this.beat = options.beat;
|
this.beat = options.beat;
|
||||||
this.setupBindings();
|
this.setupBindings();
|
||||||
}
|
}
|
||||||
|
|
||||||
private setupBindings() {
|
setBeatGroup(newBeat: Beat): void {
|
||||||
this.sub = this.beat.addSubscriber(this, EventTypeSubscriptions);
|
this.beat = newBeat;
|
||||||
}
|
|
||||||
|
|
||||||
setBeat(beat: Beat): void {
|
|
||||||
this.sub.unbind();
|
|
||||||
this.beat = beat;
|
|
||||||
this.setupBindings();
|
this.setupBindings();
|
||||||
EventTypeSubscriptions.forEach(eventType => this.notify(null, eventType));
|
EventTypeSubscriptions.forEach(eventType => this.notify(null, eventType));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setupBindings(): void {
|
||||||
|
this.beat.addSubscriber(this, EventTypeSubscriptions);
|
||||||
|
}
|
||||||
|
|
||||||
notify(publisher: unknown, event: EventTypeSubscriptions): void {
|
notify(publisher: unknown, event: EventTypeSubscriptions): void {
|
||||||
switch(event) {
|
switch(event) {
|
||||||
case BeatEvents.NewName:
|
case BeatEvents.BarCountChanged:
|
||||||
this.title.setText(this.beat.getName());
|
this.barCountInput.setValue(this.beat.getBarCount());
|
||||||
break;
|
break;
|
||||||
case BeatEvents.LoopLengthChanged:
|
case BeatEvents.TimeSigUpChanged:
|
||||||
this.loopLengthInput.setValue(this.beat.getLoopLength());
|
this.timeSigUpInput.setValue(this.beat.getTimeSigUp());
|
||||||
break;
|
break;
|
||||||
case BeatEvents.DisplayTypeChanged:
|
case BeatEvents.TrackListChanged:
|
||||||
this.loopCheckbox.setValue(this.beat.isLooping());
|
this.remakeBeatSettingsViews();
|
||||||
this.bakeButton.setDisabled(!this.beat.isLooping());
|
break;
|
||||||
if (this.beat.isLooping()) {
|
case BeatEvents.LockingChanged:
|
||||||
this.loopLengthSection.classList.remove("hide");
|
if (this.beat.barsLocked()) {
|
||||||
|
this.barCountInput.disable();
|
||||||
} else {
|
} else {
|
||||||
this.loopLengthSection.classList.add("hide");
|
this.barCountInput.enable();
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
case BeatEvents.AutoBeatSettingsChanged:
|
||||||
|
this.autoBeatLengthCheckbox.setValue(this.beat.autoBeatLengthOn());
|
||||||
|
break;
|
||||||
|
case BeatEvents.GlobalDisplayTypeChanged:
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
build(): HTMLElement {
|
private remakeBeatSettingsViews() {
|
||||||
this.title = new EditableTextFieldView({
|
const trackCount = this.beat.getTrackCount();
|
||||||
initialText: this.beat.getName(),
|
this.trackSettingsViews.splice(trackCount, this.trackSettingsViews.length - trackCount);
|
||||||
setter: (newText) => this.beat.setName(newText),
|
for (let i = 0; i < trackCount; i++) {
|
||||||
});
|
if (this.trackSettingsViews[i]) {
|
||||||
this.bakeButton = new ActionButtonView({
|
this.trackSettingsViews[i].setBeat(this.beat.getTrackByIndex(i));
|
||||||
icon: "snowflake",
|
} else {
|
||||||
type: "secondary",
|
this.trackSettingsViews.push(new TrackSettingsView({ track: this.beat.getTrackByIndex(i) }));
|
||||||
alt: "Bake Loops",
|
}
|
||||||
disabled: !this.beat.isLooping(),
|
|
||||||
onClick: () => this.beat.bakeLoops(),
|
|
||||||
});
|
|
||||||
this.loopLengthInput = new NumberInputView({
|
|
||||||
initialValue: this.beat.getLoopLength(),
|
|
||||||
onDecrement: () => this.beat.setLoopLength(this.beat.getLoopLength() - 1),
|
|
||||||
onIncrement: () => this.beat.setLoopLength(this.beat.getLoopLength() + 1),
|
|
||||||
onNewInput: (input: number) => this.beat.setLoopLength(input),
|
|
||||||
});
|
|
||||||
this.loopCheckbox = new BoolBoxView({
|
|
||||||
label: "Loop:",
|
|
||||||
value: this.beat.isLooping(),
|
|
||||||
onInput: (isChecked: boolean) => this.beat.setLooping(isChecked),
|
|
||||||
});
|
|
||||||
this.loopLengthSection = h("div", {
|
|
||||||
classes: ["loop-settings-option"],
|
|
||||||
}, [
|
|
||||||
this.loopLengthInput,
|
|
||||||
]);
|
|
||||||
if (this.beat.isLooping()) {
|
|
||||||
this.loopLengthSection.classList.remove("hide");
|
|
||||||
} else {
|
|
||||||
this.loopLengthSection.classList.add("hide");
|
|
||||||
}
|
}
|
||||||
|
if (!this.trackSettingsContainer) {
|
||||||
|
this.trackSettingsContainer = h("div", {}, this.trackSettingsViews);
|
||||||
|
} else {
|
||||||
|
this.trackSettingsContainer.replaceChildren(...this.trackSettingsViews.reverse().map(view => view.render()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
build(): HTMLElement {
|
||||||
|
this.barCountInput = new NumberInputView({
|
||||||
|
label: "Bars:",
|
||||||
|
initialValue: this.beat.getBarCount(),
|
||||||
|
setter: (input: number) => this.beat.setBarCount(input),
|
||||||
|
getter: () => this.beat.getBarCount(),
|
||||||
|
});
|
||||||
|
this.timeSigUpInput = new NumberInputView({
|
||||||
|
label: "Boxes per bar:",
|
||||||
|
initialValue: this.beat.getTimeSigUp(),
|
||||||
|
setter: (input: number) => this.beat.setTimeSigUp(input),
|
||||||
|
getter: () => this.beat.getTimeSigUp(),
|
||||||
|
});
|
||||||
|
this.autoBeatLengthCheckbox = new BoolBoxView({
|
||||||
|
label: "Auto beat length:",
|
||||||
|
value: this.beat.autoBeatLengthOn(),
|
||||||
|
onInput: (isChecked: boolean) => this.beat.setIsUsingAutoBeatLength(isChecked),
|
||||||
|
});
|
||||||
|
this.remakeBeatSettingsViews();
|
||||||
return h("div", {
|
return h("div", {
|
||||||
classes: ["beat-settings"],
|
classes: ["beat-settings"],
|
||||||
}, [
|
}, [
|
||||||
h("div", {
|
h("div", {
|
||||||
classes: ["beat-settings-title-container"]
|
classes: ["beat-settings-options"],
|
||||||
}, [
|
}, [
|
||||||
this.title,
|
|
||||||
]),
|
|
||||||
h("div", {
|
|
||||||
classes: ["beat-settings-lower"],
|
|
||||||
}, [
|
|
||||||
this.bakeButton,
|
|
||||||
new ActionButtonView({
|
|
||||||
icon: "trash",
|
|
||||||
type: "secondary",
|
|
||||||
alt: "Delete Track",
|
|
||||||
onClick: () => this.beat.delete(),
|
|
||||||
}),
|
|
||||||
h("div", {
|
h("div", {
|
||||||
classes: ["loop-settings"],
|
classes: ["beat-settings-boxes", "beat-settings-option"],
|
||||||
}, [
|
}, [
|
||||||
this.loopCheckbox,
|
this.timeSigUpInput,
|
||||||
]),
|
]),
|
||||||
this.loopLengthSection,
|
h("div", {
|
||||||
|
classes: ["beat-settings-bar-count", "beat-settings-option"]
|
||||||
|
,
|
||||||
|
}, [
|
||||||
|
this.barCountInput,
|
||||||
|
]),
|
||||||
|
h("div", {
|
||||||
|
classes: ["beat-settings-bar-count", "beat-settings-option"],
|
||||||
|
}, [
|
||||||
|
this.autoBeatLengthCheckbox,
|
||||||
|
]),
|
||||||
|
new ActionButtonView({
|
||||||
|
label: "New Track",
|
||||||
|
onClick: () => this.beat.addTrack(),
|
||||||
|
}),
|
||||||
|
this.trackSettingsContainer,
|
||||||
]),
|
]),
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,39 +1,39 @@
|
|||||||
import UINode, {h, UINodeOptions} from "@/ui/UINode";
|
import UINode, {h, UINodeOptions} from "@/ui/UINode";
|
||||||
import BeatGroupView from "@/ui/BeatGroup/BeatGroupView";
|
import BeatView from "@/ui/Beat/BeatView";
|
||||||
import BeatGroup from "@/BeatGroup";
|
import Beat from "@/Beat";
|
||||||
import "./Root.css";
|
import "./Root.css";
|
||||||
import BeatGroupSettingsView from "@/ui/BeatGroupSettings/BeatGroupSettingsView";
|
import BeatSettingsView from "@/ui/BeatSettings/BeatSettingsView";
|
||||||
import IconView from "@/ui/Widgets/Icon/IconView";
|
import IconView from "@/ui/Widgets/Icon/IconView";
|
||||||
import StageTitleBarView from "@/ui/StageTitleBar/StageTitleBarView";
|
import StageTitleBarView from "@/ui/StageTitleBar/StageTitleBarView";
|
||||||
import Ref from "@/Ref";
|
import Ref from "@/Ref";
|
||||||
|
|
||||||
export type RootUINodeOptions = UINodeOptions & {
|
export type RootUINodeOptions = UINodeOptions & {
|
||||||
title: string,
|
title: string,
|
||||||
mainBeatGroup?: BeatGroup,
|
mainBeat?: Beat,
|
||||||
orientation?: "horizontal" | "vertical",
|
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 beatView: BeatView;
|
||||||
private focusedBeatGroup: BeatGroup;
|
private focusedBeat: Beat;
|
||||||
private beatGroupSettingsView: BeatGroupSettingsView;
|
private beatSettingsView: BeatSettingsView;
|
||||||
private currentOrientation: "horizontal" | "vertical";
|
private currentOrientation: "horizontal" | "vertical";
|
||||||
private stageTitleBarView: StageTitleBarView;
|
private stageTitleBarView: StageTitleBarView;
|
||||||
private showHideSidebarButton: Ref<HTMLDivElement | null> = new Ref<HTMLDivElement | null>(null);
|
private showHideSidebarButton: Ref<HTMLDivElement | null> = Ref.new<HTMLDivElement | null>(null);
|
||||||
private sidebarActive = true;
|
private sidebarActive = true;
|
||||||
|
|
||||||
constructor(options: RootUINodeOptions) {
|
constructor(options: RootUINodeOptions) {
|
||||||
super(options);
|
super(options);
|
||||||
this.currentOrientation = options.orientation ?? "horizontal";
|
this.currentOrientation = options.orientation ?? "horizontal";
|
||||||
this.focusedBeatGroup = options.mainBeatGroup ?? RootView.defaultMainBeatGroup();
|
this.focusedBeat = options.mainBeat ?? RootView.defaultMainBeatGroup();
|
||||||
this.beatGroupView = new BeatGroupView({
|
this.beatView = new BeatView({
|
||||||
title: options.title,
|
title: options.title,
|
||||||
beatGroup: this.focusedBeatGroup,
|
beat: this.focusedBeat,
|
||||||
orientation: this.currentOrientation,
|
orientation: this.currentOrientation,
|
||||||
});
|
});
|
||||||
this.stageTitleBarView = new StageTitleBarView({beatGroup: this.focusedBeatGroup});
|
this.stageTitleBarView = new StageTitleBarView({beat: this.focusedBeat});
|
||||||
this.beatGroupSettingsView = new BeatGroupSettingsView({beatGroup: this.focusedBeatGroup});
|
this.beatSettingsView = new BeatSettingsView({beat: this.focusedBeat});
|
||||||
this.title = options.title;
|
this.title = options.title;
|
||||||
this.setOrientation(this.currentOrientation);
|
this.setOrientation(this.currentOrientation);
|
||||||
this.openSidebarForDesktop();
|
this.openSidebarForDesktop();
|
||||||
@@ -46,25 +46,25 @@ export default class RootView extends UINode {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static defaultMainBeatGroup(): BeatGroup {
|
static defaultMainBeatGroup(): Beat {
|
||||||
const defaultSettings = {
|
const defaultSettings = {
|
||||||
barCount: 2,
|
barCount: 2,
|
||||||
isLooping: false,
|
isLooping: false,
|
||||||
timeSigUp: 8,
|
timeSigUp: 8,
|
||||||
};
|
};
|
||||||
const mainBeatGroup = new BeatGroup(defaultSettings);
|
const mainBeatGroup = new Beat(defaultSettings);
|
||||||
mainBeatGroup.addBeat({name: "LF"});
|
mainBeatGroup.addTrack({name: "LF"});
|
||||||
mainBeatGroup.addBeat({name: "LH"});
|
mainBeatGroup.addTrack({name: "LH"});
|
||||||
mainBeatGroup.addBeat({name: "RH"});
|
mainBeatGroup.addTrack({name: "RH"});
|
||||||
mainBeatGroup.addBeat({name: "RF"});
|
mainBeatGroup.addTrack({name: "RF"});
|
||||||
return mainBeatGroup;
|
return mainBeatGroup;
|
||||||
}
|
}
|
||||||
|
|
||||||
setMainBeatGroup(beatGroup: BeatGroup): void {
|
setMainBeatGroup(beat: Beat): void {
|
||||||
this.focusedBeatGroup = beatGroup;
|
this.focusedBeat = beat;
|
||||||
this.beatGroupSettingsView.setBeatGroup(this.focusedBeatGroup);
|
this.beatSettingsView.setBeatGroup(this.focusedBeat);
|
||||||
this.beatGroupView.setBeatGroup(this.focusedBeatGroup);
|
this.beatView.setBeatGroup(this.focusedBeat);
|
||||||
this.stageTitleBarView.setBeatGroup(this.focusedBeatGroup);
|
this.stageTitleBarView.setBeat(this.focusedBeat);
|
||||||
}
|
}
|
||||||
|
|
||||||
toggleSidebar(): void {
|
toggleSidebar(): void {
|
||||||
@@ -88,7 +88,7 @@ export default class RootView extends UINode {
|
|||||||
} else {
|
} else {
|
||||||
this.getNode().classList.remove("vertical-mode");
|
this.getNode().classList.remove("vertical-mode");
|
||||||
}
|
}
|
||||||
this.beatGroupView.setOrientation(orientation);
|
this.beatView.setOrientation(orientation);
|
||||||
}
|
}
|
||||||
|
|
||||||
private sidebarText(): string {
|
private sidebarText(): string {
|
||||||
@@ -124,7 +124,7 @@ export default class RootView extends UINode {
|
|||||||
h("div", {
|
h("div", {
|
||||||
classes: ["root-quick-access-button"],
|
classes: ["root-quick-access-button"],
|
||||||
title: "Bake all tracks",
|
title: "Bake all tracks",
|
||||||
onclick: () => this.focusedBeatGroup.bakeLoops(),
|
onclick: () => this.focusedBeat.bakeLoops(),
|
||||||
}, [
|
}, [
|
||||||
new IconView({
|
new IconView({
|
||||||
iconName: "snowflake",
|
iconName: "snowflake",
|
||||||
@@ -149,7 +149,7 @@ export default class RootView extends UINode {
|
|||||||
h("div", {classes: ["root-sidebar"]}, [
|
h("div", {classes: ["root-sidebar"]}, [
|
||||||
h("div", {classes: ["root-settings"]}, [
|
h("div", {classes: ["root-settings"]}, [
|
||||||
h("h1", {classes: ["root-title"], innerText: this.title}),
|
h("h1", {classes: ["root-title"], innerText: this.title}),
|
||||||
this.beatGroupSettingsView,
|
this.beatSettingsView,
|
||||||
]),
|
]),
|
||||||
this.buildSidebarStrip(),
|
this.buildSidebarStrip(),
|
||||||
])
|
])
|
||||||
@@ -163,7 +163,7 @@ export default class RootView extends UINode {
|
|||||||
h("div", {classes: ["root-beat-stage-container"]}, [
|
h("div", {classes: ["root-beat-stage-container"]}, [
|
||||||
this.stageTitleBarView,
|
this.stageTitleBarView,
|
||||||
h("div", {classes: ["root-beat-stage"]}, [
|
h("div", {classes: ["root-beat-stage"]}, [
|
||||||
this.beatGroupView,
|
this.beatView,
|
||||||
])
|
])
|
||||||
])
|
])
|
||||||
])
|
])
|
||||||
|
|||||||
@@ -11,11 +11,6 @@
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.stage-title-bar-preamble {
|
|
||||||
margin-bottom: 4px;
|
|
||||||
font-size: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.stage-title-bar * {
|
.stage-title-bar * {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,50 +1,54 @@
|
|||||||
import "./StageTitleBar.css";
|
import "./StageTitleBar.css";
|
||||||
import UINode, {h, UINodeOptions} from "@/ui/UINode";
|
import UINode, {h, UINodeOptions} from "@/ui/UINode";
|
||||||
import {ISubscription} from "@/Publisher";
|
import {ISubscription} from "@/Publisher";
|
||||||
import BeatGroup, {BeatGroupEvents} from "@/BeatGroup";
|
import Beat, {BeatEvents} from "@/Beat";
|
||||||
import ISubscriber from "@/Subscriber";
|
import ISubscriber from "@/Subscriber";
|
||||||
import EditableTextFieldView from "@/ui/Widgets/EditableTextFIeld/EditableTextFieldView";
|
import EditableTextFieldView from "@/ui/Widgets/EditableTextFIeld/EditableTextFieldView";
|
||||||
|
import DropdownView, {DropdownViewOption} from "@/ui/Widgets/Dropdown/DropdownView";
|
||||||
|
import Ref from "@/Ref";
|
||||||
|
|
||||||
export type StageTitleBarViewOptions = UINodeOptions & {
|
export type StageTitleBarViewOptions = UINodeOptions & {
|
||||||
beatGroup: BeatGroup,
|
beat: Beat,
|
||||||
};
|
};
|
||||||
|
|
||||||
const EventTypeSubscription = [BeatGroupEvents.NameChanged];
|
const EventTypeSubscription = [BeatEvents.NameChanged];
|
||||||
type EventTypeSubscription = FlatArray<typeof EventTypeSubscription, 1>;
|
type EventTypeSubscription = FlatArray<typeof EventTypeSubscription, 1>;
|
||||||
|
|
||||||
export default class StageTitleBarView extends UINode implements ISubscriber<EventTypeSubscription> {
|
export default class StageTitleBarView extends UINode implements ISubscriber<EventTypeSubscription> {
|
||||||
private sub: ISubscription;
|
private sub: ISubscription;
|
||||||
private beatGroup: BeatGroup;
|
private beat: Beat;
|
||||||
private title: EditableTextFieldView;
|
private title: EditableTextFieldView;
|
||||||
|
private options: Ref<DropdownViewOption[]>;
|
||||||
|
|
||||||
constructor(options: StageTitleBarViewOptions) {
|
constructor(options: StageTitleBarViewOptions) {
|
||||||
super(options);
|
super(options);
|
||||||
this.beatGroup = options.beatGroup;
|
this.beat = options.beat;
|
||||||
this.sub = options.beatGroup.addSubscriber(this, EventTypeSubscription);
|
this.sub = options.beat.addSubscriber(this, EventTypeSubscription);
|
||||||
this.title = new EditableTextFieldView({
|
this.title = new EditableTextFieldView({
|
||||||
initialText: this.beatGroup.getName(),
|
initialText: this.beat.getName(),
|
||||||
setter: (text) => this.beatGroup.setName(text),
|
setter: (text) => this.beat.setName(text),
|
||||||
noEmpty: true,
|
noEmpty: true,
|
||||||
});
|
});
|
||||||
|
this.options = Ref.new<DropdownViewOption[]>([]);
|
||||||
}
|
}
|
||||||
|
|
||||||
notify(publisher: unknown, event: EventTypeSubscription): void {
|
notify(publisher: unknown, event: EventTypeSubscription): void {
|
||||||
if (event === BeatGroupEvents.NameChanged) {
|
if (event === BeatEvents.NameChanged) {
|
||||||
this.title.setText(this.beatGroup.getName());
|
this.title.setText(this.beat.getName());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
setBeatGroup(beatGroup: BeatGroup): void {
|
setBeat(beat: Beat): void {
|
||||||
this.sub.unbind();
|
this.sub.unbind();
|
||||||
this.beatGroup = beatGroup;
|
this.beat = beat;
|
||||||
this.sub = beatGroup.addSubscriber(this, EventTypeSubscription);
|
this.sub = beat.addSubscriber(this, EventTypeSubscription);
|
||||||
this.notify(this, BeatGroupEvents.NameChanged);
|
this.notify(this, BeatEvents.NameChanged);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected build(): HTMLElement {
|
protected build(): HTMLElement {
|
||||||
return h("div", {classes: ["stage-title-bar"]}, [
|
return h("div", {classes: ["stage-title-bar"]}, [
|
||||||
h("div", {classes: ["stage-title-bar-preamble"], innerText: "Currently editing:"}),
|
|
||||||
h("h2", {}, [this.title]),
|
h("h2", {}, [this.title]),
|
||||||
|
new DropdownView({options: this.options})
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
65
src/ui/Track/Track.css
Normal file
65
src/ui/Track/Track.css
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
.track > * {
|
||||||
|
padding-right: 1em;
|
||||||
|
padding-left: 1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vertical-mode .track > * {
|
||||||
|
padding-right: 0;
|
||||||
|
padding-left: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.track-unit-block {
|
||||||
|
height: 2em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vertical-mode .track-unit-block {
|
||||||
|
height: auto;
|
||||||
|
width: 2em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.track-title {
|
||||||
|
width: 3em;
|
||||||
|
line-height: 32px;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vertical-mode .track-title {
|
||||||
|
display: block;
|
||||||
|
width: auto;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.track-spacer {
|
||||||
|
display: inline-block;
|
||||||
|
width: 1em;
|
||||||
|
height: 2em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vertical-mode .track-spacer {
|
||||||
|
display: block;
|
||||||
|
width: 2em;
|
||||||
|
height: 1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.track-main {
|
||||||
|
display: inline-flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vertical-mode .track-main {
|
||||||
|
width: 2em;
|
||||||
|
margin-right: 4px;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.track-settings-container {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
.track {
|
||||||
|
width: max-content;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vertical-mode .track {
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
177
src/ui/Track/TrackView.ts
Normal file
177
src/ui/Track/TrackView.ts
Normal file
@@ -0,0 +1,177 @@
|
|||||||
|
import UINode, {h, UINodeOptions} from "@/ui/UINode";
|
||||||
|
import Track, {TrackEvents} from "@/Track";
|
||||||
|
import ISubscriber from "@/Subscriber";
|
||||||
|
import TrackUnitView from "@/ui/TrackUnit/TrackUnitView";
|
||||||
|
import "./Track.css";
|
||||||
|
import {ISubscription} from "@/Publisher";
|
||||||
|
import Ref from "@/Ref";
|
||||||
|
|
||||||
|
export type TrackUINodeOptions = UINodeOptions & {
|
||||||
|
track: Track,
|
||||||
|
};
|
||||||
|
|
||||||
|
const EventTypeSubscriptions = [
|
||||||
|
TrackEvents.NewName,
|
||||||
|
TrackEvents.NewTimeSig,
|
||||||
|
TrackEvents.NewBarCount,
|
||||||
|
TrackEvents.DisplayTypeChanged,
|
||||||
|
TrackEvents.LoopLengthChanged,
|
||||||
|
];
|
||||||
|
|
||||||
|
type EventTypeSubscriptions = FlatArray<typeof EventTypeSubscriptions, 1>;
|
||||||
|
|
||||||
|
export default class TrackView extends UINode implements ISubscriber<EventTypeSubscriptions> {
|
||||||
|
private track!: Track;
|
||||||
|
private title = Ref.new<HTMLHeadingElement | null>(null);
|
||||||
|
private trackUnitViews: TrackUnitView[] = [];
|
||||||
|
private trackUnitViewBlock: HTMLElement | null = null;
|
||||||
|
private lastHoveredTrackUnitView: TrackUnitView | null = null;
|
||||||
|
private sub: ISubscription | null = null;
|
||||||
|
static deselectingUnits = false;
|
||||||
|
static selectingUnits = false;
|
||||||
|
|
||||||
|
constructor(options: TrackUINodeOptions) {
|
||||||
|
super(options);
|
||||||
|
this.setBeat(options.track);
|
||||||
|
}
|
||||||
|
|
||||||
|
setBeat(track: Track | null): void {
|
||||||
|
if (track) {
|
||||||
|
this.track = track;
|
||||||
|
this.sub?.unbind();
|
||||||
|
this.sub = this.track.addSubscriber(this, EventTypeSubscriptions);
|
||||||
|
this.redraw();
|
||||||
|
} else {
|
||||||
|
this.sub?.unbind();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
notify(publisher: unknown, event: EventTypeSubscriptions): void {
|
||||||
|
switch (event) {
|
||||||
|
case TrackEvents.NewName:
|
||||||
|
this.title.val!.innerText = this.track.getName();
|
||||||
|
break;
|
||||||
|
case TrackEvents.NewTimeSig:
|
||||||
|
case TrackEvents.NewBarCount:
|
||||||
|
case TrackEvents.DisplayTypeChanged:
|
||||||
|
case TrackEvents.LoopLengthChanged:
|
||||||
|
this.setupTrackUnits();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private rebuildTrackUnitViews() {
|
||||||
|
const trackUnitCount = this.track.getBarCount() * this.track.getTimeSigUp();
|
||||||
|
for (let i = 0; i < trackUnitCount; i++) {
|
||||||
|
const trackUnit = this.track.getUnitByIndex(i);
|
||||||
|
if (trackUnit) {
|
||||||
|
let view: TrackUnitView;
|
||||||
|
if (this.trackUnitViews[i]) {
|
||||||
|
view = this.trackUnitViews[i];
|
||||||
|
view.setUnit(trackUnit);
|
||||||
|
} else {
|
||||||
|
view = new TrackUnitView({trackUnit});
|
||||||
|
this.trackUnitViews.push(view);
|
||||||
|
view.onHover(() => this.onBeatViewHover(view));
|
||||||
|
view.onMouseDown((event: MouseEvent) => this.onTrackUnitClick(event.button, i));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const deadViews = this.trackUnitViews.splice(trackUnitCount, this.trackUnitViews.length - trackUnitCount);
|
||||||
|
deadViews.forEach(trackUnitView => trackUnitView.setUnit(null));
|
||||||
|
}
|
||||||
|
|
||||||
|
private onTrackUnitClick(button: number, index: number) {
|
||||||
|
if (button === 0) {
|
||||||
|
TrackView.selectingUnits = true;
|
||||||
|
this.track.getUnitByIndex(index)?.toggle();
|
||||||
|
} else if (button === 2) {
|
||||||
|
TrackView.deselectingUnits = true;
|
||||||
|
this.track.getUnitByIndex(index)?.setOn(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private onBeatViewHover(trackView: TrackUnitView) {
|
||||||
|
this.lastHoveredTrackUnitView = trackView;
|
||||||
|
if (TrackView.selectingUnits) {
|
||||||
|
this.lastHoveredTrackUnitView.turnOn();
|
||||||
|
} else if (TrackView.deselectingUnits) {
|
||||||
|
this.lastHoveredTrackUnitView.turnOff();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private buildTrackUnitViewBlock(): void {
|
||||||
|
const trackUnitNodes: HTMLElement[] = [];
|
||||||
|
for (let i = 0; i < this.trackUnitViews.length; i++) {
|
||||||
|
trackUnitNodes.push(this.trackUnitViews[i].render());
|
||||||
|
}
|
||||||
|
if (this.trackUnitViewBlock) {
|
||||||
|
this.trackUnitViewBlock.replaceChildren(...trackUnitNodes);
|
||||||
|
} else {
|
||||||
|
this.trackUnitViewBlock = h("div", {
|
||||||
|
classes: ["track-unit-block"],
|
||||||
|
}, [
|
||||||
|
...trackUnitNodes
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private respaceTrackUnits(): void {
|
||||||
|
if (!this.trackUnitViewBlock) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.trackUnitViewBlock.querySelectorAll(".unit-spacer").forEach(spacer => spacer.remove());
|
||||||
|
const barLength = this.track.getTimeSigUp();
|
||||||
|
const barCount = this.track.getBarCount();
|
||||||
|
let bars = 0;
|
||||||
|
let i = -1;
|
||||||
|
let spacersInserted = false;
|
||||||
|
while (!spacersInserted) {
|
||||||
|
i += barLength;
|
||||||
|
const newSpacer = h("div", {classes: ["track-spacer"]});
|
||||||
|
const leftNeighbour = this.trackUnitViewBlock.children.item(i);
|
||||||
|
if (leftNeighbour) {
|
||||||
|
leftNeighbour.insertAdjacentElement("afterend", newSpacer);
|
||||||
|
} else {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
i++;
|
||||||
|
bars++;
|
||||||
|
if (bars === barCount) {
|
||||||
|
spacersInserted = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private setupTrackUnits(): void {
|
||||||
|
this.rebuildTrackUnitViews();
|
||||||
|
this.buildTrackUnitViewBlock();
|
||||||
|
this.respaceTrackUnits();
|
||||||
|
}
|
||||||
|
|
||||||
|
build(): HTMLElement {
|
||||||
|
this.setupTrackUnits();
|
||||||
|
if (!this.trackUnitViewBlock) {
|
||||||
|
throw new Error("Beat unit block setup failed!");
|
||||||
|
}
|
||||||
|
return h("div", {
|
||||||
|
classes: ["track"],
|
||||||
|
}, [
|
||||||
|
h("div", {
|
||||||
|
classes: ["track-main"],
|
||||||
|
}, [
|
||||||
|
h("h3", {
|
||||||
|
innerText: this.track.getName(),
|
||||||
|
saveTo: this.title,
|
||||||
|
classes: ["track-title"],
|
||||||
|
}),
|
||||||
|
this.trackUnitViewBlock,
|
||||||
|
]),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
window.addEventListener("mouseup", () => {
|
||||||
|
TrackView.selectingUnits = false;
|
||||||
|
TrackView.deselectingUnits = false;
|
||||||
|
});
|
||||||
54
src/ui/TrackSettings/TrackSettings.css
Normal file
54
src/ui/TrackSettings/TrackSettings.css
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
.track-settings {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
.track-settings-title-container {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
.track-settings-title-container input {
|
||||||
|
min-width: 100%;
|
||||||
|
height: 2em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.track-settings-title-container > div {
|
||||||
|
width: 100%;
|
||||||
|
font-weight: bold;
|
||||||
|
padding: 0.5em;
|
||||||
|
transition: background-color 200ms;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.track-settings-title-container > div:hover {
|
||||||
|
background-color: var(--color-ui-neutral-dark-hover);
|
||||||
|
}
|
||||||
|
|
||||||
|
.track-settings-lower {
|
||||||
|
height: 3.5em;
|
||||||
|
display: flex;
|
||||||
|
text-align: center;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
margin-bottom: 0.5em;
|
||||||
|
}
|
||||||
|
.track-settings-lower > * {
|
||||||
|
margin-right: 0.2em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.track-settings-lower:last-child {
|
||||||
|
margin-right: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.track-settings .loop-settings {
|
||||||
|
text-align: left;
|
||||||
|
flex: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.track-settings .loop-settings-option.hide {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.track-settings .loop-settings-option {
|
||||||
|
flex: auto;
|
||||||
|
padding-right: 1em;
|
||||||
|
}
|
||||||
130
src/ui/TrackSettings/TrackSettingsView.ts
Normal file
130
src/ui/TrackSettings/TrackSettingsView.ts
Normal file
@@ -0,0 +1,130 @@
|
|||||||
|
import "./TrackSettings.css";
|
||||||
|
import Track, {TrackEvents} from "@/Track";
|
||||||
|
import UINode, {h, UINodeOptions} from "@/ui/UINode";
|
||||||
|
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 & {
|
||||||
|
track: Track,
|
||||||
|
};
|
||||||
|
|
||||||
|
const EventTypeSubscriptions = [
|
||||||
|
TrackEvents.NewName,
|
||||||
|
TrackEvents.LoopLengthChanged,
|
||||||
|
TrackEvents.DisplayTypeChanged,
|
||||||
|
];
|
||||||
|
type EventTypeSubscriptions = FlatArray<typeof EventTypeSubscriptions, 1>;
|
||||||
|
|
||||||
|
export default class TrackSettingsView extends UINode implements ISubscriber<EventTypeSubscriptions> {
|
||||||
|
private track: Track;
|
||||||
|
private loopLengthInput!: NumberInputView;
|
||||||
|
private bakeButton!: ActionButtonView;
|
||||||
|
private loopCheckbox!: BoolBoxView;
|
||||||
|
private loopLengthSection!: HTMLDivElement;
|
||||||
|
private sub!: ISubscription;
|
||||||
|
private title!: EditableTextFieldView;
|
||||||
|
private editingTitle: boolean;
|
||||||
|
|
||||||
|
constructor(options: BeatSettingsViewUINodeOptions) {
|
||||||
|
super(options);
|
||||||
|
this.editingTitle = false;
|
||||||
|
this.track = options.track;
|
||||||
|
this.setupBindings();
|
||||||
|
}
|
||||||
|
|
||||||
|
private setupBindings() {
|
||||||
|
this.sub = this.track.addSubscriber(this, EventTypeSubscriptions);
|
||||||
|
}
|
||||||
|
|
||||||
|
setBeat(track: Track): void {
|
||||||
|
this.sub.unbind();
|
||||||
|
this.track = track;
|
||||||
|
this.setupBindings();
|
||||||
|
EventTypeSubscriptions.forEach(eventType => this.notify(null, eventType));
|
||||||
|
}
|
||||||
|
|
||||||
|
notify(publisher: unknown, event: EventTypeSubscriptions): void {
|
||||||
|
switch(event) {
|
||||||
|
case TrackEvents.NewName:
|
||||||
|
this.title.setText(this.track.getName());
|
||||||
|
break;
|
||||||
|
case TrackEvents.LoopLengthChanged:
|
||||||
|
this.loopLengthInput.setValue(this.track.getLoopLength());
|
||||||
|
break;
|
||||||
|
case TrackEvents.DisplayTypeChanged:
|
||||||
|
this.loopCheckbox.setValue(this.track.isLooping());
|
||||||
|
this.bakeButton.setDisabled(!this.track.isLooping());
|
||||||
|
if (this.track.isLooping()) {
|
||||||
|
this.loopLengthSection.classList.remove("hide");
|
||||||
|
} else {
|
||||||
|
this.loopLengthSection.classList.add("hide");
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
build(): HTMLElement {
|
||||||
|
this.title = new EditableTextFieldView({
|
||||||
|
initialText: this.track.getName(),
|
||||||
|
setter: (newText) => this.track.setName(newText),
|
||||||
|
});
|
||||||
|
this.bakeButton = new ActionButtonView({
|
||||||
|
icon: "snowflake",
|
||||||
|
type: "secondary",
|
||||||
|
alt: "Bake Loops",
|
||||||
|
disabled: !this.track.isLooping(),
|
||||||
|
onClick: () => this.track.bakeLoops(),
|
||||||
|
});
|
||||||
|
this.loopLengthInput = new NumberInputView({
|
||||||
|
initialValue: this.track.getLoopLength(),
|
||||||
|
onDecrement: () => this.track.setLoopLength(this.track.getLoopLength() - 1),
|
||||||
|
onIncrement: () => this.track.setLoopLength(this.track.getLoopLength() + 1),
|
||||||
|
onNewInput: (input: number) => this.track.setLoopLength(input),
|
||||||
|
});
|
||||||
|
this.loopCheckbox = new BoolBoxView({
|
||||||
|
label: "Loop:",
|
||||||
|
value: this.track.isLooping(),
|
||||||
|
onInput: (isChecked: boolean) => this.track.setLooping(isChecked),
|
||||||
|
});
|
||||||
|
this.loopLengthSection = h("div", {
|
||||||
|
classes: ["loop-settings-option"],
|
||||||
|
}, [
|
||||||
|
this.loopLengthInput,
|
||||||
|
]);
|
||||||
|
if (this.track.isLooping()) {
|
||||||
|
this.loopLengthSection.classList.remove("hide");
|
||||||
|
} else {
|
||||||
|
this.loopLengthSection.classList.add("hide");
|
||||||
|
}
|
||||||
|
return h("div", {
|
||||||
|
classes: ["track-settings"],
|
||||||
|
}, [
|
||||||
|
h("div", {
|
||||||
|
classes: ["track-settings-title-container"]
|
||||||
|
}, [
|
||||||
|
this.title,
|
||||||
|
]),
|
||||||
|
h("div", {
|
||||||
|
classes: ["track-settings-lower"],
|
||||||
|
}, [
|
||||||
|
this.bakeButton,
|
||||||
|
new ActionButtonView({
|
||||||
|
icon: "trash",
|
||||||
|
type: "secondary",
|
||||||
|
alt: "Delete Track",
|
||||||
|
onClick: () => this.track.delete(),
|
||||||
|
}),
|
||||||
|
h("div", {
|
||||||
|
classes: ["loop-settings"],
|
||||||
|
}, [
|
||||||
|
this.loopCheckbox,
|
||||||
|
]),
|
||||||
|
this.loopLengthSection,
|
||||||
|
]),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
.beat-unit {
|
.track-unit {
|
||||||
width: 2em;
|
width: 2em;
|
||||||
height: 2em;
|
height: 2em;
|
||||||
margin-right: 4px;
|
margin-right: 4px;
|
||||||
@@ -11,45 +11,45 @@
|
|||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
.beat-unit:hover {
|
.track-unit:hover {
|
||||||
border-color: #5f5f5f;
|
border-color: #5f5f5f;
|
||||||
background-color: #5f5f5f;
|
background-color: #5f5f5f;
|
||||||
transition: none;
|
transition: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.vertical-mode .beat-unit {
|
.vertical-mode .track-unit {
|
||||||
margin-bottom: 4px;
|
margin-bottom: 4px;
|
||||||
display: block;
|
display: block;
|
||||||
}
|
}
|
||||||
|
|
||||||
.beat-unit.beat-unit-on {
|
.track-unit.track-unit-on {
|
||||||
border-color: var(--color-ui-accent);
|
border-color: var(--color-ui-accent);
|
||||||
background-color: var(--color-ui-accent);
|
background-color: var(--color-ui-accent);
|
||||||
transition: none;
|
transition: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.beat-unit.beat-unit-on:hover {
|
.track-unit.track-unit-on: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);
|
||||||
}
|
}
|
||||||
|
|
||||||
.beat-unit.beat-unit-on.beat-unit-accent {
|
.track-unit.track-unit-on.track-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-accent:hover {
|
.track-unit.track-unit-on.track-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-ghost {
|
.track-unit.track-unit-on.track-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-ghost:hover {
|
.track-unit.track-unit-on.track-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);
|
||||||
}
|
}
|
||||||
@@ -1,40 +1,40 @@
|
|||||||
import BeatUnit, {BeatUnitEvent, BeatUnitType} from "@/BeatUnit";
|
import TrackUnit, {TrackUnitEvent, TrackUnitType} from "@/TrackUnit";
|
||||||
import ISubscriber from "@/Subscriber";
|
import ISubscriber from "@/Subscriber";
|
||||||
import UINode, {h, UINodeOptions} from "@/ui/UINode";
|
import UINode, {h, UINodeOptions} from "@/ui/UINode";
|
||||||
import {IPublisher, ISubscription, Publisher} from "@/Publisher";
|
import {IPublisher, ISubscription, Publisher} from "@/Publisher";
|
||||||
import "./BeatUnit.css";
|
import "./TrackUnit.css";
|
||||||
|
|
||||||
export type BeatUnitUINodeOptions = UINodeOptions & {
|
export type TrackUnitUINodeOptions = UINodeOptions & {
|
||||||
beatUnit: BeatUnit,
|
trackUnit: TrackUnit,
|
||||||
};
|
};
|
||||||
|
|
||||||
const EventTypeSubscriptions = [
|
const EventTypeSubscriptions = [
|
||||||
BeatUnitEvent.On,
|
TrackUnitEvent.On,
|
||||||
BeatUnitEvent.Off,
|
TrackUnitEvent.Off,
|
||||||
BeatUnitEvent.TypeChange,
|
TrackUnitEvent.TypeChange,
|
||||||
];
|
];
|
||||||
type EventTypeSubscriptions = FlatArray<typeof EventTypeSubscriptions, 1>;
|
type EventTypeSubscriptions = FlatArray<typeof EventTypeSubscriptions, 1>;
|
||||||
|
|
||||||
export default class BeatUnitView extends UINode implements ISubscriber<EventTypeSubscriptions> {
|
export default class TrackUnitView extends UINode implements ISubscriber<EventTypeSubscriptions> {
|
||||||
private beatUnit: BeatUnit;
|
private trackUnit: TrackUnit;
|
||||||
private subscription: ISubscription | null = null;
|
private subscription: ISubscription | null = null;
|
||||||
private publisher: IPublisher<BeatUnitEvent> = new Publisher<BeatUnitEvent, BeatUnitView>(this);
|
private publisher: IPublisher<TrackUnitEvent> = new Publisher<TrackUnitEvent, TrackUnitView>(this);
|
||||||
private touchTimeout: ReturnType<typeof setTimeout> | null = null;
|
private touchTimeout: ReturnType<typeof setTimeout> | null = null;
|
||||||
private mouseDownListeners: ((ev: MouseEvent) => void)[] = [];
|
private mouseDownListeners: ((ev: MouseEvent) => void)[] = [];
|
||||||
private hoverListeners: ((ev: MouseEvent) => void)[] = [];
|
private hoverListeners: ((ev: MouseEvent) => void)[] = [];
|
||||||
|
|
||||||
constructor(options: BeatUnitUINodeOptions) {
|
constructor(options: TrackUnitUINodeOptions) {
|
||||||
super(options);
|
super(options);
|
||||||
this.beatUnit = options.beatUnit;
|
this.trackUnit = options.trackUnit;
|
||||||
this.setupBindings();
|
this.setupBindings();
|
||||||
}
|
}
|
||||||
|
|
||||||
setUnit(beatUnit: BeatUnit | null): void {
|
setUnit(trackUnit: TrackUnit | null): void {
|
||||||
if (beatUnit) {
|
if (trackUnit) {
|
||||||
this.beatUnit = beatUnit;
|
this.trackUnit = trackUnit;
|
||||||
this.setupBindings();
|
this.setupBindings();
|
||||||
this.notify(this.publisher, beatUnit.isOn() ? BeatUnitEvent.On : BeatUnitEvent.Off);
|
this.notify(this.publisher, trackUnit.isOn() ? TrackUnitEvent.On : TrackUnitEvent.Off);
|
||||||
this.notify(this.publisher, BeatUnitEvent.TypeChange);
|
this.notify(this.publisher, TrackUnitEvent.TypeChange);
|
||||||
} else {
|
} else {
|
||||||
this.subscription?.unbind();
|
this.subscription?.unbind();
|
||||||
}
|
}
|
||||||
@@ -42,7 +42,7 @@ export default class BeatUnitView extends UINode implements ISubscriber<EventTyp
|
|||||||
|
|
||||||
private setupBindings() {
|
private setupBindings() {
|
||||||
this.subscription?.unbind();
|
this.subscription?.unbind();
|
||||||
this.subscription = this.beatUnit.addSubscriber(this, EventTypeSubscriptions);
|
this.subscription = this.trackUnit.addSubscriber(this, EventTypeSubscriptions);
|
||||||
this.mouseDownListeners.forEach(listener => this.getNode().removeEventListener("mousedown", listener));
|
this.mouseDownListeners.forEach(listener => this.getNode().removeEventListener("mousedown", listener));
|
||||||
this.hoverListeners.forEach(listener => this.getNode().removeEventListener("mouseover", listener));
|
this.hoverListeners.forEach(listener => this.getNode().removeEventListener("mouseover", listener));
|
||||||
this.redraw();
|
this.redraw();
|
||||||
@@ -55,13 +55,13 @@ export default class BeatUnitView extends UINode implements ISubscriber<EventTyp
|
|||||||
|
|
||||||
private handleMouseDown(ev: MouseEvent): void {
|
private handleMouseDown(ev: MouseEvent): void {
|
||||||
if (ev.button === 1) {
|
if (ev.button === 1) {
|
||||||
this.beatUnit.rotateType();
|
this.trackUnit.rotateType();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private handleTouchStart(ev: TouchEvent): void {
|
private handleTouchStart(ev: TouchEvent): void {
|
||||||
this.touchTimeout = this.touchTimeout || setTimeout(() => {
|
this.touchTimeout = this.touchTimeout || setTimeout(() => {
|
||||||
this.beatUnit.rotateType();
|
this.trackUnit.rotateType();
|
||||||
this.touchTimeout = null;
|
this.touchTimeout = null;
|
||||||
}, 400);
|
}, 400);
|
||||||
}
|
}
|
||||||
@@ -74,38 +74,38 @@ export default class BeatUnitView extends UINode implements ISubscriber<EventTyp
|
|||||||
}
|
}
|
||||||
|
|
||||||
toggle(): void {
|
toggle(): void {
|
||||||
this.beatUnit.toggle();
|
this.trackUnit.toggle();
|
||||||
}
|
}
|
||||||
|
|
||||||
turnOn(): void {
|
turnOn(): void {
|
||||||
this.beatUnit.setOn(true);
|
this.trackUnit.setOn(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
turnOff(): void {
|
turnOff(): void {
|
||||||
this.beatUnit.setOn(false);
|
this.trackUnit.setOn(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
notify(publisher: unknown, event: EventTypeSubscriptions): void {
|
notify(publisher: unknown, event: EventTypeSubscriptions): void {
|
||||||
switch (event) {
|
switch (event) {
|
||||||
case BeatUnitEvent.On:
|
case TrackUnitEvent.On:
|
||||||
this.getNode().classList.add("beat-unit-on");
|
this.getNode().classList.add("track-unit-on");
|
||||||
break;
|
break;
|
||||||
case BeatUnitEvent.Off:
|
case TrackUnitEvent.Off:
|
||||||
this.getNode().classList.remove("beat-unit-on");
|
this.getNode().classList.remove("track-unit-on");
|
||||||
break;
|
break;
|
||||||
case BeatUnitEvent.TypeChange:
|
case TrackUnitEvent.TypeChange:
|
||||||
switch (this.beatUnit.getType()) {
|
switch (this.trackUnit.getType()) {
|
||||||
case BeatUnitType.Normal:
|
case TrackUnitType.Normal:
|
||||||
this.getNode().classList.remove("beat-unit-ghost");
|
this.getNode().classList.remove("track-unit-ghost");
|
||||||
this.getNode().classList.remove("beat-unit-accent");
|
this.getNode().classList.remove("track-unit-accent");
|
||||||
break;
|
break;
|
||||||
case BeatUnitType.GhostNote:
|
case TrackUnitType.GhostNote:
|
||||||
this.getNode().classList.remove("beat-unit-accent");
|
this.getNode().classList.remove("track-unit-accent");
|
||||||
this.getNode().classList.add("beat-unit-ghost");
|
this.getNode().classList.add("track-unit-ghost");
|
||||||
break;
|
break;
|
||||||
case BeatUnitType.Accent:
|
case TrackUnitType.Accent:
|
||||||
this.getNode().classList.remove("beat-unit-ghost");
|
this.getNode().classList.remove("track-unit-ghost");
|
||||||
this.getNode().classList.add("beat-unit-accent");
|
this.getNode().classList.add("track-unit-accent");
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
@@ -113,9 +113,9 @@ export default class BeatUnitView extends UINode implements ISubscriber<EventTyp
|
|||||||
}
|
}
|
||||||
|
|
||||||
build(): HTMLElement {
|
build(): HTMLElement {
|
||||||
const classes = ["beat-unit"];
|
const classes = ["track-unit"];
|
||||||
if (this.beatUnit.isOn()) {
|
if (this.trackUnit.isOn()) {
|
||||||
classes.push("beat-unit-on");
|
classes.push("track-unit-on");
|
||||||
}
|
}
|
||||||
return h("div", {
|
return h("div", {
|
||||||
classes: classes,
|
classes: classes,
|
||||||
@@ -49,45 +49,6 @@ export default abstract class UINode {
|
|||||||
protected abstract build(): HTMLElement;
|
protected abstract build(): HTMLElement;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function h<
|
|
||||||
T extends keyof HTMLElementTagNameMap>(
|
|
||||||
type: T,
|
|
||||||
attributes: IRenderAttributes<T>,
|
|
||||||
subNodes?: (Node | UINode | Ref)[],
|
|
||||||
): HTMLElementTagNameMap[T] {
|
|
||||||
const element = document.createElement(type);
|
|
||||||
if (attributes) {
|
|
||||||
for (const key in attributes) {
|
|
||||||
if (!Object.prototype.hasOwnProperty.call(attributes, key)) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if (key === "classes" && attributes.classes) {
|
|
||||||
element.classList.add(...attributes.classes);
|
|
||||||
} else if (key === "saveTo" && attributes.saveTo) {
|
|
||||||
attributes.saveTo.val = element;
|
|
||||||
} else if (Object.prototype.hasOwnProperty.call(attributes, key)) {
|
|
||||||
const attribute = (attributes as any)[key];
|
|
||||||
if (attribute) {
|
|
||||||
if (attribute instanceof Ref) {
|
|
||||||
(element as any)[key] = attribute.val;
|
|
||||||
attribute.watch((newVal) => (element as any)[key] = newVal);
|
|
||||||
} else {
|
|
||||||
(element as any)[key] = attribute;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (subNodes) {
|
|
||||||
attachSubs(element, subNodes);
|
|
||||||
}
|
|
||||||
return element;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function q(text: string): Text {
|
|
||||||
return document.createTextNode(text);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function frag(subs?: Node[]): DocumentFragment {
|
export function frag(subs?: Node[]): DocumentFragment {
|
||||||
const frag = document.createDocumentFragment();
|
const frag = document.createDocumentFragment();
|
||||||
if (subs) {
|
if (subs) {
|
||||||
@@ -96,6 +57,27 @@ export function frag(subs?: Node[]): DocumentFragment {
|
|||||||
return frag;
|
return frag;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function q(text: string): Text {
|
||||||
|
return document.createTextNode(text);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function h<T extends keyof HTMLElementTagNameMap>(type: T, attributes?: IRenderAttributes<T>, subNodes?: (Node | UINode | Ref)[]): HTMLElementTagNameMap[T] {
|
||||||
|
const element = document.createElement(type);
|
||||||
|
if (attributes) {
|
||||||
|
if (attributes.classes) {
|
||||||
|
element.classList.add(...attributes.classes);
|
||||||
|
}
|
||||||
|
if (attributes.saveTo) {
|
||||||
|
attributes.saveTo.val = element;
|
||||||
|
}
|
||||||
|
applyAttributes(element, attributes);
|
||||||
|
}
|
||||||
|
if (subNodes) {
|
||||||
|
attachSubs(element, subNodes);
|
||||||
|
}
|
||||||
|
return element;
|
||||||
|
}
|
||||||
|
|
||||||
function nodeRefWatcher<T>(newVal: T extends Ref<infer U> ? U : never, textNode: Text, sub: ISubscription): void {
|
function nodeRefWatcher<T>(newVal: T extends Ref<infer U> ? U : never, textNode: Text, sub: ISubscription): void {
|
||||||
if (!textNode.parentNode) {
|
if (!textNode.parentNode) {
|
||||||
sub.unbind();
|
sub.unbind();
|
||||||
@@ -118,4 +100,22 @@ function attachSubs(node: Element | DocumentFragment, subNodes: (Node | UINode |
|
|||||||
node.append(subNode);
|
node.append(subNode);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function applyAttributes<T extends keyof HTMLElementTagNameMap>(element: HTMLElement, attributes: IRenderAttributes<T>): void {
|
||||||
|
for (const key in attributes) {
|
||||||
|
if (Object.prototype.hasOwnProperty.call(attributes, key)) {
|
||||||
|
const attribute = (attributes as Record<string, unknown>)[key];
|
||||||
|
if (attribute) {
|
||||||
|
if (attribute instanceof Ref) {
|
||||||
|
const attributeAsRef = attribute as Ref;
|
||||||
|
const elementWithAttributeKey = element as unknown as Record<string, typeof attributeAsRef.val>;
|
||||||
|
elementWithAttributeKey[key] = attributeAsRef.val;
|
||||||
|
attribute.watch((newVal) => elementWithAttributeKey[key] = newVal);
|
||||||
|
} else {
|
||||||
|
(element as unknown as ({ [key: string]: typeof attribute }))[key] = attribute;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
0
src/ui/Widgets/Dropdown/Dropdown.css
Normal file
0
src/ui/Widgets/Dropdown/Dropdown.css
Normal file
52
src/ui/Widgets/Dropdown/DropdownView.ts
Normal file
52
src/ui/Widgets/Dropdown/DropdownView.ts
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
import "./Dropdown.css";
|
||||||
|
import UINode, {h, UINodeOptions} from "@/ui/UINode";
|
||||||
|
import Ref, {MaybeRef} from "@/Ref";
|
||||||
|
|
||||||
|
export type DropdownViewOption = {
|
||||||
|
label: string,
|
||||||
|
value: string,
|
||||||
|
};
|
||||||
|
|
||||||
|
export type DropdownUINodeOptions = UINodeOptions & {
|
||||||
|
options: MaybeRef<DropdownViewOption[]>,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default class DropdownView extends UINode {
|
||||||
|
private options: Ref<DropdownViewOption[]>;
|
||||||
|
private select = Ref.new<HTMLSelectElement | null>(null);
|
||||||
|
|
||||||
|
constructor(options: DropdownUINodeOptions) {
|
||||||
|
super(options);
|
||||||
|
this.options = Ref.new(options.options);
|
||||||
|
this.options.watch((newVal) => this.updateOptionsFrom(newVal));
|
||||||
|
}
|
||||||
|
|
||||||
|
private updateOptionsFrom(newOptions: DropdownViewOption[]): void {
|
||||||
|
const select = this.select.val;
|
||||||
|
if (!select) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const children = new Array(...select.children) as HTMLOptionElement[];
|
||||||
|
for (let i = 0; i < newOptions.length; i++) {
|
||||||
|
if (children[i]) {
|
||||||
|
children[i].label = newOptions[i].label;
|
||||||
|
children[i].value = newOptions[i].value;
|
||||||
|
} else {
|
||||||
|
children.push(h("option", {
|
||||||
|
label: newOptions[i].label,
|
||||||
|
value: newOptions[i].value,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (children.length - newOptions.length > 0) {
|
||||||
|
children.splice(newOptions.length, children.length - newOptions.length).forEach(child => child.remove());
|
||||||
|
}
|
||||||
|
select.replaceChildren(...children);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected build(): HTMLSelectElement {
|
||||||
|
return h("select", {
|
||||||
|
saveTo: this.select,
|
||||||
|
}, this.options.val.map(opt => h("option", {label: opt.label})));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -52,10 +52,12 @@ input[type="number"].number-input-input::-webkit-outer-spin-button {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.number-input-inc {
|
.number-input-inc {
|
||||||
|
width: 1.4em;
|
||||||
border-radius: 0 0.5em 0.5em 0;
|
border-radius: 0 0.5em 0.5em 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.number-input-dec {
|
.number-input-dec {
|
||||||
|
width: 1.4em;
|
||||||
border-radius: 0.5em 0 0 0.5em;
|
border-radius: 0.5em 0 0 0.5em;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -27,8 +27,8 @@ type NumberInputUINodeOptionsGetSet = NumberInputUINodeOptionsBase & {
|
|||||||
export type NumberInputUINodeOptions = NumberInputUINodeOptionsGetSet | NumberInputUINodeOptionsIncDecInput;
|
export type NumberInputUINodeOptions = NumberInputUINodeOptionsGetSet | NumberInputUINodeOptionsIncDecInput;
|
||||||
|
|
||||||
export default class NumberInputView extends UINode {
|
export default class NumberInputView extends UINode {
|
||||||
private labelElement: Ref<HTMLLabelElement | null> = new Ref<HTMLLabelElement | null>(null);
|
private labelElement: Ref<HTMLLabelElement | null> = Ref.new<HTMLLabelElement | null>(null);
|
||||||
private inputElement: Ref<HTMLInputElement | null> = new Ref<HTMLInputElement | null>(null);
|
private inputElement: Ref<HTMLInputElement | null> = Ref.new<HTMLInputElement | null>(null);
|
||||||
private labelPosition: "top" | "left";
|
private labelPosition: "top" | "left";
|
||||||
private value: number;
|
private value: number;
|
||||||
private label: string | null;
|
private label: string | null;
|
||||||
|
|||||||
Reference in New Issue
Block a user