UNPKG

@tldraw/editor

Version:

tldraw infinite canvas SDK (editor).

386 lines (385 loc) • 12.5 kB
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