Skip to content

Commit

Permalink
[WEB-2509] feat: fullscreen option for editor images (#5665)
Browse files Browse the repository at this point in the history
* feat: editor image full screen mode

* fix: full screen modal visibility

* refactor: memoize calculations

* chore: update useEffect dependencies
  • Loading branch information
aaryan610 authored Sep 23, 2024
1 parent 3c1779b commit 83b8332
Show file tree
Hide file tree
Showing 5 changed files with 195 additions and 1 deletion.
Original file line number Diff line number Diff line change
@@ -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";

Expand Down Expand Up @@ -154,6 +154,14 @@ export const CustomImageBlock: React.FC<CustomImageNodeViewProps> = (props) => {
height: size.height,
}}
/>
<ImageToolbarRoot
containerClassName="absolute top-1 right-1 z-20 bg-black/40 rounded opacity-0 pointer-events-none group-hover/image-component:opacity-100 group-hover/image-component:pointer-events-auto transition-opacity"
image={{
src,
height,
width,
}}
/>
{editor.isEditable && selected && <div className="absolute inset-0 size-full bg-custom-primary-500/30" />}
{editor.isEditable && (
<>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export * from "./toolbar";
export * from "./image-block";
export * from "./image-node";
export * from "./image-uploader";
Original file line number Diff line number Diff line change
@@ -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> = (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 (
<>
<div
className={cn(
"fixed inset-0 size-full z-20 bg-black/90 opacity-0 pointer-events-none cursor-default transition-opacity",
{
"opacity-100 pointer-events-auto": isFullScreenEnabled,
}
)}
>
<div className="relative size-full grid place-items-center">
<button
type="button"
onClick={handleClose}
className="absolute top-10 right-10 size-8 grid place-items-center"
>
<X className="size-8 text-white/60 hover:text-white transition-colors" />
</button>
<img
src={src}
className="read-only-image rounded-lg transition-all duration-200"
style={{
width: `${widthInNumber * magnification}px`,
aspectRatio,
}}
/>
</div>
<div className="fixed bottom-10 left-1/2 -translate-x-1/2 flex items-center justify-center gap-1 rounded-md border border-white/20 py-2 divide-x divide-white/20">
<div className="flex items-center">
<button
type="button"
onClick={handleDecreaseMagnification}
className="size-6 grid place-items-center text-white/60 hover:text-white disabled:text-white/30 transition-colors duration-200"
disabled={magnification === MAGNIFICATION_VALUES[0]}
>
<Minus className="size-4" />
</button>
<span className="text-sm w-12 text-center text-white">{(100 * magnification).toFixed(0)}%</span>
<button
type="button"
onClick={handleIncreaseMagnification}
className="size-6 grid place-items-center text-white/60 hover:text-white disabled:text-white/30 transition-colors duration-200"
disabled={magnification === MAGNIFICATION_VALUES[MAGNIFICATION_VALUES.length - 1]}
>
<Plus className="size-4" />
</button>
</div>
<button
type="button"
onClick={handleOpenInNewTab}
className="flex-shrink-0 size-8 grid place-items-center text-white/60 hover:text-white transition-colors duration-200"
>
<ExternalLink className="size-4" />
</button>
</div>
</div>
<button
type="button"
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
toggleFullScreenMode(true);
}}
className="size-5 grid place-items-center hover:bg-black/40 text-white rounded transition-colors"
>
<Maximize className="size-3" />
</button>
</>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from "./root";
Original file line number Diff line number Diff line change
@@ -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> = (props) => {
const { containerClassName, image } = props;
// state
const [isFullScreenEnabled, setIsFullScreenEnabled] = useState(false);

return (
<>
<div
className={cn(containerClassName, {
"opacity-100 pointer-events-auto": isFullScreenEnabled,
})}
>
<ImageFullScreenAction
image={image}
isOpen={isFullScreenEnabled}
toggleFullScreenMode={(val) => setIsFullScreenEnabled(val)}
/>
</div>
</>
);
};

0 comments on commit 83b8332

Please sign in to comment.