refactor: moved to new @djledda/ladder and vite
This commit is contained in:
@@ -16,6 +16,10 @@
|
||||
"@typescript-eslint"
|
||||
],
|
||||
"rules": {
|
||||
"object-curly-spacing": [
|
||||
"error",
|
||||
"always"
|
||||
],
|
||||
"indent": [
|
||||
"error",
|
||||
4
|
||||
|
||||
@@ -7,11 +7,9 @@
|
||||
<title>Drum Slayer</title>
|
||||
|
||||
<link rel='icon' type='image/png' href='./favicon.png'>
|
||||
<link rel='stylesheet' href='./static/main.css'>
|
||||
|
||||
<script defer src='./static/bundle.js'></script>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
<div id="app"></div>
|
||||
<script type="module" src='/src/main.ts'></script>
|
||||
</body>
|
||||
</html>
|
||||
6744
package-lock.json
generated
6744
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
20
package.json
20
package.json
@@ -14,22 +14,14 @@
|
||||
"author": "Daniel Ledda",
|
||||
"license": "ISC",
|
||||
"devDependencies": {
|
||||
"@typescript-eslint/eslint-plugin": "^4.29.2",
|
||||
"@typescript-eslint/parser": "^4.29.2",
|
||||
"@webpack-cli/generators": "^2.3.0",
|
||||
"css-loader": "^6.2.0",
|
||||
"eslint": "^7.32.0",
|
||||
"mini-css-extract-plugin": "^2.2.0",
|
||||
"source-map-support": "^0.5.19",
|
||||
"style-loader": "^3.2.1",
|
||||
"ts-loader": "^9.2.5",
|
||||
"tslib": "^2.0.0",
|
||||
"typescript": "^4.4.0-insiders.20210805",
|
||||
"webpack": "^5.51.1",
|
||||
"webpack-cli": "^4.8.0",
|
||||
"webpack-dev-server": "^4.0.0"
|
||||
"@typescript-eslint/eslint-plugin": "^5.26.0",
|
||||
"@typescript-eslint/parser": "^5.26.0",
|
||||
"eslint": "^8.16.0",
|
||||
"typescript": "^4.7.2",
|
||||
"vite": "^2.9.9"
|
||||
},
|
||||
"dependencies": {
|
||||
"@djledda/ladder": "^1.0.2",
|
||||
"file-loader": "^6.2.0"
|
||||
}
|
||||
}
|
||||
|
||||
18
src/Beat.ts
18
src/Beat.ts
@@ -1,8 +1,6 @@
|
||||
import Track, {TrackEvents, TrackInitOptions} from "@/Track";
|
||||
import {IPublisher, Publisher} from "@/Publisher";
|
||||
import ISubscriber from "@/Subscriber";
|
||||
import {greatestCommonDivisor, isPosInt} from "@/utils";
|
||||
import Ref from "@/Ref";
|
||||
import Track, { TrackEvents, TrackInitOptions } from "@/Track";
|
||||
import { greatestCommonDivisor, isPosInt } from "@/utils";
|
||||
import { Capsule, ICapsule, IPublisher, ISubscriber, Publisher } from "@djledda/ladder";
|
||||
|
||||
type BeatGroupInitOptions = {
|
||||
barCount: number;
|
||||
@@ -56,14 +54,14 @@ export default class Beat implements IPublisher<BeatEvents>, ISubscriber<EventTy
|
||||
private globalIsLooping: boolean;
|
||||
private useAutoBeatLength: boolean;
|
||||
private barSettingsLocked = false;
|
||||
private name: Ref<string>;
|
||||
private name: ICapsule<string>;
|
||||
|
||||
constructor(options?: BeatGroupInitOptions) {
|
||||
Beat.globalCounter++;
|
||||
if (options?.name) {
|
||||
this.name = Ref.new<string>(options.name);
|
||||
this.name = Capsule.new<string>(options.name);
|
||||
} else {
|
||||
this.name = Ref.new<string>(`Pattern ${Beat.globalCounter}`);
|
||||
this.name = Capsule.new<string>(`Pattern ${Beat.globalCounter}`);
|
||||
}
|
||||
if (options?.tracks) {
|
||||
for (const trackOptions of options.tracks) {
|
||||
@@ -185,7 +183,7 @@ export default class Beat implements IPublisher<BeatEvents>, ISubscriber<EventTy
|
||||
}
|
||||
this.timeSigUp = timeSigUp;
|
||||
for (const track of this.tracks) {
|
||||
track.setTimeSignature({up: timeSigUp});
|
||||
track.setTimeSignature({ up: timeSigUp });
|
||||
}
|
||||
this.autoBeatLength();
|
||||
this.publisher.notifySubs(BeatEvents.TimeSigUpChanged);
|
||||
@@ -332,7 +330,7 @@ export default class Beat implements IPublisher<BeatEvents>, ISubscriber<EventTy
|
||||
this.name.val = newName;
|
||||
}
|
||||
|
||||
getName(): Ref<string> {
|
||||
getName(): ICapsule<string> {
|
||||
return this.name;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import Beat, {BeatEvents} from "@/Beat";
|
||||
import Ref from "@/Ref";
|
||||
import ISubscriber from "@/Subscriber";
|
||||
import Beat, { BeatEvents } from "@/Beat";
|
||||
import { Capsule, ICapsule, ISubscriber } from "@djledda/ladder";
|
||||
|
||||
const EventTypeSubscriptions = [
|
||||
BeatEvents.TimeSigUpChanged,
|
||||
@@ -15,7 +14,7 @@ type EventTypeSubscriptions = typeof EventTypeSubscriptions[number];
|
||||
|
||||
export default class BeatStore implements ISubscriber<EventTypeSubscriptions> {
|
||||
private readonly beats: Beat[];
|
||||
private activeBeat: Ref<Beat>;
|
||||
private activeBeat: ICapsule<Beat>;
|
||||
private onBeatChangeCbs: (() => void)[] = [];
|
||||
private autoSave: boolean;
|
||||
private orientation: "horizontal" | "vertical" | null = null;
|
||||
@@ -27,7 +26,7 @@ export default class BeatStore implements ISubscriber<EventTypeSubscriptions> {
|
||||
if (save) {
|
||||
const serial = JSON.parse(save);
|
||||
this.beats = [BeatStore.defaultMainBeatGroup()];
|
||||
this.activeBeat = Ref.new(this.beats[0]);
|
||||
this.activeBeat = Capsule.new(this.beats[0]);
|
||||
this.loadFromSave(serial);
|
||||
if (this.autoSave) {
|
||||
this.activeBeat.watch(() => this.save("localStorage"), true);
|
||||
@@ -39,7 +38,7 @@ export default class BeatStore implements ISubscriber<EventTypeSubscriptions> {
|
||||
this.beats = [
|
||||
BeatStore.defaultMainBeatGroup(),
|
||||
];
|
||||
this.activeBeat = Ref.new(this.beats[0]);
|
||||
this.activeBeat = Capsule.new(this.beats[0]);
|
||||
}
|
||||
|
||||
notify(publisher: unknown, event: EventTypeSubscriptions): void {
|
||||
@@ -53,14 +52,14 @@ export default class BeatStore implements ISubscriber<EventTypeSubscriptions> {
|
||||
timeSigUp: 8,
|
||||
};
|
||||
const mainBeatGroup = new Beat(defaultSettings);
|
||||
mainBeatGroup.addTrack({name: "LF"});
|
||||
mainBeatGroup.addTrack({name: "LH"});
|
||||
mainBeatGroup.addTrack({name: "RH"});
|
||||
mainBeatGroup.addTrack({name: "RF"});
|
||||
mainBeatGroup.addTrack({ name: "LF" });
|
||||
mainBeatGroup.addTrack({ name: "LH" });
|
||||
mainBeatGroup.addTrack({ name: "RH" });
|
||||
mainBeatGroup.addTrack({ name: "RF" });
|
||||
return mainBeatGroup;
|
||||
}
|
||||
|
||||
getActiveBeat(): Ref<Beat> {
|
||||
getActiveBeat(): ICapsule<Beat> {
|
||||
return this.activeBeat;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,91 +0,0 @@
|
||||
import ISubscriber, {LEvent} from "./Subscriber";
|
||||
|
||||
class PublisherSubscription<EventType extends LEvent> implements ISubscription {
|
||||
private subscriber: ISubscriber<EventType>;
|
||||
private readonly eventTypes: EventType[];
|
||||
private unbindCallback?: () => void;
|
||||
|
||||
constructor(subscriber: ISubscriber<EventType>, eventTypes: EventType[]) {
|
||||
this.subscriber = subscriber;
|
||||
this.eventTypes = eventTypes;
|
||||
}
|
||||
|
||||
setUnbind(unbind: () => void): void {
|
||||
this.unbindCallback = unbind;
|
||||
}
|
||||
|
||||
unbind(): void {
|
||||
this.unbindCallback?.();
|
||||
}
|
||||
|
||||
getEventTypes(): EventType[] {
|
||||
return this.eventTypes;
|
||||
}
|
||||
|
||||
getSubscriber(): ISubscriber<EventType> {
|
||||
return this.subscriber;
|
||||
}
|
||||
}
|
||||
|
||||
interface EventSubscriberRecord<T extends LEvent> {
|
||||
get<K extends T>(key: K): ISubscriber<K>[];
|
||||
set<K extends T>(key: K, subscribers: ISubscriber<K>[]): EventSubscriberRecord<T>;
|
||||
}
|
||||
|
||||
|
||||
export class Publisher<EventType extends LEvent, PublisherType> implements IPublisher<EventType> {
|
||||
private subscribers: EventSubscriberRecord<EventType>;
|
||||
private parent: PublisherType;
|
||||
|
||||
constructor(parent: PublisherType) {
|
||||
this.parent = parent;
|
||||
this.subscribers = new Map();
|
||||
}
|
||||
|
||||
addSubscriber(subscriber: ISubscriber<EventType>, subscribeTo: EventType | Readonly<EventType[]>): ISubscription {
|
||||
let eventTypes: EventType[] = [];
|
||||
if (typeof subscribeTo === "string") {
|
||||
eventTypes.push(subscribeTo);
|
||||
} else {
|
||||
eventTypes = subscribeTo.slice();
|
||||
}
|
||||
for (const key of eventTypes) {
|
||||
this.getSubscribers(key).push(subscriber);
|
||||
}
|
||||
const sub = new PublisherSubscription(subscriber, eventTypes);
|
||||
sub.setUnbind(() => this.unbind(sub));
|
||||
return sub;
|
||||
}
|
||||
|
||||
private unbind(subscription: PublisherSubscription<EventType>): void {
|
||||
for (const key of subscription.getEventTypes()) {
|
||||
const subs = this.getSubscribers(key);
|
||||
subs.splice(subs.indexOf(subscription.getSubscriber()), 1);
|
||||
}
|
||||
}
|
||||
|
||||
private getSubscribers<K extends EventType>(key: K): ISubscriber<K>[] {
|
||||
const subscribersList = this.subscribers.get(key);
|
||||
if (subscribersList === undefined) {
|
||||
const newList: ISubscriber<K>[] = [];
|
||||
this.subscribers.set(key, newList);
|
||||
return newList;
|
||||
} else {
|
||||
return subscribersList;
|
||||
}
|
||||
}
|
||||
|
||||
notifySubs<K extends EventType>(eventType: K): void {
|
||||
for (const sub of this.getSubscribers(eventType)) {
|
||||
sub.notify(this.parent, eventType);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export interface IPublisher<T extends LEvent> {
|
||||
addSubscriber(subscriber: ISubscriber<T>, subscribeTo: T | T[]): {unbind: () => void};
|
||||
}
|
||||
|
||||
export interface ISubscription {
|
||||
unbind(): void;
|
||||
}
|
||||
99
src/Ref.ts
99
src/Ref.ts
@@ -1,99 +0,0 @@
|
||||
import {ISubscription} from "@/Publisher";
|
||||
|
||||
export type MaybeRef<T> = T | Ref<T>;
|
||||
|
||||
class RefSubscription implements ISubscription {
|
||||
private unbindCallback?: () => void;
|
||||
|
||||
constructor(unbindCallback: () => void) {
|
||||
this.unbindCallback = unbindCallback;
|
||||
}
|
||||
|
||||
unbind(): void {
|
||||
this.unbindCallback?.();
|
||||
}
|
||||
}
|
||||
|
||||
interface Stringable {
|
||||
toString(): string;
|
||||
}
|
||||
|
||||
type AllowedRef = { toString(): string } | string | null;
|
||||
|
||||
export default class Ref<T extends AllowedRef = Stringable> {
|
||||
private watchers: Array<(newVal: T) => void> | null = null;
|
||||
private afterWatchers: Array<(newVal: T) => void> | null = null;
|
||||
private value: T;
|
||||
private asString?: string;
|
||||
private isString: boolean;
|
||||
|
||||
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, after?: boolean): ISubscription {
|
||||
if (after) {
|
||||
if (this.afterWatchers === null) {
|
||||
this.afterWatchers = [];
|
||||
}
|
||||
this.afterWatchers.push(watcher);
|
||||
} else {
|
||||
if (this.watchers === null) {
|
||||
this.watchers = [];
|
||||
}
|
||||
this.watchers.push(watcher);
|
||||
}
|
||||
return new RefSubscription(() => this.unbind(watcher, !!after));
|
||||
}
|
||||
|
||||
private unbind(watcher: (newVal: T) => void, after: boolean): void {
|
||||
if (after) {
|
||||
if (!this.afterWatchers) {
|
||||
return;
|
||||
}
|
||||
const index = this.afterWatchers.indexOf(watcher);
|
||||
if (index !== -1) {
|
||||
this.afterWatchers.splice(index, 1);
|
||||
}
|
||||
} else {
|
||||
if (!this.watchers) {
|
||||
return;
|
||||
}
|
||||
const index = this.watchers.indexOf(watcher);
|
||||
if (index !== -1) {
|
||||
this.watchers.splice(index, 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
get val(): T {
|
||||
return this.value;
|
||||
}
|
||||
|
||||
set val(val: T) {
|
||||
this.watchers?.forEach(watcher => watcher(val));
|
||||
this.value = val;
|
||||
this.afterWatchers?.forEach(watcher => watcher(val));
|
||||
}
|
||||
|
||||
toString(): string {
|
||||
if (!this.asString) {
|
||||
if (this.isString) {
|
||||
return this.val as unknown as string;
|
||||
} else {
|
||||
this.asString = this.val?.toString() ?? "null";
|
||||
}
|
||||
}
|
||||
return this.asString;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +0,0 @@
|
||||
export type LEvent = string;
|
||||
export default interface ISubscriber<T extends LEvent> {
|
||||
notify(publisher: unknown, event: T): void;
|
||||
}
|
||||
13
src/Track.ts
13
src/Track.ts
@@ -1,7 +1,6 @@
|
||||
import TrackUnit, {TrackUnitType} from "@/TrackUnit";
|
||||
import {IPublisher, Publisher} from "@/Publisher";
|
||||
import ISubscriber from "@/Subscriber";
|
||||
import {isPosInt} from "@/utils";
|
||||
import TrackUnit, { TrackUnitType } from "@/TrackUnit";
|
||||
import { isPosInt } from "@/utils";
|
||||
import { IPublisher, ISubscriber, Publisher } from "@djledda/ladder";
|
||||
|
||||
export type TrackInitOptions = {
|
||||
timeSig?: {
|
||||
@@ -53,7 +52,7 @@ export default class Track implements IPublisher<TrackEvents> {
|
||||
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.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;
|
||||
@@ -113,11 +112,11 @@ export default class Track implements IPublisher<TrackEvents> {
|
||||
}
|
||||
|
||||
setTimeSigUp(timeSigUp: number): void {
|
||||
this.setTimeSignature({up: timeSigUp});
|
||||
this.setTimeSignature({ up: timeSigUp });
|
||||
}
|
||||
|
||||
setTimeSigDown(timeSigUp: number): void {
|
||||
this.setTimeSignature({down: timeSigUp});
|
||||
this.setTimeSignature({ down: timeSigUp });
|
||||
}
|
||||
|
||||
setBarCount(barCount: number): void {
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import {IPublisher, Publisher} from "./Publisher";
|
||||
import ISubscriber from "./Subscriber";
|
||||
import Track from "@/Track";
|
||||
import { IPublisher, ISubscriber, Publisher } from "@djledda/ladder";
|
||||
|
||||
export const enum TrackUnitType {
|
||||
Normal="tut-0",
|
||||
|
||||
9
src/globals.d.ts
vendored
Normal file
9
src/globals.d.ts
vendored
Normal file
@@ -0,0 +1,9 @@
|
||||
import RootView from "@/ui/Root/RootView";
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
appRoot?: RootView;
|
||||
}
|
||||
}
|
||||
|
||||
export {};
|
||||
@@ -9,8 +9,6 @@ if (appNode) {
|
||||
orientation: "vertical",
|
||||
title: "Drum Slayer",
|
||||
});
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
//@ts-ignore
|
||||
window.appRoot = appRoot;
|
||||
appNode.appendChild(appRoot.render());
|
||||
console.log("OK!");
|
||||
|
||||
@@ -1,12 +1,10 @@
|
||||
import UINode, {h, UINodeOptions} from "@/ui/UINode";
|
||||
import Beat, {BeatEvents} from "@/Beat";
|
||||
import { h, ISubscriber, ISubscription, Rung, RungOptions } from "@djledda/ladder";
|
||||
import Beat, { BeatEvents } from "@/Beat";
|
||||
import TrackView from "@/ui/Track/TrackView";
|
||||
import "./Beat.css";
|
||||
import ISubscriber from "@/Subscriber";
|
||||
import {ISubscription} from "@/Publisher";
|
||||
import EditableTextFieldView from "@/ui/Widgets/EditableTextFIeld/EditableTextFieldView";
|
||||
|
||||
export type BeatUINodeOptions = UINodeOptions & {
|
||||
export type BeatUINodeOptions = RungOptions & {
|
||||
beat: Beat,
|
||||
orientation?: "horizontal" | "vertical",
|
||||
};
|
||||
@@ -16,7 +14,7 @@ const EventTypeSubscriptions = [
|
||||
] as const;
|
||||
type EventTypeSubscriptions = typeof EventTypeSubscriptions[number];
|
||||
|
||||
export default class BeatView extends UINode implements ISubscriber<EventTypeSubscriptions> {
|
||||
export default class BeatView extends Rung<HTMLElement> implements ISubscriber<EventTypeSubscriptions> {
|
||||
private beat: Beat;
|
||||
private title: EditableTextFieldView;
|
||||
private trackViews: TrackView[] = [];
|
||||
@@ -50,7 +48,7 @@ export default class BeatView extends UINode implements ISubscriber<EventTypeSub
|
||||
if (beat && this.trackViews[i]) {
|
||||
this.trackViews[i].setBeat(beat);
|
||||
} else {
|
||||
this.trackViews.push(new TrackView({track: this.beat.getTrackByIndex(i)}));
|
||||
this.trackViews.push(new TrackView({ track: this.beat.getTrackByIndex(i) }));
|
||||
}
|
||||
}
|
||||
const deadTrackViews = this.trackViews.splice(newCount, this.trackViews.length - newCount);
|
||||
@@ -69,7 +67,7 @@ export default class BeatView extends UINode implements ISubscriber<EventTypeSub
|
||||
|
||||
private reverseDisplayOrder(): void {
|
||||
this.trackViews.reverse();
|
||||
this.getNode().classList.toggle("vertical");
|
||||
this.render().classList.toggle("vertical");
|
||||
this.redraw();
|
||||
}
|
||||
|
||||
@@ -91,19 +89,9 @@ export default class BeatView extends UINode implements ISubscriber<EventTypeSub
|
||||
}
|
||||
|
||||
build(): HTMLDivElement {
|
||||
return h("div", {
|
||||
className: "beat",
|
||||
},[
|
||||
h("h2", {
|
||||
className: "beat-title",
|
||||
}, [
|
||||
this.title,
|
||||
]),
|
||||
h("div", {
|
||||
className: "beat-track-container",
|
||||
}, [
|
||||
...this.trackViews,
|
||||
]),
|
||||
]);
|
||||
return <div className={"beat"}>
|
||||
<h2 className={"beat-title"}>{this.title}</h2>
|
||||
<div className={"beat-track-container"}>{...this.trackViews}</div>
|
||||
</div> as HTMLDivElement;
|
||||
}
|
||||
}
|
||||
@@ -1,13 +1,12 @@
|
||||
import "./BeatSettings.css";
|
||||
import UINode, {h, UINodeOptions} from "@/ui/UINode";
|
||||
import NumberInputView from "@/ui/Widgets/NumberInput/NumberInputView";
|
||||
import ISubscriber from "@/Subscriber";
|
||||
import Beat, {BeatEvents} from "@/Beat";
|
||||
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 { h, ISubscriber, Rung, RungOptions } from "@djledda/ladder";
|
||||
|
||||
export type BeatSettingsUINodeOptions = UINodeOptions & {
|
||||
export type BeatSettingsUINodeOptions = RungOptions & {
|
||||
beat: Beat,
|
||||
};
|
||||
|
||||
@@ -21,7 +20,7 @@ const EventTypeSubscriptions = [
|
||||
];
|
||||
type EventTypeSubscriptions = typeof EventTypeSubscriptions[number];
|
||||
|
||||
export default class BeatSettingsView extends UINode implements ISubscriber<EventTypeSubscriptions> {
|
||||
export default class BeatSettingsView extends Rung implements ISubscriber<EventTypeSubscriptions> {
|
||||
private beat: Beat;
|
||||
private barCountInput!: NumberInputView;
|
||||
private timeSigUpInput!: NumberInputView;
|
||||
@@ -82,13 +81,13 @@ export default class BeatSettingsView extends UINode implements ISubscriber<Even
|
||||
}
|
||||
}
|
||||
if (!this.trackSettingsContainer) {
|
||||
this.trackSettingsContainer = h("div", {}, this.trackSettingsViews);
|
||||
this.trackSettingsContainer = <div>{...this.trackSettingsViews}</div> as HTMLDivElement;
|
||||
} else {
|
||||
this.trackSettingsContainer.replaceChildren(...this.trackSettingsViews.reverse().map(view => view.render()));
|
||||
}
|
||||
}
|
||||
|
||||
build(): HTMLElement {
|
||||
build(): Node {
|
||||
this.barCountInput = new NumberInputView({
|
||||
label: "Bars:",
|
||||
initialValue: this.beat.getBarCount(),
|
||||
@@ -107,34 +106,23 @@ export default class BeatSettingsView extends UINode implements ISubscriber<Even
|
||||
onInput: (isChecked: boolean) => this.beat.setIsUsingAutoBeatLength(isChecked),
|
||||
});
|
||||
this.remakeBeatSettingsViews();
|
||||
return h("div", {
|
||||
classes: ["beat-settings"],
|
||||
}, [
|
||||
h("div", {
|
||||
classes: ["beat-settings-options"],
|
||||
}, [
|
||||
h("div", {
|
||||
classes: ["beat-settings-boxes", "beat-settings-option"],
|
||||
}, [
|
||||
this.timeSigUpInput,
|
||||
]),
|
||||
h("div", {
|
||||
classes: ["beat-settings-bar-count", "beat-settings-option"]
|
||||
,
|
||||
}, [
|
||||
this.barCountInput,
|
||||
]),
|
||||
h("div", {
|
||||
classes: ["beat-settings-bar-count", "beat-settings-option"],
|
||||
}, [
|
||||
this.autoBeatLengthCheckbox,
|
||||
]),
|
||||
new ActionButtonView({
|
||||
return <div className={"beat-settings"}>
|
||||
<div className={"beat-settings-options"}>
|
||||
<div classes={["beat-settings-boxes", "beat-settings-option"]}>
|
||||
{this.timeSigUpInput}
|
||||
</div>
|
||||
<div classes={["beat-settings-bar-count", "beat-settings-option"]}>
|
||||
{this.barCountInput}
|
||||
</div>
|
||||
<div classes={["beat-settings-bar-count", "beat-settings-option"]}>
|
||||
{this.autoBeatLengthCheckbox}
|
||||
</div>
|
||||
{new ActionButtonView({
|
||||
label: "New Track",
|
||||
onClick: () => this.beat.addTrack(),
|
||||
}),
|
||||
this.trackSettingsContainer,
|
||||
]),
|
||||
]);
|
||||
})}
|
||||
{this.trackSettingsContainer}
|
||||
</div>
|
||||
</div>;
|
||||
}
|
||||
}
|
||||
@@ -1,194 +0,0 @@
|
||||
import UINode, {h, q, UINodeOptions} from "@/ui/UINode";
|
||||
import BeatView from "@/ui/Beat/BeatView";
|
||||
import Beat from "@/Beat";
|
||||
import "./Root.css";
|
||||
import BeatSettingsView from "@/ui/BeatSettings/BeatSettingsView";
|
||||
import IconView from "@/ui/Widgets/Icon/IconView";
|
||||
import Ref from "@/Ref";
|
||||
import BeatStore from "@/BeatStore";
|
||||
|
||||
export type RootUINodeOptions = UINodeOptions & {
|
||||
title: string,
|
||||
mainBeat?: Beat,
|
||||
orientation?: "horizontal" | "vertical",
|
||||
};
|
||||
|
||||
export default class RootView extends UINode {
|
||||
private title: string;
|
||||
private beatView: BeatView;
|
||||
private beatStore: BeatStore;
|
||||
private activeBeat: Ref<Beat>;
|
||||
private beatSettingsView: BeatSettingsView;
|
||||
private currentOrientation: "horizontal" | "vertical";
|
||||
private showHideSidebarButton: Ref<HTMLDivElement | null> = Ref.new<HTMLDivElement | null>(null);
|
||||
private sidebarActive = true;
|
||||
private sidebarLeftTabs: Ref<HTMLDivElement | null> = Ref.new<HTMLDivElement | null>(null);
|
||||
|
||||
constructor(options: RootUINodeOptions) {
|
||||
super(options);
|
||||
this.beatStore = new BeatStore({
|
||||
loadFromLocalStorage: true,
|
||||
autoSave: true,
|
||||
});
|
||||
this.activeBeat = this.beatStore.getActiveBeat();
|
||||
this.activeBeat.watch((newVal) => {
|
||||
this.beatSettingsView.setBeat(newVal);
|
||||
this.beatView.setBeat(newVal);
|
||||
});
|
||||
this.currentOrientation = this.beatStore.getSavedOrientation() ?? options.orientation ?? "horizontal";
|
||||
this.beatView = new BeatView({
|
||||
beat: this.activeBeat.val,
|
||||
orientation: this.currentOrientation,
|
||||
});
|
||||
this.beatStore.onBeatChanges(() => {
|
||||
this.sidebarLeftTabs.val?.replaceChildren(...this.buildTabs());
|
||||
});
|
||||
this.beatSettingsView = new BeatSettingsView({beat: this.activeBeat.val});
|
||||
this.title = options.title;
|
||||
this.setOrientation(this.currentOrientation);
|
||||
this.openSidebarForDesktop();
|
||||
}
|
||||
|
||||
private openSidebarForDesktop() {
|
||||
const mediaQueryList = window.matchMedia("screen and (max-width: 900px)");
|
||||
if (mediaQueryList.matches) {
|
||||
this.toggleSidebar();
|
||||
}
|
||||
}
|
||||
|
||||
toggleSidebar(): void {
|
||||
this.sidebarActive = !this.sidebarActive;
|
||||
this.showHideSidebarButton.val!.title = this.sidebarText();
|
||||
this.getNode().classList.toggle("sidebar-visible");
|
||||
}
|
||||
|
||||
toggleOrientation(): void {
|
||||
if (this.currentOrientation === "vertical") {
|
||||
this.setOrientation("horizontal");
|
||||
} else {
|
||||
this.setOrientation("vertical");
|
||||
}
|
||||
}
|
||||
|
||||
setOrientation(orientation: "horizontal" | "vertical"): void {
|
||||
this.currentOrientation = orientation;
|
||||
if (orientation === "vertical") {
|
||||
this.getNode().classList.add("vertical-mode");
|
||||
} else {
|
||||
this.getNode().classList.remove("vertical-mode");
|
||||
}
|
||||
this.beatStore.setOrientation(orientation);
|
||||
this.beatView.setOrientation(orientation);
|
||||
}
|
||||
|
||||
private sidebarText(): string {
|
||||
return `${this.sidebarActive ? "Hide" : "Show"} sidebar`;
|
||||
}
|
||||
|
||||
private buildSidebarStripLeft(): HTMLElement {
|
||||
return h("div", {
|
||||
className: "root-sidebar-left-strip",
|
||||
}, [
|
||||
h("div", {
|
||||
saveTo: this.sidebarLeftTabs
|
||||
}, this.buildTabs()),
|
||||
h("div", {
|
||||
className: "root-sidebar-add-beat",
|
||||
onclick: () => this.beatStore.addNewBeat(),
|
||||
innerText: "+",
|
||||
}),
|
||||
]);
|
||||
}
|
||||
|
||||
private buildTabs(): HTMLElement[] {
|
||||
return this.beatStore.getBeats().map((beat) => {
|
||||
const node = h("div", {
|
||||
className: "root-sidebar-left-tab" + (beat === this.activeBeat.val ? " active" : ""),
|
||||
onclick: () => this.beatStore.setActiveBeat(beat),
|
||||
innerText: beat.getName(),
|
||||
});
|
||||
this.activeBeat.watch((newVal) => {
|
||||
if (beat === newVal) {
|
||||
node.classList.add("active");
|
||||
} else {
|
||||
node.classList.remove("active");
|
||||
}
|
||||
});
|
||||
return node;
|
||||
});
|
||||
}
|
||||
|
||||
private buildSidebarQuickButtons(): HTMLElement {
|
||||
return h("div", {
|
||||
classes: ["root-sidebar-toggle"],
|
||||
}, [
|
||||
h("div", {
|
||||
classes: ["root-quick-access-button"],
|
||||
title: this.sidebarText(),
|
||||
saveTo: this.showHideSidebarButton,
|
||||
onclick: () => this.toggleSidebar(),
|
||||
}, [
|
||||
new IconView({
|
||||
iconName: "list",
|
||||
color: "var(--color-ui-neutral-dark)"
|
||||
})
|
||||
]),
|
||||
h("div", {
|
||||
classes: ["root-quick-access-button"],
|
||||
title: "Change orientation",
|
||||
onclick: () => this.toggleOrientation(),
|
||||
}, [
|
||||
new IconView({
|
||||
iconName: "arrowClockwise",
|
||||
color: "var(--color-ui-neutral-dark)"
|
||||
}),
|
||||
]),
|
||||
h("div", {
|
||||
classes: ["root-quick-access-button"],
|
||||
title: "Bake all tracks",
|
||||
onclick: () => this.activeBeat.val.bakeLoops(),
|
||||
}, [
|
||||
new IconView({
|
||||
iconName: "snowflake",
|
||||
color: "var(--color-ui-neutral-dark)"
|
||||
}),
|
||||
]),
|
||||
h("div", {
|
||||
classes: ["root-quick-access-button"],
|
||||
title: "Reset all",
|
||||
onclick: () => this.beatStore.resetActiveBeat(),
|
||||
}, [
|
||||
new IconView({
|
||||
iconName: "trash",
|
||||
color: "var(--color-ui-neutral-dark)"
|
||||
})
|
||||
]),
|
||||
]);
|
||||
}
|
||||
|
||||
private buildSidebar(): HTMLElement {
|
||||
return (
|
||||
h("div", {classes: ["root-sidebar"]}, [
|
||||
this.buildSidebarStripLeft(),
|
||||
h("div", {classes: ["root-settings"]}, [
|
||||
h("h1", {classes: ["root-title"], innerText: this.title}),
|
||||
this.beatSettingsView,
|
||||
]),
|
||||
this.buildSidebarQuickButtons(),
|
||||
])
|
||||
);
|
||||
}
|
||||
|
||||
build(): HTMLElement {
|
||||
return (
|
||||
h("div", {classes: ["root", "sidebar-visible"]}, [
|
||||
this.buildSidebar(),
|
||||
h("div", {classes: ["root-beat-stage-container"]}, [
|
||||
h("div", {classes: ["root-beat-stage"]}, [
|
||||
this.beatView,
|
||||
])
|
||||
])
|
||||
])
|
||||
);
|
||||
}
|
||||
}
|
||||
184
src/ui/Root/RootView.tsx
Normal file
184
src/ui/Root/RootView.tsx
Normal file
@@ -0,0 +1,184 @@
|
||||
import BeatView from "@/ui/Beat/BeatView";
|
||||
import Beat from "@/Beat";
|
||||
import "./Root.css";
|
||||
import BeatSettingsView from "@/ui/BeatSettings/BeatSettingsView";
|
||||
import IconView from "@/ui/Widgets/Icon/IconView";
|
||||
import BeatStore from "@/BeatStore";
|
||||
import { Capsule, h, frag, Rung, RungOptions, ICapsule } from "@djledda/ladder";
|
||||
|
||||
export type RootUINodeOptions = RungOptions & {
|
||||
title: string,
|
||||
mainBeat?: Beat,
|
||||
orientation?: "horizontal" | "vertical",
|
||||
};
|
||||
|
||||
export default class RootView extends Rung<HTMLDivElement> {
|
||||
private title: string;
|
||||
private beatView: BeatView;
|
||||
private beatStore: BeatStore;
|
||||
private activeBeat: ICapsule<Beat>;
|
||||
private beatSettingsView: BeatSettingsView;
|
||||
private currentOrientation: "horizontal" | "vertical";
|
||||
private showHideSidebarButton = Capsule.new<HTMLDivElement | null>(null);
|
||||
private sidebarActive = true;
|
||||
private sidebarLeftTabs = Capsule.new<HTMLDivElement | null>(null);
|
||||
|
||||
constructor(options: RootUINodeOptions) {
|
||||
super(options);
|
||||
this.beatStore = new BeatStore({
|
||||
loadFromLocalStorage: true,
|
||||
autoSave: true,
|
||||
});
|
||||
this.activeBeat = this.beatStore.getActiveBeat();
|
||||
this.activeBeat.watch((newVal) => {
|
||||
this.beatSettingsView.setBeat(newVal);
|
||||
this.beatView.setBeat(newVal);
|
||||
});
|
||||
this.currentOrientation = this.beatStore.getSavedOrientation() ?? options.orientation ?? "horizontal";
|
||||
this.beatView = new BeatView({
|
||||
beat: this.activeBeat.val,
|
||||
orientation: this.currentOrientation,
|
||||
});
|
||||
this.beatStore.onBeatChanges(() => {
|
||||
this.sidebarLeftTabs.val?.replaceChildren(<this.Tabs />);
|
||||
});
|
||||
this.beatSettingsView = new BeatSettingsView({ beat: this.activeBeat.val });
|
||||
this.title = options.title;
|
||||
this.setOrientation(this.currentOrientation);
|
||||
this.openSidebarForDesktop();
|
||||
}
|
||||
|
||||
private openSidebarForDesktop() {
|
||||
const mediaQueryList = window.matchMedia("screen and (max-width: 900px)");
|
||||
if (mediaQueryList.matches) {
|
||||
this.toggleSidebar();
|
||||
}
|
||||
}
|
||||
|
||||
toggleSidebar(): void {
|
||||
this.sidebarActive = !this.sidebarActive;
|
||||
if (this.showHideSidebarButton.val) {
|
||||
this.showHideSidebarButton.val.title = this.sidebarText();
|
||||
}
|
||||
this.render().classList.toggle("sidebar-visible");
|
||||
}
|
||||
|
||||
toggleOrientation(): void {
|
||||
if (this.currentOrientation === "vertical") {
|
||||
this.setOrientation("horizontal");
|
||||
} else {
|
||||
this.setOrientation("vertical");
|
||||
}
|
||||
}
|
||||
|
||||
setOrientation(orientation: "horizontal" | "vertical"): void {
|
||||
this.currentOrientation = orientation;
|
||||
if (orientation === "vertical") {
|
||||
this.render().classList.add("vertical-mode");
|
||||
} else {
|
||||
this.render().classList.remove("vertical-mode");
|
||||
}
|
||||
this.beatStore.setOrientation(orientation);
|
||||
this.beatView.setOrientation(orientation);
|
||||
}
|
||||
|
||||
private sidebarText(): string {
|
||||
return `${this.sidebarActive ? "Hide" : "Show"} sidebar`;
|
||||
}
|
||||
|
||||
private SidebarStripLeft = (): HTMLElement => {
|
||||
return <div className={"root-sidebar-left-strip"}>
|
||||
<div saveTo={this.sidebarLeftTabs}>
|
||||
<this.Tabs />
|
||||
</div>
|
||||
<div className={"root-sidebar-add-beat"} onclick={() => this.beatStore.addNewBeat()}>+</div>
|
||||
</div> as HTMLElement;
|
||||
};
|
||||
|
||||
private Tabs = (): Node => {
|
||||
return <div>
|
||||
{...this.beatStore.getBeats().map((beat) => {
|
||||
const node = <div
|
||||
className={"root-sidebar-left-tab" + (beat === this.activeBeat.val ? " active" : "")}
|
||||
onclick={() => this.beatStore.setActiveBeat(beat)}>
|
||||
{beat.getName()}
|
||||
</div> as HTMLDivElement;
|
||||
this.activeBeat.watch((newVal) => {
|
||||
if (beat === newVal) {
|
||||
node.classList.add("active");
|
||||
} else {
|
||||
node.classList.remove("active");
|
||||
}
|
||||
});
|
||||
return node;
|
||||
})}
|
||||
</div>;
|
||||
};
|
||||
|
||||
private SidebarQuickButtons = (): HTMLElement => {
|
||||
return <div className={"root-sidebar-toggle"}>
|
||||
<div
|
||||
className={"root-quick-access-button"}
|
||||
title={this.sidebarText()}
|
||||
saveTo={this.showHideSidebarButton}
|
||||
onclick={() => this.toggleSidebar()}>
|
||||
{new IconView({
|
||||
iconName: "list",
|
||||
color: "var(--color-ui-neutral-dark)"
|
||||
})}
|
||||
</div>
|
||||
<div
|
||||
className={"root-quick-access-button"}
|
||||
title={"Change orientation"}
|
||||
onclick={() => this.toggleOrientation()}>
|
||||
{new IconView({
|
||||
iconName: "arrowClockwise",
|
||||
color: "var(--color-ui-neutral-dark)"
|
||||
})}
|
||||
</div>
|
||||
<div
|
||||
className={"root-quick-access-button"}
|
||||
title={"Bake all tracks"}
|
||||
onclick={() => this.activeBeat.val.bakeLoops()}>
|
||||
{new IconView({
|
||||
iconName: "snowflake",
|
||||
color: "var(--color-ui-neutral-dark)"
|
||||
})}
|
||||
</div>
|
||||
<div
|
||||
className={"root-quick-access-button"}
|
||||
title={"Reset all"}
|
||||
onclick={() => this.beatStore.resetActiveBeat()}>
|
||||
{new IconView({
|
||||
iconName: "trash",
|
||||
color: "var(--color-ui-neutral-dark)"
|
||||
})}
|
||||
</div>
|
||||
</div> as HTMLElement;
|
||||
};
|
||||
|
||||
private Sidebar = (): HTMLElement => {
|
||||
return <div className={"root-sidebar"}>
|
||||
<this.SidebarStripLeft />
|
||||
<div
|
||||
className={"root-settings"}>
|
||||
<h1 className={"root-title"}>{this.title}</h1>
|
||||
{this.beatSettingsView}
|
||||
</div>
|
||||
<this.SidebarQuickButtons />
|
||||
</div> as HTMLElement;
|
||||
};
|
||||
|
||||
build(): HTMLDivElement {
|
||||
return (
|
||||
<div classes={["root", "sidebar-visible"]}>
|
||||
<this.Sidebar/>
|
||||
<div className={"root-beat-stage-container"}>
|
||||
<div className={"root-beat-stage"}>
|
||||
{this.beatView}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
) as HTMLDivElement;
|
||||
}
|
||||
}
|
||||
@@ -1,12 +1,9 @@
|
||||
import UINode, {h, UINodeOptions} from "@/ui/UINode";
|
||||
import Track, {TrackEvents} from "@/Track";
|
||||
import ISubscriber from "@/Subscriber";
|
||||
import Track, { TrackEvents } from "@/Track";
|
||||
import TrackUnitView from "@/ui/TrackUnit/TrackUnitView";
|
||||
import "./Track.css";
|
||||
import {ISubscription} from "@/Publisher";
|
||||
import Ref from "@/Ref";
|
||||
import { Capsule, h, ISubscriber, ISubscription, Rung, RungOptions } from "@djledda/ladder";
|
||||
|
||||
export type TrackUINodeOptions = UINodeOptions & {
|
||||
export type TrackUINodeOptions = RungOptions & {
|
||||
track: Track,
|
||||
};
|
||||
|
||||
@@ -20,9 +17,9 @@ const EventTypeSubscriptions = [
|
||||
|
||||
type EventTypeSubscriptions = typeof EventTypeSubscriptions[number];
|
||||
|
||||
export default class TrackView extends UINode implements ISubscriber<EventTypeSubscriptions> {
|
||||
export default class TrackView extends Rung implements ISubscriber<EventTypeSubscriptions> {
|
||||
private track!: Track;
|
||||
private title = Ref.new<HTMLHeadingElement | null>(null);
|
||||
private title = Capsule.new<HTMLHeadingElement | null>(null);
|
||||
private trackUnitViews: TrackUnitView[] = [];
|
||||
private trackUnitViewBlock: HTMLElement | null = null;
|
||||
private lastHoveredTrackUnitView: TrackUnitView | null = null;
|
||||
@@ -70,7 +67,7 @@ export default class TrackView extends UINode implements ISubscriber<EventTypeSu
|
||||
view = this.trackUnitViews[i];
|
||||
view.setUnit(trackUnit);
|
||||
} else {
|
||||
view = new TrackUnitView({trackUnit});
|
||||
view = new TrackUnitView({ trackUnit });
|
||||
this.trackUnitViews.push(view);
|
||||
view.onHover(() => this.onTrackUnitViewHover(view));
|
||||
view.onMouseDown((event: MouseEvent) => this.onTrackUnitClick(event.button, i));
|
||||
@@ -107,11 +104,7 @@ export default class TrackView extends UINode implements ISubscriber<EventTypeSu
|
||||
if (this.trackUnitViewBlock) {
|
||||
this.trackUnitViewBlock.replaceChildren(...trackUnitNodes);
|
||||
} else {
|
||||
this.trackUnitViewBlock = h("div", {
|
||||
classes: ["track-unit-block"],
|
||||
}, [
|
||||
...trackUnitNodes
|
||||
]);
|
||||
this.trackUnitViewBlock = <div className={"track-unit-block"}>{...trackUnitNodes}</div> as HTMLDivElement;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -127,7 +120,7 @@ export default class TrackView extends UINode implements ISubscriber<EventTypeSu
|
||||
let spacersInserted = false;
|
||||
while (!spacersInserted) {
|
||||
i += barLength;
|
||||
const newSpacer = h("div", {classes: ["track-spacer"]});
|
||||
const newSpacer = <div className={"track-spacer"} /> as HTMLDivElement;
|
||||
const leftNeighbour = this.trackUnitViewBlock.children.item(i);
|
||||
if (leftNeighbour) {
|
||||
leftNeighbour.insertAdjacentElement("afterend", newSpacer);
|
||||
@@ -153,20 +146,16 @@ export default class TrackView extends UINode implements ISubscriber<EventTypeSu
|
||||
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,
|
||||
]),
|
||||
]);
|
||||
return <div className={"track"}>
|
||||
<div className={"track-main"}>
|
||||
<h3
|
||||
innerText={this.track.getName()}
|
||||
saveTo={this.title}
|
||||
className={"track-title"}>
|
||||
</h3>
|
||||
{this.trackUnitViewBlock}
|
||||
</div>
|
||||
</div> as HTMLElement;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,14 +1,12 @@
|
||||
import "./TrackSettings.css";
|
||||
import Track, {TrackEvents} from "@/Track";
|
||||
import UINode, {h, UINodeOptions} from "@/ui/UINode";
|
||||
import ISubscriber from "@/Subscriber";
|
||||
import {ISubscription} from "@/Publisher";
|
||||
import Track, { TrackEvents } from "@/Track";
|
||||
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";
|
||||
import { h, ISubscriber, ISubscription, Rung, RungOptions } from "@djledda/ladder";
|
||||
|
||||
export type BeatSettingsViewUINodeOptions = UINodeOptions & {
|
||||
export type BeatSettingsViewUINodeOptions = RungOptions & {
|
||||
track: Track,
|
||||
};
|
||||
|
||||
@@ -19,7 +17,7 @@ const EventTypeSubscriptions = [
|
||||
];
|
||||
type EventTypeSubscriptions = typeof EventTypeSubscriptions[number];
|
||||
|
||||
export default class TrackSettingsView extends UINode implements ISubscriber<EventTypeSubscriptions> {
|
||||
export default class TrackSettingsView extends Rung implements ISubscriber<EventTypeSubscriptions> {
|
||||
private track: Track;
|
||||
private loopLengthInput!: NumberInputView;
|
||||
private bakeButton!: ActionButtonView;
|
||||
@@ -67,10 +65,10 @@ export default class TrackSettingsView extends UINode implements ISubscriber<Eve
|
||||
}
|
||||
}
|
||||
|
||||
build(): HTMLElement {
|
||||
build(): Node {
|
||||
this.title = new EditableTextFieldView({
|
||||
initialText: this.track.getName(),
|
||||
setter: (newText) => this.track.setName(newText),
|
||||
setter: (newText: string) => this.track.setName(newText),
|
||||
});
|
||||
this.bakeButton = new ActionButtonView({
|
||||
icon: "snowflake",
|
||||
@@ -90,41 +88,29 @@ export default class TrackSettingsView extends UINode implements ISubscriber<Eve
|
||||
value: this.track.isLooping(),
|
||||
onInput: (isChecked: boolean) => this.track.setLooping(isChecked),
|
||||
});
|
||||
this.loopLengthSection = h("div", {
|
||||
classes: ["loop-settings-option"],
|
||||
}, [
|
||||
this.loopLengthInput,
|
||||
]);
|
||||
this.loopLengthSection = <div className={"loop-settings-option"}>{this.loopLengthInput}</div> as HTMLDivElement;
|
||||
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({
|
||||
return <div className={"track-settings"}>
|
||||
<div className={"track-settings-title-container"}>
|
||||
{this.title}
|
||||
</div>
|
||||
<div className={"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,
|
||||
]),
|
||||
]);
|
||||
})}
|
||||
<div className={"loop-settings"}>
|
||||
{this.loopCheckbox}
|
||||
</div>
|
||||
{this.loopLengthSection}
|
||||
</div>
|
||||
</div>;
|
||||
}
|
||||
}
|
||||
@@ -1,10 +1,8 @@
|
||||
import TrackUnit, {TrackUnitEvent, TrackUnitType} from "@/TrackUnit";
|
||||
import ISubscriber from "@/Subscriber";
|
||||
import UINode, {h, UINodeOptions} from "@/ui/UINode";
|
||||
import {IPublisher, ISubscription, Publisher} from "@/Publisher";
|
||||
import TrackUnit, { TrackUnitEvent, TrackUnitType } from "@/TrackUnit";
|
||||
import "./TrackUnit.css";
|
||||
import { h, IPublisher, ISubscriber, ISubscription, Publisher, Rung, RungOptions } from "@djledda/ladder";
|
||||
|
||||
export type TrackUnitUINodeOptions = UINodeOptions & {
|
||||
export type TrackUnitUINodeOptions = RungOptions & {
|
||||
trackUnit: TrackUnit,
|
||||
};
|
||||
|
||||
@@ -15,7 +13,7 @@ const EventTypeSubscriptions = [
|
||||
];
|
||||
type EventTypeSubscriptions = typeof EventTypeSubscriptions[number];
|
||||
|
||||
export default class TrackUnitView extends UINode implements ISubscriber<EventTypeSubscriptions> {
|
||||
export default class TrackUnitView extends Rung<HTMLElement> implements ISubscriber<EventTypeSubscriptions> {
|
||||
private trackUnit: TrackUnit;
|
||||
private subscription: ISubscription | null = null;
|
||||
private publisher: IPublisher<TrackUnitEvent> = new Publisher<TrackUnitEvent, TrackUnitView>(this);
|
||||
@@ -44,16 +42,16 @@ export default class TrackUnitView extends UINode implements ISubscriber<EventTy
|
||||
private setupBindings() {
|
||||
this.subscription?.unbind();
|
||||
this.subscription = this.trackUnit.addSubscriber(this, EventTypeSubscriptions);
|
||||
this.hoverListeners.forEach(listener => this.getNode().removeEventListener("mouseover", listener));
|
||||
this.mouseDownListeners.forEach(listener => this.getNode().removeEventListener("mousedown", listener));
|
||||
this.hoverListeners.forEach(listener => this.render().removeEventListener("mouseover", listener));
|
||||
this.mouseDownListeners.forEach(listener => this.render().removeEventListener("mousedown", listener));
|
||||
this.redraw();
|
||||
this.hoverListeners.forEach(listener => this.getNode().addEventListener("mouseover", listener));
|
||||
this.mouseDownListeners.forEach(listener => this.getNode().addEventListener("mousedown", listener));
|
||||
this.getNode().addEventListener("mousedown", (ev) => this.handleMouseDown(ev));
|
||||
this.getNode().addEventListener("mouseout", (ev) => this.handleMouseOut(ev));
|
||||
this.getNode().addEventListener("mouseup", (ev) => this.handleMouseUp(ev));
|
||||
this.getNode().addEventListener("touchstart", (ev) => this.handleTouchStart(ev));
|
||||
this.getNode().addEventListener("touchend", (ev) => this.handleTouchEnd(ev));
|
||||
this.hoverListeners.forEach(listener => this.render().addEventListener("mouseover", listener));
|
||||
this.mouseDownListeners.forEach(listener => this.render().addEventListener("mousedown", listener));
|
||||
this.render().addEventListener("mousedown", (ev) => this.handleMouseDown(ev));
|
||||
this.render().addEventListener("mouseout", (ev) => this.handleMouseOut(ev));
|
||||
this.render().addEventListener("mouseup", (ev) => this.handleMouseUp(ev));
|
||||
this.render().addEventListener("touchstart", (ev) => this.handleTouchStart(ev));
|
||||
this.render().addEventListener("touchend", (ev) => this.handleTouchEnd(ev));
|
||||
}
|
||||
|
||||
private handleMouseDown(ev: MouseEvent): void {
|
||||
@@ -116,28 +114,28 @@ export default class TrackUnitView extends UINode implements ISubscriber<EventTy
|
||||
notify(publisher: unknown, event: EventTypeSubscriptions): void {
|
||||
switch (event) {
|
||||
case TrackUnitEvent.On:
|
||||
this.getNode().classList.add("track-unit-on");
|
||||
this.render().classList.add("track-unit-on");
|
||||
break;
|
||||
case TrackUnitEvent.Off:
|
||||
this.getNode().classList.remove("track-unit-on");
|
||||
this.render().classList.remove("track-unit-on");
|
||||
break;
|
||||
case TrackUnitEvent.TypeChange:
|
||||
switch (this.trackUnit.getType()) {
|
||||
case TrackUnitType.Normal:
|
||||
this.getNode().classList.remove("track-unit-ghost");
|
||||
this.getNode().classList.remove("track-unit-accent");
|
||||
this.render().classList.remove("track-unit-ghost");
|
||||
this.render().classList.remove("track-unit-accent");
|
||||
break;
|
||||
case TrackUnitType.GhostNote:
|
||||
this.getNode().classList.add("track-unit-ghost");
|
||||
this.getNode().classList.remove("track-unit-accent");
|
||||
this.render().classList.add("track-unit-ghost");
|
||||
this.render().classList.remove("track-unit-accent");
|
||||
break;
|
||||
case TrackUnitType.Accent:
|
||||
this.getNode().classList.remove("track-unit-ghost");
|
||||
this.getNode().classList.add("track-unit-accent");
|
||||
this.render().classList.remove("track-unit-ghost");
|
||||
this.render().classList.add("track-unit-accent");
|
||||
break;
|
||||
case TrackUnitType.GhostNoteAccent:
|
||||
this.getNode().classList.add("track-unit-ghost");
|
||||
this.getNode().classList.add("track-unit-accent");
|
||||
this.render().classList.add("track-unit-ghost");
|
||||
this.render().classList.add("track-unit-accent");
|
||||
break;
|
||||
}
|
||||
break;
|
||||
@@ -149,19 +147,16 @@ export default class TrackUnitView extends UINode implements ISubscriber<EventTy
|
||||
if (this.trackUnit.isOn()) {
|
||||
classes.push("track-unit-on");
|
||||
}
|
||||
return h("div", {
|
||||
classes: classes,
|
||||
oncontextmenu: () => false,
|
||||
});
|
||||
return <div classes={classes} oncontextmenu={() => false} /> as HTMLElement;
|
||||
}
|
||||
|
||||
onHover(cb: () => void): void {
|
||||
this.hoverListeners.push(cb);
|
||||
this.getNode().addEventListener("mouseover", cb);
|
||||
this.render().addEventListener("mouseover", cb);
|
||||
}
|
||||
|
||||
onMouseDown(cb: (ev: MouseEvent) => void): void {
|
||||
this.mouseDownListeners.push(cb);
|
||||
this.getNode().addEventListener("mousedown", cb);
|
||||
this.render().addEventListener("mousedown", cb);
|
||||
}
|
||||
}
|
||||
121
src/ui/UINode.ts
121
src/ui/UINode.ts
@@ -1,121 +0,0 @@
|
||||
import Ref from "@/Ref";
|
||||
import {ISubscription} from "@/Publisher";
|
||||
|
||||
export type UINodeOptions = {
|
||||
|
||||
};
|
||||
|
||||
type IRenderAttributes<T extends keyof HTMLElementTagNameMap> = Partial<{
|
||||
[K in keyof HTMLElementTagNameMap[T]]: HTMLElementTagNameMap[T][K] | Ref<HTMLElementTagNameMap[T][K]>
|
||||
}> & {
|
||||
classes?: string[],
|
||||
saveTo?: Ref<HTMLElementTagNameMap[T] | null>,
|
||||
};
|
||||
|
||||
export default abstract class UINode {
|
||||
protected node: HTMLElement | null = null;
|
||||
|
||||
constructor(options: UINodeOptions) { /* dummy */ }
|
||||
|
||||
render(): HTMLElement {
|
||||
if (!this.node) {
|
||||
this.node = this.build();
|
||||
}
|
||||
return this.node;
|
||||
}
|
||||
|
||||
protected getNode(): HTMLElement {
|
||||
if (!this.node) {
|
||||
return this.render();
|
||||
} else {
|
||||
return this.node;
|
||||
}
|
||||
}
|
||||
|
||||
redraw(): void {
|
||||
const oldNode = this.node;
|
||||
if (!oldNode || !this.node) {
|
||||
return;
|
||||
}
|
||||
const parent = this.node.parentElement;
|
||||
if (parent) {
|
||||
this.node = this.build();
|
||||
parent.replaceChild(this.node, oldNode);
|
||||
} else {
|
||||
this.render();
|
||||
}
|
||||
}
|
||||
|
||||
protected abstract build(): HTMLElement;
|
||||
}
|
||||
|
||||
export function frag(subs?: Node[]): DocumentFragment {
|
||||
const frag = document.createDocumentFragment();
|
||||
if (subs) {
|
||||
attachSubs(frag, subs);
|
||||
}
|
||||
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();
|
||||
textNode.remove();
|
||||
} else {
|
||||
textNode.replaceWith(newVal?.toString() ?? "null");
|
||||
}
|
||||
}
|
||||
|
||||
function attachSubs(node: Element | DocumentFragment, subNodes: (Node | UINode | Ref)[]): void {
|
||||
for (let i = 0; i < subNodes.length; i++) {
|
||||
const subNode = subNodes[i];
|
||||
if (subNode instanceof UINode) {
|
||||
node.append(subNode.render());
|
||||
} else if (subNode instanceof Ref) {
|
||||
const textNode = q(subNode.val.toString());
|
||||
const sub = subNode.watch((newVal) => nodeRefWatcher<Ref>(newVal, textNode, sub));
|
||||
node.append(textNode);
|
||||
} else {
|
||||
node.append(subNode);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function applyAttributes<T extends keyof HTMLElementTagNameMap>(element: HTMLElement, attributes: IRenderAttributes<T>): void {
|
||||
for (const key in attributes) {
|
||||
if (Object.prototype.hasOwnProperty.call(attributes, key)) {
|
||||
const attribute = (attributes as Record<string, unknown>)[key];
|
||||
if (attribute) {
|
||||
if (attribute instanceof Ref) {
|
||||
const attributeAsRef = attribute as Ref;
|
||||
const elementWithAttributeKey = element as unknown as Record<string, typeof attributeAsRef.val>;
|
||||
elementWithAttributeKey[key] = attributeAsRef.val;
|
||||
attribute.watch((newVal) => elementWithAttributeKey[key] = newVal);
|
||||
} else {
|
||||
(element as unknown as ({ [key: string]: typeof attribute }))[key] = attribute;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,8 @@
|
||||
import "./ActionButton.css";
|
||||
import UINode, {h, UINodeOptions} from "@/ui/UINode";
|
||||
import IconView, {IconName} from "@/ui/Widgets/Icon/IconView";
|
||||
import IconView, { IconName } from "@/ui/Widgets/Icon/IconView";
|
||||
import { h, Rung, RungOptions } from "@djledda/ladder";
|
||||
|
||||
export type ActionButtonUINodeOptions = UINodeOptions & {
|
||||
export type ActionButtonUINodeOptions = RungOptions & {
|
||||
type?: "primary" | "secondary",
|
||||
onClick?: (event: MouseEvent) => void,
|
||||
alt?: string,
|
||||
@@ -15,7 +15,7 @@ export type ActionButtonUINodeOptions = UINodeOptions & {
|
||||
icon?: never,
|
||||
});
|
||||
|
||||
export default class ActionButtonView extends UINode {
|
||||
export default class ActionButtonView extends Rung<HTMLButtonElement> {
|
||||
private label: string | null = null;
|
||||
private icon: IconName | null = null;
|
||||
private buttonElement!: HTMLButtonElement;
|
||||
@@ -48,17 +48,18 @@ export default class ActionButtonView extends UINode {
|
||||
}
|
||||
|
||||
protected build(): HTMLButtonElement {
|
||||
this.buttonElement = h("button", {
|
||||
classes: ["action-button", `action-button-${this.type}`],
|
||||
onclick: (event: MouseEvent) => this.disabled || this.onClick(event)
|
||||
}, [
|
||||
this.icon !== null ? new IconView({
|
||||
this.buttonElement = (
|
||||
<button
|
||||
classes={["action-button", `action-button-${this.type}`]}
|
||||
onclick={(event: MouseEvent) => this.disabled || this.onClick(event)}>
|
||||
{
|
||||
this.icon ? new IconView({
|
||||
iconName: this.icon,
|
||||
color: "var(--color-p-light)",
|
||||
}) : h("span", {
|
||||
innerText: this.label ?? ""
|
||||
}),
|
||||
]);
|
||||
}) : <span>{this.label ?? ""}</span>
|
||||
}
|
||||
</button>
|
||||
) as HTMLButtonElement;
|
||||
if (this.alt) {
|
||||
this.buttonElement.title = this.alt;
|
||||
}
|
||||
@@ -1,64 +0,0 @@
|
||||
import "./BoolBox.css";
|
||||
import UINode, {h, UINodeOptions} from "@/ui/UINode";
|
||||
import Ref from "@/Ref";
|
||||
|
||||
export type BoolBoxUINodeOptions = UINodeOptions & {
|
||||
label?: string,
|
||||
value?: boolean,
|
||||
onInput?: (isChecked: boolean) => void,
|
||||
};
|
||||
|
||||
export default class BoolBoxView extends UINode {
|
||||
private label: string | null;
|
||||
private labelElement!: HTMLLabelElement;
|
||||
private checkboxElement!: HTMLInputElement;
|
||||
private onInput: (isChecked: boolean) => void;
|
||||
|
||||
constructor(options: BoolBoxUINodeOptions) {
|
||||
super(options);
|
||||
this.label = options.label ?? "";
|
||||
this.onInput = options.onInput ?? (() => { /* dummy */ });
|
||||
}
|
||||
|
||||
setLabel(newLabel: string | null): void {
|
||||
if (newLabel !== null) {
|
||||
this.label = newLabel;
|
||||
this.labelElement.innerText = newLabel;
|
||||
this.labelElement.classList.add("visible");
|
||||
} else {
|
||||
this.label = newLabel;
|
||||
this.labelElement.innerText = "";
|
||||
this.labelElement.classList.remove("visible");
|
||||
}
|
||||
}
|
||||
|
||||
setValue(isChecked: boolean): void {
|
||||
this.checkboxElement.checked = isChecked;
|
||||
}
|
||||
|
||||
build(): HTMLDivElement {
|
||||
this.labelElement = h("label", {
|
||||
classes: ["bool-box-label"],
|
||||
onclick: () => {
|
||||
this.onInput(!this.checkboxElement.checked);
|
||||
},
|
||||
});
|
||||
if (this.label !== null) {
|
||||
this.labelElement.innerText = this.label;
|
||||
this.labelElement.classList.add("visible");
|
||||
}
|
||||
this.checkboxElement = h("input", {
|
||||
type: "checkbox",
|
||||
classes: ["bool-box-checkbox"],
|
||||
onclick: (event: Event) => {
|
||||
this.onInput((event.target as HTMLInputElement).checked);
|
||||
},
|
||||
});
|
||||
return h("div", {
|
||||
classes: ["bool-box"],
|
||||
},[
|
||||
this.labelElement,
|
||||
this.checkboxElement,
|
||||
]);
|
||||
}
|
||||
}
|
||||
58
src/ui/Widgets/BoolBox/BoolBoxView.tsx
Normal file
58
src/ui/Widgets/BoolBox/BoolBoxView.tsx
Normal file
@@ -0,0 +1,58 @@
|
||||
import "./BoolBox.css";
|
||||
import { Capsule, h, Rung, RungOptions } from "@djledda/ladder";
|
||||
|
||||
export type BoolBoxUINodeOptions = RungOptions & {
|
||||
label?: string,
|
||||
value?: boolean,
|
||||
onInput?: (isChecked: boolean) => void,
|
||||
};
|
||||
|
||||
export default class BoolBoxView extends Rung {
|
||||
private label: string | null;
|
||||
private labelElement = Capsule.new<HTMLLabelElement | null>(null);
|
||||
private checkboxElement = Capsule.new<HTMLInputElement | null>(null);
|
||||
private onInput: (isChecked: boolean) => void;
|
||||
|
||||
constructor(options: BoolBoxUINodeOptions) {
|
||||
super(options);
|
||||
this.label = options.label ?? "";
|
||||
this.onInput = options.onInput ?? (() => { /* dummy */ });
|
||||
}
|
||||
|
||||
setLabel(newLabel: string | null): void {
|
||||
if (!this.labelElement.val) {
|
||||
return;
|
||||
}
|
||||
if (newLabel !== null) {
|
||||
this.label = newLabel;
|
||||
this.labelElement.val.innerText = newLabel;
|
||||
this.labelElement.val.classList.add("visible");
|
||||
} else {
|
||||
this.label = newLabel;
|
||||
this.labelElement.val.innerText = "";
|
||||
this.labelElement.val.classList.remove("visible");
|
||||
}
|
||||
}
|
||||
|
||||
setValue(isChecked: boolean): void {
|
||||
if (this.checkboxElement.val) {
|
||||
this.checkboxElement.val.checked = isChecked;
|
||||
}
|
||||
}
|
||||
|
||||
build(): HTMLDivElement {
|
||||
return <div className={"bool-box"}>
|
||||
<label
|
||||
saveTo={this.labelElement}
|
||||
classes={this.label ? ["bool-box-label", "visible"] : ["bool-box-label"]}
|
||||
onclick={() => this.onInput(!this.checkboxElement.val?.checked)}>
|
||||
{this.label ?? ""}
|
||||
</label>
|
||||
<input
|
||||
type={"checkbox"}
|
||||
className={"bool-box-checkbox"}
|
||||
saveTo={this.checkboxElement}
|
||||
onclick={(event: Event) => this.onInput((event.target as HTMLInputElement).checked)}/>
|
||||
</div> as HTMLDivElement;
|
||||
}
|
||||
}
|
||||
@@ -1,23 +1,22 @@
|
||||
import "./Dropdown.css";
|
||||
import UINode, {h, UINodeOptions} from "@/ui/UINode";
|
||||
import Ref, {MaybeRef} from "@/Ref";
|
||||
import { Capsule, h, ICapsule, Rung, RungOptions } from "@djledda/ladder";
|
||||
|
||||
export type DropdownViewOption = {
|
||||
label: string,
|
||||
value: string,
|
||||
};
|
||||
|
||||
export type DropdownUINodeOptions = UINodeOptions & {
|
||||
options: MaybeRef<DropdownViewOption[]>,
|
||||
export type DropdownUINodeOptions = RungOptions & {
|
||||
options: ICapsule<DropdownViewOption[]> | DropdownViewOption[],
|
||||
};
|
||||
|
||||
export default class DropdownView extends UINode {
|
||||
private options: Ref<DropdownViewOption[]>;
|
||||
private select = Ref.new<HTMLSelectElement | null>(null);
|
||||
export default class DropdownView extends Rung {
|
||||
private options: ICapsule<DropdownViewOption[]>;
|
||||
private select = Capsule.new<HTMLSelectElement | null>(null);
|
||||
|
||||
constructor(options: DropdownUINodeOptions) {
|
||||
super(options);
|
||||
this.options = Ref.new(options.options);
|
||||
this.options = Capsule.new(options.options);
|
||||
this.options.watch((newVal) => this.updateOptionsFrom(newVal));
|
||||
}
|
||||
|
||||
@@ -32,10 +31,7 @@ export default class DropdownView extends UINode {
|
||||
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,
|
||||
}));
|
||||
children.push(<option label={newOptions[i].label} value={newOptions[i].value} /> as HTMLOptionElement);
|
||||
}
|
||||
}
|
||||
if (children.length - newOptions.length > 0) {
|
||||
@@ -45,8 +41,8 @@ export default class DropdownView extends UINode {
|
||||
}
|
||||
|
||||
protected build(): HTMLSelectElement {
|
||||
return h("select", {
|
||||
saveTo: this.select,
|
||||
}, this.options.val.map(opt => h("option", {label: opt.label})));
|
||||
return <select saveTo={this.select}>
|
||||
{this.options.val.map(opt => <option label={opt.label} />)}
|
||||
</select> as HTMLSelectElement;
|
||||
}
|
||||
}
|
||||
@@ -1,13 +1,13 @@
|
||||
import UINode, {h, UINodeOptions} from "@/ui/UINode";
|
||||
import "./EditableTextFieldView.css";
|
||||
import { Rung, h, RungOptions } from "@djledda/ladder";
|
||||
|
||||
export type EditableTextFieldViewOptions = UINodeOptions & {
|
||||
export type EditableTextFieldViewOptions = RungOptions & {
|
||||
initialText?: string,
|
||||
setter?: (newString: string) => void,
|
||||
noEmpty?: boolean,
|
||||
};
|
||||
|
||||
export default class EditableTextFieldView extends UINode {
|
||||
export default class EditableTextFieldView extends Rung {
|
||||
private text: string;
|
||||
private titleInput!: HTMLInputElement;
|
||||
private setter: (newString: string) => void;
|
||||
@@ -30,12 +30,12 @@ export default class EditableTextFieldView extends UINode {
|
||||
}
|
||||
}
|
||||
|
||||
build(): HTMLSpanElement {
|
||||
this.titleInput = h("input", {
|
||||
value: this.text,
|
||||
classes: ["editable-text-field-view"],
|
||||
type: "text",
|
||||
oninput: (event: Event) => {
|
||||
build(): Node {
|
||||
this.titleInput = <input
|
||||
value={this.text}
|
||||
className={"editable-text-field-view"}
|
||||
type={"text"}
|
||||
oninput={(event: Event) => {
|
||||
const input = (event.target as HTMLInputElement).value;
|
||||
if (input === "") {
|
||||
if (!this.noEmpty) {
|
||||
@@ -45,27 +45,28 @@ export default class EditableTextFieldView extends UINode {
|
||||
this.setter(input);
|
||||
this.lastNonEmptyInput = input;
|
||||
}
|
||||
},
|
||||
onblur: (event: FocusEvent) => {
|
||||
}}
|
||||
onblur={(event: FocusEvent) => {
|
||||
if ((event.target as HTMLInputElement).value === "") {
|
||||
this.setText(this.lastNonEmptyInput);
|
||||
}
|
||||
this.titleInput.replaceWith(this.titleDisplay);
|
||||
},
|
||||
onkeyup: (event: KeyboardEvent) => {
|
||||
}}
|
||||
onkeyup={(event: KeyboardEvent) => {
|
||||
if (event.key === "Enter") {
|
||||
(event.target as HTMLInputElement).blur();
|
||||
}
|
||||
},
|
||||
});
|
||||
this.titleDisplay = h("div", {
|
||||
innerText: this.text,
|
||||
classes: ["editable-text-field-view"],
|
||||
onclick: () => {
|
||||
}}
|
||||
/> as HTMLInputElement;
|
||||
|
||||
this.titleDisplay = <div
|
||||
innerText={this.text}
|
||||
className={"editable-text-field-view"}
|
||||
onclick={() => {
|
||||
this.titleDisplay.replaceWith(this.titleInput);
|
||||
this.titleInput.focus();
|
||||
},
|
||||
});
|
||||
}} /> as HTMLDivElement;
|
||||
|
||||
return this.titleDisplay;
|
||||
}
|
||||
}
|
||||
@@ -1,9 +1,9 @@
|
||||
import UINode, {h, UINodeOptions} from "@/ui/UINode";
|
||||
import "./Icon.css";
|
||||
import List from "assets/svgs/list.svg";
|
||||
import ArrowClockwise from "assets/svgs/arrow-clockwise.svg";
|
||||
import Trash from "assets/svgs/trash.svg";
|
||||
import Snowflake from "assets/svgs/snowflake.svg";
|
||||
import { h, Rung, RungOptions } from "@djledda/ladder";
|
||||
|
||||
const IconUrlMap = {
|
||||
arrowClockwise: ArrowClockwise,
|
||||
@@ -14,12 +14,12 @@ const IconUrlMap = {
|
||||
|
||||
export type IconName = keyof typeof IconUrlMap;
|
||||
|
||||
export type IconViewOptions = UINodeOptions & {
|
||||
export type IconViewOptions = RungOptions & {
|
||||
iconName: IconName,
|
||||
color?: string,
|
||||
};
|
||||
|
||||
export default class IconView extends UINode {
|
||||
export default class IconView extends Rung {
|
||||
private iconUrl: string;
|
||||
private color: string | null;
|
||||
|
||||
@@ -30,9 +30,7 @@ export default class IconView extends UINode {
|
||||
}
|
||||
|
||||
build(): HTMLSpanElement {
|
||||
const icon = h("div", {
|
||||
classes: ["icon-view"],
|
||||
});
|
||||
const icon = <div className={"icon-view"} /> as HTMLDivElement;
|
||||
const colorString = this.color ? `--icon-bg:${this.color}` : "";
|
||||
icon.style.cssText = `-webkit-mask-image: url(${this.iconUrl}); mask-image: url(${this.iconUrl});${colorString}`;
|
||||
return icon;
|
||||
@@ -1,8 +1,7 @@
|
||||
import UINode, {h, UINodeOptions} from "@/ui/UINode";
|
||||
import "./NumberInput.css";
|
||||
import Ref from "@/Ref";
|
||||
import { Capsule, h, Rung, RungOptions } from "@djledda/ladder";
|
||||
|
||||
type NumberInputUINodeOptionsBase = UINodeOptions & {
|
||||
type NumberInputUINodeOptionsBase = RungOptions & {
|
||||
label?: string,
|
||||
initialValue?: number,
|
||||
labelPosition?: "top" | "left",
|
||||
@@ -26,9 +25,9 @@ type NumberInputUINodeOptionsGetSet = NumberInputUINodeOptionsBase & {
|
||||
|
||||
export type NumberInputUINodeOptions = NumberInputUINodeOptionsGetSet | NumberInputUINodeOptionsIncDecInput;
|
||||
|
||||
export default class NumberInputView extends UINode {
|
||||
private labelElement: Ref<HTMLLabelElement | null> = Ref.new<HTMLLabelElement | null>(null);
|
||||
private inputElement: Ref<HTMLInputElement | null> = Ref.new<HTMLInputElement | null>(null);
|
||||
export default class NumberInputView extends Rung<HTMLDivElement> {
|
||||
private labelElement = Capsule.new<HTMLLabelElement | null>(null);
|
||||
private inputElement = Capsule.new<HTMLInputElement | null>(null);
|
||||
private labelPosition: "top" | "left";
|
||||
private value: number;
|
||||
private label: string | null;
|
||||
@@ -51,24 +50,27 @@ export default class NumberInputView extends UINode {
|
||||
}
|
||||
|
||||
setLabel(newLabel: string | null): void {
|
||||
if (!this.labelElement.val) {
|
||||
return;
|
||||
}
|
||||
if (newLabel !== null) {
|
||||
this.label = newLabel;
|
||||
this.labelElement.val!.innerText = newLabel;
|
||||
this.labelElement.val!.classList.add("visible");
|
||||
this.labelElement.val.innerText = newLabel;
|
||||
this.labelElement.val.classList.add("visible");
|
||||
} else {
|
||||
this.label = newLabel;
|
||||
this.labelElement.val!.innerText = "";
|
||||
this.labelElement.val!.classList.remove("visible");
|
||||
this.labelElement.val.innerText = "";
|
||||
this.labelElement.val.classList.remove("visible");
|
||||
}
|
||||
}
|
||||
|
||||
disable(): void {
|
||||
this.node?.classList.add("disabled");
|
||||
this.render().classList.add("disabled");
|
||||
this.inputElement.val!.disabled = true;
|
||||
}
|
||||
|
||||
enable(): void {
|
||||
this.node?.classList.remove("disabled");
|
||||
this.render().classList.remove("disabled");
|
||||
this.inputElement.val!.disabled = false;
|
||||
}
|
||||
|
||||
@@ -82,31 +84,29 @@ export default class NumberInputView extends UINode {
|
||||
if (this.label !== null) {
|
||||
labelClasses.push("visible");
|
||||
}
|
||||
return h("div", {
|
||||
classes: ["number-input"],
|
||||
}, [
|
||||
h("label", {
|
||||
classes: labelClasses,
|
||||
saveTo: this.labelElement,
|
||||
innerText: this.label ?? "",
|
||||
}),
|
||||
h("button", {
|
||||
innerText: "-",
|
||||
classes: ["number-input-button", "number-input-dec"],
|
||||
onclick: () => {
|
||||
return <div className={"number-input"}>
|
||||
<label
|
||||
classes={labelClasses}
|
||||
saveTo={this.labelElement}>
|
||||
{this.label ?? ""}
|
||||
</label>
|
||||
<button
|
||||
classes={["number-input-button", "number-input-dec"]}
|
||||
onclick={() => {
|
||||
if (this.onDecrement) {
|
||||
this.onDecrement();
|
||||
} else if (this.setter && this.getter) {
|
||||
this.setter(this.getter() - 1);
|
||||
}
|
||||
},
|
||||
}),
|
||||
h("input", {
|
||||
type: "number",
|
||||
saveTo: this.inputElement,
|
||||
classes: ["number-input-input"],
|
||||
valueAsNumber: this.value,
|
||||
onblur: (event: Event) => {
|
||||
}}>
|
||||
-
|
||||
</button>
|
||||
<input
|
||||
type={"number"}
|
||||
saveTo={this.inputElement}
|
||||
className={"number-input-input"}
|
||||
valueAsNumber={this.value}
|
||||
onblur={(event: Event) => {
|
||||
const input = (event.target as HTMLInputElement).valueAsNumber;
|
||||
if (!isNaN(input)) {
|
||||
if (this.onNewInput) {
|
||||
@@ -115,19 +115,18 @@ export default class NumberInputView extends UINode {
|
||||
this.setter(input);
|
||||
}
|
||||
}
|
||||
},
|
||||
}),
|
||||
h("button", {
|
||||
innerText: "+",
|
||||
classes: ["number-input-button", "number-input-inc"],
|
||||
onclick: () => {
|
||||
}} />
|
||||
<button
|
||||
classes={["number-input-button", "number-input-inc"]}
|
||||
onclick={() => {
|
||||
if (this.onIncrement) {
|
||||
this.onIncrement();
|
||||
} else if (this.setter && this.getter) {
|
||||
this.setter(this.getter() + 1);
|
||||
}
|
||||
},
|
||||
}),
|
||||
]);
|
||||
}}>
|
||||
+
|
||||
</button>
|
||||
</div> as HTMLDivElement;
|
||||
}
|
||||
}
|
||||
@@ -12,7 +12,10 @@
|
||||
"paths": {
|
||||
"@/*": ["src/*"],
|
||||
"assets/*": ["assets/*"]
|
||||
}
|
||||
},
|
||||
"jsxFactory": "h",
|
||||
"jsxFragmentFactory": "frag",
|
||||
"jsx": "react"
|
||||
},
|
||||
"include": [
|
||||
"./src/**/*",
|
||||
|
||||
10
vite.config.ts
Normal file
10
vite.config.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import { defineConfig } from "vite";
|
||||
|
||||
export default defineConfig({
|
||||
resolve: {
|
||||
alias: {
|
||||
'@': '/src',
|
||||
'assets': '/assets',
|
||||
}
|
||||
},
|
||||
});
|
||||
@@ -1,90 +0,0 @@
|
||||
const path = require("path");
|
||||
const webpack = require("webpack");
|
||||
const config = require("./config.json");
|
||||
|
||||
const TerserWebpackPlugin = require("terser-webpack-plugin");
|
||||
const MiniCssExtractPlugin = require("mini-css-extract-plugin");
|
||||
|
||||
const webpackConfig = {
|
||||
mode: "development",
|
||||
entry: "./src/main.ts",
|
||||
plugins: [new webpack.ProgressPlugin(), new MiniCssExtractPlugin()],
|
||||
module: {
|
||||
rules: [
|
||||
{
|
||||
test: /\.(ts|tsx)$/,
|
||||
loader: "ts-loader",
|
||||
include: [path.resolve(__dirname, "src")],
|
||||
exclude: [/node_modules/],
|
||||
},
|
||||
{
|
||||
test: /.css$/,
|
||||
|
||||
use: [{
|
||||
loader: config.development ? "style-loader" : MiniCssExtractPlugin.loader,
|
||||
}, {
|
||||
loader: "css-loader",
|
||||
options: {
|
||||
url: true,
|
||||
sourceMap: true
|
||||
}
|
||||
}]
|
||||
},
|
||||
{
|
||||
test: /\.(png|jpe?g|gif|ttf|woff2?|eot|svg)$/i,
|
||||
use: [
|
||||
{
|
||||
loader: "file-loader",
|
||||
},
|
||||
],
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
resolve: {
|
||||
alias: {
|
||||
"@": path.resolve(__dirname, "./src"),
|
||||
"assets": path.resolve(__dirname, "./assets"),
|
||||
},
|
||||
extensions: [".tsx", ".ts", ".js", ".svg", ".ttf"]
|
||||
},
|
||||
|
||||
output: {
|
||||
filename: "bundle.js",
|
||||
publicPath: `${config.baseUrl}/static/`,
|
||||
path: path.resolve(__dirname, "./public/static"),
|
||||
},
|
||||
|
||||
devServer: {
|
||||
static: {
|
||||
directory: path.join(__dirname, "./public"),
|
||||
publicPath: `${config.baseUrl}/`,
|
||||
},
|
||||
hot: true,
|
||||
compress: true,
|
||||
port: 9000,
|
||||
},
|
||||
};
|
||||
|
||||
if (!config.development) {
|
||||
webpackConfig.optimization = {
|
||||
minimizer: [new TerserWebpackPlugin()],
|
||||
|
||||
splitChunks: {
|
||||
cacheGroups: {
|
||||
vendors: {
|
||||
priority: -10,
|
||||
test: /[\\/]node_modules[\\/]/
|
||||
}
|
||||
},
|
||||
|
||||
chunks: "async",
|
||||
minChunks: 1,
|
||||
minSize: 30000,
|
||||
name: false
|
||||
}
|
||||
};
|
||||
webpackConfig.mode = "production";
|
||||
}
|
||||
|
||||
module.exports = {...webpackConfig};
|
||||
Reference in New Issue
Block a user