Skip to content

Commit

Permalink
Creates a JS util script to preview images. (#1292)
Browse files Browse the repository at this point in the history
* Moved constants to a directory.

* Created a template to preview the images.

* Created a utility class the manages the image preview and the associated events.

* Removed the unused constants from the root of the EditorContent.

* optimized the image preview logic by reusing the container and only removes the image and caption when the image preview is closed.

* Fixed issue of closing the image preview when clicking on the image itself.

* Removed the redundant wrapper and adjusted the logic to insert image and bid event listeners accordingly.

* Moved a few functions to the utils file.

* Renamed the utils and constants to editorUtils for easier readability.

* Improvised the image preview styles.

* Implemented the fade in after image is fetched feature.

* Added the rollup entry for the editor utils script.

* A small optimization and minor code refactoring.

* Added the documentation on how to use the script.

* Fixed a few edge cases identified while testing.
  • Loading branch information
deepakjosp authored Dec 13, 2024
1 parent 9b01439 commit 39eb5f2
Show file tree
Hide file tree
Showing 8 changed files with 228 additions and 5 deletions.
9 changes: 9 additions & 0 deletions rollup.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,15 @@ const config = args => {
},
plugins,
},
{
input: "./src/components/EditorContent/editorUtils.js",
output: {
dir: `${__dirname}/dist`,
format: "cjs",
sourcemap: false,
assetFileNames: "[name][extname]",
},
},
]
}

Expand Down
2 changes: 1 addition & 1 deletion src/components/EditorContent/ImagePreview.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ const ImagePreview = ({ imagePreviewDetails, setImagePreviewDetails }) => {
}, []);

