diff --git a/packages/editor/src/core/extensions/custom-image/components/image-block.tsx b/packages/editor/src/core/extensions/custom-image/components/image-block.tsx index c951c3bf2cb..89ace94a57f 100644 --- a/packages/editor/src/core/extensions/custom-image/components/image-block.tsx +++ b/packages/editor/src/core/extensions/custom-image/components/image-block.tsx @@ -1,7 +1,7 @@ import React, { useRef, useState, useCallback, useLayoutEffect, useEffect } from "react"; import { NodeSelection } from "@tiptap/pm/state"; // extensions -import { CustomImageNodeViewProps } from "@/extensions/custom-image"; +import { CustomImageNodeViewProps, ImageToolbarRoot } from "@/extensions/custom-image"; // helpers import { cn } from "@/helpers/common"; @@ -154,6 +154,14 @@ export const CustomImageBlock: React.FC = (props) => { height: size.height, }} /> + {editor.isEditable && selected &&
} {editor.isEditable && ( <> diff --git a/packages/editor/src/core/extensions/custom-image/components/index.ts b/packages/editor/src/core/extensions/custom-image/components/index.ts index d16be13c869..9d12c3ecf19 100644 --- a/packages/editor/src/core/extensions/custom-image/components/index.ts +++ b/packages/editor/src/core/extensions/custom-image/components/index.ts @@ -1,3 +1,4 @@ +export * from "./toolbar"; export * from "./image-block"; export * from "./image-node"; export * from "./image-uploader"; diff --git a/packages/editor/src/core/extensions/custom-image/components/toolbar/full-screen.tsx b/packages/editor/src/core/extensions/custom-image/components/toolbar/full-screen.tsx new file mode 100644 index 00000000000..9333fb21c58 --- /dev/null +++ b/packages/editor/src/core/extensions/custom-image/components/toolbar/full-screen.tsx @@ -0,0 +1,148 @@ +import { useCallback, useEffect, useMemo, useState } from "react"; +import { ExternalLink, Maximize, Minus, Plus, X } from "lucide-react"; +// helpers +import { cn } from "@/helpers/common"; + +type Props = { + image: { + src: string; + height: string; + width: string; + }; + isOpen: boolean; + toggleFullScreenMode: (val: boolean) => void; +}; + +const MAGNIFICATION_VALUES = [0.5, 0.75, 1, 1.5, 1.75, 2]; + +export const ImageFullScreenAction: React.FC = (props) => { + const { image, isOpen: isFullScreenEnabled, toggleFullScreenMode } = props; + const { height, src, width } = image; + // states + const [magnification, setMagnification] = useState(1); + // derived values + const widthInNumber = useMemo(() => Number(width.replace("px", "")), [width]); + const heightInNumber = useMemo(() => Number(height.replace("px", "")), [height]); + const aspectRatio = useMemo(() => widthInNumber / heightInNumber, [heightInNumber, widthInNumber]); + // close handler + const handleClose = useCallback(() => { + toggleFullScreenMode(false); + setTimeout(() => { + setMagnification(1); + }, 200); + }, [toggleFullScreenMode]); + // download handler + const handleOpenInNewTab = () => { + const link = document.createElement("a"); + link.href = src; + link.target = "_blank"; + document.body.appendChild(link); + link.click(); + document.body.removeChild(link); + }; + // magnification decrease handler + const handleDecreaseMagnification = useCallback(() => { + const currentIndex = MAGNIFICATION_VALUES.indexOf(magnification); + if (currentIndex === 0) return; + setMagnification(MAGNIFICATION_VALUES[currentIndex - 1]); + }, [magnification]); + // magnification increase handler + const handleIncreaseMagnification = useCallback(() => { + const currentIndex = MAGNIFICATION_VALUES.indexOf(magnification); + if (currentIndex === MAGNIFICATION_VALUES.length - 1) return; + setMagnification(MAGNIFICATION_VALUES[currentIndex + 1]); + }, [magnification]); + // keydown handler + const handleKeyDown = useCallback( + (e: KeyboardEvent) => { + e.preventDefault(); + e.stopPropagation(); + if (e.key === "Escape") handleClose(); + if (e.key === "+" || e.key === "=") handleIncreaseMagnification(); + if (e.key === "-") handleDecreaseMagnification(); + }, + [handleClose, handleDecreaseMagnification, handleIncreaseMagnification] + ); + // register keydown listener + useEffect(() => { + document.addEventListener("keydown", handleKeyDown); + + if (!isFullScreenEnabled) { + document.removeEventListener("keydown", handleKeyDown); + } + + return () => { + document.removeEventListener("keydown", handleKeyDown); + }; + }, [handleKeyDown, isFullScreenEnabled]); + + return ( + <> +
+
+ + +
+
+
+ + {(100 * magnification).toFixed(0)}% + +
+ +
+
+ + + ); +}; diff --git a/packages/editor/src/core/extensions/custom-image/components/toolbar/index.ts b/packages/editor/src/core/extensions/custom-image/components/toolbar/index.ts new file mode 100644 index 00000000000..1efe34c51ec --- /dev/null +++ b/packages/editor/src/core/extensions/custom-image/components/toolbar/index.ts @@ -0,0 +1 @@ +export * from "./root"; diff --git a/packages/editor/src/core/extensions/custom-image/components/toolbar/root.tsx b/packages/editor/src/core/extensions/custom-image/components/toolbar/root.tsx new file mode 100644 index 00000000000..3cfc844be59 --- /dev/null +++ b/packages/editor/src/core/extensions/custom-image/components/toolbar/root.tsx @@ -0,0 +1,36 @@ +import { useState } from "react"; +// helpers +import { cn } from "@/helpers/common"; +// components +import { ImageFullScreenAction } from "./full-screen"; + +type Props = { + containerClassName?: string; + image: { + src: string; + height: string; + width: string; + }; +}; + +export const ImageToolbarRoot: React.FC = (props) => { + const { containerClassName, image } = props; + // state + const [isFullScreenEnabled, setIsFullScreenEnabled] = useState(false); + + return ( + <> +
+ setIsFullScreenEnabled(val)} + /> +
+ + ); +};