-
Notifications
You must be signed in to change notification settings - Fork 1.6k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
[WEB-2509] feat: fullscreen option for editor images (#5665)
* feat: editor image full screen mode * fix: full screen modal visibility * refactor: memoize calculations * chore: update useEffect dependencies
- Loading branch information
Showing
5 changed files
with
195 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
1 change: 1 addition & 0 deletions
1
packages/editor/src/core/extensions/custom-image/components/index.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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"; |
148 changes: 148 additions & 0 deletions
148
packages/editor/src/core/extensions/custom-image/components/toolbar/full-screen.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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> | ||
</> | ||
); | ||
}; |
1 change: 1 addition & 0 deletions
1
packages/editor/src/core/extensions/custom-image/components/toolbar/index.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
export * from "./root"; |
36 changes: 36 additions & 0 deletions
36
packages/editor/src/core/extensions/custom-image/components/toolbar/root.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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> | ||
</> | ||
); | ||
}; |