refactor: loads of sensible renaming
This commit is contained in:
386
src/Beat.ts
386
src/Beat.ts
@@ -1,169 +1,305 @@
|
||||
import BeatUnit from "@/BeatUnit";
|
||||
import Track, {TrackEvents, TrackInitOptions} from "@/Track";
|
||||
import {IPublisher, Publisher} from "@/Publisher";
|
||||
import ISubscriber from "@/Subscriber";
|
||||
import BeatLike from "@/BeatLike";
|
||||
import {isPosInt} from "@/utils";
|
||||
import {greatestCommonDivisor, isPosInt} from "@/utils";
|
||||
|
||||
export type BeatInitOptions = {
|
||||
timeSig?: {
|
||||
up: number,
|
||||
down: number,
|
||||
},
|
||||
name?: string,
|
||||
bars?: number,
|
||||
isLooping?: boolean,
|
||||
type BeatGroupInitOptions = {
|
||||
barCount: number;
|
||||
isLooping: boolean;
|
||||
timeSigUp: number;
|
||||
tracks?: TrackInitOptions[],
|
||||
loopLength?: number,
|
||||
useAutoBeatLength?: boolean,
|
||||
name?: string,
|
||||
};
|
||||
|
||||
export const enum BeatEvents {
|
||||
NewTimeSig="be-0",
|
||||
NewBarCount="be-1",
|
||||
NewName="be-2",
|
||||
DisplayTypeChanged="be-3",
|
||||
LoopLengthChanged="be-4",
|
||||
WantsRemoval="be-5",
|
||||
Baked="be-6",
|
||||
TrackOrderChanged="be-0",
|
||||
TrackListChanged="be-1",
|
||||
BarCountChanged="be-2",
|
||||
TimeSigUpChanged="be-3",
|
||||
AutoBeatSettingsChanged="be-4",
|
||||
LockingChanged="be-5",
|
||||
GlobalLoopLengthChanged="be-5",
|
||||
GlobalDisplayTypeChanged="be-6",
|
||||
NameChanged="be-7",
|
||||
}
|
||||
|
||||
export default class Beat implements IPublisher<BeatEvents>, BeatLike {
|
||||
private static count = 0;
|
||||
private readonly key: string;
|
||||
type EventTypeSubscriptions =
|
||||
| TrackEvents.LoopLengthChanged
|
||||
| TrackEvents.DisplayTypeChanged
|
||||
| TrackEvents.WantsRemoval
|
||||
| TrackEvents.Baked;
|
||||
|
||||
export default class Beat implements IPublisher<BeatEvents>, ISubscriber<EventTypeSubscriptions> {
|
||||
private static globalCounter = 0;
|
||||
private tracks: Track[] = [];
|
||||
private publisher: Publisher<BeatEvents, Beat> = new Publisher<BeatEvents, Beat>(this);
|
||||
private barCount: number;
|
||||
private timeSigUp: number;
|
||||
private globalLoopLength: number;
|
||||
private globalIsLooping: boolean;
|
||||
private useAutoBeatLength: boolean;
|
||||
private barSettingsLocked = false;
|
||||
private name: string;
|
||||
private timeSigUp = 4;
|
||||
private timeSigDown = 4;
|
||||
private readonly unitRecord: BeatUnit[] = [];
|
||||
private barCount = 1;
|
||||
private publisher = new Publisher<BeatEvents, Beat>(this);
|
||||
private loopLength: number;
|
||||
private looping: boolean;
|
||||
|
||||
constructor(options?: BeatInitOptions) {
|
||||
this.key = `B-${Beat.count}`;
|
||||
this.name = options?.name ?? this.key;
|
||||
this.setTimeSignature({up: options?.timeSig?.up ?? 4, down: options?.timeSig?.down ?? 4});
|
||||
this.setBarCount(options?.bars ?? 4);
|
||||
Beat.count++;
|
||||
this.loopLength = options?.loopLength ?? this.timeSigUp * this.barCount;
|
||||
this.looping = options?.isLooping ?? false;
|
||||
constructor(options?: BeatGroupInitOptions) {
|
||||
Beat.globalCounter++;
|
||||
if (options?.name) {
|
||||
this.name = options.name;
|
||||
} else {
|
||||
this.name = `Pattern ${Beat.globalCounter}`;
|
||||
}
|
||||
if (options?.tracks) {
|
||||
for (const trackOptions of options.tracks) {
|
||||
this.addTrack(trackOptions);
|
||||
}
|
||||
}
|
||||
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;
|
||||
}
|
||||
|
||||
setLoopLength(loopLength: number): void {
|
||||
if (!isPosInt(loopLength) || loopLength < 2) {
|
||||
loopLength = this.loopLength;
|
||||
notify(publisher: unknown, event: EventTypeSubscriptions): void {
|
||||
switch (event) {
|
||||
case TrackEvents.LoopLengthChanged:
|
||||
case TrackEvents.DisplayTypeChanged:
|
||||
this.autoBeatLength();
|
||||
break;
|
||||
case TrackEvents.WantsRemoval:
|
||||
this.removeTrack((publisher as Track).getKey());
|
||||
break;
|
||||
case TrackEvents.Baked:
|
||||
this.setIsUsingAutoBeatLength(false);
|
||||
break;
|
||||
}
|
||||
this.loopLength = loopLength;
|
||||
this.publisher.notifySubs(BeatEvents.LoopLengthChanged);
|
||||
}
|
||||
|
||||
setLooping(isLooping: boolean): void {
|
||||
this.looping = isLooping;
|
||||
this.publisher.notifySubs(BeatEvents.DisplayTypeChanged);
|
||||
}
|
||||
|
||||
addSubscriber(subscriber: ISubscriber<BeatEvents>, eventType: BeatEvents | BeatEvents[]): { unbind: () => void } {
|
||||
return this.publisher.addSubscriber(subscriber, eventType);
|
||||
}
|
||||
|
||||
setTimeSignature(timeSig: {up?: number, down?: number}): void {
|
||||
if (timeSig.up && Beat.isValidTimeSigRange(timeSig.up)) {
|
||||
this.timeSigUp = timeSig.up | 0;
|
||||
}
|
||||
if (timeSig.down && Beat.isValidTimeSigRange(timeSig.down)) {
|
||||
this.timeSigDown = timeSig.down | 0;
|
||||
}
|
||||
this.updateBeatUnitLength();
|
||||
this.publisher.notifySubs(BeatEvents.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) {
|
||||
private setBarCountInternal(barCount: number): void {
|
||||
if (!isPosInt(barCount)) {
|
||||
barCount = this.barCount;
|
||||
}
|
||||
this.barCount = barCount;
|
||||
this.updateBeatUnitLength();
|
||||
this.publisher.notifySubs(BeatEvents.NewBarCount);
|
||||
for (const track of this.tracks) {
|
||||
track.setBarCount(barCount);
|
||||
}
|
||||
this.publisher.notifySubs(BeatEvents.BarCountChanged);
|
||||
}
|
||||
|
||||
getUnitByIndex(index: number): BeatUnit | null {
|
||||
if (this.looping) {
|
||||
index %= this.loopLength;
|
||||
setBarCount(barCount: number): void {
|
||||
if (!this.barSettingsLocked) {
|
||||
this.setBarCountInternal(barCount);
|
||||
} else {
|
||||
this.setBarCountInternal(this.barCount);
|
||||
}
|
||||
return this.unitRecord[index] ?? null;
|
||||
}
|
||||
|
||||
private updateBeatUnitLength() {
|
||||
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 BeatUnit());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
getTimeSigUp(): number {
|
||||
return this.timeSigUp;
|
||||
}
|
||||
|
||||
getTimeSigDown(): number {
|
||||
return this.timeSigDown;
|
||||
}
|
||||
|
||||
getBarCount(): number {
|
||||
return this.barCount;
|
||||
}
|
||||
|
||||
getKey(): string {
|
||||
return this.key;
|
||||
setLoopLength(loopLength: number): void {
|
||||
if (!isPosInt(loopLength)) {
|
||||
return;
|
||||
}
|
||||
this.globalLoopLength = loopLength;
|
||||
for (const track of this.tracks) {
|
||||
track.setLoopLength(loopLength);
|
||||
}
|
||||
this.publisher.notifySubs(BeatEvents.GlobalLoopLengthChanged);
|
||||
}
|
||||
|
||||
static isValidTimeSigRange(sig: number): boolean {
|
||||
return sig >= 2 && sig <= 32;
|
||||
getLoopLength(): number {
|
||||
return this.globalLoopLength;
|
||||
}
|
||||
|
||||
setLooping(isLooping: boolean): void {
|
||||
this.globalIsLooping = isLooping;
|
||||
for (const track of this.tracks) {
|
||||
track.setLooping(isLooping);
|
||||
}
|
||||
this.publisher.notifySubs(BeatEvents.GlobalDisplayTypeChanged);
|
||||
}
|
||||
|
||||
isLooping(): boolean {
|
||||
return this.globalIsLooping;
|
||||
}
|
||||
|
||||
private findSmallestLoopLength(): number {
|
||||
const loopLengths = [this.timeSigUp];
|
||||
for (const track of this.tracks) {
|
||||
if (track.isLooping()) {
|
||||
const loopLength = track.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 (!Track.isValidTimeSigRange(timeSigUp)) {
|
||||
timeSigUp = this.timeSigUp;
|
||||
}
|
||||
this.timeSigUp = timeSigUp;
|
||||
for (const track of this.tracks) {
|
||||
track.setTimeSignature({up: timeSigUp});
|
||||
}
|
||||
this.autoBeatLength();
|
||||
this.publisher.notifySubs(BeatEvents.TimeSigUpChanged);
|
||||
}
|
||||
|
||||
getTimeSigUp(): number {
|
||||
return this.timeSigUp;
|
||||
}
|
||||
|
||||
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.NewName);
|
||||
this.publisher.notifySubs(BeatEvents.NameChanged);
|
||||
}
|
||||
|
||||
getName(): string {
|
||||
return this.name;
|
||||
}
|
||||
|
||||
isLooping(): boolean {
|
||||
return this.looping;
|
||||
}
|
||||
|
||||
getLoopLength(): number {
|
||||
return this.loopLength;
|
||||
}
|
||||
|
||||
delete(): void {
|
||||
this.publisher.notifySubs(BeatEvents.WantsRemoval);
|
||||
}
|
||||
|
||||
bakeLoops(): void {
|
||||
if (this.isLooping()) {
|
||||
this.unitRecord.forEach((unit, i) => {
|
||||
const reprUnitAtPos = this.getUnitByIndex(i);
|
||||
if (reprUnitAtPos) {
|
||||
unit.mimic(reprUnitAtPos);
|
||||
}
|
||||
});
|
||||
this.publisher.notifySubs(BeatEvents.Baked);
|
||||
this.setLooping(false);
|
||||
} else {
|
||||
this.publisher.notifySubs(BeatEvents.Baked);
|
||||
}
|
||||
}
|
||||
}
|
||||
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";
|
||||
|
||||
export type MaybeRef<T> = T | Ref<T>;
|
||||
|
||||
class RefSubscription implements ISubscription {
|
||||
private unbindCallback?: () => void;
|
||||
|
||||
@@ -16,17 +18,27 @@ interface Stringable {
|
||||
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 value: T;
|
||||
private asString?: string;
|
||||
private isString: boolean;
|
||||
|
||||
constructor(val: T) {
|
||||
private constructor(val: T) {
|
||||
this.value = val;
|
||||
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 {
|
||||
if (this.watchers === null) {
|
||||
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 Beat from "./Beat";
|
||||
import {TrackUnitType} from "./TrackUnit";
|
||||
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 {
|
||||
width: max-content;
|
||||
margin-bottom: 4px;
|
||||
padding: 1em;
|
||||
overflow-x: scroll;
|
||||
overflow-y: hidden;
|
||||
display: flex;
|
||||
width: inherit;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.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 Beat, {BeatEvents} from "@/Beat";
|
||||
import ISubscriber from "@/Subscriber";
|
||||
import BeatUnitView from "@/ui/BeatUnit/BeatUnitView";
|
||||
import TrackView from "@/ui/Track/TrackView";
|
||||
import "./Beat.css";
|
||||
import ISubscriber from "@/Subscriber";
|
||||
import {ISubscription} from "@/Publisher";
|
||||
import Ref from "@/Ref";
|
||||
|
||||
export type BeatUINodeOptions = UINodeOptions & {
|
||||
title: string,
|
||||
beat: Beat,
|
||||
orientation?: "horizontal" | "vertical",
|
||||
};
|
||||
|
||||
const EventTypeSubscriptions = [
|
||||
BeatEvents.NewName,
|
||||
BeatEvents.NewTimeSig,
|
||||
BeatEvents.NewBarCount,
|
||||
BeatEvents.DisplayTypeChanged,
|
||||
BeatEvents.LoopLengthChanged,
|
||||
BeatEvents.TrackListChanged
|
||||
];
|
||||
|
||||
type EventTypeSubscriptions = FlatArray<typeof EventTypeSubscriptions, 1>;
|
||||
|
||||
export default class BeatView extends UINode implements ISubscriber<EventTypeSubscriptions> {
|
||||
private beat!: Beat;
|
||||
private title = new Ref<HTMLHeadingElement | null>(null);
|
||||
private beatUnitViews: BeatUnitView[] = [];
|
||||
private beatUnitViewBlock: HTMLElement | null = null;
|
||||
private lastHoveredBeatUnitView: BeatUnitView | null = null;
|
||||
private sub: ISubscription | null = null;
|
||||
static deselectingUnits = false;
|
||||
static selectingUnits = false;
|
||||
private title: string;
|
||||
private beat: Beat;
|
||||
private trackViews: TrackView[] = [];
|
||||
private currentOrientation: "vertical" | "horizontal";
|
||||
private subscription: ISubscription;
|
||||
|
||||
constructor(options: BeatUINodeOptions) {
|
||||
super(options);
|
||||
this.setBeat(options.beat);
|
||||
}
|
||||
|
||||
setBeat(beat: Beat | null): void {
|
||||
if (beat) {
|
||||
this.beat = beat;
|
||||
this.sub?.unbind();
|
||||
this.sub = this.beat.addSubscriber(this, EventTypeSubscriptions);
|
||||
this.redraw();
|
||||
} else {
|
||||
this.sub?.unbind();
|
||||
}
|
||||
this.beat = options.beat;
|
||||
this.title = options.title;
|
||||
this.currentOrientation = options.orientation ?? "horizontal";
|
||||
this.subscription = this.beat.addSubscriber(this, EventTypeSubscriptions);
|
||||
this.setupBeatViews();
|
||||
}
|
||||
|
||||
notify(publisher: unknown, event: EventTypeSubscriptions): void {
|
||||
switch (event) {
|
||||
case BeatEvents.NewName:
|
||||
this.title.val!.innerText = this.beat.getName();
|
||||
break;
|
||||
case BeatEvents.NewTimeSig:
|
||||
case BeatEvents.NewBarCount:
|
||||
case BeatEvents.DisplayTypeChanged:
|
||||
case BeatEvents.LoopLengthChanged:
|
||||
this.setupBeatUnits();
|
||||
break;
|
||||
if (event === BeatEvents.TrackListChanged) {
|
||||
this.setupBeatViews();
|
||||
this.redraw();
|
||||
}
|
||||
}
|
||||
|
||||
private rebuildBeatUnitViews() {
|
||||
const beatUnitCount = this.beat.getBarCount() * this.beat.getTimeSigUp();
|
||||
for (let i = 0; i < beatUnitCount; i++) {
|
||||
const beatUnit = this.beat.getUnitByIndex(i);
|
||||
if (beatUnit) {
|
||||
let view: BeatUnitView;
|
||||
if (this.beatUnitViews[i]) {
|
||||
view = this.beatUnitViews[i];
|
||||
view.setUnit(beatUnit);
|
||||
private setupBeatViews(): void {
|
||||
const newCount = this.beat.getTrackCount();
|
||||
for (let i = 0; i < newCount; i++) {
|
||||
const beat = this.beat.getTrackByIndex(i);
|
||||
if (beat && this.trackViews[i]) {
|
||||
this.trackViews[i].setBeat(beat);
|
||||
} else {
|
||||
view = new BeatUnitView({beatUnit});
|
||||
this.beatUnitViews.push(view);
|
||||
view.onHover(() => this.onBeatViewHover(view));
|
||||
view.onMouseDown((event: MouseEvent) => this.onBeatUnitClick(event.button, i));
|
||||
this.trackViews.push(new TrackView({track: this.beat.getTrackByIndex(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);
|
||||
const deadTrackViews = this.trackViews.splice(newCount, this.trackViews.length - newCount);
|
||||
deadTrackViews.forEach(beatView => beatView.setBeat(null));
|
||||
if (this.currentOrientation === "horizontal") {
|
||||
this.reverseDisplayOrder();
|
||||
}
|
||||
}
|
||||
|
||||
private onBeatViewHover(beatView: BeatUnitView) {
|
||||
this.lastHoveredBeatUnitView = beatView;
|
||||
if (BeatView.selectingUnits) {
|
||||
this.lastHoveredBeatUnitView.turnOn();
|
||||
} else if (BeatView.deselectingUnits) {
|
||||
this.lastHoveredBeatUnitView.turnOff();
|
||||
setOrientation(orientation: "vertical" | "horizontal"): void {
|
||||
if (this.currentOrientation !== orientation) {
|
||||
this.reverseDisplayOrder();
|
||||
this.currentOrientation = orientation;
|
||||
}
|
||||
}
|
||||
|
||||
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 reverseDisplayOrder(): void {
|
||||
this.trackViews.reverse();
|
||||
this.getNode().classList.toggle("vertical");
|
||||
this.redraw();
|
||||
}
|
||||
|
||||
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 {
|
||||
break;
|
||||
}
|
||||
i++;
|
||||
bars++;
|
||||
if (bars === barCount) {
|
||||
spacersInserted = true;
|
||||
}
|
||||
}
|
||||
setBeatGroup(newBeatGroup: Beat): void {
|
||||
this.beat = newBeatGroup;
|
||||
this.subscription.unbind();
|
||||
this.subscription = this.beat.addSubscriber(this, BeatEvents.TrackListChanged);
|
||||
this.setupBeatViews();
|
||||
this.redraw();
|
||||
}
|
||||
|
||||
private setupBeatUnits(): void {
|
||||
this.rebuildBeatUnitViews();
|
||||
this.buildBeatUnitViewBlock();
|
||||
this.respaceBeatUnits();
|
||||
}
|
||||
|
||||
build(): HTMLElement {
|
||||
this.setupBeatUnits();
|
||||
if (!this.beatUnitViewBlock) {
|
||||
throw new Error("Beat unit block setup failed!");
|
||||
}
|
||||
build(): HTMLDivElement {
|
||||
return h("div", {
|
||||
classes: ["beat"],
|
||||
},[
|
||||
h("div", {
|
||||
classes: ["beat-main"],
|
||||
}, [
|
||||
h("h3", {
|
||||
innerText: this.beat.getName(),
|
||||
saveTo: this.title,
|
||||
classes: ["beat-title"],
|
||||
}),
|
||||
this.beatUnitViewBlock,
|
||||
]),
|
||||
...this.trackViews
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
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-title-container {
|
||||
|
||||
}
|
||||
|
||||
.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;
|
||||
.beat-settings-options {
|
||||
padding: 1em;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-evenly;
|
||||
}
|
||||
|
||||
.beat-settings-option {
|
||||
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 {
|
||||
margin-right: 0;
|
||||
}
|
||||
|
||||
.beat-settings .loop-settings {
|
||||
text-align: left;
|
||||
flex: auto;
|
||||
}
|
||||
|
||||
.beat-settings .loop-settings-option.hide {
|
||||
.beat-settings-option-group {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.beat-settings .loop-settings-option {
|
||||
flex: auto;
|
||||
padding-right: 1em;
|
||||
.beat-settings-option-group.visible {
|
||||
display: inline-block;
|
||||
}
|
||||
@@ -1,129 +1,139 @@
|
||||
import "./BeatSettings.css";
|
||||
import Beat, {BeatEvents} from "@/Beat";
|
||||
import UINode, {h, UINodeOptions} from "@/ui/UINode";
|
||||
import ISubscriber from "@/Subscriber";
|
||||
import {ISubscription} from "@/Publisher";
|
||||
import NumberInputView from "@/ui/Widgets/NumberInput/NumberInputView";
|
||||
import ISubscriber from "@/Subscriber";
|
||||
import Beat, {BeatEvents} from "@/Beat";
|
||||
import BoolBoxView from "@/ui/Widgets/BoolBox/BoolBoxView";
|
||||
import TrackSettingsView from "@/ui/TrackSettings/TrackSettingsView";
|
||||
import ActionButtonView from "@/ui/Widgets/ActionButton/ActionButtonView";
|
||||
import EditableTextFieldView from "@/ui/Widgets/EditableTextFIeld/EditableTextFieldView";
|
||||
|
||||
export type BeatSettingsViewUINodeOptions = UINodeOptions & {
|
||||
export type BeatSettingsUINodeOptions = UINodeOptions & {
|
||||
beat: Beat,
|
||||
};
|
||||
|
||||
const EventTypeSubscriptions = [
|
||||
BeatEvents.NewName,
|
||||
BeatEvents.LoopLengthChanged,
|
||||
BeatEvents.DisplayTypeChanged,
|
||||
BeatEvents.TimeSigUpChanged,
|
||||
BeatEvents.BarCountChanged,
|
||||
BeatEvents.GlobalDisplayTypeChanged,
|
||||
BeatEvents.TrackListChanged,
|
||||
BeatEvents.LockingChanged,
|
||||
BeatEvents.AutoBeatSettingsChanged,
|
||||
];
|
||||
type EventTypeSubscriptions = FlatArray<typeof EventTypeSubscriptions, 1>;
|
||||
|
||||
export default class BeatSettingsView extends UINode implements ISubscriber<EventTypeSubscriptions> {
|
||||
private beat: Beat;
|
||||
private loopLengthInput!: NumberInputView;
|
||||
private bakeButton!: ActionButtonView;
|
||||
private loopCheckbox!: BoolBoxView;
|
||||
private loopLengthSection!: HTMLDivElement;
|
||||
private sub!: ISubscription;
|
||||
private title!: EditableTextFieldView;
|
||||
private editingTitle: boolean;
|
||||
private barCountInput!: NumberInputView;
|
||||
private timeSigUpInput!: NumberInputView;
|
||||
private autoBeatLengthCheckbox!: BoolBoxView;
|
||||
private trackSettingsViews: TrackSettingsView[] = [];
|
||||
private trackSettingsContainer!: HTMLDivElement;
|
||||
|
||||
constructor(options: BeatSettingsViewUINodeOptions) {
|
||||
constructor(options: BeatSettingsUINodeOptions) {
|
||||
super(options);
|
||||
this.editingTitle = false;
|
||||
this.beat = options.beat;
|
||||
this.setupBindings();
|
||||
}
|
||||
|
||||
private setupBindings() {
|
||||
this.sub = this.beat.addSubscriber(this, EventTypeSubscriptions);
|
||||
}
|
||||
|
||||
setBeat(beat: Beat): void {
|
||||
this.sub.unbind();
|
||||
this.beat = beat;
|
||||
setBeatGroup(newBeat: Beat): void {
|
||||
this.beat = newBeat;
|
||||
this.setupBindings();
|
||||
EventTypeSubscriptions.forEach(eventType => this.notify(null, eventType));
|
||||
}
|
||||
|
||||
setupBindings(): void {
|
||||
this.beat.addSubscriber(this, EventTypeSubscriptions);
|
||||
}
|
||||
|
||||
notify(publisher: unknown, event: EventTypeSubscriptions): void {
|
||||
switch(event) {
|
||||
case BeatEvents.NewName:
|
||||
this.title.setText(this.beat.getName());
|
||||
case BeatEvents.BarCountChanged:
|
||||
this.barCountInput.setValue(this.beat.getBarCount());
|
||||
break;
|
||||
case BeatEvents.LoopLengthChanged:
|
||||
this.loopLengthInput.setValue(this.beat.getLoopLength());
|
||||
case BeatEvents.TimeSigUpChanged:
|
||||
this.timeSigUpInput.setValue(this.beat.getTimeSigUp());
|
||||
break;
|
||||
case BeatEvents.DisplayTypeChanged:
|
||||
this.loopCheckbox.setValue(this.beat.isLooping());
|
||||
this.bakeButton.setDisabled(!this.beat.isLooping());
|
||||
if (this.beat.isLooping()) {
|
||||
this.loopLengthSection.classList.remove("hide");
|
||||
case BeatEvents.TrackListChanged:
|
||||
this.remakeBeatSettingsViews();
|
||||
break;
|
||||
case BeatEvents.LockingChanged:
|
||||
if (this.beat.barsLocked()) {
|
||||
this.barCountInput.disable();
|
||||
} else {
|
||||
this.loopLengthSection.classList.add("hide");
|
||||
this.barCountInput.enable();
|
||||
}
|
||||
break;
|
||||
case BeatEvents.AutoBeatSettingsChanged:
|
||||
this.autoBeatLengthCheckbox.setValue(this.beat.autoBeatLengthOn());
|
||||
break;
|
||||
case BeatEvents.GlobalDisplayTypeChanged:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
build(): HTMLElement {
|
||||
this.title = new EditableTextFieldView({
|
||||
initialText: this.beat.getName(),
|
||||
setter: (newText) => this.beat.setName(newText),
|
||||
});
|
||||
this.bakeButton = new ActionButtonView({
|
||||
icon: "snowflake",
|
||||
type: "secondary",
|
||||
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");
|
||||
private remakeBeatSettingsViews() {
|
||||
const trackCount = this.beat.getTrackCount();
|
||||
this.trackSettingsViews.splice(trackCount, this.trackSettingsViews.length - trackCount);
|
||||
for (let i = 0; i < trackCount; i++) {
|
||||
if (this.trackSettingsViews[i]) {
|
||||
this.trackSettingsViews[i].setBeat(this.beat.getTrackByIndex(i));
|
||||
} else {
|
||||
this.loopLengthSection.classList.add("hide");
|
||||
this.trackSettingsViews.push(new TrackSettingsView({ track: this.beat.getTrackByIndex(i) }));
|
||||
}
|
||||
}
|
||||
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", {
|
||||
classes: ["beat-settings"],
|
||||
}, [
|
||||
h("div", {
|
||||
classes: ["beat-settings-title-container"]
|
||||
classes: ["beat-settings-options"],
|
||||
}, [
|
||||
this.title,
|
||||
h("div", {
|
||||
classes: ["beat-settings-boxes", "beat-settings-option"],
|
||||
}, [
|
||||
this.timeSigUpInput,
|
||||
]),
|
||||
h("div", {
|
||||
classes: ["beat-settings-lower"],
|
||||
classes: ["beat-settings-bar-count", "beat-settings-option"]
|
||||
,
|
||||
}, [
|
||||
this.bakeButton,
|
||||
this.barCountInput,
|
||||
]),
|
||||
h("div", {
|
||||
classes: ["beat-settings-bar-count", "beat-settings-option"],
|
||||
}, [
|
||||
this.autoBeatLengthCheckbox,
|
||||
]),
|
||||
new ActionButtonView({
|
||||
icon: "trash",
|
||||
type: "secondary",
|
||||
alt: "Delete Track",
|
||||
onClick: () => this.beat.delete(),
|
||||
label: "New Track",
|
||||
onClick: () => this.beat.addTrack(),
|
||||
}),
|
||||
h("div", {
|
||||
classes: ["loop-settings"],
|
||||
}, [
|
||||
this.loopCheckbox,
|
||||
]),
|
||||
this.loopLengthSection,
|
||||
this.trackSettingsContainer,
|
||||
]),
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -1,39 +1,39 @@
|
||||
import UINode, {h, UINodeOptions} from "@/ui/UINode";
|
||||
import BeatGroupView from "@/ui/BeatGroup/BeatGroupView";
|
||||
import BeatGroup from "@/BeatGroup";
|
||||
import BeatView from "@/ui/Beat/BeatView";
|
||||
import Beat from "@/Beat";
|
||||
import "./Root.css";
|
||||
import BeatGroupSettingsView from "@/ui/BeatGroupSettings/BeatGroupSettingsView";
|
||||
import BeatSettingsView from "@/ui/BeatSettings/BeatSettingsView";
|
||||
import IconView from "@/ui/Widgets/Icon/IconView";
|
||||
import StageTitleBarView from "@/ui/StageTitleBar/StageTitleBarView";
|
||||
import Ref from "@/Ref";
|
||||
|
||||
export type RootUINodeOptions = UINodeOptions & {
|
||||
title: string,
|
||||
mainBeatGroup?: BeatGroup,
|
||||
mainBeat?: Beat,
|
||||
orientation?: "horizontal" | "vertical",
|
||||
};
|
||||
|
||||
export default class RootView extends UINode {
|
||||
private title: string;
|
||||
private beatGroupView: BeatGroupView;
|
||||
private focusedBeatGroup: BeatGroup;
|
||||
private beatGroupSettingsView: BeatGroupSettingsView;
|
||||
private beatView: BeatView;
|
||||
private focusedBeat: Beat;
|
||||
private beatSettingsView: BeatSettingsView;
|
||||
private currentOrientation: "horizontal" | "vertical";
|
||||
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;
|
||||
|
||||
constructor(options: RootUINodeOptions) {
|
||||
super(options);
|
||||
this.currentOrientation = options.orientation ?? "horizontal";
|
||||
this.focusedBeatGroup = options.mainBeatGroup ?? RootView.defaultMainBeatGroup();
|
||||
this.beatGroupView = new BeatGroupView({
|
||||
this.focusedBeat = options.mainBeat ?? RootView.defaultMainBeatGroup();
|
||||
this.beatView = new BeatView({
|
||||
title: options.title,
|
||||
beatGroup: this.focusedBeatGroup,
|
||||
beat: this.focusedBeat,
|
||||
orientation: this.currentOrientation,
|
||||
});
|
||||
this.stageTitleBarView = new StageTitleBarView({beatGroup: this.focusedBeatGroup});
|
||||
this.beatGroupSettingsView = new BeatGroupSettingsView({beatGroup: this.focusedBeatGroup});
|
||||
this.stageTitleBarView = new StageTitleBarView({beat: this.focusedBeat});
|
||||
this.beatSettingsView = new BeatSettingsView({beat: this.focusedBeat});
|
||||
this.title = options.title;
|
||||
this.setOrientation(this.currentOrientation);
|
||||
this.openSidebarForDesktop();
|
||||
@@ -46,25 +46,25 @@ export default class RootView extends UINode {
|
||||
}
|
||||
}
|
||||
|
||||
static defaultMainBeatGroup(): BeatGroup {
|
||||
static defaultMainBeatGroup(): Beat {
|
||||
const defaultSettings = {
|
||||
barCount: 2,
|
||||
isLooping: false,
|
||||
timeSigUp: 8,
|
||||
};
|
||||
const mainBeatGroup = new BeatGroup(defaultSettings);
|
||||
mainBeatGroup.addBeat({name: "LF"});
|
||||
mainBeatGroup.addBeat({name: "LH"});
|
||||
mainBeatGroup.addBeat({name: "RH"});
|
||||
mainBeatGroup.addBeat({name: "RF"});
|
||||
const mainBeatGroup = new Beat(defaultSettings);
|
||||
mainBeatGroup.addTrack({name: "LF"});
|
||||
mainBeatGroup.addTrack({name: "LH"});
|
||||
mainBeatGroup.addTrack({name: "RH"});
|
||||
mainBeatGroup.addTrack({name: "RF"});
|
||||
return mainBeatGroup;
|
||||
}
|
||||
|
||||
setMainBeatGroup(beatGroup: BeatGroup): void {
|
||||
this.focusedBeatGroup = beatGroup;
|
||||
this.beatGroupSettingsView.setBeatGroup(this.focusedBeatGroup);
|
||||
this.beatGroupView.setBeatGroup(this.focusedBeatGroup);
|
||||
this.stageTitleBarView.setBeatGroup(this.focusedBeatGroup);
|
||||
setMainBeatGroup(beat: Beat): void {
|
||||
this.focusedBeat = beat;
|
||||
this.beatSettingsView.setBeatGroup(this.focusedBeat);
|
||||
this.beatView.setBeatGroup(this.focusedBeat);
|
||||
this.stageTitleBarView.setBeat(this.focusedBeat);
|
||||
}
|
||||
|
||||
toggleSidebar(): void {
|
||||
@@ -88,7 +88,7 @@ export default class RootView extends UINode {
|
||||
} else {
|
||||
this.getNode().classList.remove("vertical-mode");
|
||||
}
|
||||
this.beatGroupView.setOrientation(orientation);
|
||||
this.beatView.setOrientation(orientation);
|
||||
}
|
||||
|
||||
private sidebarText(): string {
|
||||
@@ -124,7 +124,7 @@ export default class RootView extends UINode {
|
||||
h("div", {
|
||||
classes: ["root-quick-access-button"],
|
||||
title: "Bake all tracks",
|
||||
onclick: () => this.focusedBeatGroup.bakeLoops(),
|
||||
onclick: () => this.focusedBeat.bakeLoops(),
|
||||
}, [
|
||||
new IconView({
|
||||
iconName: "snowflake",
|
||||
@@ -149,7 +149,7 @@ export default class RootView extends UINode {
|
||||
h("div", {classes: ["root-sidebar"]}, [
|
||||
h("div", {classes: ["root-settings"]}, [
|
||||
h("h1", {classes: ["root-title"], innerText: this.title}),
|
||||
this.beatGroupSettingsView,
|
||||
this.beatSettingsView,
|
||||
]),
|
||||
this.buildSidebarStrip(),
|
||||
])
|
||||
@@ -163,7 +163,7 @@ export default class RootView extends UINode {
|
||||
h("div", {classes: ["root-beat-stage-container"]}, [
|
||||
this.stageTitleBarView,
|
||||
h("div", {classes: ["root-beat-stage"]}, [
|
||||
this.beatGroupView,
|
||||
this.beatView,
|
||||
])
|
||||
])
|
||||
])
|
||||
|
||||
@@ -11,11 +11,6 @@
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.stage-title-bar-preamble {
|
||||
margin-bottom: 4px;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.stage-title-bar * {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
@@ -1,50 +1,54 @@
|
||||
import "./StageTitleBar.css";
|
||||
import UINode, {h, UINodeOptions} from "@/ui/UINode";
|
||||
import {ISubscription} from "@/Publisher";
|
||||
import BeatGroup, {BeatGroupEvents} from "@/BeatGroup";
|
||||
import Beat, {BeatEvents} from "@/Beat";
|
||||
import ISubscriber from "@/Subscriber";
|
||||
import EditableTextFieldView from "@/ui/Widgets/EditableTextFIeld/EditableTextFieldView";
|
||||
import DropdownView, {DropdownViewOption} from "@/ui/Widgets/Dropdown/DropdownView";
|
||||
import Ref from "@/Ref";
|
||||
|
||||
export type StageTitleBarViewOptions = UINodeOptions & {
|
||||
beatGroup: BeatGroup,
|
||||
beat: Beat,
|
||||
};
|
||||
|
||||
const EventTypeSubscription = [BeatGroupEvents.NameChanged];
|
||||
const EventTypeSubscription = [BeatEvents.NameChanged];
|
||||
type EventTypeSubscription = FlatArray<typeof EventTypeSubscription, 1>;
|
||||
|
||||
export default class StageTitleBarView extends UINode implements ISubscriber<EventTypeSubscription> {
|
||||
private sub: ISubscription;
|
||||
private beatGroup: BeatGroup;
|
||||
private beat: Beat;
|
||||
private title: EditableTextFieldView;
|
||||
private options: Ref<DropdownViewOption[]>;
|
||||
|
||||
constructor(options: StageTitleBarViewOptions) {
|
||||
super(options);
|
||||
this.beatGroup = options.beatGroup;
|
||||
this.sub = options.beatGroup.addSubscriber(this, EventTypeSubscription);
|
||||
this.beat = options.beat;
|
||||
this.sub = options.beat.addSubscriber(this, EventTypeSubscription);
|
||||
this.title = new EditableTextFieldView({
|
||||
initialText: this.beatGroup.getName(),
|
||||
setter: (text) => this.beatGroup.setName(text),
|
||||
initialText: this.beat.getName(),
|
||||
setter: (text) => this.beat.setName(text),
|
||||
noEmpty: true,
|
||||
});
|
||||
this.options = Ref.new<DropdownViewOption[]>([]);
|
||||
}
|
||||
|
||||
notify(publisher: unknown, event: EventTypeSubscription): void {
|
||||
if (event === BeatGroupEvents.NameChanged) {
|
||||
this.title.setText(this.beatGroup.getName());
|
||||
if (event === BeatEvents.NameChanged) {
|
||||
this.title.setText(this.beat.getName());
|
||||
}
|
||||
}
|
||||
|
||||
setBeatGroup(beatGroup: BeatGroup): void {
|
||||
setBeat(beat: Beat): void {
|
||||
this.sub.unbind();
|
||||
this.beatGroup = beatGroup;
|
||||
this.sub = beatGroup.addSubscriber(this, EventTypeSubscription);
|
||||
this.notify(this, BeatGroupEvents.NameChanged);
|
||||
this.beat = beat;
|
||||
this.sub = beat.addSubscriber(this, EventTypeSubscription);
|
||||
this.notify(this, BeatEvents.NameChanged);
|
||||
}
|
||||
|
||||
protected build(): HTMLElement {
|
||||
return h("div", {classes: ["stage-title-bar"]}, [
|
||||
h("div", {classes: ["stage-title-bar-preamble"], innerText: "Currently editing:"}),
|
||||
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;
|
||||
height: 2em;
|
||||
margin-right: 4px;
|
||||
@@ -11,45 +11,45 @@
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.beat-unit:hover {
|
||||
.track-unit:hover {
|
||||
border-color: #5f5f5f;
|
||||
background-color: #5f5f5f;
|
||||
transition: none;
|
||||
}
|
||||
|
||||
.vertical-mode .beat-unit {
|
||||
.vertical-mode .track-unit {
|
||||
margin-bottom: 4px;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.beat-unit.beat-unit-on {
|
||||
.track-unit.track-unit-on {
|
||||
border-color: var(--color-ui-accent);
|
||||
background-color: var(--color-ui-accent);
|
||||
transition: none;
|
||||
}
|
||||
|
||||
.beat-unit.beat-unit-on:hover {
|
||||
.track-unit.track-unit-on:hover {
|
||||
border-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);
|
||||
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);
|
||||
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);
|
||||
background-color: var(--color-ui-accent);
|
||||
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);
|
||||
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 UINode, {h, UINodeOptions} from "@/ui/UINode";
|
||||
import {IPublisher, ISubscription, Publisher} from "@/Publisher";
|
||||
import "./BeatUnit.css";
|
||||
import "./TrackUnit.css";
|
||||
|
||||
export type BeatUnitUINodeOptions = UINodeOptions & {
|
||||
beatUnit: BeatUnit,
|
||||
export type TrackUnitUINodeOptions = UINodeOptions & {
|
||||
trackUnit: TrackUnit,
|
||||
};
|
||||
|
||||
const EventTypeSubscriptions = [
|
||||
BeatUnitEvent.On,
|
||||
BeatUnitEvent.Off,
|
||||
BeatUnitEvent.TypeChange,
|
||||
TrackUnitEvent.On,
|
||||
TrackUnitEvent.Off,
|
||||
TrackUnitEvent.TypeChange,
|
||||
];
|
||||
type EventTypeSubscriptions = FlatArray<typeof EventTypeSubscriptions, 1>;
|
||||
|
||||
export default class BeatUnitView extends UINode implements ISubscriber<EventTypeSubscriptions> {
|
||||
private beatUnit: BeatUnit;
|
||||
export default class TrackUnitView extends UINode implements ISubscriber<EventTypeSubscriptions> {
|
||||
private trackUnit: TrackUnit;
|
||||
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 mouseDownListeners: ((ev: MouseEvent) => void)[] = [];
|
||||
private hoverListeners: ((ev: MouseEvent) => void)[] = [];
|
||||
|
||||
constructor(options: BeatUnitUINodeOptions) {
|
||||
constructor(options: TrackUnitUINodeOptions) {
|
||||
super(options);
|
||||
this.beatUnit = options.beatUnit;
|
||||
this.trackUnit = options.trackUnit;
|
||||
this.setupBindings();
|
||||
}
|
||||
|
||||
setUnit(beatUnit: BeatUnit | null): void {
|
||||
if (beatUnit) {
|
||||
this.beatUnit = beatUnit;
|
||||
setUnit(trackUnit: TrackUnit | null): void {
|
||||
if (trackUnit) {
|
||||
this.trackUnit = trackUnit;
|
||||
this.setupBindings();
|
||||
this.notify(this.publisher, beatUnit.isOn() ? BeatUnitEvent.On : BeatUnitEvent.Off);
|
||||
this.notify(this.publisher, BeatUnitEvent.TypeChange);
|
||||
this.notify(this.publisher, trackUnit.isOn() ? TrackUnitEvent.On : TrackUnitEvent.Off);
|
||||
this.notify(this.publisher, TrackUnitEvent.TypeChange);
|
||||
} else {
|
||||
this.subscription?.unbind();
|
||||
}
|
||||
@@ -42,7 +42,7 @@ export default class BeatUnitView extends UINode implements ISubscriber<EventTyp
|
||||
|
||||
private setupBindings() {
|
||||
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.hoverListeners.forEach(listener => this.getNode().removeEventListener("mouseover", listener));
|
||||
this.redraw();
|
||||
@@ -55,13 +55,13 @@ export default class BeatUnitView extends UINode implements ISubscriber<EventTyp
|
||||
|
||||
private handleMouseDown(ev: MouseEvent): void {
|
||||
if (ev.button === 1) {
|
||||
this.beatUnit.rotateType();
|
||||
this.trackUnit.rotateType();
|
||||
}
|
||||
}
|
||||
|
||||
private handleTouchStart(ev: TouchEvent): void {
|
||||
this.touchTimeout = this.touchTimeout || setTimeout(() => {
|
||||
this.beatUnit.rotateType();
|
||||
this.trackUnit.rotateType();
|
||||
this.touchTimeout = null;
|
||||
}, 400);
|
||||
}
|
||||
@@ -74,38 +74,38 @@ export default class BeatUnitView extends UINode implements ISubscriber<EventTyp
|
||||
}
|
||||
|
||||
toggle(): void {
|
||||
this.beatUnit.toggle();
|
||||
this.trackUnit.toggle();
|
||||
}
|
||||
|
||||
turnOn(): void {
|
||||
this.beatUnit.setOn(true);
|
||||
this.trackUnit.setOn(true);
|
||||
}
|
||||
|
||||
turnOff(): void {
|
||||
this.beatUnit.setOn(false);
|
||||
this.trackUnit.setOn(false);
|
||||
}
|
||||
|
||||
notify(publisher: unknown, event: EventTypeSubscriptions): void {
|
||||
switch (event) {
|
||||
case BeatUnitEvent.On:
|
||||
this.getNode().classList.add("beat-unit-on");
|
||||
case TrackUnitEvent.On:
|
||||
this.getNode().classList.add("track-unit-on");
|
||||
break;
|
||||
case BeatUnitEvent.Off:
|
||||
this.getNode().classList.remove("beat-unit-on");
|
||||
case TrackUnitEvent.Off:
|
||||
this.getNode().classList.remove("track-unit-on");
|
||||
break;
|
||||
case BeatUnitEvent.TypeChange:
|
||||
switch (this.beatUnit.getType()) {
|
||||
case BeatUnitType.Normal:
|
||||
this.getNode().classList.remove("beat-unit-ghost");
|
||||
this.getNode().classList.remove("beat-unit-accent");
|
||||
case TrackUnitEvent.TypeChange:
|
||||
switch (this.trackUnit.getType()) {
|
||||
case TrackUnitType.Normal:
|
||||
this.getNode().classList.remove("track-unit-ghost");
|
||||
this.getNode().classList.remove("track-unit-accent");
|
||||
break;
|
||||
case BeatUnitType.GhostNote:
|
||||
this.getNode().classList.remove("beat-unit-accent");
|
||||
this.getNode().classList.add("beat-unit-ghost");
|
||||
case TrackUnitType.GhostNote:
|
||||
this.getNode().classList.remove("track-unit-accent");
|
||||
this.getNode().classList.add("track-unit-ghost");
|
||||
break;
|
||||
case BeatUnitType.Accent:
|
||||
this.getNode().classList.remove("beat-unit-ghost");
|
||||
this.getNode().classList.add("beat-unit-accent");
|
||||
case TrackUnitType.Accent:
|
||||
this.getNode().classList.remove("track-unit-ghost");
|
||||
this.getNode().classList.add("track-unit-accent");
|
||||
break;
|
||||
}
|
||||
break;
|
||||
@@ -113,9 +113,9 @@ export default class BeatUnitView extends UINode implements ISubscriber<EventTyp
|
||||
}
|
||||
|
||||
build(): HTMLElement {
|
||||
const classes = ["beat-unit"];
|
||||
if (this.beatUnit.isOn()) {
|
||||
classes.push("beat-unit-on");
|
||||
const classes = ["track-unit"];
|
||||
if (this.trackUnit.isOn()) {
|
||||
classes.push("track-unit-on");
|
||||
}
|
||||
return h("div", {
|
||||
classes: classes,
|
||||
@@ -49,45 +49,6 @@ export default abstract class UINode {
|
||||
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 {
|
||||
const frag = document.createDocumentFragment();
|
||||
if (subs) {
|
||||
@@ -96,6 +57,27 @@ export function frag(subs?: Node[]): DocumentFragment {
|
||||
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 {
|
||||
if (!textNode.parentNode) {
|
||||
sub.unbind();
|
||||
@@ -119,3 +101,21 @@ function attachSubs(node: Element | DocumentFragment, subNodes: (Node | UINode |
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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 {
|
||||
width: 1.4em;
|
||||
border-radius: 0 0.5em 0.5em 0;
|
||||
}
|
||||
|
||||
.number-input-dec {
|
||||
width: 1.4em;
|
||||
border-radius: 0.5em 0 0 0.5em;
|
||||
}
|
||||
|
||||
|
||||
@@ -27,8 +27,8 @@ type NumberInputUINodeOptionsGetSet = NumberInputUINodeOptionsBase & {
|
||||
export type NumberInputUINodeOptions = NumberInputUINodeOptionsGetSet | NumberInputUINodeOptionsIncDecInput;
|
||||
|
||||
export default class NumberInputView extends UINode {
|
||||
private labelElement: Ref<HTMLLabelElement | null> = new Ref<HTMLLabelElement | null>(null);
|
||||
private inputElement: Ref<HTMLInputElement | null> = new Ref<HTMLInputElement | null>(null);
|
||||
private labelElement: Ref<HTMLLabelElement | null> = Ref.new<HTMLLabelElement | null>(null);
|
||||
private inputElement: Ref<HTMLInputElement | null> = Ref.new<HTMLInputElement | null>(null);
|
||||
private labelPosition: "top" | "left";
|
||||
private value: number;
|
||||
private label: string | null;
|
||||
|
||||
Reference in New Issue
Block a user