tldraw
Version:
A tiny little drawing editor.
285 lines (284 loc) • 11.9 kB
JavaScript
;
var __defProp = Object.defineProperty;
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
var __getOwnPropNames = Object.getOwnPropertyNames;
var __hasOwnProp = Object.prototype.hasOwnProperty;
var __export = (target, all) => {
for (var name in all)
__defProp(target, name, { get: all[name], enumerable: true });
};
var __copyProps = (to, from, except, desc) => {
if (from && typeof from === "object" || typeof from === "function") {
for (let key of __getOwnPropNames(from))
if (!__hasOwnProp.call(to, key) && key !== except)
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
}
return to;
};
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
var DefaultImageToolbarContent_exports = {};
__export(DefaultImageToolbarContent_exports, {
DefaultImageToolbarContent: () => DefaultImageToolbarContent
});
module.exports = __toCommonJS(DefaultImageToolbarContent_exports);
var import_jsx_runtime = require("react/jsx-runtime");
var import_editor = require("@tldraw/editor");
var import_react = require("react");
var import_crop = require("../../../shapes/shared/crop");
var import_actions = require("../../context/actions");
var import_events = require("../../context/events");
var import_useTranslation = require("../../hooks/useTranslation/useTranslation");
var import_TldrawUiButtonIcon = require("../primitives/Button/TldrawUiButtonIcon");
var import_TldrawUiButtonLabel = require("../primitives/Button/TldrawUiButtonLabel");
var import_TldrawUiDropdownMenu = require("../primitives/TldrawUiDropdownMenu");
var import_TldrawUiSlider = require("../primitives/TldrawUiSlider");
var import_TldrawUiToolbar = require("../primitives/TldrawUiToolbar");
const DefaultImageToolbarContent = (0, import_editor.track)(function DefaultImageToolbarContent2({
imageShapeId,
isManipulating,
onEditAltTextStart,
onManipulatingStart,
onManipulatingEnd
}) {
const editor = (0, import_editor.useEditor)();
const trackEvent = (0, import_events.useUiEvents)();
const msg = (0, import_useTranslation.useTranslation)();
const source = "image-toolbar";
const sliderRef = (0, import_react.useRef)(null);
const isReadonly = editor.getIsReadonly();
const crop = (0, import_editor.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] = (0, import_react.useState)(
crop ? Math.max(zoom, 1 - 1 / import_crop.MAX_ZOOM) : import_crop.MAX_ZOOM
);
const actions = (0, import_actions.useActions)();
(0, import_react.useEffect)(() => {
setMaxZoom(crop ? Math.max(zoom, 1 - 1 / import_crop.MAX_ZOOM) : import_crop.MAX_ZOOM);
}, [crop, zoom, maxZoom]);
const onHistoryMark = (0, import_react.useCallback)((id) => editor.markHistoryStoppingPoint(id), [editor]);
const easeZoom = (0, import_react.useCallback)((value, maxValue) => {
const maxRatioConversion = import_crop.MAX_ZOOM / (import_crop.MAX_ZOOM - 1);
return Math.pow(value / maxValue, maxRatioConversion) * maxValue;
}, []);
const displayValue = crop && maxZoom ? (0, import_editor.modulate)(
easeZoom(zoom, maxZoom),
[0, maxZoom],
[0, 100],
true
/* clamp */
) : 0;
const handleZoomChange = (0, import_react.useCallback)(
(value) => {
editor.setCurrentTool("select.crop.idle");
const sliderPercent = value / 100;
const maxDimension = 1 - 1 / import_crop.MAX_ZOOM;
const clampedMaxZoom = Math.min(maxDimension, maxZoom ?? maxDimension);
const maxRatioConversion = import_crop.MAX_ZOOM / (import_crop.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 = (0, import_crop.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 = (0, import_react.useCallback)(
() => actions["image-replace"].onSelect("image-toolbar"),
[actions]
);
const handleImageDownload = (0, import_react.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 = (0, import_crop.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
}
});
(0, import_editor.kickoutOccludedShapes)(editor, [imageShapeId]);
});
};
const altText = (0, import_editor.useValue)(
"altText",
() => editor.getShape(imageShapeId).props.altText,
[editor, imageShapeId]
);
const shapeAspectRatio = (0, import_editor.useValue)(
"shapeAspectRatio",
() => {
const imageShape = editor.getShape(imageShapeId);
return imageShape.props.w / imageShape.props.h;
},
[editor, imageShapeId]
);
const isOriginalCrop = !crop || (0, import_editor.isEqual)(crop, (0, import_crop.getDefaultCrop)());
(0, import_react.useEffect)(() => {
if (isManipulating) {
editor.timers.setTimeout(() => sliderRef.current?.focus(), 0);
}
}, [editor, isManipulating]);
(0, import_react.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__ */ (0, import_jsx_runtime.jsxs)(import_jsx_runtime.Fragment, { children: [
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(
import_TldrawUiSlider.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__ */ (0, import_jsx_runtime.jsxs)(import_TldrawUiDropdownMenu.TldrawUiDropdownMenuRoot, { id: "image-toolbar-aspect-ratio", children: [
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_TldrawUiDropdownMenu.TldrawUiDropdownMenuTrigger, { children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
import_TldrawUiToolbar.TldrawUiToolbarButton,
{
title: msg("tool.aspect-ratio"),
type: "icon",
"data-testid": "tool.image-aspect-ratio",
children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_TldrawUiButtonIcon.TldrawUiButtonIcon, { icon: "corners" })
}
) }),
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_TldrawUiDropdownMenu.TldrawUiDropdownMenuContent, { side: "top", align: "center", children: import_crop.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 && (0, import_editor.approximately)(shapeAspectRatio, import_crop.ASPECT_RATIO_TO_VALUE[aspectRatio], 0.1);
} else if (aspectRatio === "original") {
checked = false;
} else {
checked = !isOriginalCrop && (0, import_editor.approximately)(shapeAspectRatio, import_crop.ASPECT_RATIO_TO_VALUE[aspectRatio], 0.01);
}
}
return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
import_TldrawUiDropdownMenu.TldrawUiDropdownMenuCheckboxItem,
{
onSelect: () => handleAspectRatioChange(aspectRatio),
checked,
title: msg(`tool.aspect-ratio.${aspectRatio}`),
children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_TldrawUiButtonLabel.TldrawUiButtonLabel, { children: msg(`tool.aspect-ratio.${aspectRatio}`) })
},
aspectRatio
);
}) })
] }),
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(
import_TldrawUiToolbar.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__ */ (0, import_jsx_runtime.jsx)(import_TldrawUiButtonIcon.TldrawUiButtonIcon, { small: true, icon: "check" })
}
)
] });
}
return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_jsx_runtime.Fragment, { children: [
!isReadonly && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
import_TldrawUiToolbar.TldrawUiToolbarButton,
{
type: "icon",
"data-testid": "tool.image-replace",
onClick: handleImageReplace,
title: msg("tool.replace-media"),
children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_TldrawUiButtonIcon.TldrawUiButtonIcon, { small: true, icon: "tool-media" })
}
),
!isReadonly && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
import_TldrawUiToolbar.TldrawUiToolbarButton,
{
type: "icon",
title: msg("tool.image-crop"),
onClick: onManipulatingStart,
"data-testid": "tool.image-crop",
children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_TldrawUiButtonIcon.TldrawUiButtonIcon, { small: true, icon: "crop" })
}
),
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(
import_TldrawUiToolbar.TldrawUiToolbarButton,
{
type: "icon",
title: msg("action.download-original"),
onClick: handleImageDownload,
"data-testid": "tool.image-download",
children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_TldrawUiButtonIcon.TldrawUiButtonIcon, { small: true, icon: "download" })
}
),
(altText || !isReadonly) && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
import_TldrawUiToolbar.TldrawUiToolbarButton,
{
type: "icon",
title: msg("tool.media-alt-text"),
"data-testid": "tool.image-alt-text",
onClick: () => {
trackEvent("alt-text-start", { source });
onEditAltTextStart();
},
children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_TldrawUiButtonIcon.TldrawUiButtonIcon, { small: true, icon: "alt" })
}
)
] });
});
//# sourceMappingURL=DefaultImageToolbarContent.js.map