diff --git a/src/components/EditorContent/ImagePreview.jsx b/src/components/EditorContent/ImagePreview.jsx new file mode 100644 index 00000000..ec2bfc0b --- /dev/null +++ b/src/components/EditorContent/ImagePreview.jsx @@ -0,0 +1,52 @@ +import React, { useEffect, useRef, useState } from "react"; + +import { useOnClickOutside } from "neetocommons/react-utils"; +import { Close } from "neetoicons"; +import { Button, Spinner } from "neetoui"; +import { createPortal } from "react-dom"; + +const ImagePreview = ({ imagePreviewUrl, setImagePreviewUrl }) => { + const [isLoading, setIsLoading] = useState(true); + + const imagePreviewRef = useRef(null); + + useOnClickOutside(imagePreviewRef, () => setImagePreviewUrl(null), { + enabled: true, + }); + + useEffect(() => { + document.addEventListener( + "keydown", + e => e.key === "Escape" && setImagePreviewUrl(null) + ); + + return () => + document.removeEventListener( + "keydown", + e => e.key === "Escape" && setImagePreviewUrl(null) + ); + }, []); + + return createPortal( +
+
+
+ {isLoading && } + Image Preview setIsLoading(false)} + /> +
, + document.body + ); +}; + +export default ImagePreview; diff --git a/src/components/EditorContent/index.jsx b/src/components/EditorContent/index.jsx index 92055bfb..3b1b9c92 100644 --- a/src/components/EditorContent/index.jsx +++ b/src/components/EditorContent/index.jsx @@ -1,11 +1,13 @@ -import React, { useEffect, useRef } from "react"; +import React, { useEffect, useRef, useState } from "react"; import classnames from "classnames"; import DOMPurify from "dompurify"; import CopyToClipboardButton from "neetomolecules/CopyToClipboardButton"; +import { not } from "ramda"; import { createRoot } from "react-dom/client"; import { EDITOR_CONTENT_CLASSNAME, SANITIZE_OPTIONS } from "./constants"; +import ImagePreview from "./ImagePreview"; import { highlightCode, substituteVariables } from "./utils"; const EditorContent = ({ @@ -14,6 +16,7 @@ const EditorContent = ({ className, ...otherProps }) => { + const [imagePreviewUrl, setImagePreviewUrl] = useState(null); const editorContentRef = useRef(null); const htmlContent = substituteVariables(highlightCode(content), variables); @@ -37,6 +40,22 @@ const EditorContent = ({ ); preTag.appendChild(button); }); + + const figureTags = editorContentRef.current?.querySelectorAll("figure"); + figureTags.forEach(figureTag => { + const image = figureTag.querySelector("img"); + const link = figureTag.querySelector("a"); + if ( + !image || + not(window.getComputedStyle(link).pointerEvents === "none") + ) { + return; + } + figureTag.style.cursor = "pointer"; + figureTag.addEventListener("click", () => { + setImagePreviewUrl(image.src); + }); + }); }; useEffect(() => { @@ -44,17 +63,22 @@ const EditorContent = ({ }, [content]); return ( -
+ <> +
+ {imagePreviewUrl && ( + + )} + ); }; diff --git a/src/index.scss b/src/index.scss index e467a127..d76db390 100644 --- a/src/index.scss +++ b/src/index.scss @@ -7,6 +7,7 @@ @import "./styles/editor/editor-content"; @import "./styles/editor/menu"; @import "./styles/editor/media-uploader"; +@import "./styles/editor/image-preview"; @import "./styles/editor/emoji"; @import "./styles/editor/table"; @import "./styles/editor/link-popover"; diff --git a/src/styles/editor/_image-preview.scss b/src/styles/editor/_image-preview.scss new file mode 100644 index 00000000..3529e8b8 --- /dev/null +++ b/src/styles/editor/_image-preview.scss @@ -0,0 +1,29 @@ +.image-preview-wrapper { + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + background: rgba(var(--neeto-ui-gray-800), 0.5); + display: flex; + justify-content: center; + align-items: center; + z-index: 999999; + + .close-button { + position: absolute; + top: 5px; + right: 5px; + } + + .image-preview { + max-width: 70%; + max-height: 70%; + } + + .spinner { + i { + background-color: rgb(var(--neeto-ui-gray-300)); + } + } +}