UNPKG

@tldraw/editor

Version:

tldraw infinite canvas SDK (editor).

487 lines (486 loc) • 20.2 kB
import { Fragment, jsx, jsxs } from "react/jsx-runtime"; import { react } from "@tldraw/state"; import { useQuickReactor, useValue } from "@tldraw/state-react"; import { dedupe, modulate, objectMapValues } from "@tldraw/utils"; import classNames from "classnames"; import { Fragment as Fragment2, useEffect, useRef, useState } from "react"; import { tlenv } from "../../globals/environment.mjs"; import { useCanvasEvents } from "../../hooks/useCanvasEvents.mjs"; import { useCoarsePointer } from "../../hooks/useCoarsePointer.mjs"; import { useContainer } from "../../hooks/useContainer.mjs"; import { useDocumentEvents } from "../../hooks/useDocumentEvents.mjs"; import { useEditor } from "../../hooks/useEditor.mjs"; import { useEditorComponents } from "../../hooks/useEditorComponents.mjs"; import { useFixSafariDoubleTapZoomPencilEvents } from "../../hooks/useFixSafariDoubleTapZoomPencilEvents.mjs"; import { useGestureEvents } from "../../hooks/useGestureEvents.mjs"; import { useHandleEvents } from "../../hooks/useHandleEvents.mjs"; import { useSharedSafeId } from "../../hooks/useSafeId.mjs"; import { useScreenBounds } from "../../hooks/useScreenBounds.mjs"; import { Mat } from "../../primitives/Mat.mjs"; import { Vec } from "../../primitives/Vec.mjs"; import { toDomPrecision } from "../../primitives/utils.mjs"; import { debugFlags } from "../../utils/debug-flags.mjs"; import { setStyleProperty } from "../../utils/dom.mjs"; import { nearestMultiple } from "../../utils/nearestMultiple.mjs"; import { GeometryDebuggingView } from "../GeometryDebuggingView.mjs"; import { LiveCollaborators } from "../LiveCollaborators.mjs"; import { MenuClickCapture } from "../MenuClickCapture.mjs"; import { Shape } from "../Shape.mjs"; function DefaultCanvas({ className }) { const editor = useEditor(); const { SelectionBackground, Background, SvgDefs, ShapeIndicators } = useEditorComponents(); const rCanvas = useRef(null); const rHtmlLayer = useRef(null); const rHtmlLayer2 = useRef(null); const container = useContainer(); useScreenBounds(rCanvas); useDocumentEvents(); useCoarsePointer(); useGestureEvents(rCanvas); useFixSafariDoubleTapZoomPencilEvents(rCanvas); const rMemoizedStuff = useRef({ lodDisableTextOutline: false, allowTextOutline: true }); useQuickReactor( "position layers", function positionLayersWhenCameraMoves() { const { x, y, z } = editor.getCamera(); if (rMemoizedStuff.current.allowTextOutline && tlenv.isSafari) { container.style.setProperty("--tl-text-outline", "none"); rMemoizedStuff.current.allowTextOutline = false; } if (rMemoizedStuff.current.allowTextOutline && z < editor.options.textShadowLod !== rMemoizedStuff.current.lodDisableTextOutline) { const lodDisableTextOutline = z < editor.options.textShadowLod; container.style.setProperty( "--tl-text-outline", lodDisableTextOutline ? "none" : `var(--tl-text-outline-reference)` ); rMemoizedStuff.current.lodDisableTextOutline = lodDisableTextOutline; } const offset = z >= 1 ? modulate(z, [1, 8], [0.125, 0.5], true) : modulate(z, [0.1, 1], [-2, 0.125], true); const transform = `scale(${toDomPrecision(z)}) translate(${toDomPrecision( x + offset )}px,${toDomPrecision(y + offset)}px)`; setStyleProperty(rHtmlLayer.current, "transform", transform); setStyleProperty(rHtmlLayer2.current, "transform", transform); }, [editor, container] ); const events = useCanvasEvents(); const shapeSvgDefs = useValue( "shapeSvgDefs", () => { const shapeSvgDefsByKey = /* @__PURE__ */ new Map(); for (const util of objectMapValues(editor.shapeUtils)) { if (!util) return; const defs = util.getCanvasSvgDefs(); for (const { key, component: Component } of defs) { if (shapeSvgDefsByKey.has(key)) continue; shapeSvgDefsByKey.set(key, /* @__PURE__ */ jsx(Component, {}, key)); } } return [...shapeSvgDefsByKey.values()]; }, [editor] ); const hideShapes = useValue("debug_shapes", () => debugFlags.hideShapes.get(), [debugFlags]); const debugSvg = useValue("debug_svg", () => debugFlags.debugSvg.get(), [debugFlags]); const debugGeometry = useValue("debug_geometry", () => debugFlags.debugGeometry.get(), [ debugFlags ]); const isEditingAnything = useValue( "isEditingAnything", () => editor.getEditingShapeId() !== null, [editor] ); const isSelectingAnything = useValue( "isSelectingAnything", () => !!editor.getSelectedShapeIds().length, [editor] ); return /* @__PURE__ */ jsxs(Fragment, { children: [ /* @__PURE__ */ jsxs( "div", { ref: rCanvas, draggable: false, "data-iseditinganything": isEditingAnything, "data-isselectinganything": isSelectingAnything, className: classNames("tl-canvas", className), "data-testid": "canvas", ...events, children: [ /* @__PURE__ */ jsx("svg", { className: "tl-svg-context", "aria-hidden": "true", children: /* @__PURE__ */ jsxs("defs", { children: [ shapeSvgDefs, /* @__PURE__ */ jsx(CursorDef, {}), /* @__PURE__ */ jsx(CollaboratorHintDef, {}), SvgDefs && /* @__PURE__ */ jsx(SvgDefs, {}) ] }) }), Background && /* @__PURE__ */ jsx("div", { className: "tl-background__wrapper", children: /* @__PURE__ */ jsx(Background, {}) }), /* @__PURE__ */ jsx(GridWrapper, {}), /* @__PURE__ */ jsxs("div", { ref: rHtmlLayer, className: "tl-html-layer tl-shapes", draggable: false, children: [ /* @__PURE__ */ jsx(OnTheCanvasWrapper, {}), SelectionBackground && /* @__PURE__ */ jsx(SelectionBackgroundWrapper, {}), hideShapes ? null : debugSvg ? /* @__PURE__ */ jsx(ShapesWithSVGs, {}) : /* @__PURE__ */ jsx(ShapesToDisplay, {}) ] }), /* @__PURE__ */ jsx("div", { className: "tl-overlays", children: /* @__PURE__ */ jsxs("div", { ref: rHtmlLayer2, className: "tl-html-layer", children: [ debugGeometry ? /* @__PURE__ */ jsx(GeometryDebuggingView, {}) : null, /* @__PURE__ */ jsx(BrushWrapper, {}), /* @__PURE__ */ jsx(ScribbleWrapper, {}), /* @__PURE__ */ jsx(ZoomBrushWrapper, {}), ShapeIndicators && /* @__PURE__ */ jsx(ShapeIndicators, {}), /* @__PURE__ */ jsx(HintedShapeIndicator, {}), /* @__PURE__ */ jsx(SnapIndicatorWrapper, {}), /* @__PURE__ */ jsx(SelectionForegroundWrapper, {}), /* @__PURE__ */ jsx(HandlesWrapper, {}), /* @__PURE__ */ jsx(OverlaysWrapper, {}), /* @__PURE__ */ jsx(LiveCollaborators, {}) ] }) }), /* @__PURE__ */ jsx(MovingCameraHitTestBlocker, {}) ] } ), /* @__PURE__ */ jsx(MenuClickCapture, {}), /* @__PURE__ */ jsx(InFrontOfTheCanvasWrapper, {}) ] }); } function InFrontOfTheCanvasWrapper() { const { InFrontOfTheCanvas } = useEditorComponents(); if (!InFrontOfTheCanvas) return null; return /* @__PURE__ */ jsx(InFrontOfTheCanvas, {}); } function GridWrapper() { const editor = useEditor(); const gridSize = useValue("gridSize", () => editor.getDocumentSettings().gridSize, [editor]); const { x, y, z } = useValue("camera", () => editor.getCamera(), [editor]); const isGridMode = useValue("isGridMode", () => editor.getInstanceState().isGridMode, [editor]); const { Grid } = useEditorComponents(); if (!(Grid && isGridMode)) return null; return /* @__PURE__ */ jsx(Grid, { x, y, z, size: gridSize }); } function ScribbleWrapper() { const editor = useEditor(); const scribbles = useValue("scribbles", () => editor.getInstanceState().scribbles, [editor]); const zoomLevel = useValue("zoomLevel", () => editor.getZoomLevel(), [editor]); const { Scribble } = useEditorComponents(); if (!(Scribble && scribbles.length)) return null; return scribbles.map((scribble) => /* @__PURE__ */ jsx(Scribble, { className: "tl-user-scribble", scribble, zoom: zoomLevel }, scribble.id)); } function BrushWrapper() { const editor = useEditor(); const brush = useValue("brush", () => editor.getInstanceState().brush, [editor]); const { Brush } = useEditorComponents(); if (!(Brush && brush)) return null; return /* @__PURE__ */ jsx(Brush, { className: "tl-user-brush", brush }); } function ZoomBrushWrapper() { const editor = useEditor(); const zoomBrush = useValue("zoomBrush", () => editor.getInstanceState().zoomBrush, [editor]); const { ZoomBrush } = useEditorComponents(); if (!(ZoomBrush && zoomBrush)) return null; return /* @__PURE__ */ jsx(ZoomBrush, { className: "tl-user-brush tl-zoom-brush", brush: zoomBrush }); } function SnapIndicatorWrapper() { const editor = useEditor(); const lines = useValue("snapLines", () => editor.snaps.getIndicators(), [editor]); const zoomLevel = useValue("zoomLevel", () => editor.getZoomLevel(), [editor]); const { SnapIndicator } = useEditorComponents(); if (!(SnapIndicator && lines.length > 0)) return null; return lines.map((line) => /* @__PURE__ */ jsx(SnapIndicator, { className: "tl-user-snapline", line, zoom: zoomLevel }, line.id)); } function HandlesWrapper() { const editor = useEditor(); const shapeIdWithHandles = useValue( "handles shapeIdWithHandles", () => { const { isReadonly, isChangingStyle } = editor.getInstanceState(); if (isReadonly || isChangingStyle) return false; const onlySelectedShape = editor.getOnlySelectedShape(); if (!onlySelectedShape) return false; const handles = editor.getShapeHandles(onlySelectedShape); if (!handles) return false; return onlySelectedShape.id; }, [editor] ); if (!shapeIdWithHandles) return null; return /* @__PURE__ */ jsx(HandlesWrapperInner, { shapeId: shapeIdWithHandles }); } function HandlesWrapperInner({ shapeId }) { const editor = useEditor(); const { Handles } = useEditorComponents(); const zoomLevel = useValue("zoomLevel", () => editor.getZoomLevel(), [editor]); const isCoarse = useValue("coarse pointer", () => editor.getInstanceState().isCoarsePointer, [ editor ]); const transform = useValue("handles transform", () => editor.getShapePageTransform(shapeId), [ editor, shapeId ]); const handles = useValue( "handles", () => { const handles2 = editor.getShapeHandles(shapeId); if (!handles2) return null; const minDistBetweenVirtualHandlesAndRegularHandles = (isCoarse ? editor.options.coarseHandleRadius : editor.options.handleRadius) / zoomLevel * 2; return handles2.filter( (handle) => ( // if the handle isn't a virtual handle, we'll display it (// but for virtual handles, we'll only display them if they're far enough away from vertex handles handle.type !== "virtual" || !handles2.some( (h) => ( // skip the handle we're checking against (// and check that their distance isn't below the minimum distance h !== handle && // only check against vertex handles h.type === "vertex" && Vec.Dist(handle, h) < minDistBetweenVirtualHandlesAndRegularHandles) ) )) ) ).sort((a) => a.type === "vertex" ? 1 : -1); }, [editor, zoomLevel, isCoarse, shapeId] ); const isHidden = useValue("isHidden", () => editor.isShapeHidden(shapeId), [editor, shapeId]); if (!Handles || !handles || !transform || isHidden) { return null; } return /* @__PURE__ */ jsx(Handles, { children: /* @__PURE__ */ jsx("g", { transform: Mat.toCssString(transform), children: handles.map((handle) => { return /* @__PURE__ */ jsx( HandleWrapper, { shapeId, handle, zoom: zoomLevel, isCoarse }, handle.id ); }) }) }); } function HandleWrapper({ shapeId, handle, zoom, isCoarse }) { const events = useHandleEvents(shapeId, handle.id); const { Handle } = useEditorComponents(); if (!Handle) return null; return /* @__PURE__ */ jsx( "g", { role: "button", "aria-label": handle.label || "handle", transform: `translate(${handle.x}, ${handle.y})`, ...events, children: /* @__PURE__ */ jsx(Handle, { shapeId, handle, zoom, isCoarse }) } ); } function OverlaysWrapper() { const { Overlays } = useEditorComponents(); if (!Overlays) return null; return /* @__PURE__ */ jsx("div", { className: "tl-custom-overlays tl-overlays__item", children: /* @__PURE__ */ jsx(Overlays, {}) }); } function ShapesWithSVGs() { const editor = useEditor(); const renderingShapes = useValue("rendering shapes", () => editor.getRenderingShapes(), [editor]); const dprMultiple = useValue( "dpr multiple", () => ( // dprMultiple is the smallest number we can multiply dpr by to get an integer // it's usually 1, 2, or 4 (for e.g. dpr of 2, 2.5 and 2.25 respectively) (nearestMultiple(Math.floor(editor.getInstanceState().devicePixelRatio * 100) / 100)) ), [editor] ); return renderingShapes.map((result) => /* @__PURE__ */ jsxs(Fragment2, { children: [ /* @__PURE__ */ jsx(Shape, { ...result, dprMultiple }), /* @__PURE__ */ jsx(DebugSvgCopy, { id: result.id, mode: "iframe" }) ] }, result.id + "_fragment")); } function ReflowIfNeeded() { const editor = useEditor(); const culledShapesRef = useRef(/* @__PURE__ */ new Set()); useQuickReactor( "reflow for culled shapes", () => { const culledShapes = editor.getCulledShapes(); if (culledShapesRef.current.size === culledShapes.size && [...culledShapes].every((id) => culledShapesRef.current.has(id))) return; culledShapesRef.current = culledShapes; const canvas = document.getElementsByClassName("tl-canvas"); if (canvas.length === 0) return; const _height = canvas[0].offsetHeight; }, [editor] ); return null; } function ShapesToDisplay() { const editor = useEditor(); const renderingShapes = useValue("rendering shapes", () => editor.getRenderingShapes(), [editor]); const dprMultiple = useValue( "dpr multiple", () => ( // dprMultiple is the smallest number we can multiply dpr by to get an integer // it's usually 1, 2, or 4 (for e.g. dpr of 2, 2.5 and 2.25 respectively) (nearestMultiple(Math.floor(editor.getInstanceState().devicePixelRatio * 100) / 100)) ), [editor] ); return /* @__PURE__ */ jsxs(Fragment, { children: [ renderingShapes.map((result) => /* @__PURE__ */ jsx(Shape, { ...result, dprMultiple }, result.id + "_shape")), tlenv.isSafari && /* @__PURE__ */ jsx(ReflowIfNeeded, {}) ] }); } function HintedShapeIndicator() { const editor = useEditor(); const { ShapeIndicator } = useEditorComponents(); const ids = useValue("hinting shape ids", () => dedupe(editor.getHintingShapeIds()), [editor]); if (!ids.length) return null; if (!ShapeIndicator) return null; return ids.map((id) => /* @__PURE__ */ jsx(ShapeIndicator, { className: "tl-user-indicator__hint", shapeId: id }, id + "_hinting")); } function CursorDef() { return /* @__PURE__ */ jsxs("g", { id: useSharedSafeId("cursor"), children: [ /* @__PURE__ */ jsxs("g", { fill: "rgba(0,0,0,.2)", transform: "translate(-11,-11)", children: [ /* @__PURE__ */ jsx("path", { d: "m12 24.4219v-16.015l11.591 11.619h-6.781l-.411.124z" }), /* @__PURE__ */ jsx("path", { d: "m21.0845 25.0962-3.605 1.535-4.682-11.089 3.686-1.553z" }) ] }), /* @__PURE__ */ jsxs("g", { fill: "white", transform: "translate(-12,-12)", children: [ /* @__PURE__ */ jsx("path", { d: "m12 24.4219v-16.015l11.591 11.619h-6.781l-.411.124z" }), /* @__PURE__ */ jsx("path", { d: "m21.0845 25.0962-3.605 1.535-4.682-11.089 3.686-1.553z" }) ] }), /* @__PURE__ */ jsxs("g", { fill: "currentColor", transform: "translate(-12,-12)", children: [ /* @__PURE__ */ jsx("path", { d: "m19.751 24.4155-1.844.774-3.1-7.374 1.841-.775z" }), /* @__PURE__ */ jsx("path", { d: "m13 10.814v11.188l2.969-2.866.428-.139h4.768z" }) ] }) ] }); } function CollaboratorHintDef() { const cursorHintId = useSharedSafeId("cursor_hint"); return /* @__PURE__ */ jsx("path", { id: cursorHintId, fill: "currentColor", d: "M -2,-5 2,0 -2,5 Z" }); } function DebugSvgCopy({ id, mode }) { const editor = useEditor(); const [image, setImage] = useState(null); const isInRoot = useValue( "is in root", () => { const shape = editor.getShape(id); return shape?.parentId === editor.getCurrentPageId(); }, [editor, id] ); useEffect(() => { if (!isInRoot) return; let latest = null; const unsubscribe = react("shape to svg", async () => { const renderId = Math.random(); latest = renderId; const isSingleFrame = editor.isShapeOfType(id, "frame"); const padding = isSingleFrame ? 0 : 10; let bounds = editor.getShapePageBounds(id); if (!bounds) return; bounds = bounds.clone().expandBy(padding); const result = await editor.getSvgString([id], { padding }); if (latest !== renderId || !result) return; const svgDataUrl = `data:image/svg+xml;utf8,${encodeURIComponent(result.svg)}`; setImage({ src: svgDataUrl, bounds }); }); return () => { latest = null; unsubscribe(); }; }, [editor, id, isInRoot]); if (!isInRoot || !image) return null; if (mode === "iframe") { return /* @__PURE__ */ jsx( "iframe", { src: image.src, width: image.bounds.width, height: image.bounds.height, referrerPolicy: "no-referrer", style: { position: "absolute", top: 0, left: 0, border: "none", transform: `translate(${image.bounds.x}px, ${image.bounds.maxY + 12}px)`, outline: "1px solid black", maxWidth: "none" } } ); } return /* @__PURE__ */ jsx( "img", { src: image.src, width: image.bounds.width, height: image.bounds.height, referrerPolicy: "no-referrer", style: { position: "absolute", top: 0, left: 0, transform: `translate(${image.bounds.x}px, ${image.bounds.maxY + 12}px)`, outline: "1px solid black", maxWidth: "none" } } ); } function SelectionForegroundWrapper() { const editor = useEditor(); const selectionRotation = useValue( "selection rotation", function getSelectionRotation() { return editor.getSelectionRotation(); }, [editor] ); const selectionBounds = useValue( "selection bounds", () => editor.getSelectionRotatedPageBounds(), [editor] ); const { SelectionForeground } = useEditorComponents(); if (!selectionBounds || !SelectionForeground) return null; return /* @__PURE__ */ jsx(SelectionForeground, { bounds: selectionBounds, rotation: selectionRotation }); } function SelectionBackgroundWrapper() { const editor = useEditor(); const selectionRotation = useValue("selection rotation", () => editor.getSelectionRotation(), [ editor ]); const selectionBounds = useValue( "selection bounds", () => editor.getSelectionRotatedPageBounds(), [editor] ); const { SelectionBackground } = useEditorComponents(); if (!selectionBounds || !SelectionBackground) return null; return /* @__PURE__ */ jsx(SelectionBackground, { bounds: selectionBounds, rotation: selectionRotation }); } function OnTheCanvasWrapper() { const { OnTheCanvas } = useEditorComponents(); if (!OnTheCanvas) return null; return /* @__PURE__ */ jsx(OnTheCanvas, {}); } function MovingCameraHitTestBlocker() { const editor = useEditor(); const cameraState = useValue("camera state", () => editor.getCameraState(), [editor]); return /* @__PURE__ */ jsx( "div", { className: classNames("tl-hit-test-blocker", { "tl-hit-test-blocker__hidden": cameraState === "idle" }) } ); } export { DefaultCanvas }; //# sourceMappingURL=DefaultCanvas.mjs.map