From 50f41698df8836f654d9c8e9c4ecc585dd0d30f5 Mon Sep 17 00:00:00 2001 From: Palanikannan M Date: Fri, 4 Oct 2024 08:43:36 +0530 Subject: [PATCH 1/8] fix: image deletion on submit fixed in comments --- packages/editor/src/core/hooks/use-editor.ts | 2 +- packages/editor/src/core/plugins/image/delete-image.ts | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/editor/src/core/hooks/use-editor.ts b/packages/editor/src/core/hooks/use-editor.ts index fee33061235..65e36c01ae6 100644 --- a/packages/editor/src/core/hooks/use-editor.ts +++ b/packages/editor/src/core/hooks/use-editor.ts @@ -126,7 +126,7 @@ export const useEditor = (props: CustomEditorProps) => { forwardedRef, () => ({ clearEditor: (emitUpdate = false) => { - editorRef.current?.commands.clearContent(emitUpdate); + editorRef.current?.chain().setMeta("skipImageDeletion", true).clearContent(emitUpdate).run(); }, setEditorValue: (content: string) => { editorRef.current?.commands.setContent(content, false, { preserveWhitespace: "full" }); diff --git a/packages/editor/src/core/plugins/image/delete-image.ts b/packages/editor/src/core/plugins/image/delete-image.ts index 72bb913ae70..21c8cd24f61 100644 --- a/packages/editor/src/core/plugins/image/delete-image.ts +++ b/packages/editor/src/core/plugins/image/delete-image.ts @@ -17,6 +17,8 @@ export const TrackImageDeletionPlugin = (editor: Editor, deleteImage: DeleteImag }); transactions.forEach((transaction) => { + // if the transaction has meta of skipImageDeletion get to true, then return (like while clearing the editor content programatically) + if (transaction.getMeta("skipImageDeletion")) return; // transaction could be a selection if (!transaction.docChanged) return; From 6173a047cbbb2809482f501bd16baf9c87debb78 Mon Sep 17 00:00:00 2001 From: Palanikannan M Date: Fri, 4 Oct 2024 12:51:23 +0530 Subject: [PATCH 2/8] fix: cleareditor added to read only editor --- packages/editor/src/core/hooks/use-read-only-editor.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/editor/src/core/hooks/use-read-only-editor.ts b/packages/editor/src/core/hooks/use-read-only-editor.ts index add0508b996..6d1ed6fa9f1 100644 --- a/packages/editor/src/core/hooks/use-read-only-editor.ts +++ b/packages/editor/src/core/hooks/use-read-only-editor.ts @@ -70,8 +70,8 @@ export const useReadOnlyEditor = (props: CustomReadOnlyEditorProps) => { const editorRef: MutableRefObject = useRef(null); useImperativeHandle(forwardedRef, () => ({ - clearEditor: () => { - editorRef.current?.commands.clearContent(); + clearEditor: (emitUpdate = false) => { + editorRef.current?.chain().setMeta("skipImageDeletion", true).clearContent(emitUpdate).run(); }, setEditorValue: (content: string) => { editorRef.current?.commands.setContent(content, false, { preserveWhitespace: "full" }); From 59dd1aebcda839c4fe335283fbbb4726bf52747f Mon Sep 17 00:00:00 2001 From: Palanikannan M Date: Fri, 4 Oct 2024 12:51:54 +0530 Subject: [PATCH 3/8] fix: image component double drop fixed --- .../custom-image/components/image-uploader.tsx | 14 ++------------ 1 file changed, 2 insertions(+), 12 deletions(-) diff --git a/packages/editor/src/core/extensions/custom-image/components/image-uploader.tsx b/packages/editor/src/core/extensions/custom-image/components/image-uploader.tsx index 89cf36ca52b..d2cfbd3d111 100644 --- a/packages/editor/src/core/extensions/custom-image/components/image-uploader.tsx +++ b/packages/editor/src/core/extensions/custom-image/components/image-uploader.tsx @@ -82,9 +82,6 @@ export const CustomImageUploader = (props: { [imageComponentImageFileMap, imageEntityId] ); - // if the image component is dropped, we check if it has an existing file - const existingFile = useMemo(() => (meta && meta.event === "drop" ? meta.file : undefined), [meta]); - // after the image component is mounted we start the upload process based on // it's uploaded useEffect(() => { @@ -100,13 +97,6 @@ export const CustomImageUploader = (props: { } }, [meta, uploadFile, imageComponentImageFileMap]); - // check if the image is dropped and set the local image as the existing file - useEffect(() => { - if (existingFile) { - uploadFile(existingFile); - } - }, [existingFile, uploadFile]); - const onFileChange = useCallback( (e: ChangeEvent) => { const file = e.target.files?.[0]; @@ -120,7 +110,7 @@ export const CustomImageUploader = (props: { ); const getDisplayMessage = useCallback(() => { - const isUploading = isImageBeingUploaded || existingFile; + const isUploading = isImageBeingUploaded; if (failedToLoadImage) { return "Error loading image"; } @@ -134,7 +124,7 @@ export const CustomImageUploader = (props: { } return "Add an image"; - }, [draggedInside, failedToLoadImage, existingFile, isImageBeingUploaded]); + }, [draggedInside, failedToLoadImage, isImageBeingUploaded]); return (
Date: Fri, 4 Oct 2024 14:04:40 +0530 Subject: [PATCH 4/8] feat: multiple image selection and uploading --- .../custom-image/components/image-block.tsx | 14 ++-- .../components/image-uploader.tsx | 20 ++--- packages/editor/src/core/extensions/drop.tsx | 13 +--- .../editor/src/core/hooks/use-file-upload.ts | 76 ++++++++++++------- 4 files changed, 70 insertions(+), 53 deletions(-) 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 42b51de5fb7..aa74f72fe10 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 @@ -108,14 +108,16 @@ export const CustomImageBlock: React.FC = (props) => { updateAttributes(initialComputedSize); } else { // as the aspect ratio in not stored for old images, we need to update the attrs - setSize((prevSize) => { - const newSize = { ...prevSize, aspectRatio }; - updateAttributes(newSize); - return newSize; - }); + if (!aspectRatio) { + setSize((prevSize) => { + const newSize = { ...prevSize, aspectRatio }; + updateAttributes(newSize); + return newSize; + }); + } } setInitialResizeComplete(true); - }, [width, updateAttributes, editorContainer]); + }, [width, updateAttributes, editorContainer, aspectRatio]); // for real time resizing useLayoutEffect(() => { diff --git a/packages/editor/src/core/extensions/custom-image/components/image-uploader.tsx b/packages/editor/src/core/extensions/custom-image/components/image-uploader.tsx index d2cfbd3d111..b26923acf0e 100644 --- a/packages/editor/src/core/extensions/custom-image/components/image-uploader.tsx +++ b/packages/editor/src/core/extensions/custom-image/components/image-uploader.tsx @@ -5,9 +5,7 @@ import { ImageIcon } from "lucide-react"; // helpers import { cn } from "@/helpers/common"; // hooks -import { useUploader, useDropZone } from "@/hooks/use-file-upload"; -// plugins -import { isFileValid } from "@/plugins/image"; +import { useUploader, useDropZone, uploadFirstImageAndInsertRemaining } from "@/hooks/use-file-upload"; // extensions import { getImageComponentImageFileMap, ImageAttributes } from "@/extensions/custom-image"; @@ -74,7 +72,11 @@ export const CustomImageUploader = (props: { ); // hooks const { uploading: isImageBeingUploaded, uploadFile } = useUploader({ onUpload, editor, loadImageFromFileSystem }); - const { draggedInside, onDrop, onDragEnter, onDragLeave } = useDropZone({ uploader: uploadFile }); + const { draggedInside, onDrop, onDragEnter, onDragLeave } = useDropZone({ + uploader: uploadFile, + editor, + pos: getPos(), + }); // the meta data of the image component const meta = useMemo( @@ -99,12 +101,9 @@ export const CustomImageUploader = (props: { const onFileChange = useCallback( (e: ChangeEvent) => { - const file = e.target.files?.[0]; - if (file) { - if (isFileValid(file)) { - uploadFile(file); - } - } + e.preventDefault(); + const fileList = e.target.files; + uploadFirstImageAndInsertRemaining(editor, fileList, getPos(), uploadFile); }, [uploadFile] ); @@ -157,6 +156,7 @@ export const CustomImageUploader = (props: { type="file" accept=".jpg,.jpeg,.png,.webp" onChange={onFileChange} + multiple />
); diff --git a/packages/editor/src/core/extensions/drop.tsx b/packages/editor/src/core/extensions/drop.tsx index 8d66a5f9f58..2044f03bf5d 100644 --- a/packages/editor/src/core/extensions/drop.tsx +++ b/packages/editor/src/core/extensions/drop.tsx @@ -21,7 +21,7 @@ export const DropHandlerExtension = () => if (imageFiles.length > 0) { const pos = view.state.selection.from; - insertImages({ editor, files: imageFiles, initialPos: pos, event: "drop" }); + insertImagesSafely({ editor, files: imageFiles, initialPos: pos, event: "drop" }); } return true; } @@ -41,7 +41,7 @@ export const DropHandlerExtension = () => if (coordinates) { const pos = coordinates.pos; - insertImages({ editor, files: imageFiles, initialPos: pos, event: "drop" }); + insertImagesSafely({ editor, files: imageFiles, initialPos: pos, event: "drop" }); } return true; } @@ -54,7 +54,7 @@ export const DropHandlerExtension = () => }, }); -const insertImages = async ({ +export const insertImagesSafely = async ({ editor, files, initialPos, @@ -72,13 +72,6 @@ const insertImages = async ({ const docSize = editor.state.doc.content.size; pos = Math.min(pos, docSize); - // Check if the position has a non-empty node - const nodeAtPos = editor.state.doc.nodeAt(pos); - if (nodeAtPos && nodeAtPos.content.size > 0) { - // Move to the end of the current node - pos += nodeAtPos.nodeSize; - } - try { // Insert the image at the current position editor.commands.insertImageComponent({ file, pos, event }); diff --git a/packages/editor/src/core/hooks/use-file-upload.ts b/packages/editor/src/core/hooks/use-file-upload.ts index 5dfa025e594..ca428bbda55 100644 --- a/packages/editor/src/core/hooks/use-file-upload.ts +++ b/packages/editor/src/core/hooks/use-file-upload.ts @@ -1,6 +1,7 @@ import { DragEvent, useCallback, useEffect, useState } from "react"; import { Editor } from "@tiptap/core"; import { isFileValid } from "@/plugins/image"; +import { insertImagesSafely } from "@/extensions/drop"; export const useUploader = ({ onUpload, @@ -63,7 +64,15 @@ export const useUploader = ({ return { uploading, uploadFile }; }; -export const useDropZone = ({ uploader }: { uploader: (file: File) => void }) => { +export const useDropZone = ({ + uploader, + editor, + pos, +}: { + uploader: (file: File) => Promise; + editor: Editor; + pos: number; +}) => { const [isDragging, setIsDragging] = useState(false); const [draggedInside, setDraggedInside] = useState(false); @@ -91,33 +100,8 @@ export const useDropZone = ({ uploader }: { uploader: (file: File) => void }) => if (e.dataTransfer.files.length === 0) { return; } - const fileList = e.dataTransfer.files; - - const files: File[] = []; - - for (let i = 0; i < fileList.length; i += 1) { - const item = fileList.item(i); - if (item) { - files.push(item); - } - } - - if (files.some((file) => file.type.indexOf("image") === -1)) { - return; - } - - e.preventDefault(); - - const filteredFiles = files.filter((f) => f.type.indexOf("image") !== -1); - - const file = filteredFiles.length > 0 ? filteredFiles[0] : undefined; - - if (file) { - uploader(file); - } else { - console.error("No file found"); - } + uploadFirstImageAndInsertRemaining(editor, fileList, pos, uploader); }, [uploader] ); @@ -143,3 +127,41 @@ function trimFileName(fileName: string, maxLength = 100) { return fileName; } + +// Upload the first image and insert the remaining images for uploading multiple image +// post insertion of image-component +export function uploadFirstImageAndInsertRemaining( + editor: Editor, + fileList: FileList, + pos: number, + uploaderFn: (file: File) => Promise +) { + const files: File[] = []; + for (let i = 0; i < fileList.length; i += 1) { + const item = fileList.item(i); + if (item) { + files.push(item); + } + } + if (files.some((file) => file.type.indexOf("image") === -1)) { + return; + } + const filteredFiles = files.filter((f) => f.type.indexOf("image") !== -1); + if (filteredFiles.length === 0) { + console.error("No image files found to upload"); + return; + } + + // Upload the first image + const firstFile = filteredFiles[0]; + uploaderFn(firstFile); + + // Insert the remaining images + const remainingFiles = filteredFiles.slice(1); + + if (remainingFiles.length > 0) { + const docSize = editor.state.doc.content.size; + const posOfNextImageToBeInserted = Math.min(pos + 1, docSize); + insertImagesSafely({ editor, files: remainingFiles, initialPos: posOfNextImageToBeInserted, event: "drop" }); + } +} From a474b9c64cc7f0c0c1987cd4437fe732ce9714a6 Mon Sep 17 00:00:00 2001 From: Palanikannan M Date: Fri, 4 Oct 2024 19:30:34 +0530 Subject: [PATCH 5/8] fix: click event on read only instance --- .../custom-image/components/image-block.tsx | 23 ++++++++++++++++--- .../components/image-uploader.tsx | 5 ++-- 2 files changed, 23 insertions(+), 5 deletions(-) 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 aa74f72fe10..f067ee94780 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 @@ -71,6 +71,17 @@ export const CustomImageBlock: React.FC = (props) => { const containerRect = useRef(null); const imageRef = useRef(null); + const updateAttributesSafely = useCallback( + (attributes: Partial, errorMessage: string) => { + try { + updateAttributes(attributes); + } catch (error) { + console.error(`${errorMessage}:`, error); + } + }, + [updateAttributes] + ); + const handleImageLoad = useCallback(() => { const img = imageRef.current; if (!img) return; @@ -105,13 +116,19 @@ export const CustomImageBlock: React.FC = (props) => { }; setSize(initialComputedSize); - updateAttributes(initialComputedSize); + updateAttributesSafely( + initialComputedSize, + "Failed to update attributes while initializing an image for the first time:" + ); } else { // as the aspect ratio in not stored for old images, we need to update the attrs if (!aspectRatio) { setSize((prevSize) => { const newSize = { ...prevSize, aspectRatio }; - updateAttributes(newSize); + updateAttributesSafely( + newSize, + "Failed to update attributes while initializing images with width but no aspect ratio:" + ); return newSize; }); } @@ -144,7 +161,7 @@ export const CustomImageBlock: React.FC = (props) => { const handleResizeEnd = useCallback(() => { setIsResizing(false); - updateAttributes(size); + updateAttributesSafely(size, "Failed to update attributes at the end of resizing:"); }, [size, updateAttributes]); const handleResizeStart = useCallback((e: React.MouseEvent | React.TouchEvent) => { diff --git a/packages/editor/src/core/extensions/custom-image/components/image-uploader.tsx b/packages/editor/src/core/extensions/custom-image/components/image-uploader.tsx index b26923acf0e..6f2ef9b998b 100644 --- a/packages/editor/src/core/extensions/custom-image/components/image-uploader.tsx +++ b/packages/editor/src/core/extensions/custom-image/components/image-uploader.tsx @@ -128,8 +128,9 @@ export const CustomImageUploader = (props: { return (
{ - if (!failedToLoadImage) { + if (!failedToLoadImage && editor.isEditable) { fileInputRef.current?.click(); } }} From 3496a79c0ab4b5f6a39f18b70c372e749df95bc1 Mon Sep 17 00:00:00 2001 From: Palanikannan M Date: Fri, 4 Oct 2024 19:35:37 +0530 Subject: [PATCH 6/8] fix: made things async --- .../custom-image/components/image-uploader.tsx | 6 +++--- packages/editor/src/core/hooks/use-file-upload.ts | 13 ++++++++----- 2 files changed, 11 insertions(+), 8 deletions(-) diff --git a/packages/editor/src/core/extensions/custom-image/components/image-uploader.tsx b/packages/editor/src/core/extensions/custom-image/components/image-uploader.tsx index 6f2ef9b998b..29868d80aed 100644 --- a/packages/editor/src/core/extensions/custom-image/components/image-uploader.tsx +++ b/packages/editor/src/core/extensions/custom-image/components/image-uploader.tsx @@ -100,12 +100,12 @@ export const CustomImageUploader = (props: { }, [meta, uploadFile, imageComponentImageFileMap]); const onFileChange = useCallback( - (e: ChangeEvent) => { + async (e: ChangeEvent) => { e.preventDefault(); const fileList = e.target.files; - uploadFirstImageAndInsertRemaining(editor, fileList, getPos(), uploadFile); + await uploadFirstImageAndInsertRemaining(editor, fileList, getPos(), uploadFile); }, - [uploadFile] + [uploadFile, editor, getPos, uploadFile] ); const getDisplayMessage = useCallback(() => { diff --git a/packages/editor/src/core/hooks/use-file-upload.ts b/packages/editor/src/core/hooks/use-file-upload.ts index ca428bbda55..021eff0baf6 100644 --- a/packages/editor/src/core/hooks/use-file-upload.ts +++ b/packages/editor/src/core/hooks/use-file-upload.ts @@ -95,15 +95,15 @@ export const useDropZone = ({ }, []); const onDrop = useCallback( - (e: DragEvent) => { + async (e: DragEvent) => { setDraggedInside(false); if (e.dataTransfer.files.length === 0) { return; } const fileList = e.dataTransfer.files; - uploadFirstImageAndInsertRemaining(editor, fileList, pos, uploader); + await uploadFirstImageAndInsertRemaining(editor, fileList, pos, uploader); }, - [uploader] + [uploader, editor, pos] ); const onDragEnter = () => { @@ -130,7 +130,7 @@ function trimFileName(fileName: string, maxLength = 100) { // Upload the first image and insert the remaining images for uploading multiple image // post insertion of image-component -export function uploadFirstImageAndInsertRemaining( +export async function uploadFirstImageAndInsertRemaining( editor: Editor, fileList: FileList, pos: number, @@ -147,6 +147,9 @@ export function uploadFirstImageAndInsertRemaining( return; } const filteredFiles = files.filter((f) => f.type.indexOf("image") !== -1); + if (filteredFiles.length !== files.length) { + console.warn("Some files were not images and have been ignored."); + } if (filteredFiles.length === 0) { console.error("No image files found to upload"); return; @@ -154,7 +157,7 @@ export function uploadFirstImageAndInsertRemaining( // Upload the first image const firstFile = filteredFiles[0]; - uploaderFn(firstFile); + await uploaderFn(firstFile); // Insert the remaining images const remainingFiles = filteredFiles.slice(1); From 2cbf5c3f5b7c62256a6803608e236b851d62f773 Mon Sep 17 00:00:00 2001 From: Palanikannan M Date: Fri, 4 Oct 2024 19:59:27 +0530 Subject: [PATCH 7/8] fix: prevented default behaviour --- packages/editor/src/core/hooks/use-file-upload.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/editor/src/core/hooks/use-file-upload.ts b/packages/editor/src/core/hooks/use-file-upload.ts index 021eff0baf6..e04b2b707d0 100644 --- a/packages/editor/src/core/hooks/use-file-upload.ts +++ b/packages/editor/src/core/hooks/use-file-upload.ts @@ -96,6 +96,7 @@ export const useDropZone = ({ const onDrop = useCallback( async (e: DragEvent) => { + e.preventDefault(); setDraggedInside(false); if (e.dataTransfer.files.length === 0) { return; @@ -157,7 +158,7 @@ export async function uploadFirstImageAndInsertRemaining( // Upload the first image const firstFile = filteredFiles[0]; - await uploaderFn(firstFile); + uploaderFn(firstFile); // Insert the remaining images const remainingFiles = filteredFiles.slice(1); From dd2fc989193dc80d534cb15bf80583a248897891 Mon Sep 17 00:00:00 2001 From: Palanikannan M Date: Mon, 7 Oct 2024 13:02:59 +0530 Subject: [PATCH 8/8] fix: removed extra dep and cleaned up logic --- .../custom-image/components/image-uploader.tsx | 7 +++++-- packages/editor/src/core/hooks/use-file-upload.ts | 12 ++++-------- 2 files changed, 9 insertions(+), 10 deletions(-) diff --git a/packages/editor/src/core/extensions/custom-image/components/image-uploader.tsx b/packages/editor/src/core/extensions/custom-image/components/image-uploader.tsx index 29868d80aed..b5c52db66c3 100644 --- a/packages/editor/src/core/extensions/custom-image/components/image-uploader.tsx +++ b/packages/editor/src/core/extensions/custom-image/components/image-uploader.tsx @@ -103,9 +103,12 @@ export const CustomImageUploader = (props: { async (e: ChangeEvent) => { e.preventDefault(); const fileList = e.target.files; + if (!fileList) { + return; + } await uploadFirstImageAndInsertRemaining(editor, fileList, getPos(), uploadFile); }, - [uploadFile, editor, getPos, uploadFile] + [uploadFile, editor, getPos] ); const getDisplayMessage = useCallback(() => { @@ -128,7 +131,7 @@ export const CustomImageUploader = (props: { return (
Promise ) { - const files: File[] = []; + const filteredFiles: File[] = []; for (let i = 0; i < fileList.length; i += 1) { const item = fileList.item(i); - if (item) { - files.push(item); + if (item && item.type.indexOf("image") !== -1 && isFileValid(item)) { + filteredFiles.push(item); } } - if (files.some((file) => file.type.indexOf("image") === -1)) { - return; - } - const filteredFiles = files.filter((f) => f.type.indexOf("image") !== -1); - if (filteredFiles.length !== files.length) { + if (filteredFiles.length !== fileList.length) { console.warn("Some files were not images and have been ignored."); } if (filteredFiles.length === 0) {