mirror of
https://github.com/vrtmrz/obsidian-livesync.git
synced 2026-07-31 08:51:23 +00:00
refactor: compose browser application runtimes
This commit is contained in:
@@ -2,18 +2,16 @@ import { Modal } from "@/deps";
|
||||
|
||||
import {
|
||||
SvelteDialogManagerBase,
|
||||
SvelteDialogMixIn,
|
||||
type ComponentHasResult,
|
||||
type SvelteDialogManagerDependencies,
|
||||
} from "@vrtmrz/livesync-commonlib/compat/services/implements/base/SvelteDialog";
|
||||
import type { ObsidianServiceContext } from "@/modules/services/ObsidianServiceContext";
|
||||
import DialogHost from "@/modules/services/LiveSyncUI/DialogHost.svelte";
|
||||
export const SvelteDialogBase = SvelteDialogMixIn(Modal, DialogHost);
|
||||
export class SvelteDialogObsidian<
|
||||
T,
|
||||
U,
|
||||
C extends ObsidianServiceContext = ObsidianServiceContext,
|
||||
> extends SvelteDialogBase<T, U, C> {
|
||||
import { SvelteDialogSession } from "@/modules/services/SvelteDialogSession";
|
||||
|
||||
export class SvelteDialogObsidian<T, U, C extends ObsidianServiceContext = ObsidianServiceContext> extends Modal {
|
||||
private readonly session: SvelteDialogSession<T, U, C>;
|
||||
|
||||
constructor(
|
||||
context: C,
|
||||
dependents: SvelteDialogManagerDependencies<C>,
|
||||
@@ -21,13 +19,28 @@ export class SvelteDialogObsidian<
|
||||
initialData?: U
|
||||
) {
|
||||
super(context.app);
|
||||
this.initDialog(context, dependents, component, initialData);
|
||||
this.session = new SvelteDialogSession({
|
||||
surface: this,
|
||||
context,
|
||||
dependencies: dependents,
|
||||
dialogHost: DialogHost,
|
||||
component,
|
||||
initialData,
|
||||
});
|
||||
}
|
||||
|
||||
override onOpen(): void {
|
||||
super.onOpen();
|
||||
this.session.onOpen();
|
||||
this.contentEl.closest(".modal-container")?.classList.add("livesync-svelte-dialog-container");
|
||||
}
|
||||
|
||||
override onClose(): void {
|
||||
this.session.onClose();
|
||||
}
|
||||
|
||||
waitForClose(): Promise<T | undefined> {
|
||||
return this.session.waitForClose();
|
||||
}
|
||||
}
|
||||
|
||||
export class ObsidianSvelteDialogManager<T extends ObsidianServiceContext> extends SvelteDialogManagerBase<T> {
|
||||
|
||||
@@ -0,0 +1,138 @@
|
||||
import { EVENT_PLUGIN_UNLOADED } from "@vrtmrz/livesync-commonlib/compat/events/coreEvents";
|
||||
import type { ServiceContext } from "@vrtmrz/livesync-commonlib/context";
|
||||
import {
|
||||
setupDialogContext,
|
||||
type ComponentHasResult,
|
||||
type DialogContext,
|
||||
type DialogHostProps,
|
||||
type DialogSvelteComponentBaseProps,
|
||||
type SvelteDialogManagerDependencies,
|
||||
} from "@vrtmrz/livesync-commonlib/compat/services/implements/base/SvelteDialog";
|
||||
import { fireAndForget, promiseWithResolvers, type PromiseWithResolvers } from "octagonal-wheels/promises";
|
||||
import { mount, unmount, type Component } from "svelte";
|
||||
|
||||
/** Host dialogue surface controlled by an explicit session. */
|
||||
export interface SvelteDialogSurface {
|
||||
readonly contentEl: HTMLElement;
|
||||
setTitle(title: string): unknown;
|
||||
close(): void;
|
||||
}
|
||||
|
||||
type MountedDialog = ReturnType<typeof mount>;
|
||||
|
||||
/** Injectable renderer used to keep the dialogue lifecycle independently testable. */
|
||||
export interface SvelteDialogRenderer {
|
||||
mount(dialogHost: Component<DialogHostProps>, target: HTMLElement, props: DialogHostProps): MountedDialog;
|
||||
unmount(component: MountedDialog): Promise<void>;
|
||||
}
|
||||
|
||||
export interface SvelteDialogSessionOptions<TResult, TInitial, TContext extends ServiceContext> {
|
||||
surface: SvelteDialogSurface;
|
||||
context: TContext;
|
||||
dependencies: SvelteDialogManagerDependencies<TContext>;
|
||||
dialogHost: Component<DialogHostProps>;
|
||||
component: ComponentHasResult<TResult, TInitial>;
|
||||
initialData?: TInitial;
|
||||
renderer?: SvelteDialogRenderer;
|
||||
}
|
||||
|
||||
const svelteDialogRenderer: SvelteDialogRenderer = {
|
||||
mount(dialogHost, target, props) {
|
||||
return mount(dialogHost, { target, props });
|
||||
},
|
||||
async unmount(component) {
|
||||
await unmount(component);
|
||||
},
|
||||
};
|
||||
|
||||
/** Owns one Svelte dialogue result and mount lifecycle for a concrete host surface. */
|
||||
export class SvelteDialogSession<
|
||||
TResult,
|
||||
TInitial,
|
||||
TContext extends ServiceContext = ServiceContext,
|
||||
> {
|
||||
private readonly surface: SvelteDialogSurface;
|
||||
private readonly context: TContext;
|
||||
private readonly dependencies: SvelteDialogManagerDependencies<TContext>;
|
||||
private readonly dialogHost: Component<DialogHostProps>;
|
||||
private readonly component: ComponentHasResult<TResult, TInitial>;
|
||||
private readonly initialData?: TInitial;
|
||||
private readonly renderer: SvelteDialogRenderer;
|
||||
|
||||
private result?: TResult;
|
||||
private pendingResult?: PromiseWithResolvers<TResult | undefined>;
|
||||
private mountedDialog?: MountedDialog;
|
||||
private unregisterUnload?: () => void;
|
||||
|
||||
constructor(options: SvelteDialogSessionOptions<TResult, TInitial, TContext>) {
|
||||
this.surface = options.surface;
|
||||
this.context = options.context;
|
||||
this.dependencies = options.dependencies;
|
||||
this.dialogHost = options.dialogHost;
|
||||
this.component = options.component;
|
||||
this.initialData = options.initialData;
|
||||
this.renderer = options.renderer ?? svelteDialogRenderer;
|
||||
}
|
||||
|
||||
onOpen(): void {
|
||||
this.surface.contentEl.replaceChildren();
|
||||
this.unregisterUnload?.();
|
||||
this.unregisterUnload = undefined;
|
||||
if (this.pendingResult) {
|
||||
this.pendingResult.reject(new Error("Dialogue opened again"));
|
||||
}
|
||||
|
||||
this.result = undefined;
|
||||
const pendingResult = promiseWithResolvers<TResult | undefined>();
|
||||
this.pendingResult = pendingResult;
|
||||
this.unregisterUnload = this.context.events.onceEvent(EVENT_PLUGIN_UNLOADED, () => {
|
||||
if (this.pendingResult !== pendingResult) {
|
||||
return;
|
||||
}
|
||||
this.pendingResult = undefined;
|
||||
pendingResult.reject(new Error("Plug-in unloaded"));
|
||||
this.surface.close();
|
||||
});
|
||||
|
||||
this.mountedDialog = this.renderer.mount(this.dialogHost, this.surface.contentEl, {
|
||||
onSetupContext: (props: DialogSvelteComponentBaseProps<TResult, TInitial>) => {
|
||||
setupDialogContext({
|
||||
...props,
|
||||
context: this.context,
|
||||
services: this.dependencies,
|
||||
} satisfies DialogContext<TContext, TResult, TInitial>);
|
||||
},
|
||||
setTitle: (title: string) => {
|
||||
this.surface.setTitle(title);
|
||||
},
|
||||
closeDialog: () => {
|
||||
this.surface.close();
|
||||
},
|
||||
setResult: (result: TResult) => {
|
||||
this.result = result;
|
||||
},
|
||||
getInitialData: () => this.initialData,
|
||||
mountComponent: this.component,
|
||||
});
|
||||
}
|
||||
|
||||
waitForClose(): Promise<TResult | undefined> {
|
||||
if (!this.pendingResult) {
|
||||
throw new Error("Dialogue has not been opened");
|
||||
}
|
||||
return this.pendingResult.promise;
|
||||
}
|
||||
|
||||
onClose(): void {
|
||||
this.unregisterUnload?.();
|
||||
this.unregisterUnload = undefined;
|
||||
this.pendingResult?.resolve(this.result);
|
||||
this.pendingResult = undefined;
|
||||
|
||||
const mountedDialog = this.mountedDialog;
|
||||
this.mountedDialog = undefined;
|
||||
if (mountedDialog) {
|
||||
fireAndForget(() => this.renderer.unmount(mountedDialog));
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user