Skip to content

Commit 3845004

Browse files
add new modal component
1 parent cde9fae commit 3845004

2 files changed

Lines changed: 142 additions & 0 deletions

File tree

src/api/local-api.tsx

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ import { ScriptCache } from "./script-cache";
2828
import { Expression } from "expression/expression";
2929
import { Card } from "./ui/views/cards";
3030
import { ListView } from "./ui/views/list";
31+
import { Modal, Modals, SubmittableModal, useModalContext } from "./ui/views/modal";
3132

3233
/**
3334
* Local API provided to specific codeblocks when they are executing.
@@ -37,6 +38,8 @@ export class DatacoreLocalApi {
3738
/** @internal The cache of all currently loaded scripts in this context. */
3839
private scriptCache: ScriptCache;
3940

41+
private modalTypes: Modals = new Modals();
42+
4043
public constructor(public api: DatacoreApi, public path: string) {
4144
this.scriptCache = new ScriptCache(this.core.datastore);
4245
}
@@ -389,6 +392,19 @@ export class DatacoreLocalApi {
389392
return <ErrorMessage message={`No valid embedding for element '${element.$id}' from '${element.$file}'`} />;
390393
}).bind(this);
391394

395+
/** Accessor for raw modal classes. */
396+
public get modals() {
397+
return this.modalTypes;
398+
}
399+
400+
/** Wrapper around an obsidian modal. */
401+
public Modal = Modal;
402+
403+
/** Wrapper around an obsidian modal that returns a result when submitted. */
404+
public SubmittableModal = SubmittableModal;
405+
406+
public useModalContext = useModalContext;
407+
392408
///////////
393409
// Views //
394410
///////////

src/api/ui/views/modal.tsx

Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
import {
2+
ReactNode,
3+
RefObject,
4+
forwardRef,
5+
useContext,
6+
createContext,
7+
memo,
8+
Context as ReactContext,
9+
useEffect,
10+
useRef,
11+
createPortal,
12+
ComponentProps,
13+
ForwardedRef,
14+
useImperativeHandle,
15+
useMemo,
16+
} from "preact/compat";
17+
import { App, Modal as ObsidianModal } from "obsidian";
18+
import { APP_CONTEXT } from "ui/markdown";
19+
import { Literal } from "expression/literal";
20+
import { VNode } from "preact";
21+
22+
class DatacoreModal extends ObsidianModal {
23+
constructor(app: App, public openCallback?: ObsidianModal["onOpen"], public onCancel?: ObsidianModal["onClose"]) {
24+
super(app);
25+
}
26+
public onOpen() {
27+
super.onOpen();
28+
this.openCallback?.();
29+
}
30+
public onClose() {
31+
super.onClose();
32+
this.onCancel?.();
33+
}
34+
}
35+
36+
class SubmittableDatacoreModal<T> extends DatacoreModal {
37+
constructor(
38+
app: App,
39+
public submitCallback?: (result: T) => void | Promise<void>,
40+
public openCallback?: ObsidianModal["onOpen"],
41+
public onCancel?: ObsidianModal["onClose"]
42+
) {
43+
super(app, openCallback, onCancel);
44+
}
45+
public onSubmit(result: T) {
46+
this.close();
47+
this.submitCallback?.(result);
48+
}
49+
}
50+
51+
export class Modals {
52+
get submittableModal() {
53+
return SubmittableDatacoreModal;
54+
}
55+
get modal() {
56+
return DatacoreModal;
57+
}
58+
}
59+
60+
interface ModalContextType<M extends ObsidianModal> {
61+
modal: M;
62+
}
63+
64+
interface BaseModalProps {
65+
title?: Literal | VNode | ReactNode;
66+
children: ReactNode;
67+
onCancel?: ObsidianModal["onClose"];
68+
onOpen?: ObsidianModal["onOpen"];
69+
}
70+
71+
const MODAL_CONTEXT = createContext<ModalContextType<any> | null>(null);
72+
73+
function ModalContext<M extends ObsidianModal>({ modal, children }: { modal: M; children: ReactNode }) {
74+
const Ctx = MODAL_CONTEXT as ReactContext<ModalContextType<M>>;
75+
return <Ctx.Provider value={{ modal }}>{children}</Ctx.Provider>;
76+
}
77+
78+
function useReusableImperativeHandle<M extends ObsidianModal>(modal: M, ref: ForwardedRef<M>) {
79+
useImperativeHandle(ref, () => modal, [modal]);
80+
}
81+
82+
function InnerSubmittableModal<T>(
83+
{
84+
children,
85+
onSubmit,
86+
onCancel,
87+
onOpen,
88+
title,
89+
}: BaseModalProps & {
90+
onSubmit?: (result: T) => void | Promise<void>;
91+
},
92+
ref: ForwardedRef<SubmittableDatacoreModal<T>>
93+
) {
94+
const app = useContext(APP_CONTEXT)!;
95+
const modal = useMemo(
96+
() => new SubmittableDatacoreModal<T>(app, onSubmit, onOpen, onCancel),
97+
[app, onSubmit, onOpen, onCancel]
98+
);
99+
useReusableImperativeHandle(modal, ref);
100+
return (
101+
<ModalContext modal={modal}>
102+
{createPortal(<>{title}</>, modal.titleEl)}
103+
{createPortal(<>{children}</>, modal.contentEl)}
104+
</ModalContext>
105+
);
106+
}
107+
108+
function InnerModal({ children, onCancel, onOpen, title }: BaseModalProps, ref: ForwardedRef<DatacoreModal>) {
109+
const app = useContext(APP_CONTEXT)!;
110+
const modal = useMemo(() => new DatacoreModal(app, onOpen, onCancel), [app, onOpen, onCancel]);
111+
useReusableImperativeHandle(modal, ref);
112+
return (
113+
<ModalContext modal={modal}>
114+
{createPortal(<>{title}</>, modal.titleEl)}
115+
{createPortal(<>{children}</>, modal.contentEl)}
116+
</ModalContext>
117+
);
118+
}
119+
120+
export function useModalContext<M extends ObsidianModal>() {
121+
return useContext(MODAL_CONTEXT) as ModalContextType<M>;
122+
}
123+
124+
export const SubmittableModal = forwardRef(InnerSubmittableModal) as typeof InnerSubmittableModal;
125+
126+
export const Modal = forwardRef(InnerModal) as typeof InnerModal;

0 commit comments

Comments
 (0)