@tldraw/editor
Version:
tldraw infinite canvas SDK (editor).
386 lines (385 loc) • 12.5 kB
JavaScript
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
import { Store } from "@tldraw/store";
import { annotateError } from "@tldraw/utils";
import React, {
memo,
useCallback,
useEffect,
useLayoutEffect,
useMemo,
useRef,
useState,
useSyncExternalStore
} from "react";
import classNames from "classnames";
import { version } from "../version.mjs";
import { OptionalErrorBoundary } from "./components/ErrorBoundary.mjs";
import { DefaultErrorFallback } from "./components/default-components/DefaultErrorFallback.mjs";
import { createTLUser } from "./config/createTLUser.mjs";
import { Editor } from "./editor/Editor.mjs";
import { ContainerProvider, useContainer } from "./hooks/useContainer.mjs";
import { useCursor } from "./hooks/useCursor.mjs";
import { useDarkMode } from "./hooks/useDarkMode.mjs";
import { EditorProvider, useEditor } from "./hooks/useEditor.mjs";
import {
EditorComponentsProvider,
useEditorComponents
} from "./hooks/useEditorComponents.mjs";
import { useEvent } from "./hooks/useEvent.mjs";
import { useForceUpdate } from "./hooks/useForceUpdate.mjs";
import { useShallowObjectIdentity } from "./hooks/useIdentity.mjs";
import { useLocalStore } from "./hooks/useLocalStore.mjs";
import { useRefState } from "./hooks/useRefState.mjs";
import { useZoomCss } from "./hooks/useZoomCss.mjs";
import { LicenseProvider } from "./license/LicenseProvider.mjs";
import { Watermark } from "./license/Watermark.mjs";
import { stopEventPropagation } from "./utils/dom.mjs";
const EMPTY_SHAPE_UTILS_ARRAY = [];
const EMPTY_BINDING_UTILS_ARRAY = [];
const EMPTY_TOOLS_ARRAY = [];
const TL_CONTAINER_CLASS = "tl-container";
const TldrawEditor = memo(function TldrawEditor2({
store,
components,
className,
user: _user,
options: _options,
...rest
}) {
const [container, setContainer] = useState(null);
const user = useMemo(() => _user ?? createTLUser(), [_user]);
const ErrorFallback = components?.ErrorFallback === void 0 ? DefaultErrorFallback : components?.ErrorFallback;
const withDefaults = {
...rest,
shapeUtils: rest.shapeUtils ?? EMPTY_SHAPE_UTILS_ARRAY,
bindingUtils: rest.bindingUtils ?? EMPTY_BINDING_UTILS_ARRAY,
tools: rest.tools ?? EMPTY_TOOLS_ARRAY,
components,
options: useShallowObjectIdentity(_options)
};
return (
/* @__PURE__ */ jsx(
"div",
{
ref: setContainer,
"data-tldraw": version,
draggable: false,
className: classNames(`${TL_CONTAINER_CLASS} tl-theme__light`, className),
onPointerDown: stopEventPropagation,
tabIndex: -1,
role: "application",
"aria-label": _options?.branding ?? "tldraw",
children: /* @__PURE__ */ jsx(
OptionalErrorBoundary,
{
fallback: ErrorFallback,
onError: (error) => annotateError(error, { tags: { origin: "react.tldraw-before-app" } }),
children: container && /* @__PURE__ */ jsx(LicenseProvider, { licenseKey: rest.licenseKey, children: /* @__PURE__ */ jsx(ContainerProvider, { container, children: /* @__PURE__ */ jsx(EditorComponentsProvider, { overrides: components, children: store ? store instanceof Store ? (
// Store is ready to go, whether externally synced or not
/* @__PURE__ */ (jsx(TldrawEditorWithReadyStore, { ...withDefaults, store, user }))
) : (
// Store is a synced store, so handle syncing stages internally
/* @__PURE__ */ (jsx(TldrawEditorWithLoadingStore, { ...withDefaults, store, user }))
) : (
// We have no store (it's undefined) so create one and possibly sync it
/* @__PURE__ */ (jsx(TldrawEditorWithOwnStore, { ...withDefaults, store, user }))
) }) }) })
}
)
}
)
);
});
function TldrawEditorWithOwnStore(props) {
const {
defaultName,
snapshot,
initialData,
shapeUtils,
bindingUtils,
persistenceKey,
sessionId,
user,
assets,
migrations
} = props;
const syncedStore = useLocalStore({
shapeUtils,
bindingUtils,
initialData,
persistenceKey,
sessionId,
defaultName,
snapshot,
assets,
migrations
});
return /* @__PURE__ */ jsx(TldrawEditorWithLoadingStore, { ...props, store: syncedStore, user });
}
const TldrawEditorWithLoadingStore = memo(function TldrawEditorBeforeLoading({
store,
user,
...rest
}) {
const container = useContainer();
useLayoutEffect(() => {
if (user.userPreferences.get().colorScheme === "dark") {
container.classList.remove("tl-theme__light");
container.classList.add("tl-theme__dark");
}
}, [container, user]);
const { LoadingScreen: LoadingScreen2 } = useEditorComponents();
switch (store.status) {
case "error": {
throw store.error;
}
case "loading": {
return LoadingScreen2 ? /* @__PURE__ */ jsx(LoadingScreen2, {}) : null;
}
case "not-synced": {
break;
}
case "synced-local": {
break;
}
case "synced-remote": {
break;
}
}
return /* @__PURE__ */ jsx(TldrawEditorWithReadyStore, { ...rest, store: store.store, user });
});
const noAutoFocus = () => document.location.search.includes("tldraw_preserve_focus");
function TldrawEditorWithReadyStore({
onMount,
children,
store,
tools,
shapeUtils,
bindingUtils,
user,
initialState,
autoFocus = true,
inferDarkMode,
cameraOptions,
textOptions,
options,
licenseKey,
deepLinks: _deepLinks,
// eslint-disable-next-line @typescript-eslint/no-deprecated
isShapeHidden,
getShapeVisibility,
assetUrls
}) {
const { ErrorFallback } = useEditorComponents();
const container = useContainer();
const [editor, setEditor] = useRefState(null);
const canvasRef = useRef(null);
const deepLinks = useShallowObjectIdentity(_deepLinks === true ? {} : _deepLinks);
const editorOptionsRef = useRef({
// for these, it's because they're only used when the editor first mounts:
autoFocus: autoFocus && !noAutoFocus(),
inferDarkMode,
initialState,
// for these, it's because we keep them up to date in a separate effect:
cameraOptions,
deepLinks
});
useLayoutEffect(() => {
editorOptionsRef.current = {
autoFocus: autoFocus && !noAutoFocus(),
inferDarkMode,
initialState,
cameraOptions,
deepLinks
};
}, [autoFocus, inferDarkMode, initialState, cameraOptions, deepLinks]);
useLayoutEffect(
() => {
const { autoFocus: autoFocus2, inferDarkMode: inferDarkMode2, initialState: initialState2, cameraOptions: cameraOptions2, deepLinks: deepLinks2 } = editorOptionsRef.current;
const editor2 = new Editor({
store,
shapeUtils,
bindingUtils,
tools,
getContainer: () => container,
user,
initialState: initialState2,
// we should check for some kind of query parameter that turns off autofocus
autoFocus: autoFocus2,
inferDarkMode: inferDarkMode2,
cameraOptions: cameraOptions2,
textOptions,
options,
licenseKey,
isShapeHidden,
getShapeVisibility,
fontAssetUrls: assetUrls?.fonts
});
editor2.updateViewportScreenBounds(canvasRef.current ?? container);
if (deepLinks2) {
if (!deepLinks2?.getUrl) {
editor2.navigateToDeepLink(deepLinks2);
} else {
editor2.navigateToDeepLink({ ...deepLinks2, url: deepLinks2.getUrl(editor2) });
}
}
setEditor(editor2);
return () => {
editor2.dispose();
};
},
// if any of these change, we need to recreate the editor.
[
bindingUtils,
container,
options,
shapeUtils,
store,
tools,
user,
setEditor,
licenseKey,
isShapeHidden,
getShapeVisibility,
textOptions,
assetUrls
]
);
useLayoutEffect(() => {
if (!editor) return;
if (deepLinks) {
return editor.registerDeepLinkListener(deepLinks);
}
}, [editor, deepLinks]);
useLayoutEffect(() => {
if (editor && cameraOptions) {
editor.setCameraOptions(cameraOptions);
}
}, [editor, cameraOptions]);
const crashingError = useSyncExternalStore(
useCallback(
(onStoreChange) => {
if (editor) {
editor.on("crash", onStoreChange);
return () => editor.off("crash", onStoreChange);
}
return () => {
};
},
[editor]
),
() => editor?.getCrashingError() ?? null
);
useEffect(
function handleFocusOnPointerDownForPreserveFocusMode() {
if (!editor) return;
function handleFocusOnPointerDown() {
if (!editor) return;
editor.focus();
}
function handleBlurOnPointerDown() {
if (!editor) return;
editor.blur();
}
if (autoFocus && noAutoFocus()) {
editor.getContainer().addEventListener("pointerdown", handleFocusOnPointerDown);
document.body.addEventListener("pointerdown", handleBlurOnPointerDown);
return () => {
editor.getContainer()?.removeEventListener("pointerdown", handleFocusOnPointerDown);
document.body.removeEventListener("pointerdown", handleBlurOnPointerDown);
};
}
},
[editor, autoFocus]
);
const [_fontLoadingState, setFontLoadingState] = useState(null);
let fontLoadingState = _fontLoadingState;
if (editor !== fontLoadingState?.editor) {
fontLoadingState = null;
}
useEffect(() => {
if (!editor) return;
let isCancelled = false;
setFontLoadingState({ editor, isLoaded: false });
editor.fonts.loadRequiredFontsForCurrentPage(editor.options.maxFontsToLoadBeforeRender).finally(() => {
if (isCancelled) return;
setFontLoadingState({ editor, isLoaded: true });
});
return () => {
isCancelled = true;
};
}, [editor]);
const { Canvas, LoadingScreen: LoadingScreen2 } = useEditorComponents();
if (!editor || !fontLoadingState?.isLoaded) {
return /* @__PURE__ */ jsxs(Fragment, { children: [
LoadingScreen2 && /* @__PURE__ */ jsx(LoadingScreen2, {}),
/* @__PURE__ */ jsx("div", { className: "tl-canvas", ref: canvasRef })
] });
}
return (
// the top-level tldraw component also renders an error boundary almost
// identical to this one. the reason we have two is because this one has
// access to `App`, which means that here we can enrich errors with data
// from app for reporting, and also still attempt to render the user's
// document in the event of an error to reassure them that their work is
// not lost.
/* @__PURE__ */ (jsx(OptionalErrorBoundary, {
fallback: ErrorFallback,
onError: (error) => editor.annotateError(error, { origin: "react.tldraw", willCrashApp: true }),
children: crashingError ? /* @__PURE__ */ jsx(Crash, { crashingError }) : /* @__PURE__ */ jsx(EditorProvider, { editor, children: /* @__PURE__ */ jsxs(Layout, { onMount, children: [
children ?? (Canvas ? /* @__PURE__ */ jsx(Canvas, {}, editor.contextId) : null),
/* @__PURE__ */ jsx(Watermark, {})
] }) })
}))
);
}
function Layout({ children, onMount }) {
useZoomCss();
useCursor();
useDarkMode();
useForceUpdate();
useOnMount((editor) => {
const teardownStore = editor.store.props.onMount(editor);
const teardownCallback = onMount?.(editor);
return () => {
teardownStore?.();
teardownCallback?.();
};
});
return children;
}
function Crash({ crashingError }) {
throw crashingError;
}
function LoadingScreen({ children }) {
return /* @__PURE__ */ jsx("div", { className: "tl-loading", "aria-busy": "true", tabIndex: 0, children });
}
function ErrorScreen({ children }) {
return /* @__PURE__ */ jsx("div", { className: "tl-loading", children });
}
function useOnMount(onMount) {
const editor = useEditor();
const onMountEvent = useEvent((editor2) => {
let teardown = void 0;
editor2.run(
() => {
teardown = onMount?.(editor2);
editor2.emit("mount");
},
{ history: "ignore" }
);
window.tldrawReady = true;
return teardown;
});
React.useLayoutEffect(() => {
if (editor) return onMountEvent?.(editor);
}, [editor, onMountEvent]);
}
export {
ErrorScreen,
LoadingScreen,
TL_CONTAINER_CLASS,
TldrawEditor,
useOnMount
};
//# sourceMappingURL=TldrawEditor.mjs.map