UNPKG

tldraw

Version:

A tiny little drawing editor.

482 lines (481 loc) • 17.9 kB
import { Fragment, jsx, jsxs } from "react/jsx-runtime"; import { Box, getCursor, tlenv, toDomPrecision, track, useEditor, useSelectionEvents, useTransform, useValue } from "@tldraw/editor"; import classNames from "classnames"; import { useRef } from "react"; import { useReadonly } from "../ui/hooks/useReadonly.mjs"; import { TldrawCropHandles } from "./TldrawCropHandles.mjs"; const TldrawSelectionForeground = track(function TldrawSelectionForeground2({ bounds, rotation }) { const editor = useEditor(); const rSvg = useRef(null); const isReadonlyMode = useReadonly(); const topEvents = useSelectionEvents("top"); const rightEvents = useSelectionEvents("right"); const bottomEvents = useSelectionEvents("bottom"); const leftEvents = useSelectionEvents("left"); const topLeftEvents = useSelectionEvents("top_left"); const topRightEvents = useSelectionEvents("top_right"); const bottomRightEvents = useSelectionEvents("bottom_right"); const bottomLeftEvents = useSelectionEvents("bottom_left"); const isDefaultCursor = editor.getInstanceState().cursor.type === "default"; const isCoarsePointer = editor.getInstanceState().isCoarsePointer; const onlyShape = editor.getOnlySelectedShape(); const isLockedShape = onlyShape && editor.isShapeOrAncestorLocked(onlyShape); const expandOutlineBy = onlyShape ? editor.getShapeUtil(onlyShape).expandSelectionOutlinePx(onlyShape) : 0; const expandedBounds = expandOutlineBy instanceof Box ? bounds.clone().expand(expandOutlineBy).zeroFix() : bounds.clone().expandBy(expandOutlineBy).zeroFix(); useTransform(rSvg, bounds?.x, bounds?.y, 1, editor.getSelectionRotation(), { x: expandedBounds.x - bounds.x, y: expandedBounds.y - bounds.y }); if (onlyShape && editor.isShapeHidden(onlyShape)) return null; const zoom = editor.getZoomLevel(); const isChangingStyle = editor.getInstanceState().isChangingStyle; const width = expandedBounds.width; const height = expandedBounds.height; const size = 8 / zoom; const isTinyX = width < size * 2; const isTinyY = height < size * 2; const isSmallX = width < size * 4; const isSmallY = height < size * 4; const isSmallCropX = width < size * 5; const isSmallCropY = height < size * 5; const mobileHandleMultiplier = isCoarsePointer ? 1.75 : 1; const targetSize = 6 / zoom * mobileHandleMultiplier; const targetSizeX = (isSmallX ? targetSize / 2 : targetSize) * (mobileHandleMultiplier * 0.75); const targetSizeY = (isSmallY ? targetSize / 2 : targetSize) * (mobileHandleMultiplier * 0.75); const showSelectionBounds = (onlyShape ? !editor.getShapeUtil(onlyShape).hideSelectionBoundsFg(onlyShape) : true) && !isChangingStyle; let shouldDisplayBox = showSelectionBounds && editor.isInAny( "select.idle", "select.brushing", "select.scribble_brushing", "select.pointing_canvas", "select.pointing_selection", "select.pointing_shape", "select.crop.idle", "select.crop.pointing_crop", "select.crop.pointing_crop_handle", "select.pointing_resize_handle" ) || showSelectionBounds && editor.isIn("select.resizing") && onlyShape && editor.isShapeOfType(onlyShape, "text"); if (onlyShape && shouldDisplayBox) { if (tlenv.isFirefox && editor.isShapeOfType(onlyShape, "embed")) { shouldDisplayBox = false; } } const showCropHandles = editor.isInAny( "select.crop.idle", "select.crop.pointing_crop", "select.crop.pointing_crop_handle" ) && !isChangingStyle && !isReadonlyMode; const shouldDisplayControls = editor.isInAny( "select.idle", "select.pointing_selection", "select.pointing_shape", "select.crop.idle" ) && !isChangingStyle && !isReadonlyMode; const showCornerRotateHandles = !isCoarsePointer && !(isTinyX || isTinyY) && (shouldDisplayControls || showCropHandles) && (onlyShape ? !editor.getShapeUtil(onlyShape).hideRotateHandle(onlyShape) : true) && !isLockedShape; const showMobileRotateHandle = isCoarsePointer && (!isSmallX || !isSmallY) && (shouldDisplayControls || showCropHandles) && (onlyShape ? !editor.getShapeUtil(onlyShape).hideRotateHandle(onlyShape) : true) && !isLockedShape; const showResizeHandles = shouldDisplayControls && (onlyShape ? editor.getShapeUtil(onlyShape).canResize(onlyShape) && !editor.getShapeUtil(onlyShape).hideResizeHandles(onlyShape) : true) && !showCropHandles && !isLockedShape; const hideAlternateCornerHandles = isTinyX || isTinyY; const showOnlyOneHandle = isTinyX && isTinyY; const hideAlternateCropHandles = isSmallCropX || isSmallCropY; const showHandles = showResizeHandles || showCropHandles; const hideRotateCornerHandles = !showCornerRotateHandles; const hideMobileRotateHandle = !shouldDisplayControls || !showMobileRotateHandle; const hideTopLeftCorner = !shouldDisplayControls || !showHandles; const hideTopRightCorner = !shouldDisplayControls || !showHandles || hideAlternateCornerHandles; const hideBottomLeftCorner = !shouldDisplayControls || !showHandles || hideAlternateCornerHandles; const hideBottomRightCorner = !shouldDisplayControls || !showHandles || showOnlyOneHandle && !showCropHandles; let hideVerticalEdgeTargets = true; let hideHorizontalEdgeTargets = true; if (showCropHandles) { hideVerticalEdgeTargets = hideAlternateCropHandles; hideHorizontalEdgeTargets = hideAlternateCropHandles; } else if (showResizeHandles) { hideVerticalEdgeTargets = hideAlternateCornerHandles || showOnlyOneHandle || isCoarsePointer; const isMobileAndTextShape = isCoarsePointer && onlyShape && onlyShape.type === "text"; hideHorizontalEdgeTargets = hideVerticalEdgeTargets && !isMobileAndTextShape; } const textHandleHeight = Math.min(24 / zoom, height - targetSizeY * 3); const showTextResizeHandles = shouldDisplayControls && isCoarsePointer && onlyShape && editor.isShapeOfType(onlyShape, "text") && textHandleHeight * zoom >= 4; return /* @__PURE__ */ jsx("svg", { className: "tl-overlays__item tl-selection__fg", "data-testid": "selection-foreground", children: /* @__PURE__ */ jsxs("g", { ref: rSvg, children: [ shouldDisplayBox && /* @__PURE__ */ jsx( "rect", { className: "tl-selection__fg__outline", width: toDomPrecision(width), height: toDomPrecision(height) } ), /* @__PURE__ */ jsx( RotateCornerHandle, { "data-testid": "selection.rotate.top-left", cx: 0, cy: 0, targetSize, corner: "top_left_rotate", cursor: isDefaultCursor ? getCursor("nwse-rotate", rotation) : void 0, isHidden: hideRotateCornerHandles } ), /* @__PURE__ */ jsx( RotateCornerHandle, { "data-testid": "selection.rotate.top-right", cx: width + targetSize * 3, cy: 0, targetSize, corner: "top_right_rotate", cursor: isDefaultCursor ? getCursor("nesw-rotate", rotation) : void 0, isHidden: hideRotateCornerHandles } ), /* @__PURE__ */ jsx( RotateCornerHandle, { "data-testid": "selection.rotate.bottom-left", cx: 0, cy: height + targetSize * 3, targetSize, corner: "bottom_left_rotate", cursor: isDefaultCursor ? getCursor("swne-rotate", rotation) : void 0, isHidden: hideRotateCornerHandles } ), /* @__PURE__ */ jsx( RotateCornerHandle, { "data-testid": "selection.rotate.bottom-right", cx: width + targetSize * 3, cy: height + targetSize * 3, targetSize, corner: "bottom_right_rotate", cursor: isDefaultCursor ? getCursor("senw-rotate", rotation) : void 0, isHidden: hideRotateCornerHandles } ), /* @__PURE__ */ jsx( MobileRotateHandle, { "data-testid": "selection.rotate.mobile", cx: isSmallX ? -targetSize * 1.5 : width / 2, cy: isSmallX ? height / 2 : -targetSize * 1.5, size, isHidden: hideMobileRotateHandle } ), /* @__PURE__ */ jsx( "rect", { className: classNames("tl-transparent", { "tl-hidden": hideVerticalEdgeTargets }), "data-testid": "selection.resize.top", "aria-label": "top target", pointerEvents: "all", x: 0, y: toDomPrecision(0 - (isSmallY ? targetSizeY * 2 : targetSizeY)), width: toDomPrecision(width), height: toDomPrecision(Math.max(1, targetSizeY * 2)), style: isDefaultCursor ? { cursor: getCursor("ns-resize", rotation) } : void 0, ...topEvents } ), /* @__PURE__ */ jsx( "rect", { className: classNames("tl-transparent", { "tl-hidden": hideHorizontalEdgeTargets }), "data-testid": "selection.resize.right", "aria-label": "right target", pointerEvents: "all", x: toDomPrecision(width - (isSmallX ? 0 : targetSizeX)), y: 0, height: toDomPrecision(height), width: toDomPrecision(Math.max(1, targetSizeX * 2)), style: isDefaultCursor ? { cursor: getCursor("ew-resize", rotation) } : void 0, ...rightEvents } ), /* @__PURE__ */ jsx( "rect", { className: classNames("tl-transparent", { "tl-hidden": hideVerticalEdgeTargets }), "data-testid": "selection.resize.bottom", "aria-label": "bottom target", pointerEvents: "all", x: 0, y: toDomPrecision(height - (isSmallY ? 0 : targetSizeY)), width: toDomPrecision(width), height: toDomPrecision(Math.max(1, targetSizeY * 2)), style: isDefaultCursor ? { cursor: getCursor("ns-resize", rotation) } : void 0, ...bottomEvents } ), /* @__PURE__ */ jsx( "rect", { className: classNames("tl-transparent", { "tl-hidden": hideHorizontalEdgeTargets }), "data-testid": "selection.resize.left", "aria-label": "left target", pointerEvents: "all", x: toDomPrecision(0 - (isSmallX ? targetSizeX * 2 : targetSizeX)), y: 0, height: toDomPrecision(height), width: toDomPrecision(Math.max(1, targetSizeX * 2)), style: isDefaultCursor ? { cursor: getCursor("ew-resize", rotation) } : void 0, ...leftEvents } ), /* @__PURE__ */ jsx( "rect", { className: classNames("tl-transparent", { "tl-hidden": hideTopLeftCorner }), "data-testid": "selection.target.top-left", "aria-label": "top-left target", pointerEvents: "all", x: toDomPrecision(0 - (isSmallX ? targetSizeX * 2 : targetSizeX * 1.5)), y: toDomPrecision(0 - (isSmallY ? targetSizeY * 2 : targetSizeY * 1.5)), width: toDomPrecision(targetSizeX * 3), height: toDomPrecision(targetSizeY * 3), style: isDefaultCursor ? { cursor: getCursor("nwse-resize", rotation) } : void 0, ...topLeftEvents } ), /* @__PURE__ */ jsx( "rect", { className: classNames("tl-transparent", { "tl-hidden": hideTopRightCorner }), "data-testid": "selection.target.top-right", "aria-label": "top-right target", pointerEvents: "all", x: toDomPrecision(width - (isSmallX ? 0 : targetSizeX * 1.5)), y: toDomPrecision(0 - (isSmallY ? targetSizeY * 2 : targetSizeY * 1.5)), width: toDomPrecision(targetSizeX * 3), height: toDomPrecision(targetSizeY * 3), style: isDefaultCursor ? { cursor: getCursor("nesw-resize", rotation) } : void 0, ...topRightEvents } ), /* @__PURE__ */ jsx( "rect", { className: classNames("tl-transparent", { "tl-hidden": hideBottomRightCorner }), "data-testid": "selection.target.bottom-right", "aria-label": "bottom-right target", pointerEvents: "all", x: toDomPrecision(width - (isSmallX ? targetSizeX : targetSizeX * 1.5)), y: toDomPrecision(height - (isSmallY ? targetSizeY : targetSizeY * 1.5)), width: toDomPrecision(targetSizeX * 3), height: toDomPrecision(targetSizeY * 3), style: isDefaultCursor ? { cursor: getCursor("nwse-resize", rotation) } : void 0, ...bottomRightEvents } ), /* @__PURE__ */ jsx( "rect", { className: classNames("tl-transparent", { "tl-hidden": hideBottomLeftCorner }), "data-testid": "selection.target.bottom-left", "aria-label": "bottom-left target", pointerEvents: "all", x: toDomPrecision(0 - (isSmallX ? targetSizeX * 3 : targetSizeX * 1.5)), y: toDomPrecision(height - (isSmallY ? 0 : targetSizeY * 1.5)), width: toDomPrecision(targetSizeX * 3), height: toDomPrecision(targetSizeY * 3), style: isDefaultCursor ? { cursor: getCursor("nesw-resize", rotation) } : void 0, ...bottomLeftEvents } ), showResizeHandles && /* @__PURE__ */ jsxs(Fragment, { children: [ /* @__PURE__ */ jsx( "rect", { "data-testid": "selection.resize.top-left", className: classNames("tl-corner-handle", { "tl-hidden": hideTopLeftCorner }), "aria-label": "top_left handle", x: toDomPrecision(0 - size / 2), y: toDomPrecision(0 - size / 2), width: toDomPrecision(size), height: toDomPrecision(size) } ), /* @__PURE__ */ jsx( "rect", { "data-testid": "selection.resize.top-right", className: classNames("tl-corner-handle", { "tl-hidden": hideTopRightCorner }), "aria-label": "top_right handle", x: toDomPrecision(width - size / 2), y: toDomPrecision(0 - size / 2), width: toDomPrecision(size), height: toDomPrecision(size) } ), /* @__PURE__ */ jsx( "rect", { "data-testid": "selection.resize.bottom-right", className: classNames("tl-corner-handle", { "tl-hidden": hideBottomRightCorner }), "aria-label": "bottom_right handle", x: toDomPrecision(width - size / 2), y: toDomPrecision(height - size / 2), width: toDomPrecision(size), height: toDomPrecision(size) } ), /* @__PURE__ */ jsx( "rect", { "data-testid": "selection.resize.bottom-left", className: classNames("tl-corner-handle", { "tl-hidden": hideBottomLeftCorner }), "aria-label": "bottom_left handle", x: toDomPrecision(0 - size / 2), y: toDomPrecision(height - size / 2), width: toDomPrecision(size), height: toDomPrecision(size) } ) ] }), showTextResizeHandles && /* @__PURE__ */ jsxs(Fragment, { children: [ /* @__PURE__ */ jsx( "rect", { "data-testid": "selection.text-resize.left.handle", className: "tl-text-handle", "aria-label": "bottom_left handle", x: toDomPrecision(0 - size / 4), y: toDomPrecision(height / 2 - textHandleHeight / 2), rx: size / 4, width: toDomPrecision(size / 2), height: toDomPrecision(textHandleHeight) } ), /* @__PURE__ */ jsx( "rect", { "data-testid": "selection.text-resize.right.handle", className: "tl-text-handle", "aria-label": "bottom_left handle", rx: size / 4, x: toDomPrecision(width - size / 4), y: toDomPrecision(height / 2 - textHandleHeight / 2), width: toDomPrecision(size / 2), height: toDomPrecision(textHandleHeight) } ) ] }), showCropHandles && /* @__PURE__ */ jsx( TldrawCropHandles, { ...{ size, width, height, hideAlternateHandles: hideAlternateCropHandles } } ) ] }) }); }); const RotateCornerHandle = function RotateCornerHandle2({ cx, cy, targetSize, corner, cursor, isHidden, "data-testid": testId }) { const events = useSelectionEvents(corner); return /* @__PURE__ */ jsx( "rect", { className: classNames("tl-transparent", "tl-rotate-corner", { "tl-hidden": isHidden }), "data-testid": testId, "aria-label": `${corner} target`, pointerEvents: "all", x: toDomPrecision(cx - targetSize * 3), y: toDomPrecision(cy - targetSize * 3), width: toDomPrecision(Math.max(1, targetSize * 3)), height: toDomPrecision(Math.max(1, targetSize * 3)), cursor, ...events } ); }; const SQUARE_ROOT_PI = Math.sqrt(Math.PI); const MobileRotateHandle = function RotateHandle({ cx, cy, size, isHidden, "data-testid": testId }) { const events = useSelectionEvents("mobile_rotate"); const editor = useEditor(); const zoom = useValue("zoom level", () => editor.getZoomLevel(), [editor]); const bgRadius = Math.max(14 * (1 / zoom), 20 / Math.max(1, zoom)); return /* @__PURE__ */ jsxs("g", { children: [ /* @__PURE__ */ jsx( "circle", { "data-testid": testId, pointerEvents: "all", className: classNames("tl-transparent", "tl-mobile-rotate__bg", { "tl-hidden": isHidden }), cx, cy, r: bgRadius, ...events } ), /* @__PURE__ */ jsx( "circle", { className: classNames("tl-mobile-rotate__fg", { "tl-hidden": isHidden }), cx, cy, r: size / SQUARE_ROOT_PI } ) ] }); }; export { MobileRotateHandle, RotateCornerHandle, TldrawSelectionForeground }; //# sourceMappingURL=TldrawSelectionForeground.mjs.map