return createPortal(
<div className="ne-image-preview-wrapper">
<div className="ne-image-preview-wrapper active">
{isLoading && <Spinner className="ne-image-preview-wrapper__spinner" />}
{!isLoading && (
<div className="close-button">
Expand Down
24 changes: 24 additions & 0 deletions src/components/EditorContent/constants/editorUtils.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
export const IMAGE_PREVIEW_CONTAINER_TEMPLATE = `
<div class="close-button">
<button id="neImagePreviewCloseButton">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="20" height="20">
<path fill="none" d="M0 0h24v24H0z"></path><path d="M12 10.586l4.95-4.95 1.414 1.414-4.95 4.95 4.95 4.95-1.414 1.414-4.95-4.95-4.95 4.95-1.414-1.414 4.95-4.95-4.95-4.95L7.05 5.636z"></path>
</svg>
</button>
</div>
<!-- append the IMAGE_PREVIEW_CONTENT_TEMPLATE here -->
`;

export const IMAGE_PREVIEW_CONTENT_TEMPLATE = `
<div class="ne-image-preview" id="neImagePreviewContentContainer">
<img
alt="{{imageCaption}}"
src="{{imageSource}}"
/>
<p class="ne-image-preview__caption">
{{imageCaption}}
</p>
</div>
`;

export const IMG_TAGS = ["FIGURE", "IMG"];
File renamed without changes.
133 changes: 133 additions & 0 deletions src/components/EditorContent/editorUtils.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
/* eslint-disable @bigbinary/neeto/file-name-and-export-name-standards */
import {
IMG_TAGS,
IMAGE_PREVIEW_CONTAINER_TEMPLATE,
} from "./constants/editorUtils";
import { getImageDetails, getPreviewHtml } from "./utils/editorUtils";

(() => {
if (window.neetoEditor?.utils) return;

class EditorUtils {
constructor() {
this.init();
}

imagePreviewCloseButtonId = "neImagePreviewCloseButton";
imagePreviewContainerId = "neImagePreviewContainer";
imagePreviewContentContainerId = "neImagePreviewContentContainer";

init() {
[this.editorContentContainer] = document.getElementsByClassName(
"neeto-editor-content"
) ?? [document];
this.bindClickListeners();
}

destroy() {
this.removeEventListeners();
this.imagePreviewContainer?.remove();
this.imagePreviewContainer = null;
}

bindClickListeners() {
this.editorContentContainer.addEventListener(
"click",
this.handleClickEvents.bind(this)
);
}

handleClickEvents(e) {
const { tagName } = e.target;

if (IMG_TAGS.includes(tagName)) this.showImagePreview(e);
}

showImagePreview(event) {
const { imageUrl, caption } = getImageDetails(event);

if (!this.imagePreviewContainer) this.mountContainerAndAttachEvents();

this.imagePreviewContainer.innerHTML += getPreviewHtml(imageUrl, caption);
this.imagePreviewContainer.classList.add("active");

this.imagePreview = this.imagePreviewContainer?.querySelector(
`#${this.imagePreviewContentContainerId}`
);

this.imagePreview?.addEventListener(
"click",
this.stopImageClickPropagation.bind(this)
);

this.imagePreview
?.querySelector("img")
.addEventListener("load", this.setImageLoadedClass.bind(this));
}

mountContainerAndAttachEvents() {
const imagePreviewContainer = document.createElement("div");
imagePreviewContainer.setAttribute("id", this.imagePreviewContainerId);
imagePreviewContainer.setAttribute("class", "ne-image-preview-wrapper");
imagePreviewContainer.innerHTML = IMAGE_PREVIEW_CONTAINER_TEMPLATE;

document.body.appendChild(imagePreviewContainer);
this.imagePreviewContainer = imagePreviewContainer;

this.bindImagePreviewEventListeners();
}

bindImagePreviewEventListeners() {
document
.getElementById(this.imagePreviewCloseButtonId)
.addEventListener("click", this.closeImagePreview.bind(this));

this.imagePreviewContainer.addEventListener(
"click",
this.closeImagePreview.bind(this)
);

document.addEventListener("keyup", this.handleKeyDown.bind(this));
}

stopImageClickPropagation(event) {
event.stopPropagation();
}

closeImagePreview() {
this.imagePreviewContainer.classList.remove("active");
this.imagePreview?.removeEventListener(
"click",
this.stopImageClickPropagation.bind(this)
);

this.imagePreview
?.querySelector("img")
.removeEventListener("load", this.setImageLoadedClass.bind(this));
this.imagePreview?.remove();
this.imagePreview = null;
}

setImageLoadedClass() {
this.imagePreview?.classList.add("image-loaded");
}

handleKeyDown(event) {
if (event.key === "Escape") this.closeImagePreview();
}

removeEventListeners() {
this.editorContentContainer.removeEventListener(
"click",
this.handleClickEvents.bind(this)
);
document.removeEventListener("keyup", this.handleKeyDown.bind(this));
document
.getElementById(this.imagePreviewCloseButtonId)
?.removeEventListener("click", this.closeImagePreview.bind(this));
}
}

window.neetoEditor = window.neetoEditor ?? {};
window.neetoEditor.utils = new EditorUtils();
})();
16 changes: 16 additions & 0 deletions src/components/EditorContent/utils/editorUtils.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { IMAGE_PREVIEW_CONTENT_TEMPLATE } from "../constants/editorUtils";

export const getImageDetails = event => {
const imageElement = event.target.querySelector("IMG");
const captionElement = event.target.querySelector("FIGCAPTION");
const imageUrl = imageElement.getAttribute("src");
const caption = captionElement.textContent?.trim?.();

return { imageUrl, caption };
};

export const getPreviewHtml = (imageUrl, caption) =>
IMAGE_PREVIEW_CONTENT_TEMPLATE.replaceAll(
"{{imageCaption}}",
caption
).replace("{{imageSource}}", imageUrl);
22 changes: 19 additions & 3 deletions src/styles/editor/editor-content.scss
Original file line number Diff line number Diff line change
Expand Up @@ -537,6 +537,7 @@
max-width: 100%;
overflow: hidden;
margin: 0;
cursor: pointer;
}

.neeto-editor__image {
Expand Down Expand Up @@ -639,11 +640,15 @@
width: 100%;
height: 100%;
background: rgba(var(--neeto-editor-gray-800), 0.5);
display: flex;
display: none;
justify-content: center;
align-items: center;
z-index: 999999;

&.active {
display: flex;
}

.ne-image-preview {
opacity: 0;
transition: opacity 100ms ease-in-out;
Expand All @@ -662,13 +667,24 @@
&__caption {
line-height: 1.5;
color: rgb(var(--neeto-editor-gray-800));
margin-top: auto;
text-align: center;
width: 100%;
}
}

.close-button {
position: absolute;
top: 5px;
right: 5px;
top: 12px;
right: 12px;
background-color: rgb(var(--neeto-editor-white));
border-radius: var(--neeto-editor-rounded-full);
width: 30px;
height: 30px;
cursor: pointer;
display: flex;
justify-content: center;
align-items: center;
}

.image-preview {
Expand Down
27 changes: 26 additions & 1 deletion stories/Walkthroughs/Output/Output.stories.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,7 @@ Add the following lines to the SSG/SSR pages. The pages need not be converted to
```js
<Script
src="https://cdn.jsdelivr.net/npm/@bigbinary/neeto-editor/dist/codeBlockHighlight.js"
strategy="beforeInteractive"
strategy="afterInteractive"
/>
```
Expand All @@ -136,3 +136,28 @@ useEffect(() => {
window.neetoEditor?.applyHeaderDecorations?.()
}, [])
```
#### Image preview
Add the following lines to the SSG/SSR pages. The pages need not be converted to client side rendering for this to work.
```js
<Script
src="https://cdn.jsdelivr.net/npm/@bigbinary/neeto-editor/dist/editorUtils.js"
strategy="afterInteractive"
/>
```
This script adds necessary event listeners to the images in the editor content and previews them when the user clicks on it.
The script initiates on load, you can also make use of the `init` function as below. The script exposes a `destroy` function which
can be used to clean up when the component is unmounted.
```react
useEffect(() => {
window.neetoEditor?.utils?.init?.()
return () => {
window.neetoEditor?.utils?.destroy?.()
}
}, [])
```

0 comments on commit 39eb5f2

Please sign in to comment.