UNPKG

tldraw

Version:

A tiny little drawing editor.

285 lines (284 loc) • 9.69 kB
import { Fragment, jsx, jsxs } from "react/jsx-runtime"; import { approximately, isEqual, kickoutOccludedShapes, modulate, track, useEditor, useValue } from "@tldraw/editor"; import { useCallback, useEffect, useRef, useState } from "react"; import { ASPECT_RATIO_OPTIONS, ASPECT_RATIO_TO_VALUE, getCroppedImageDataForAspectRatio, getCroppedImageDataWhenZooming, getDefaultCrop, MAX_ZOOM } from "../../../shapes/shared/crop.mjs"; import { useActions } from "../../context/actions.mjs"; import { useUiEvents } from "../../context/events.mjs"; import { useTranslation } from "../../hooks/useTranslation/useTranslation.mjs"; import { TldrawUiButtonIcon } from "../primitives/Button/TldrawUiButtonIcon.mjs"; import { TldrawUiButtonLabel } from "../primitives/Button/TldrawUiButtonLabel.mjs"; import { TldrawUiDropdownMenuCheckboxItem, TldrawUiDropdownMenuContent, TldrawUiDropdownMenuRoot, TldrawUiDropdownMenuTrigger } from "../primitives/TldrawUiDropdownMenu.mjs"; import { TldrawUiSlider } from "../primitives/TldrawUiSlider.mjs"; import { TldrawUiToolbarButton } from "../primitives/TldrawUiToolbar.mjs"; const DefaultImageToolbarContent = track(function DefaultImageToolbarContent2({ imageShapeId, isManipulating, onEditAltTextStart, onManipulatingStart, onManipulatingEnd }) { const editor = useEditor(); const trackEvent = useUiEvents(); const msg = useTranslation(); const source = "image-toolbar"; const sliderRef = useRef(null); const isReadonly = editor.getIsReadonly(); const crop = useValue("crop", () => editor.getShape(imageShapeId).props.crop, [ editor, imageShapeId ]); const zoom = crop ? Math.min(1 - (crop.bottomRight.x - crop.topLeft.x), 1 - (crop.bottomRight.y - crop.topLeft.y)) : 0; const [maxZoom, setMaxZoom] = useState( crop ? Math.max(zoom, 1 - 1 / MAX_ZOOM) : MAX_ZOOM ); const actions = useActions(); useEffect(() => { setMaxZoom(crop ? Math.max(zoom, 1 - 1 / MAX_ZOOM) : MAX_ZOOM); }, [crop, zoom, maxZoom]); const onHistoryMark = useCallback((id) => editor.markHistoryStoppingPoint(id), [editor]); const easeZoom = useCallback((value, maxValue) => { const maxRatioConversion = MAX_ZOOM / (MAX_ZOOM - 1); return Math.pow(value / maxValue, maxRatioConversion) * maxValue; }, []); const displayValue = crop && maxZoom ? modulate( easeZoom(zoom, maxZoom), [0, maxZoom], [0, 100], true /* clamp */ ) : 0; const handleZoomChange = useCallback( (value) => { editor.setCurrentTool("select.crop.idle"); const sliderPercent = value / 100; const maxDimension = 1 - 1 / MAX_ZOOM; const clampedMaxZoom = Math.min(maxDimension, maxZoom ?? maxDimension); const maxRatioConversion = MAX_ZOOM / (MAX_ZOOM - 1); const zOut = Math.pow(sliderPercent, 1 / maxRatioConversion) * clampedMaxZoom; const zoom2 = zOut >= 1 ? 1 : zOut / (2 * (1 - zOut)); const imageShape = editor.getShape(imageShapeId); if (!imageShape) return; const change = getCroppedImageDataWhenZooming(zoom2, imageShape, maxZoom); editor.updateShape({ id: imageShape.id, type: imageShape.type, x: change.x, y: change.y, props: { w: change.w, h: change.h, crop: change.crop } }); trackEvent("set-style", { source: "image-toolbar", id: "zoom", value }); }, [editor, trackEvent, imageShapeId, maxZoom] ); const handleImageReplace = useCallback( () => actions["image-replace"].onSelect("image-toolbar"), [actions] ); const handleImageDownload = useCallback( () => actions["download-original"].onSelect("image-toolbar"), [actions] ); const handleAspectRatioChange = (aspectRatio) => { const imageShape = editor.getShape(imageShapeId); if (!imageShape) return; editor.run(() => { editor.setCurrentTool("select.crop.idle"); const change = getCroppedImageDataForAspectRatio(aspectRatio, imageShape); editor.markHistoryStoppingPoint("aspect ratio"); editor.updateShape({ id: imageShapeId, type: "image", x: change.x, y: change.y, props: { crop: change.crop, w: change.w, h: change.h } }); kickoutOccludedShapes(editor, [imageShapeId]); }); }; const altText = useValue( "altText", () => editor.getShape(imageShapeId).props.altText, [editor, imageShapeId] ); const shapeAspectRatio = useValue( "shapeAspectRatio", () => { const imageShape = editor.getShape(imageShapeId); return imageShape.props.w / imageShape.props.h; }, [editor, imageShapeId] ); const isOriginalCrop = !crop || isEqual(crop, getDefaultCrop()); useEffect(() => { if (isManipulating) { editor.timers.setTimeout(() => sliderRef.current?.focus(), 0); } }, [editor, isManipulating]); useEffect(() => { function handleKeyDown(e) { if (isManipulating) { if (e.key === "Escape") { editor.cancel(); onManipulatingEnd(); } else if (e.key === "Enter") { editor.complete(); onManipulatingEnd(); } } } const elm = sliderRef.current; if (elm) { elm.addEventListener("keydown", handleKeyDown); } return () => { if (elm) { elm.removeEventListener("keydown", handleKeyDown); } }; }, [editor, isManipulating, onManipulatingEnd]); if (isManipulating) { return /* @__PURE__ */ jsxs(Fragment, { children: [ /* @__PURE__ */ jsx( TldrawUiSlider, { ref: sliderRef, value: displayValue, label: "tool.image-zoom", onValueChange: handleZoomChange, onHistoryMark, min: 0, steps: 100, "data-testid": "tool.image-zoom", title: msg("tool.image-zoom") } ), /* @__PURE__ */ jsxs(TldrawUiDropdownMenuRoot, { id: "image-toolbar-aspect-ratio", children: [ /* @__PURE__ */ jsx(TldrawUiDropdownMenuTrigger, { children: /* @__PURE__ */ jsx( TldrawUiToolbarButton, { title: msg("tool.aspect-ratio"), type: "icon", "data-testid": "tool.image-aspect-ratio", children: /* @__PURE__ */ jsx(TldrawUiButtonIcon, { icon: "corners" }) } ) }), /* @__PURE__ */ jsx(TldrawUiDropdownMenuContent, { side: "top", align: "center", children: ASPECT_RATIO_OPTIONS.map((aspectRatio) => { let checked = false; if (isOriginalCrop) { if (aspectRatio === "original") { checked = true; } } else { if (aspectRatio === "circle") { checked = !!crop.isCircle; } else if (aspectRatio === "square") { checked = !crop?.isCircle && approximately(shapeAspectRatio, ASPECT_RATIO_TO_VALUE[aspectRatio], 0.1); } else if (aspectRatio === "original") { checked = false; } else { checked = !isOriginalCrop && approximately(shapeAspectRatio, ASPECT_RATIO_TO_VALUE[aspectRatio], 0.01); } } return /* @__PURE__ */ jsx( TldrawUiDropdownMenuCheckboxItem, { onSelect: () => handleAspectRatioChange(aspectRatio), checked, title: msg(`tool.aspect-ratio.${aspectRatio}`), children: /* @__PURE__ */ jsx(TldrawUiButtonLabel, { children: msg(`tool.aspect-ratio.${aspectRatio}`) }) }, aspectRatio ); }) }) ] }), /* @__PURE__ */ jsx( TldrawUiToolbarButton, { type: "icon", onClick: onManipulatingEnd, "data-testid": "tool.image-crop-confirm", style: { borderLeft: "1px solid var(--tl-color-divider)", marginLeft: "2px" }, title: msg("tool.image-crop-confirm"), children: /* @__PURE__ */ jsx(TldrawUiButtonIcon, { small: true, icon: "check" }) } ) ] }); } return /* @__PURE__ */ jsxs(Fragment, { children: [ !isReadonly && /* @__PURE__ */ jsx( TldrawUiToolbarButton, { type: "icon", "data-testid": "tool.image-replace", onClick: handleImageReplace, title: msg("tool.replace-media"), children: /* @__PURE__ */ jsx(TldrawUiButtonIcon, { small: true, icon: "tool-media" }) } ), !isReadonly && /* @__PURE__ */ jsx( TldrawUiToolbarButton, { type: "icon", title: msg("tool.image-crop"), onClick: onManipulatingStart, "data-testid": "tool.image-crop", children: /* @__PURE__ */ jsx(TldrawUiButtonIcon, { small: true, icon: "crop" }) } ), /* @__PURE__ */ jsx( TldrawUiToolbarButton, { type: "icon", title: msg("action.download-original"), onClick: handleImageDownload, "data-testid": "tool.image-download", children: /* @__PURE__ */ jsx(TldrawUiButtonIcon, { small: true, icon: "download" }) } ), (altText || !isReadonly) && /* @__PURE__ */ jsx( TldrawUiToolbarButton, { type: "icon", title: msg("tool.media-alt-text"), "data-testid": "tool.image-alt-text", onClick: () => { trackEvent("alt-text-start", { source }); onEditAltTextStart(); }, children: /* @__PURE__ */ jsx(TldrawUiButtonIcon, { small: true, icon: "alt" }) } ) ] }); }); export { DefaultImageToolbarContent }; //# sourceMappingURL=DefaultImageToolbarContent.mjs.map