feat(editor): mermaid code editor & improve parsing (#10897)
This commit is contained in:
@@ -1,28 +1,84 @@
|
||||
import { useEffect, useRef } from "react";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
|
||||
import { EVENT, KEYS } from "@excalidraw/common";
|
||||
|
||||
import type { ChangeEventHandler } from "react";
|
||||
import Spinner from "../Spinner";
|
||||
|
||||
import { useUIAppState } from "../../context/ui-appState";
|
||||
|
||||
import type { ComponentType } from "react";
|
||||
import type { CodeMirrorEditorProps } from "./CodeMirrorEditor";
|
||||
|
||||
interface TTDDialogInputProps {
|
||||
input: string;
|
||||
placeholder: string;
|
||||
onChange: ChangeEventHandler<HTMLTextAreaElement>;
|
||||
onChange: (value: string) => void;
|
||||
onKeyboardSubmit?: () => void;
|
||||
errorLine?: number | null;
|
||||
}
|
||||
|
||||
type EditorState =
|
||||
| { type: "loading" }
|
||||
| { type: "ready"; component: ComponentType<CodeMirrorEditorProps> }
|
||||
| { type: "fallback" };
|
||||
|
||||
const SPINNER_DELAY_MS = 300;
|
||||
|
||||
export const TTDDialogInput = ({
|
||||
input,
|
||||
placeholder,
|
||||
onChange,
|
||||
onKeyboardSubmit,
|
||||
errorLine,
|
||||
}: TTDDialogInputProps) => {
|
||||
const ref = useRef<HTMLTextAreaElement>(null);
|
||||
|
||||
const callbackRef = useRef(onKeyboardSubmit);
|
||||
callbackRef.current = onKeyboardSubmit;
|
||||
|
||||
const [editorState, setEditorState] = useState<EditorState>({
|
||||
type: "loading",
|
||||
});
|
||||
const [showSpinner, setShowSpinner] = useState(false);
|
||||
|
||||
const { theme } = useUIAppState();
|
||||
|
||||
// Lazy-load CodeMirror editor
|
||||
useEffect(() => {
|
||||
let cancelled = false;
|
||||
|
||||
const spinnerTimer = setTimeout(() => {
|
||||
if (!cancelled) {
|
||||
setShowSpinner(true);
|
||||
}
|
||||
}, SPINNER_DELAY_MS);
|
||||
|
||||
import("./CodeMirrorEditor")
|
||||
.then((mod) => {
|
||||
if (!cancelled) {
|
||||
setEditorState({ type: "ready", component: mod.default });
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
if (!cancelled) {
|
||||
setEditorState({ type: "fallback" });
|
||||
}
|
||||
})
|
||||
.finally(() => {
|
||||
clearTimeout(spinnerTimer);
|
||||
});
|
||||
|
||||
return () => {
|
||||
cancelled = true;
|
||||
clearTimeout(spinnerTimer);
|
||||
};
|
||||
}, []);
|
||||
|
||||
// Keyboard shortcut + focus for textarea fallback
|
||||
useEffect(() => {
|
||||
if (editorState.type !== "fallback") {
|
||||
return;
|
||||
}
|
||||
if (!callbackRef.current) {
|
||||
return;
|
||||
}
|
||||
@@ -40,15 +96,42 @@ export const TTDDialogInput = ({
|
||||
textarea.removeEventListener(EVENT.KEYDOWN, handleKeyDown);
|
||||
};
|
||||
}
|
||||
}, []);
|
||||
}, [editorState.type]);
|
||||
|
||||
return (
|
||||
<textarea
|
||||
className="ttd-dialog-input"
|
||||
onChange={onChange}
|
||||
value={input}
|
||||
placeholder={placeholder}
|
||||
ref={ref}
|
||||
/>
|
||||
);
|
||||
if (editorState.type === "ready") {
|
||||
const CodeMirrorEditor = editorState.component;
|
||||
return (
|
||||
<CodeMirrorEditor
|
||||
value={input}
|
||||
onChange={onChange}
|
||||
onKeyboardSubmit={onKeyboardSubmit}
|
||||
placeholder={placeholder}
|
||||
theme={theme}
|
||||
errorLine={errorLine}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (editorState.type === "fallback") {
|
||||
return (
|
||||
<textarea
|
||||
className="ttd-dialog-input"
|
||||
onChange={(e) => onChange(e.target.value)}
|
||||
value={input}
|
||||
placeholder={placeholder}
|
||||
ref={ref}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
// Loading state
|
||||
if (showSpinner) {
|
||||
return (
|
||||
<div className="ttd-dialog-input ttd-dialog-input--loading">
|
||||
<Spinner />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user