@tldraw/editor
Version:
tldraw infinite canvas SDK (editor).
487 lines (486 loc) • 20.2 kB
JavaScript
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