From 16be6ff37d4ff8d0a3be2b2b271ea5c16ee2fce9 Mon Sep 17 00:00:00 2001 From: Palanikannan M Date: Mon, 9 Sep 2024 16:02:14 +0530 Subject: [PATCH 01/29] fix: svg not supported in image uploads --- packages/editor/src/core/helpers/editor-commands.ts | 2 +- packages/editor/src/core/plugins/image/utils/validate-file.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/editor/src/core/helpers/editor-commands.ts b/packages/editor/src/core/helpers/editor-commands.ts index db3b4d66d0e..7cf3e8d1f17 100644 --- a/packages/editor/src/core/helpers/editor-commands.ts +++ b/packages/editor/src/core/helpers/editor-commands.ts @@ -146,7 +146,7 @@ export const insertImageCommand = ( if (range) editor.chain().focus().deleteRange(range).run(); const input = document.createElement("input"); input.type = "file"; - input.accept = ".jpeg, .jpg, .png, .webp, .svg"; + input.accept = ".jpeg, .jpg, .png, .webp"; input.onchange = async () => { if (input.files?.length) { const file = input.files[0]; diff --git a/packages/editor/src/core/plugins/image/utils/validate-file.ts b/packages/editor/src/core/plugins/image/utils/validate-file.ts index a7952a0e116..97c5e9e8407 100644 --- a/packages/editor/src/core/plugins/image/utils/validate-file.ts +++ b/packages/editor/src/core/plugins/image/utils/validate-file.ts @@ -4,7 +4,7 @@ export function isFileValid(file: File): boolean { return false; } - const allowedTypes = ["image/jpeg", "image/jpg", "image/png", "image/webp", "image/svg+xml"]; + const allowedTypes = ["image/jpeg", "image/jpg", "image/png", "image/webp"]; if (!allowedTypes.includes(file.type)) { alert("Invalid file type. Please select a JPEG, JPG, PNG, WEBP, or SVG image file."); return false; From f71ca5802f31f8bccb9bfb7f6c90056c0a5ed5aa Mon Sep 17 00:00:00 2001 From: Palanikannan M Date: Mon, 9 Sep 2024 20:27:02 +0530 Subject: [PATCH 02/29] fix: svg image file error message fixed --- packages/editor/src/core/plugins/image/utils/validate-file.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/editor/src/core/plugins/image/utils/validate-file.ts b/packages/editor/src/core/plugins/image/utils/validate-file.ts index 97c5e9e8407..b79ca6683e2 100644 --- a/packages/editor/src/core/plugins/image/utils/validate-file.ts +++ b/packages/editor/src/core/plugins/image/utils/validate-file.ts @@ -6,7 +6,7 @@ export function isFileValid(file: File): boolean { const allowedTypes = ["image/jpeg", "image/jpg", "image/png", "image/webp"]; if (!allowedTypes.includes(file.type)) { - alert("Invalid file type. Please select a JPEG, JPG, PNG, WEBP, or SVG image file."); + alert("Invalid file type. Please select a JPEG, JPG, PNG, or WEBP image file."); return false; } From 7ebd714e2a678403285d4583682046b108a6f47c Mon Sep 17 00:00:00 2001 From: Palanikannan M Date: Wed, 11 Sep 2024 19:20:55 +0530 Subject: [PATCH 03/29] feat: add custom image node for uploads --- .../src/core/components/menus/menu-items.ts | 2 +- packages/editor/src/core/extensions/drop.tsx | 62 ++++++--- .../editor/src/core/extensions/extensions.tsx | 17 ++- .../components/image-block-view.tsx | 122 +++++++++++++++++ .../extensions/image-block/image-block.ts | 64 +++++++++ .../src/core/extensions/image-block/index.ts | 1 + .../extensions/image-upload/image-upload.ts | 129 ++++++++++++++++++ .../src/core/extensions/image-upload/index.ts | 1 + .../image-upload/view/image-upload.tsx | 76 +++++++++++ .../image-upload/view/image-uploader.tsx | 82 +++++++++++ .../extensions/image-upload/view/index.tsx | 1 + .../core/extensions/image/image-resize.tsx | 2 +- .../src/core/extensions/slash-commands.tsx | 8 +- .../src/core/helpers/editor-commands.ts | 20 +++ .../core/hooks/use-collaborative-editor.ts | 1 + packages/editor/src/core/hooks/use-editor.ts | 23 +++- .../editor/src/core/hooks/use-file-upload.ts | 106 ++++++++++++++ packages/editor/src/styles/drag-drop.css | 12 +- 18 files changed, 695 insertions(+), 34 deletions(-) create mode 100644 packages/editor/src/core/extensions/image-block/components/image-block-view.tsx create mode 100644 packages/editor/src/core/extensions/image-block/image-block.ts create mode 100644 packages/editor/src/core/extensions/image-block/index.ts create mode 100644 packages/editor/src/core/extensions/image-upload/image-upload.ts create mode 100644 packages/editor/src/core/extensions/image-upload/index.ts create mode 100644 packages/editor/src/core/extensions/image-upload/view/image-upload.tsx create mode 100644 packages/editor/src/core/extensions/image-upload/view/image-uploader.tsx create mode 100644 packages/editor/src/core/extensions/image-upload/view/index.tsx create mode 100644 packages/editor/src/core/hooks/use-file-upload.ts diff --git a/packages/editor/src/core/components/menus/menu-items.ts b/packages/editor/src/core/components/menus/menu-items.ts index 60db11704d4..71d1576d8d3 100644 --- a/packages/editor/src/core/components/menus/menu-items.ts +++ b/packages/editor/src/core/components/menus/menu-items.ts @@ -194,7 +194,7 @@ export const ImageItem = (editor: Editor, uploadFile: UploadImage) => key: "image", name: "Image", isActive: () => editor?.isActive("image"), - command: (savedSelection: Selection | null) => insertImageCommand(editor, uploadFile, savedSelection), + command: (savedSelection: Selection | null) => editor?.commands.setImageUpload({ event: "insert" }), icon: ImageIcon, }) as const; diff --git a/packages/editor/src/core/extensions/drop.tsx b/packages/editor/src/core/extensions/drop.tsx index d56f802d97a..943ab60d46f 100644 --- a/packages/editor/src/core/extensions/drop.tsx +++ b/packages/editor/src/core/extensions/drop.tsx @@ -1,11 +1,8 @@ import { Extension } from "@tiptap/core"; import { Plugin, PluginKey } from "prosemirror-state"; -// plugins -import { startImageUpload } from "@/plugins/image"; -// types -import { UploadImage } from "@/types"; +import { EditorView } from "prosemirror-view"; -export const DropHandlerExtension = (uploadFile: UploadImage) => +export const DropHandlerExtension = () => Extension.create({ name: "dropHandler", priority: 1000, @@ -15,28 +12,51 @@ export const DropHandlerExtension = (uploadFile: UploadImage) => new Plugin({ key: new PluginKey("drop-handler-plugin"), props: { - handlePaste: (view, event) => { - if (event.clipboardData && event.clipboardData.files && event.clipboardData.files[0]) { + handlePaste: (view: EditorView, event: ClipboardEvent) => { + if (event.clipboardData && event.clipboardData.files && event.clipboardData.files.length > 0) { event.preventDefault(); - const file = event.clipboardData.files[0]; - const pos = view.state.selection.from; - startImageUpload(this.editor, file, view, pos, uploadFile); - return true; + const files = Array.from(event.clipboardData.files); + const imageFiles = files.filter((file) => file.type.startsWith("image")); + + if (imageFiles.length > 0) { + const pos = view.state.selection.from; + imageFiles.forEach((file, index) => { + this.editor + .chain() + .focus() + .setImageUpload({ file, pos: pos + index, event: "drop" }) + .run(); + }); + return true; + } } return false; }, - handleDrop: (view, event, _slice, moved) => { - if (!moved && event.dataTransfer && event.dataTransfer.files && event.dataTransfer.files[0]) { + handleDrop: (view: EditorView, event: DragEvent, _slice: any, moved: boolean) => { + if (!moved && event.dataTransfer && event.dataTransfer.files && event.dataTransfer.files.length > 0) { event.preventDefault(); - const file = event.dataTransfer.files[0]; - const coordinates = view.posAtCoords({ - left: event.clientX, - top: event.clientY, - }); - if (coordinates) { - startImageUpload(this.editor, file, view, coordinates.pos - 1, uploadFile); + const files = Array.from(event.dataTransfer.files); + const imageFiles = files.filter((file) => file.type.startsWith("image")); + + if (imageFiles.length > 0) { + const coordinates = view.posAtCoords({ + left: event.clientX, + top: event.clientY, + }); + + if (coordinates) { + imageFiles.forEach((file, index) => { + setTimeout(() => { + this.editor + .chain() + .focus() + .setImageUpload({ file, pos: coordinates.pos + index, event: "drop" }) + .run(); + }, index * 100); // Slight delay between insertions + }); + } + return true; } - return true; } return false; }, diff --git a/packages/editor/src/core/extensions/extensions.tsx b/packages/editor/src/core/extensions/extensions.tsx index 823754a9317..5b34c644dd6 100644 --- a/packages/editor/src/core/extensions/extensions.tsx +++ b/packages/editor/src/core/extensions/extensions.tsx @@ -29,6 +29,10 @@ import { import { isValidHttpUrl } from "@/helpers/common"; // types import { DeleteImage, IMentionHighlight, IMentionSuggestion, RestoreImage, UploadImage } from "@/types"; +import { TrailingNode } from "./trailing-node"; +import { ImageBlock } from "./image-block"; +import { ImageUpload } from "./image-upload"; +import { HocuspocusProvider } from "@hocuspocus/provider"; type TArguments = { enableHistory: boolean; @@ -44,6 +48,7 @@ type TArguments = { }; placeholder?: string | ((isFocused: boolean, value: string) => string); tabIndex?: number; + provider?: HocuspocusProvider | null; }; export const CoreEditorExtensions = ({ @@ -52,6 +57,7 @@ export const CoreEditorExtensions = ({ mentionConfig, placeholder, tabIndex, + provider, }: TArguments) => [ StarterKit.configure({ bulletList: { @@ -104,6 +110,15 @@ export const CoreEditorExtensions = ({ class: "rounded-md", }, }), + ImageUpload({ + deleteFile, + restoreFile, + uploadFile, + cancelUploadImage, + }).configure({ + clientId: provider?.document?.clientID, + }), + ImageBlock, TiptapUnderline, TextStyle, TaskList.configure({ @@ -142,7 +157,7 @@ export const CoreEditorExtensions = ({ placeholder: ({ editor, node }) => { if (node.type.name === "heading") return `Heading ${node.attrs.level}`; - if (editor.storage.image.uploadInProgress) return ""; + // if (editor.storage.image.uploadInProgress) return ""; const shouldHidePlaceholder = editor.isActive("table") || editor.isActive("codeBlock") || editor.isActive("image"); diff --git a/packages/editor/src/core/extensions/image-block/components/image-block-view.tsx b/packages/editor/src/core/extensions/image-block/components/image-block-view.tsx new file mode 100644 index 00000000000..6d8b7cdb432 --- /dev/null +++ b/packages/editor/src/core/extensions/image-block/components/image-block-view.tsx @@ -0,0 +1,122 @@ +import React, { useRef, useState, useCallback, useEffect } from "react"; +import { Node as ProsemirrorNode } from "@tiptap/pm/model"; +import { Editor, NodeViewWrapper } from "@tiptap/react"; + +interface ImageBlockViewProps { + editor: Editor; + getPos: () => number; + node: ProsemirrorNode & { + attrs: { + src: string; + width: string; + height: string; + }; + }; + updateAttributes: (attrs: Record) => void; +} + +const MIN_SIZE = 100; + +export const ImageBlockView: React.FC = (props) => { + const { node, updateAttributes } = props; + const { src, width, height } = node.attrs; + + const [size, setSize] = useState({ width, height }); + const [isSelected, setIsSelected] = useState(false); + const containerRef = useRef(null); + const imageRef = useRef(null); + const isResizing = useRef(false); + const aspectRatio = useRef(1); + + useEffect(() => { + if (imageRef.current) { + const img = imageRef.current; + img.onload = () => { + aspectRatio.current = img.naturalWidth / img.naturalHeight; + if (width === "35%" && height === "auto") { + const containerWidth = containerRef.current?.offsetWidth || 0; + const newWidth = Math.max(containerWidth * 0.35, MIN_SIZE); + const newHeight = newWidth / aspectRatio.current; + setSize({ width: `${newWidth}px`, height: `${newHeight}px` }); + } + }; + } + }, [src, width, height]); + + const handleResizeStart = useCallback((e: React.MouseEvent | React.TouchEvent) => { + e.preventDefault(); + e.stopPropagation(); + isResizing.current = true; + setIsSelected(true); + }, []); + + const handleResize = useCallback((e: MouseEvent | TouchEvent) => { + if (!isResizing.current || !containerRef.current) return; + + const containerRect = containerRef.current.getBoundingClientRect(); + const clientX = "touches" in e ? e.touches[0].clientX : e.clientX; + + const newWidth = Math.max(clientX - containerRect.left, MIN_SIZE); + const newHeight = newWidth / aspectRatio.current; + + setSize({ width: `${newWidth}px`, height: `${newHeight}px` }); + }, []); + + const handleResizeEnd = useCallback(() => { + if (isResizing.current) { + isResizing.current = false; + updateAttributes(size); + } + }, [size, updateAttributes]); + + const handleMouseDown = useCallback((e: React.MouseEvent) => { + e.stopPropagation(); + setIsSelected(true); + }, []); + + useEffect(() => { + const handleGlobalMouseDown = (e: MouseEvent) => { + if (containerRef.current && !containerRef.current.contains(e.target as Element)) { + setIsSelected(false); + } + }; + + const handleGlobalMouseMove = (e: MouseEvent) => handleResize(e); + const handleGlobalMouseUp = () => handleResizeEnd(); + + document.addEventListener("mousedown", handleGlobalMouseDown); + document.addEventListener("mousemove", handleGlobalMouseMove); + document.addEventListener("mouseup", handleGlobalMouseUp); + + return () => { + document.removeEventListener("mousedown", handleGlobalMouseDown); + document.removeEventListener("mousemove", handleGlobalMouseMove); + document.removeEventListener("mouseup", handleGlobalMouseUp); + }; + }, [handleResize, handleResizeEnd]); + + return ( + +
+ + {isSelected && ( + <> +
+
+ + )} +
+ + ); +}; + +export default ImageBlockView; diff --git a/packages/editor/src/core/extensions/image-block/image-block.ts b/packages/editor/src/core/extensions/image-block/image-block.ts new file mode 100644 index 00000000000..8bf50745888 --- /dev/null +++ b/packages/editor/src/core/extensions/image-block/image-block.ts @@ -0,0 +1,64 @@ +import { mergeAttributes, Range } from "@tiptap/core"; +import { Image } from "@tiptap/extension-image"; +import { ReactNodeViewRenderer } from "@tiptap/react"; + +import { ImageBlockView } from "./components/image-block-view"; + +declare module "@tiptap/core" { + interface Commands { + imageBlock: { + setImageBlock: (attributes: { src: string; width?: number; height?: number }) => ReturnType; + setImageBlockAt: (attributes: { + src: string; + pos: number | Range; + width?: number; + height?: number; + }) => ReturnType; + }; + } +} + +export const ImageBlock = Image.extend({ + name: "imageBlock", + group: "inline", + draggable: true, + + addAttributes() { + return { + ...this.parent?.(), + width: { + default: "35%", + }, + height: { + default: "auto", + }, + }; + }, + + addCommands() { + return { + setImageBlock: + (attrs) => + ({ commands }) => + commands.insertContent({ + type: this.name, + attrs: { src: attrs.src }, + }), + setImageBlockAt: + (attrs) => + ({ commands }) => + commands.insertContentAt(attrs.pos, { + type: this.name, + attrs: { src: attrs.src }, + }), + }; + }, + + addNodeView() { + return ReactNodeViewRenderer(ImageBlockView); + }, +}).configure({ + inline: true, +}); + +export default ImageBlock; diff --git a/packages/editor/src/core/extensions/image-block/index.ts b/packages/editor/src/core/extensions/image-block/index.ts new file mode 100644 index 00000000000..3f622202102 --- /dev/null +++ b/packages/editor/src/core/extensions/image-block/index.ts @@ -0,0 +1 @@ +export * from "./image-block"; diff --git a/packages/editor/src/core/extensions/image-upload/image-upload.ts b/packages/editor/src/core/extensions/image-upload/image-upload.ts new file mode 100644 index 00000000000..1569baf492f --- /dev/null +++ b/packages/editor/src/core/extensions/image-upload/image-upload.ts @@ -0,0 +1,129 @@ +import { mergeAttributes, Node, ReactNodeViewRenderer } from "@tiptap/react"; +import { v4 as uuidv4 } from "uuid"; +import { DeleteImage, RestoreImage, UploadImage } from "@/types"; +import { ImageUpload as ImageUploadComponent } from "./view/image-upload"; + +declare module "@tiptap/core" { + interface Commands { + imageUpload: { + setImageUpload: ({ + file, + pos, + event, + }: { + file?: File; + pos?: number; + event: "insert" | "replace" | "drop"; + }) => ReturnType; + uploadImage: (file: File) => () => Promise | undefined; + restoreImage: (assetUrlWithWorkspaceId: string) => Promise; + deleteImage: (assetUrlWithWorkspaceId: string) => Promise; + }; + } +} + +export interface UploadImageExtensionStorage { + fileMap: Map; +} + +export type UploadEntity = ({ event: "insert" } | { event: "replace" } | { event: "drop"; file: File }) & { + pos?: number; +}; + +export const ImageUpload = ({ + uploadFile, + // deleteFile, + // restoreFile, + // cancelUploadImage, +}: { + uploadFile: UploadImage; + deleteFile: DeleteImage; + restoreFile: RestoreImage; + cancelUploadImage?: () => void; +}) => + Node.create({ + name: "imageUpload", + + isolating: true, + + defining: true, + + group: "block", + + draggable: true, + + selectable: true, + + inline: false, + + addAttributes() { + return { + ["data-type"]: { + default: this.name, + }, + ["data-file"]: { + default: null, + }, + ["id"]: { + default: null, + }, + }; + }, + + parseHTML() { + return [ + { + tag: "image-upload", + }, + ]; + }, + + renderHTML({ HTMLAttributes }) { + return ["image-upload", mergeAttributes(HTMLAttributes)]; + }, + + addStorage() { + return { + fileMap: new Map(), + }; + }, + + addCommands() { + return { + setImageUpload: + (props: { file?: File; pos?: number; event: "insert" | "replace" | "drop" }) => + ({ commands }) => { + const fileId = uuidv4(); + if (props?.file && props?.event === "drop") { + (this.editor.storage.imageUpload as UploadImageExtensionStorage).fileMap.set(fileId, { + file: props.file, + event: props.event, + }); + } else if (props.event !== "drop") { + (this.editor.storage.imageUpload as UploadImageExtensionStorage).fileMap.set(fileId, { + event: props.event, + }); + } + return commands.insertContent( + `` + ); + }, + uploadImage: (file: File) => async () => { + const fileUrl = await uploadFile(file); + return fileUrl; + }, + // restoreImage: (assetUrlWithWorkspaceId: string) => () => { + // restoreFile(assetUrlWithWorkspaceId); + // }, + // deleteImage: (assetUrlWithWorkspaceId: string) => () => { + // deleteFile(assetUrlWithWorkspaceId); + // }, + }; + }, + + addNodeView() { + return ReactNodeViewRenderer(ImageUploadComponent); + }, + }); + +export default ImageUpload; diff --git a/packages/editor/src/core/extensions/image-upload/index.ts b/packages/editor/src/core/extensions/image-upload/index.ts new file mode 100644 index 00000000000..0d0b9c26381 --- /dev/null +++ b/packages/editor/src/core/extensions/image-upload/index.ts @@ -0,0 +1 @@ +export * from "./image-upload"; diff --git a/packages/editor/src/core/extensions/image-upload/view/image-upload.tsx b/packages/editor/src/core/extensions/image-upload/view/image-upload.tsx new file mode 100644 index 00000000000..cabfb3386f5 --- /dev/null +++ b/packages/editor/src/core/extensions/image-upload/view/image-upload.tsx @@ -0,0 +1,76 @@ +import React, { useCallback, useEffect, useRef } from "react"; +import { Node } from "@tiptap/pm/model"; +import { Editor, NodeViewWrapper } from "@tiptap/react"; +import { UploadImageExtensionStorage, UploadEntity } from "../image-upload"; +import { ImageUploader } from "./image-uploader"; + +interface ImageUploadProps { + getPos: () => number; + editor: Editor; + node: Node; +} + +export const ImageUpload: React.FC = ({ getPos, editor, node }) => { + const fileInputRef = useRef(null); + const hasTriggeredFilePickerRef = useRef(false); + + const id = node.attrs.id as string; + const editorStorage = editor.storage.imageUpload as UploadImageExtensionStorage | undefined; + + const getUploadEntity = useCallback( + (): UploadEntity | undefined => editorStorage?.fileMap.get(id), + [editorStorage, id] + ); + + const onUpload = useCallback( + (url: string) => { + if (url) { + editor.chain().setImageBlock({ src: url }).deleteRange({ from: getPos(), to: getPos() }).focus().run(); + } + }, + [editor, getPos] + ); + + const uploadFile = useCallback( + async (file: File) => { + try { + const result = await editor.commands.uploadImage(file)(); + if (result) { + onUpload(result); + } + } catch (error) { + console.error("Error uploading file:", error); + } + }, + [editor.commands, onUpload] + ); + + useEffect(() => { + const uploadEntity = getUploadEntity(); + console.log("uploadEntity", uploadEntity); + + if (uploadEntity) { + if (uploadEntity.event === "drop" && "file" in uploadEntity) { + uploadFile(uploadEntity.file); + } else if (uploadEntity.event === "insert" && fileInputRef.current && !hasTriggeredFilePickerRef.current) { + fileInputRef.current.click(); + hasTriggeredFilePickerRef.current = true; + } + } + }, [getUploadEntity, uploadFile]); + + const existingFile = React.useMemo(() => { + const entity = getUploadEntity(); + return entity && entity.event === "drop" ? entity.file : undefined; + }, [getUploadEntity]); + + return ( + +
+ +
+
+ ); +}; + +export default ImageUpload; diff --git a/packages/editor/src/core/extensions/image-upload/view/image-uploader.tsx b/packages/editor/src/core/extensions/image-upload/view/image-uploader.tsx new file mode 100644 index 00000000000..e6367247118 --- /dev/null +++ b/packages/editor/src/core/extensions/image-upload/view/image-uploader.tsx @@ -0,0 +1,82 @@ +import { ChangeEvent, useCallback, useEffect } from "react"; +import { Editor } from "@tiptap/core"; +import { ImageIcon } from "lucide-react"; +import { Spinner } from "@plane/ui"; +import { cn } from "@/helpers/common"; +import { useDropZone, useFileUpload, useUploader } from "../../../hooks/use-file-upload"; + +export const ImageUploader = ({ + onUpload, + editor, + fileInputRef, + existingFile, +}: { + onUpload: (url: string) => void; + editor: Editor; + fileInputRef: React.RefObject | ((ref: HTMLInputElement) => void); + existingFile?: File; +}) => { + const { loading, uploadFile } = useUploader({ onUpload, editor }); + const { handleUploadClick, ref: internalRef } = useFileUpload(); + const { draggedInside, onDrop, onDragEnter, onDragLeave } = useDropZone({ uploader: uploadFile }); + + const onFileChange = useCallback( + (e: ChangeEvent) => (e.target.files ? uploadFile(e.target.files[0]) : null), + [uploadFile] + ); + + useEffect(() => { + if (existingFile) { + uploadFile(existingFile); + } + }, [existingFile, uploadFile]); + + if (loading) { + return ( +
+ +
+ ); + } + + const wrapperClass = cn( + "flex justify-start px-2 py-2 rounded-lg bg-opacity-80 border-2 border-dotted border-custom-border-400 cursor-pointer gap-2", + "transition-all duration-200 ease-in-out", + "hover:bg-custom-background-80 hover:border-custom-border-200", + draggedInside && "bg-custom-background-80" + ); + + return ( +
+ +
+
+ {draggedInside ? "Drop image here" : existingFile ? "Uploading..." : "Add an image"} +
+
+ { + internalRef.current = el; + if (typeof fileInputRef === "function") { + fileInputRef(el); + } else if (fileInputRef) { + fileInputRef.current = el; + } + }} + type="file" + accept=".jpg,.jpeg,.png,.webp" + onChange={onFileChange} + /> +
+ ); +}; + +export default ImageUploader; diff --git a/packages/editor/src/core/extensions/image-upload/view/index.tsx b/packages/editor/src/core/extensions/image-upload/view/index.tsx new file mode 100644 index 00000000000..0d0b9c26381 --- /dev/null +++ b/packages/editor/src/core/extensions/image-upload/view/index.tsx @@ -0,0 +1 @@ +export * from "./image-upload"; diff --git a/packages/editor/src/core/extensions/image/image-resize.tsx b/packages/editor/src/core/extensions/image/image-resize.tsx index c50e3189660..334c402bc9f 100644 --- a/packages/editor/src/core/extensions/image/image-resize.tsx +++ b/packages/editor/src/core/extensions/image/image-resize.tsx @@ -8,7 +8,7 @@ type Props = { }; const getImageElement = (editorId: string): HTMLImageElement | null => - document.querySelector(`#editor-container-${editorId}.active-editor .ProseMirror-selectednode`); + document.querySelector(`#editor-container-${editorId} .ProseMirror-selectednode`); export const ImageResizer = (props: Props) => { const { editor, id } = props; diff --git a/packages/editor/src/core/extensions/slash-commands.tsx b/packages/editor/src/core/extensions/slash-commands.tsx index a61198db2bd..5d4e706f783 100644 --- a/packages/editor/src/core/extensions/slash-commands.tsx +++ b/packages/editor/src/core/extensions/slash-commands.tsx @@ -28,13 +28,13 @@ import { toggleBulletList, toggleOrderedList, toggleTaskList, - insertImageCommand, toggleHeadingOne, toggleHeadingTwo, toggleHeadingThree, toggleHeadingFour, toggleHeadingFive, toggleHeadingSix, + insertImageCommand, } from "@/helpers/editor-commands"; // types import { CommandProps, ISlashCommandItem, UploadImage } from "@/types"; @@ -224,11 +224,11 @@ const getSuggestionItems = { key: "image", title: "Image", - description: "Upload an image from your computer.", - searchTerms: ["img", "photo", "picture", "media"], icon: , + description: "Insert an image", + searchTerms: ["img", "photo", "picture", "media", "upload"], command: ({ editor, range }: CommandProps) => { - insertImageCommand(editor, uploadFile, null, range); + editor.chain().focus().deleteRange(range).setImageUpload({ event: "insert" }).run(); }, }, { diff --git a/packages/editor/src/core/helpers/editor-commands.ts b/packages/editor/src/core/helpers/editor-commands.ts index 7cf3e8d1f17..eebb69f0a6c 100644 --- a/packages/editor/src/core/helpers/editor-commands.ts +++ b/packages/editor/src/core/helpers/editor-commands.ts @@ -156,3 +156,23 @@ export const insertImageCommand = ( }; input.click(); }; + +export const insertImageNewCommand = (editor: Editor, saveSelection?: Selection | null, range?: Range) => { + // if (range) editor.chain().focus().deleteRange(range).setImageUpload(saveSelection).run(); + // const input = document.createElement("input"); + // input.type = "file"; + // input.accept = ".jpeg, .jpg, .png, .webp"; + // input.onchange = async () => { + // if (input.files?.length) { + // const file = input.files[0]; + // const pos = saveSelection?.anchor ?? editor.view.state.selection.from; + // const url = await (editor?.commands.uploadImage(file) as unknown as Promise); + // if (!url) { + // throw new Error("Something went wrong while uploading the image"); + // } + // editor.chain().setImageBlock({ src: url }).focus().run(); + // // startImageUpload(editor, file, editor.view, pos, uploadFile); + // } + // }; + // input.click(); +}; diff --git a/packages/editor/src/core/hooks/use-collaborative-editor.ts b/packages/editor/src/core/hooks/use-collaborative-editor.ts index eba56b099b8..cc504feb6fc 100644 --- a/packages/editor/src/core/hooks/use-collaborative-editor.ts +++ b/packages/editor/src/core/hooks/use-collaborative-editor.ts @@ -72,6 +72,7 @@ export const useCollaborativeEditor = (props: TCollaborativeEditorProps) => { handleEditorReady, forwardedRef, mentionHandler, + provider, extensions: [ SideMenuExtension({ aiEnabled: !disabledExtensions?.includes("ai"), diff --git a/packages/editor/src/core/hooks/use-editor.ts b/packages/editor/src/core/hooks/use-editor.ts index e1349b9c1ec..a837a7491d4 100644 --- a/packages/editor/src/core/hooks/use-editor.ts +++ b/packages/editor/src/core/hooks/use-editor.ts @@ -15,6 +15,7 @@ import { IMarking, scrollSummary } from "@/helpers/scroll-to-node"; import { CoreEditorProps } from "@/props"; // types import { EditorRefApi, IMentionHighlight, IMentionSuggestion, TEditorCommands, TFileHandler } from "@/types"; +import { HocuspocusProvider } from "@hocuspocus/provider"; export interface CustomEditorProps { editorClassName: string; @@ -36,6 +37,7 @@ export interface CustomEditorProps { // undefined when prop is not passed, null if intentionally passed to stop // swr syncing value?: string | null | undefined; + provider?: HocuspocusProvider | null; } export const useEditor = (props: CustomEditorProps) => { @@ -54,6 +56,7 @@ export const useEditor = (props: CustomEditorProps) => { placeholder, tabIndex, value, + provider, } = props; // states const [savedSelection, setSavedSelection] = useState(null); @@ -67,6 +70,11 @@ export const useEditor = (props: CustomEditorProps) => { }), ...editorProps, }, + shouldRerenderOnTransaction: false, + immediatelyRender: true, + onContentError: (error) => { + console.error("Error rendering content:", error); + }, extensions: [ ...CoreEditorExtensions({ enableHistory, @@ -82,6 +90,7 @@ export const useEditor = (props: CustomEditorProps) => { }, placeholder, tabIndex, + provider, }), ...extensions, ], @@ -218,16 +227,22 @@ export const useEditor = (props: CustomEditorProps) => { return selection; }, insertText: (contentHTML, insertOnNextLine) => { - if (!editor) return; + if (!editorRef.current) return; // get selection - const { from, to, empty } = editor.state.selection; + const { from, to, empty } = editorRef.current.state.selection; if (empty) return; if (insertOnNextLine) { // move cursor to the end of the selection and insert a new line - editor.chain().focus().setTextSelection(to).insertContent("
").insertContent(contentHTML).run(); + editorRef.current + .chain() + .focus() + .setTextSelection(to) + .insertContent("
") + .insertContent(contentHTML) + .run(); } else { // replace selected text with the content provided - editor.chain().focus().deleteRange({ from, to }).insertContent(contentHTML).run(); + editorRef.current.chain().focus().deleteRange({ from, to }).insertContent(contentHTML).run(); } }, documentInfo: { diff --git a/packages/editor/src/core/hooks/use-file-upload.ts b/packages/editor/src/core/hooks/use-file-upload.ts new file mode 100644 index 00000000000..97a09f0588b --- /dev/null +++ b/packages/editor/src/core/hooks/use-file-upload.ts @@ -0,0 +1,106 @@ +import { DragEvent, useCallback, useEffect, useRef, useState } from "react"; +import { Editor } from "@tiptap/core"; + +export const useUploader = ({ onUpload, editor }: { onUpload: (url: string) => void; editor: Editor }) => { + const [loading, setLoading] = useState(false); + + const uploadFile = useCallback( + async (file: File) => { + setLoading(true); + try { + const url = await editor?.commands.uploadImage(file); + + if (!url) { + throw new Error("Something went wrong while uploading the image"); + } + onUpload(url); + } catch (errPayload: any) { + console.log(errPayload); + const error = errPayload?.response?.data?.error || "Something went wrong"; + console.error(error); + } + setLoading(false); + }, + [onUpload, editor] + ); + + return { loading, uploadFile }; +}; + +export const useFileUpload = () => { + const fileInput = useRef(null); + + const handleUploadClick = useCallback(() => { + fileInput.current?.click(); + }, []); + + return { ref: fileInput, handleUploadClick }; +}; + +export const useDropZone = ({ uploader }: { uploader: (file: File) => void }) => { + const [isDragging, setIsDragging] = useState(false); + const [draggedInside, setDraggedInside] = useState(false); + + useEffect(() => { + const dragStartHandler = () => { + setIsDragging(true); + }; + + const dragEndHandler = () => { + setIsDragging(false); + }; + + document.body.addEventListener("dragstart", dragStartHandler); + document.body.addEventListener("dragend", dragEndHandler); + + return () => { + document.body.removeEventListener("dragstart", dragStartHandler); + document.body.removeEventListener("dragend", dragEndHandler); + }; + }, []); + + const onDrop = useCallback( + (e: DragEvent) => { + setDraggedInside(false); + 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); + } + }, + [uploader] + ); + + const onDragEnter = () => { + setDraggedInside(true); + }; + + const onDragLeave = () => { + setDraggedInside(false); + }; + + return { isDragging, draggedInside, onDragEnter, onDragLeave, onDrop }; +}; diff --git a/packages/editor/src/styles/drag-drop.css b/packages/editor/src/styles/drag-drop.css index 3bea5dcf256..848dc7fe48f 100644 --- a/packages/editor/src/styles/drag-drop.css +++ b/packages/editor/src/styles/drag-drop.css @@ -39,7 +39,7 @@ } /* end ai handle */ -.ProseMirror:not(.dragging) .ProseMirror-selectednode { +.ProseMirror:not(.dragging) .ProseMirror-selectednode:not(.node-imageBlock) { position: relative; cursor: grab; outline: none !important; @@ -63,6 +63,14 @@ border-radius: 4px; pointer-events: none; } + + &.node-imageUpload { + --horizontal-offset: 0px; + + &::after { + background-color: rgba(var(--color-background-100), 0.2); + } + } } /* for targeting the task list items */ @@ -96,7 +104,7 @@ ol > li:nth-child(n + 100).ProseMirror-selectednode:not(.dragging)::after { margin-left: -35px; } -.ProseMirror img { +.ProseMirror node-imageBlock { transition: filter 0.1s ease-in-out; cursor: pointer; From 5db937cbf75b01f31cf268a170f14fa90b6e3da9 Mon Sep 17 00:00:00 2001 From: Palanikannan M Date: Wed, 11 Sep 2024 20:46:25 +0530 Subject: [PATCH 04/29] fix: combine two extensions --- .../editor/src/core/extensions/extensions.tsx | 3 +- .../components/image-block-view.tsx | 40 +++-- .../extensions/image-block/image-block.ts | 147 +++++++++++++----- .../extensions/image-upload/image-upload.ts | 4 +- .../image-upload/view/image-upload.tsx | 51 ++++-- packages/editor/src/core/hooks/use-editor.ts | 1 - 6 files changed, 170 insertions(+), 76 deletions(-) diff --git a/packages/editor/src/core/extensions/extensions.tsx b/packages/editor/src/core/extensions/extensions.tsx index 5b34c644dd6..95625410ad1 100644 --- a/packages/editor/src/core/extensions/extensions.tsx +++ b/packages/editor/src/core/extensions/extensions.tsx @@ -110,7 +110,7 @@ export const CoreEditorExtensions = ({ class: "rounded-md", }, }), - ImageUpload({ + ImageBlock({ deleteFile, restoreFile, uploadFile, @@ -118,7 +118,6 @@ export const CoreEditorExtensions = ({ }).configure({ clientId: provider?.document?.clientID, }), - ImageBlock, TiptapUnderline, TextStyle, TaskList.configure({ diff --git a/packages/editor/src/core/extensions/image-block/components/image-block-view.tsx b/packages/editor/src/core/extensions/image-block/components/image-block-view.tsx index 6d8b7cdb432..a7cf3926b13 100644 --- a/packages/editor/src/core/extensions/image-block/components/image-block-view.tsx +++ b/packages/editor/src/core/extensions/image-block/components/image-block-view.tsx @@ -1,6 +1,6 @@ import React, { useRef, useState, useCallback, useEffect } from "react"; import { Node as ProsemirrorNode } from "@tiptap/pm/model"; -import { Editor, NodeViewWrapper } from "@tiptap/react"; +import { Editor } from "@tiptap/react"; interface ImageBlockViewProps { editor: Editor; @@ -96,26 +96,24 @@ export const ImageBlockView: React.FC = (props) => { }, [handleResize, handleResizeEnd]); return ( - -
- - {isSelected && ( - <> -
-
- - )} -
- +
+ + {isSelected && ( + <> +
+
+ + )} +
); }; diff --git a/packages/editor/src/core/extensions/image-block/image-block.ts b/packages/editor/src/core/extensions/image-block/image-block.ts index 8bf50745888..00f137aa4e5 100644 --- a/packages/editor/src/core/extensions/image-block/image-block.ts +++ b/packages/editor/src/core/extensions/image-block/image-block.ts @@ -1,8 +1,10 @@ +import { UploadImage, DeleteImage, RestoreImage } from "@/types"; import { mergeAttributes, Range } from "@tiptap/core"; import { Image } from "@tiptap/extension-image"; import { ReactNodeViewRenderer } from "@tiptap/react"; - -import { ImageBlockView } from "./components/image-block-view"; +import { v4 as uuidv4 } from "uuid"; +import { UploadImageExtensionStorage } from "../image-upload"; +import { ImageUpload } from "../image-upload/view"; declare module "@tiptap/core" { interface Commands { @@ -18,47 +20,110 @@ declare module "@tiptap/core" { } } -export const ImageBlock = Image.extend({ - name: "imageBlock", - group: "inline", - draggable: true, +export const ImageBlock = ({ + uploadFile, + // deleteFile, + // restoreFile, + // cancelUploadImage, +}: { + uploadFile: UploadImage; + deleteFile: DeleteImage; + restoreFile: RestoreImage; + cancelUploadImage?: () => void; +}) => + Image.extend<{}, UploadImageExtensionStorage>({ + name: "imageBlock", + group: "inline", + draggable: true, - addAttributes() { - return { - ...this.parent?.(), - width: { - default: "35%", - }, - height: { - default: "auto", - }, - }; - }, + addAttributes() { + return { + ...this.parent?.(), + width: { + default: "35%", + }, + src: { + default: null, + }, + height: { + default: "auto", + }, + ["data-type"]: { + default: this.name, + }, + ["data-file"]: { + default: null, + }, + ["id"]: { + default: null, + }, + }; + }, - addCommands() { - return { - setImageBlock: - (attrs) => - ({ commands }) => - commands.insertContent({ - type: this.name, - attrs: { src: attrs.src }, - }), - setImageBlockAt: - (attrs) => - ({ commands }) => - commands.insertContentAt(attrs.pos, { - type: this.name, - attrs: { src: attrs.src }, - }), - }; - }, + parseHTML() { + return [ + { + tag: "image-block", + }, + ]; + }, + + renderHTML({ HTMLAttributes }) { + return ["image-block", mergeAttributes(HTMLAttributes)]; + }, + + addStorage() { + return { + fileMap: new Map(), + }; + }, + + addCommands() { + return { + setImageBlock: + (attrs) => + ({ commands }) => + commands.insertContent({ + type: this.name, + attrs: { src: attrs.src }, + }), + setImageBlockAt: + (attrs) => + ({ commands }) => + commands.insertContentAt(attrs.pos, { + type: this.name, + attrs: { src: attrs.src }, + }), + setImageUpload: + (props: { file?: File; pos?: number; event: "insert" | "replace" | "drop" }) => + ({ commands }) => { + const fileId = uuidv4(); + if (props?.file && props?.event === "drop") { + (this.editor.storage.imageBlock as UploadImageExtensionStorage).fileMap.set(fileId, { + file: props.file, + event: props.event, + }); + } else if (props.event !== "drop") { + (this.editor.storage.imageBlock as UploadImageExtensionStorage).fileMap.set(fileId, { + event: props.event, + }); + } + return commands.insertContent( + `` + ); + }, + uploadImage: (file: File) => async () => { + const fileUrl = await uploadFile(file); + return fileUrl; + }, + }; + }, - addNodeView() { - return ReactNodeViewRenderer(ImageBlockView); - }, -}).configure({ - inline: true, -}); + addNodeView() { + return ReactNodeViewRenderer(ImageUpload); + }, + }).configure({ + inline: true, + }); export default ImageBlock; diff --git a/packages/editor/src/core/extensions/image-upload/image-upload.ts b/packages/editor/src/core/extensions/image-upload/image-upload.ts index 1569baf492f..f3913bc4212 100644 --- a/packages/editor/src/core/extensions/image-upload/image-upload.ts +++ b/packages/editor/src/core/extensions/image-upload/image-upload.ts @@ -95,12 +95,12 @@ export const ImageUpload = ({ ({ commands }) => { const fileId = uuidv4(); if (props?.file && props?.event === "drop") { - (this.editor.storage.imageUpload as UploadImageExtensionStorage).fileMap.set(fileId, { + (this.editor.storage.imageBlock as UploadImageExtensionStorage).fileMap.set(fileId, { file: props.file, event: props.event, }); } else if (props.event !== "drop") { - (this.editor.storage.imageUpload as UploadImageExtensionStorage).fileMap.set(fileId, { + (this.editor.storage.imageBlock as UploadImageExtensionStorage).fileMap.set(fileId, { event: props.event, }); } diff --git a/packages/editor/src/core/extensions/image-upload/view/image-upload.tsx b/packages/editor/src/core/extensions/image-upload/view/image-upload.tsx index cabfb3386f5..cfbbd8ef278 100644 --- a/packages/editor/src/core/extensions/image-upload/view/image-upload.tsx +++ b/packages/editor/src/core/extensions/image-upload/view/image-upload.tsx @@ -1,21 +1,30 @@ -import React, { useCallback, useEffect, useRef } from "react"; -import { Node } from "@tiptap/pm/model"; +import React, { useCallback, useEffect, useRef, useState } from "react"; +import { Node as ProsemirrorNode } from "@tiptap/pm/model"; import { Editor, NodeViewWrapper } from "@tiptap/react"; +import ImageBlockView from "@/extensions/image-block/components/image-block-view"; import { UploadImageExtensionStorage, UploadEntity } from "../image-upload"; import { ImageUploader } from "./image-uploader"; interface ImageUploadProps { getPos: () => number; editor: Editor; - node: Node; + node: ProsemirrorNode & { + attrs: { + src: string; + width: string; + height: string; + }; + }; } -export const ImageUpload: React.FC = ({ getPos, editor, node }) => { +export const ImageUpload: React.FC = ({ getPos, editor, node, updateAttributes }) => { const fileInputRef = useRef(null); const hasTriggeredFilePickerRef = useRef(false); + const [isUploaded, setIsUploaded] = useState(!!node.attrs.src); + const [uploadedImageUrl, setUploadedImageUrl] = useState(""); const id = node.attrs.id as string; - const editorStorage = editor.storage.imageUpload as UploadImageExtensionStorage | undefined; + const editorStorage = editor.storage.imageBlock as UploadImageExtensionStorage | undefined; const getUploadEntity = useCallback( (): UploadEntity | undefined => editorStorage?.fileMap.get(id), @@ -25,10 +34,14 @@ export const ImageUpload: React.FC = ({ getPos, editor, node } const onUpload = useCallback( (url: string) => { if (url) { - editor.chain().setImageBlock({ src: url }).deleteRange({ from: getPos(), to: getPos() }).focus().run(); + setUploadedImageUrl(url); + setIsUploaded(true); + // Update the node attributes with the new image URL + updateAttributes({ src: url }); + editorStorage?.fileMap.delete(id); } }, - [editor, getPos] + [editorStorage?.fileMap, id, updateAttributes] ); const uploadFile = useCallback( @@ -40,6 +53,7 @@ export const ImageUpload: React.FC = ({ getPos, editor, node } } } catch (error) { console.error("Error uploading file:", error); + // Handle error state here if needed } }, [editor.commands, onUpload] @@ -47,7 +61,6 @@ export const ImageUpload: React.FC = ({ getPos, editor, node } useEffect(() => { const uploadEntity = getUploadEntity(); - console.log("uploadEntity", uploadEntity); if (uploadEntity) { if (uploadEntity.event === "drop" && "file" in uploadEntity) { @@ -59,6 +72,12 @@ export const ImageUpload: React.FC = ({ getPos, editor, node } } }, [getUploadEntity, uploadFile]); + useEffect(() => { + if (node.attrs.src) { + setIsUploaded(true); + } + }, [node.attrs]); + const existingFile = React.useMemo(() => { const entity = getUploadEntity(); return entity && entity.event === "drop" ? entity.file : undefined; @@ -67,7 +86,21 @@ export const ImageUpload: React.FC = ({ getPos, editor, node } return (
- + {isUploaded ? ( + editor.commands.updateAttributes("imageBlock", attrs)} + /> + ) : ( + + )}
); diff --git a/packages/editor/src/core/hooks/use-editor.ts b/packages/editor/src/core/hooks/use-editor.ts index a837a7491d4..273465442ab 100644 --- a/packages/editor/src/core/hooks/use-editor.ts +++ b/packages/editor/src/core/hooks/use-editor.ts @@ -223,7 +223,6 @@ export const useEditor = (props: CustomEditorProps) => { } }); const selection = nodesArray.join(""); - console.log(selection); return selection; }, insertText: (contentHTML, insertOnNextLine) => { From 3d81be7da89903ca7cf68ab17c58a68abf5fc943 Mon Sep 17 00:00:00 2001 From: Palanikannan M Date: Wed, 11 Sep 2024 21:17:54 +0530 Subject: [PATCH 05/29] fix: added new image extension to backend --- .../src/core/extensions/core-without-props.ts | 2 + .../editor/src/core/extensions/extensions.tsx | 6 +- .../image-upload/view/image-upload.tsx | 2 +- .../image-upload/view/image-uploader.tsx | 17 +---- .../image/image-block-without-props.tsx | 70 +++++++++++++++++++ 5 files changed, 78 insertions(+), 19 deletions(-) create mode 100644 packages/editor/src/core/extensions/image/image-block-without-props.tsx diff --git a/packages/editor/src/core/extensions/core-without-props.ts b/packages/editor/src/core/extensions/core-without-props.ts index c0f066c3ff9..26f9b2ca8eb 100644 --- a/packages/editor/src/core/extensions/core-without-props.ts +++ b/packages/editor/src/core/extensions/core-without-props.ts @@ -11,6 +11,7 @@ import { CustomCodeInlineExtension } from "./code-inline"; import { CustomLinkExtension } from "./custom-link"; import { CustomHorizontalRule } from "./horizontal-rule"; import { ImageExtensionWithoutProps } from "./image"; +import { ImageBlockWithoutProps } from "./image/image-block-without-props"; import { IssueWidgetWithoutProps } from "./issue-embed/issue-embed-without-props"; import { CustomMentionWithoutProps } from "./mentions/mentions-without-props"; import { CustomQuoteExtension } from "./quote"; @@ -61,6 +62,7 @@ export const CoreEditorExtensionsWithoutProps = [ class: "rounded-md", }, }), + ImageBlockWithoutProps(), TiptapUnderline, TextStyle, TaskList.configure({ diff --git a/packages/editor/src/core/extensions/extensions.tsx b/packages/editor/src/core/extensions/extensions.tsx index 95625410ad1..de979e51772 100644 --- a/packages/editor/src/core/extensions/extensions.tsx +++ b/packages/editor/src/core/extensions/extensions.tsx @@ -1,3 +1,4 @@ +import { HocuspocusProvider } from "@hocuspocus/provider"; import CharacterCount from "@tiptap/extension-character-count"; import Placeholder from "@tiptap/extension-placeholder"; import TaskItem from "@tiptap/extension-task-item"; @@ -29,10 +30,7 @@ import { import { isValidHttpUrl } from "@/helpers/common"; // types import { DeleteImage, IMentionHighlight, IMentionSuggestion, RestoreImage, UploadImage } from "@/types"; -import { TrailingNode } from "./trailing-node"; import { ImageBlock } from "./image-block"; -import { ImageUpload } from "./image-upload"; -import { HocuspocusProvider } from "@hocuspocus/provider"; type TArguments = { enableHistory: boolean; @@ -85,7 +83,7 @@ export const CoreEditorExtensions = ({ ...(enableHistory ? {} : { history: false }), }), CustomQuoteExtension, - DropHandlerExtension(uploadFile), + DropHandlerExtension(), CustomHorizontalRule.configure({ HTMLAttributes: { class: "my-4 border-custom-border-400", diff --git a/packages/editor/src/core/extensions/image-upload/view/image-upload.tsx b/packages/editor/src/core/extensions/image-upload/view/image-upload.tsx index cfbbd8ef278..d2b0c3d2e63 100644 --- a/packages/editor/src/core/extensions/image-upload/view/image-upload.tsx +++ b/packages/editor/src/core/extensions/image-upload/view/image-upload.tsx @@ -96,7 +96,7 @@ export const ImageUpload: React.FC = ({ getPos, editor, node, ...node.attrs, }, }} - updateAttributes={(attrs) => editor.commands.updateAttributes("imageBlock", attrs)} + updateAttributes={updateAttributes} /> ) : ( diff --git a/packages/editor/src/core/extensions/image-upload/view/image-uploader.tsx b/packages/editor/src/core/extensions/image-upload/view/image-uploader.tsx index e6367247118..04b7671e9ab 100644 --- a/packages/editor/src/core/extensions/image-upload/view/image-uploader.tsx +++ b/packages/editor/src/core/extensions/image-upload/view/image-uploader.tsx @@ -1,7 +1,6 @@ import { ChangeEvent, useCallback, useEffect } from "react"; import { Editor } from "@tiptap/core"; import { ImageIcon } from "lucide-react"; -import { Spinner } from "@plane/ui"; import { cn } from "@/helpers/common"; import { useDropZone, useFileUpload, useUploader } from "../../../hooks/use-file-upload"; @@ -31,14 +30,6 @@ export const ImageUploader = ({ } }, [existingFile, uploadFile]); - if (loading) { - return ( -
- -
- ); - } - const wrapperClass = cn( "flex justify-start px-2 py-2 rounded-lg bg-opacity-80 border-2 border-dotted border-custom-border-400 cursor-pointer gap-2", "transition-all duration-200 ease-in-out", @@ -55,11 +46,9 @@ export const ImageUploader = ({ contentEditable={false} onClick={handleUploadClick} > - -
-
- {draggedInside ? "Drop image here" : existingFile ? "Uploading..." : "Add an image"} -
+ +
+ {loading ? "Uploading..." : draggedInside ? "Drop image here" : existingFile ? "Uploading..." : "Add an image"}
{ + imageBlock: { + setImageBlock: (attributes: { src: string; width?: number; height?: number }) => ReturnType; + setImageBlockAt: (attributes: { + src: string; + pos: number | Range; + width?: number; + height?: number; + }) => ReturnType; + }; + } +} + +export const ImageBlockWithoutProps = () => + Image.extend<{}, UploadImageExtensionStorage>({ + name: "imageBlock", + group: "inline", + draggable: true, + + addAttributes() { + return { + ...this.parent?.(), + width: { + default: "35%", + }, + src: { + default: null, + }, + height: { + default: "auto", + }, + ["data-type"]: { + default: this.name, + }, + ["data-file"]: { + default: null, + }, + ["id"]: { + default: null, + }, + }; + }, + + parseHTML() { + return [ + { + tag: "image-block", + }, + ]; + }, + + renderHTML({ HTMLAttributes }) { + return ["image-block", mergeAttributes(HTMLAttributes)]; + }, + + addStorage() { + return { + fileMap: new Map(), + }; + }, + }).configure({ + inline: true, + }); + +export default ImageBlockWithoutProps; From bd5482afef5d404b353e1884748514e8dba4ba2c Mon Sep 17 00:00:00 2001 From: Palanikannan M Date: Thu, 12 Sep 2024 12:58:49 +0530 Subject: [PATCH 06/29] fix: type errors --- .../image-upload/view/image-uploader.tsx | 31 ++++++++++++------- 1 file changed, 20 insertions(+), 11 deletions(-) diff --git a/packages/editor/src/core/extensions/image-upload/view/image-uploader.tsx b/packages/editor/src/core/extensions/image-upload/view/image-uploader.tsx index 04b7671e9ab..c362bfc3a01 100644 --- a/packages/editor/src/core/extensions/image-upload/view/image-uploader.tsx +++ b/packages/editor/src/core/extensions/image-upload/view/image-uploader.tsx @@ -1,8 +1,18 @@ -import { ChangeEvent, useCallback, useEffect } from "react"; +import { ChangeEvent, useCallback, useEffect, useRef } from "react"; import { Editor } from "@tiptap/core"; import { ImageIcon } from "lucide-react"; import { cn } from "@/helpers/common"; -import { useDropZone, useFileUpload, useUploader } from "../../../hooks/use-file-upload"; +import { useUploader, useFileUpload, useDropZone } from "@/hooks/use-file-upload"; + +type RefType = React.RefObject | ((instance: HTMLInputElement | null) => void); + +const assignRef = (ref: RefType, value: HTMLInputElement | null) => { + if (typeof ref === "function") { + ref(value); + } else if (ref && typeof ref === "object") { + (ref as React.MutableRefObject).current = value; + } +}; export const ImageUploader = ({ onUpload, @@ -12,13 +22,15 @@ export const ImageUploader = ({ }: { onUpload: (url: string) => void; editor: Editor; - fileInputRef: React.RefObject | ((ref: HTMLInputElement) => void); + fileInputRef: RefType; existingFile?: File; }) => { const { loading, uploadFile } = useUploader({ onUpload, editor }); const { handleUploadClick, ref: internalRef } = useFileUpload(); const { draggedInside, onDrop, onDragEnter, onDragLeave } = useDropZone({ uploader: uploadFile }); + const localRef = useRef(null); + const onFileChange = useCallback( (e: ChangeEvent) => (e.target.files ? uploadFile(e.target.files[0]) : null), [uploadFile] @@ -31,7 +43,7 @@ export const ImageUploader = ({ }, [existingFile, uploadFile]); const wrapperClass = cn( - "flex justify-start px-2 py-2 rounded-lg bg-opacity-80 border-2 border-dotted border-custom-border-400 cursor-pointer gap-2", + "flex items-center justify-start px-2 py-2 rounded-lg bg-opacity-80 border-2 border-dotted border-custom-border-400 cursor-pointer gap-2", "transition-all duration-200 ease-in-out", "hover:bg-custom-background-80 hover:border-custom-border-200", draggedInside && "bg-custom-background-80" @@ -52,13 +64,10 @@ export const ImageUploader = ({
{ - internalRef.current = el; - if (typeof fileInputRef === "function") { - fileInputRef(el); - } else if (fileInputRef) { - fileInputRef.current = el; - } + ref={(element) => { + localRef.current = element; + assignRef(fileInputRef, element); + assignRef(internalRef as RefType, element); }} type="file" accept=".jpg,.jpeg,.png,.webp" From f080e3f0988a95e219f730f411dbadfbddcbd9d9 Mon Sep 17 00:00:00 2001 From: Aaryan Khandelwal Date: Thu, 12 Sep 2024 13:35:04 +0530 Subject: [PATCH 07/29] style: image drop node --- .../src/ce/extensions/document-extensions.tsx | 7 +++--- .../components/editors/rich-text/editor.tsx | 2 +- .../image-upload/view/image-uploader.tsx | 22 +++++++++---------- .../src/core/extensions/slash-commands.tsx | 6 ++--- .../core/hooks/use-collaborative-editor.ts | 1 - 5 files changed, 18 insertions(+), 20 deletions(-) diff --git a/packages/editor/src/ce/extensions/document-extensions.tsx b/packages/editor/src/ce/extensions/document-extensions.tsx index bf2937fcd63..6bb58021324 100644 --- a/packages/editor/src/ce/extensions/document-extensions.tsx +++ b/packages/editor/src/ce/extensions/document-extensions.tsx @@ -4,20 +4,19 @@ import { SlashCommand } from "@/extensions"; // plane editor types import { TIssueEmbedConfig } from "@/plane-editor/types"; // types -import { TExtensions, TFileHandler, TUserDetails } from "@/types"; +import { TExtensions, TUserDetails } from "@/types"; type Props = { disabledExtensions?: TExtensions[]; - fileHandler: TFileHandler; issueEmbedConfig: TIssueEmbedConfig | undefined; provider: HocuspocusProvider; userDetails: TUserDetails; }; export const DocumentEditorAdditionalExtensions = (props: Props) => { - const { fileHandler } = props; + const {} = props; - const extensions: Extensions = [SlashCommand(fileHandler.upload)]; + const extensions: Extensions = [SlashCommand()]; return extensions; }; diff --git a/packages/editor/src/core/components/editors/rich-text/editor.tsx b/packages/editor/src/core/components/editors/rich-text/editor.tsx index 28204237275..b1f9eed7d0f 100644 --- a/packages/editor/src/core/components/editors/rich-text/editor.tsx +++ b/packages/editor/src/core/components/editors/rich-text/editor.tsx @@ -11,7 +11,7 @@ const RichTextEditor = (props: IRichTextEditor) => { const { dragDropEnabled, fileHandler } = props; const getExtensions = useCallback(() => { - const extensions = [SlashCommand(fileHandler.upload)]; + const extensions = [SlashCommand()]; extensions.push( SideMenuExtension({ diff --git a/packages/editor/src/core/extensions/image-upload/view/image-uploader.tsx b/packages/editor/src/core/extensions/image-upload/view/image-uploader.tsx index c362bfc3a01..7ec16918b44 100644 --- a/packages/editor/src/core/extensions/image-upload/view/image-uploader.tsx +++ b/packages/editor/src/core/extensions/image-upload/view/image-uploader.tsx @@ -1,7 +1,9 @@ import { ChangeEvent, useCallback, useEffect, useRef } from "react"; import { Editor } from "@tiptap/core"; import { ImageIcon } from "lucide-react"; +// helpers import { cn } from "@/helpers/common"; +// hooks import { useUploader, useFileUpload, useDropZone } from "@/hooks/use-file-upload"; type RefType = React.RefObject | ((instance: HTMLInputElement | null) => void); @@ -42,28 +44,26 @@ export const ImageUploader = ({ } }, [existingFile, uploadFile]); - const wrapperClass = cn( - "flex items-center justify-start px-2 py-2 rounded-lg bg-opacity-80 border-2 border-dotted border-custom-border-400 cursor-pointer gap-2", - "transition-all duration-200 ease-in-out", - "hover:bg-custom-background-80 hover:border-custom-border-200", - draggedInside && "bg-custom-background-80" - ); - return (
- -
+ +
{loading ? "Uploading..." : draggedInside ? "Drop image here" : existingFile ? "Uploading..." : "Add an image"}
{ localRef.current = element; assignRef(fileInputRef, element); diff --git a/packages/editor/src/core/extensions/slash-commands.tsx b/packages/editor/src/core/extensions/slash-commands.tsx index 5d4e706f783..15ffe0d5d03 100644 --- a/packages/editor/src/core/extensions/slash-commands.tsx +++ b/packages/editor/src/core/extensions/slash-commands.tsx @@ -89,7 +89,7 @@ const Command = Extension.create({ }); const getSuggestionItems = - (uploadFile: UploadImage, additionalOptions?: Array) => + (additionalOptions?: Array) => ({ query }: { query: string }) => { let slashCommands: ISlashCommandItem[] = [ { @@ -415,10 +415,10 @@ const renderItems = () => { }; }; -export const SlashCommand = (uploadFile: UploadImage, additionalOptions?: Array) => +export const SlashCommand = (additionalOptions?: Array) => Command.configure({ suggestion: { - items: getSuggestionItems(uploadFile, additionalOptions), + items: getSuggestionItems(additionalOptions), render: renderItems, }, }); diff --git a/packages/editor/src/core/hooks/use-collaborative-editor.ts b/packages/editor/src/core/hooks/use-collaborative-editor.ts index cc504feb6fc..a16480d5fad 100644 --- a/packages/editor/src/core/hooks/use-collaborative-editor.ts +++ b/packages/editor/src/core/hooks/use-collaborative-editor.ts @@ -84,7 +84,6 @@ export const useCollaborativeEditor = (props: TCollaborativeEditorProps) => { ...(extensions ?? []), ...DocumentEditorAdditionalExtensions({ disabledExtensions, - fileHandler, issueEmbedConfig: embedHandler?.issue, provider, userDetails: user, From 8eaf6ed0de19481aad8fa6e0f0844410824f8218 Mon Sep 17 00:00:00 2001 From: Aaryan Khandelwal Date: Thu, 12 Sep 2024 13:57:45 +0530 Subject: [PATCH 08/29] style: image resize handler --- .../image-block/components/image-block-view.tsx | 11 +++++++---- packages/editor/src/styles/editor.css | 2 +- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/packages/editor/src/core/extensions/image-block/components/image-block-view.tsx b/packages/editor/src/core/extensions/image-block/components/image-block-view.tsx index a7cf3926b13..96dd1891b54 100644 --- a/packages/editor/src/core/extensions/image-block/components/image-block-view.tsx +++ b/packages/editor/src/core/extensions/image-block/components/image-block-view.tsx @@ -101,14 +101,17 @@ export const ImageBlockView: React.FC = (props) => { ref={imageRef} src={src} alt="" - className="max-w-full object-contain" - style={{ width: size.width, height: size.height }} + className="max-w-full object-contain rounded-md" + style={{ + width: size.width, + height: size.height, + }} /> {isSelected && ( <> -
+
diff --git a/packages/editor/src/styles/editor.css b/packages/editor/src/styles/editor.css index 1117b7c5cb0..f1a0e26739a 100644 --- a/packages/editor/src/styles/editor.css +++ b/packages/editor/src/styles/editor.css @@ -123,7 +123,7 @@ /* Custom image styles */ .ProseMirror img { transition: filter 0.1s ease-in-out; - margin-top: 8px; + margin-top: 0 !important; margin-bottom: 0; &:hover { From d5a1d4e26ade85d2d2c76d911b8e5aeae6a76aa8 Mon Sep 17 00:00:00 2001 From: Palanikannan M Date: Thu, 12 Sep 2024 15:09:22 +0530 Subject: [PATCH 09/29] fix: removed unused stuff --- .../core/extensions/image-block/image-block.ts | 16 ++++++++++++---- .../image-upload/view/image-upload.tsx | 12 ++++++++---- .../src/core/extensions/slash-commands.tsx | 3 +-- 3 files changed, 21 insertions(+), 10 deletions(-) diff --git a/packages/editor/src/core/extensions/image-block/image-block.ts b/packages/editor/src/core/extensions/image-block/image-block.ts index 00f137aa4e5..d2973682306 100644 --- a/packages/editor/src/core/extensions/image-block/image-block.ts +++ b/packages/editor/src/core/extensions/image-block/image-block.ts @@ -1,8 +1,9 @@ -import { UploadImage, DeleteImage, RestoreImage } from "@/types"; import { mergeAttributes, Range } from "@tiptap/core"; import { Image } from "@tiptap/extension-image"; import { ReactNodeViewRenderer } from "@tiptap/react"; import { v4 as uuidv4 } from "uuid"; +import { UploadImage, DeleteImage, RestoreImage } from "@/types"; + import { UploadImageExtensionStorage } from "../image-upload"; import { ImageUpload } from "../image-upload/view"; @@ -108,9 +109,16 @@ export const ImageBlock = ({ event: props.event, }); } - return commands.insertContent( - `` - ); + const attributes = { + "data-type": this.name, + id: fileId, + "data-file": props?.file ? `data-file="${props.file}"` : "", + }; + + return commands.insertContent({ + type: this.name, + attrs: attributes, + }); }, uploadImage: (file: File) => async () => { const fileUrl = await uploadFile(file); diff --git a/packages/editor/src/core/extensions/image-upload/view/image-upload.tsx b/packages/editor/src/core/extensions/image-upload/view/image-upload.tsx index d2b0c3d2e63..50743d61ea8 100644 --- a/packages/editor/src/core/extensions/image-upload/view/image-upload.tsx +++ b/packages/editor/src/core/extensions/image-upload/view/image-upload.tsx @@ -21,7 +21,6 @@ export const ImageUpload: React.FC = ({ getPos, editor, node, const fileInputRef = useRef(null); const hasTriggeredFilePickerRef = useRef(false); const [isUploaded, setIsUploaded] = useState(!!node.attrs.src); - const [uploadedImageUrl, setUploadedImageUrl] = useState(""); const id = node.attrs.id as string; const editorStorage = editor.storage.imageBlock as UploadImageExtensionStorage | undefined; @@ -34,9 +33,8 @@ export const ImageUpload: React.FC = ({ getPos, editor, node, const onUpload = useCallback( (url: string) => { if (url) { - setUploadedImageUrl(url); setIsUploaded(true); - // Update the node attributes with the new image URL + // Update the node view's src attribute updateAttributes({ src: url }); editorStorage?.fileMap.delete(id); } @@ -99,7 +97,13 @@ export const ImageUpload: React.FC = ({ getPos, editor, node, updateAttributes={updateAttributes} /> ) : ( - + )}
diff --git a/packages/editor/src/core/extensions/slash-commands.tsx b/packages/editor/src/core/extensions/slash-commands.tsx index 15ffe0d5d03..3b1789781cf 100644 --- a/packages/editor/src/core/extensions/slash-commands.tsx +++ b/packages/editor/src/core/extensions/slash-commands.tsx @@ -34,10 +34,9 @@ import { toggleHeadingFour, toggleHeadingFive, toggleHeadingSix, - insertImageCommand, } from "@/helpers/editor-commands"; // types -import { CommandProps, ISlashCommandItem, UploadImage } from "@/types"; +import { CommandProps, ISlashCommandItem } from "@/types"; interface CommandItemProps { key: string; From 56a0b9f2361f6ea56cd3933a5b1c421207dd29e8 Mon Sep 17 00:00:00 2001 From: Palanikannan M Date: Thu, 12 Sep 2024 15:15:02 +0530 Subject: [PATCH 10/29] fix: types of updateAttributes --- .../image-upload/view/image-upload.tsx | 21 +++---------------- 1 file changed, 3 insertions(+), 18 deletions(-) diff --git a/packages/editor/src/core/extensions/image-upload/view/image-upload.tsx b/packages/editor/src/core/extensions/image-upload/view/image-upload.tsx index 50743d61ea8..7914f7129d4 100644 --- a/packages/editor/src/core/extensions/image-upload/view/image-upload.tsx +++ b/packages/editor/src/core/extensions/image-upload/view/image-upload.tsx @@ -15,6 +15,7 @@ interface ImageUploadProps { height: string; }; }; + updateAttributes: (attrs: Record) => void; } export const ImageUpload: React.FC = ({ getPos, editor, node, updateAttributes }) => { @@ -85,25 +86,9 @@ export const ImageUpload: React.FC = ({ getPos, editor, node,
{isUploaded ? ( - + ) : ( - + )}
From d8308cfce43946cec1c753dbaa27ae30df456800 Mon Sep 17 00:00:00 2001 From: Palanikannan M Date: Thu, 12 Sep 2024 16:26:00 +0530 Subject: [PATCH 11/29] fix: image insertion at pos and loading effect added --- .../src/core/components/menus/menu-items.ts | 9 ++-- .../src/core/extensions/core-without-props.ts | 4 +- .../editor/src/core/extensions/extensions.tsx | 4 +- .../components/image-block-view.tsx | 5 ++ .../image-block/components/image-loader.tsx | 3 ++ .../extensions/image-block/image-block.ts | 53 +++++++------------ .../extensions/image-upload/image-upload.ts | 27 ++-------- .../image-upload/view/image-upload.tsx | 2 +- ....tsx => image-component-without-props.tsx} | 26 +++------ packages/editor/src/styles/drag-drop.css | 4 +- 10 files changed, 50 insertions(+), 87 deletions(-) create mode 100644 packages/editor/src/core/extensions/image-block/components/image-loader.tsx rename packages/editor/src/core/extensions/image/{image-block-without-props.tsx => image-component-without-props.tsx} (58%) diff --git a/packages/editor/src/core/components/menus/menu-items.ts b/packages/editor/src/core/components/menus/menu-items.ts index 71d1576d8d3..63d76117ab4 100644 --- a/packages/editor/src/core/components/menus/menu-items.ts +++ b/packages/editor/src/core/components/menus/menu-items.ts @@ -189,16 +189,17 @@ export const TableItem = (editor: Editor): EditorMenuItem => ({ icon: TableIcon, }); -export const ImageItem = (editor: Editor, uploadFile: UploadImage) => +export const ImageItem = (editor: Editor) => ({ key: "image", name: "Image", isActive: () => editor?.isActive("image"), - command: (savedSelection: Selection | null) => editor?.commands.setImageUpload({ event: "insert" }), + command: (savedSelection: Selection | null) => + editor?.commands.setImageUpload({ event: "insert", pos: savedSelection?.from }), icon: ImageIcon, }) as const; -export function getEditorMenuItems(editor: Editor | null, uploadFile: UploadImage) { +export function getEditorMenuItems(editor: Editor | null) { if (!editor) { return []; } @@ -220,6 +221,6 @@ export function getEditorMenuItems(editor: Editor | null, uploadFile: UploadImag NumberedListItem(editor), QuoteItem(editor), TableItem(editor), - ImageItem(editor, uploadFile), + ImageItem(editor), ]; } diff --git a/packages/editor/src/core/extensions/core-without-props.ts b/packages/editor/src/core/extensions/core-without-props.ts index 26f9b2ca8eb..1cedd513966 100644 --- a/packages/editor/src/core/extensions/core-without-props.ts +++ b/packages/editor/src/core/extensions/core-without-props.ts @@ -11,7 +11,7 @@ import { CustomCodeInlineExtension } from "./code-inline"; import { CustomLinkExtension } from "./custom-link"; import { CustomHorizontalRule } from "./horizontal-rule"; import { ImageExtensionWithoutProps } from "./image"; -import { ImageBlockWithoutProps } from "./image/image-block-without-props"; +import { CustomImageComponentWithoutProps } from "./image/image-component-without-props"; import { IssueWidgetWithoutProps } from "./issue-embed/issue-embed-without-props"; import { CustomMentionWithoutProps } from "./mentions/mentions-without-props"; import { CustomQuoteExtension } from "./quote"; @@ -62,7 +62,7 @@ export const CoreEditorExtensionsWithoutProps = [ class: "rounded-md", }, }), - ImageBlockWithoutProps(), + CustomImageComponentWithoutProps(), TiptapUnderline, TextStyle, TaskList.configure({ diff --git a/packages/editor/src/core/extensions/extensions.tsx b/packages/editor/src/core/extensions/extensions.tsx index de979e51772..13093bfa33e 100644 --- a/packages/editor/src/core/extensions/extensions.tsx +++ b/packages/editor/src/core/extensions/extensions.tsx @@ -30,7 +30,7 @@ import { import { isValidHttpUrl } from "@/helpers/common"; // types import { DeleteImage, IMentionHighlight, IMentionSuggestion, RestoreImage, UploadImage } from "@/types"; -import { ImageBlock } from "./image-block"; +import { CustomImageComponent } from "./image-block"; type TArguments = { enableHistory: boolean; @@ -108,7 +108,7 @@ export const CoreEditorExtensions = ({ class: "rounded-md", }, }), - ImageBlock({ + CustomImageComponent({ deleteFile, restoreFile, uploadFile, diff --git a/packages/editor/src/core/extensions/image-block/components/image-block-view.tsx b/packages/editor/src/core/extensions/image-block/components/image-block-view.tsx index 96dd1891b54..87db97debe0 100644 --- a/packages/editor/src/core/extensions/image-block/components/image-block-view.tsx +++ b/packages/editor/src/core/extensions/image-block/components/image-block-view.tsx @@ -1,6 +1,7 @@ import React, { useRef, useState, useCallback, useEffect } from "react"; import { Node as ProsemirrorNode } from "@tiptap/pm/model"; import { Editor } from "@tiptap/react"; +import { ImageShimmer } from "./image-loader"; interface ImageBlockViewProps { editor: Editor; @@ -27,6 +28,7 @@ export const ImageBlockView: React.FC = (props) => { const imageRef = useRef(null); const isResizing = useRef(false); const aspectRatio = useRef(1); + const [isLoading, setIsLoading] = useState(true); useEffect(() => { if (imageRef.current) { @@ -39,6 +41,7 @@ export const ImageBlockView: React.FC = (props) => { const newHeight = newWidth / aspectRatio.current; setSize({ width: `${newWidth}px`, height: `${newHeight}px` }); } + setIsLoading(false); }; } }, [src, width, height]); @@ -97,6 +100,7 @@ export const ImageBlockView: React.FC = (props) => { return (
+ {isLoading ? : null} = (props) => { style={{ width: size.width, height: size.height, + display: isLoading ? "none" : "block", }} /> {isSelected && ( diff --git a/packages/editor/src/core/extensions/image-block/components/image-loader.tsx b/packages/editor/src/core/extensions/image-block/components/image-loader.tsx new file mode 100644 index 00000000000..52c62493aaa --- /dev/null +++ b/packages/editor/src/core/extensions/image-block/components/image-loader.tsx @@ -0,0 +1,3 @@ +export const ImageShimmer: React.FC<{ width: string; height: string }> = ({ width, height }) => ( +
+); diff --git a/packages/editor/src/core/extensions/image-block/image-block.ts b/packages/editor/src/core/extensions/image-block/image-block.ts index d2973682306..316c876f725 100644 --- a/packages/editor/src/core/extensions/image-block/image-block.ts +++ b/packages/editor/src/core/extensions/image-block/image-block.ts @@ -1,4 +1,4 @@ -import { mergeAttributes, Range } from "@tiptap/core"; +import { mergeAttributes } from "@tiptap/core"; import { Image } from "@tiptap/extension-image"; import { ReactNodeViewRenderer } from "@tiptap/react"; import { v4 as uuidv4 } from "uuid"; @@ -9,19 +9,14 @@ import { ImageUpload } from "../image-upload/view"; declare module "@tiptap/core" { interface Commands { - imageBlock: { - setImageBlock: (attributes: { src: string; width?: number; height?: number }) => ReturnType; - setImageBlockAt: (attributes: { - src: string; - pos: number | Range; - width?: number; - height?: number; - }) => ReturnType; + imageComponent: { + setImageUpload: ({ file, pos, event }: { file?: File; pos?: number; event: "insert" | "drop" }) => ReturnType; + uploadImage: (file: File) => () => Promise | undefined; }; } } -export const ImageBlock = ({ +export const CustomImageComponent = ({ uploadFile, // deleteFile, // restoreFile, @@ -33,7 +28,7 @@ export const ImageBlock = ({ cancelUploadImage?: () => void; }) => Image.extend<{}, UploadImageExtensionStorage>({ - name: "imageBlock", + name: "imageComponent", group: "inline", draggable: true, @@ -64,13 +59,13 @@ export const ImageBlock = ({ parseHTML() { return [ { - tag: "image-block", + tag: "image-component", }, ]; }, renderHTML({ HTMLAttributes }) { - return ["image-block", mergeAttributes(HTMLAttributes)]; + return ["image-component", mergeAttributes(HTMLAttributes)]; }, addStorage() { @@ -81,31 +76,17 @@ export const ImageBlock = ({ addCommands() { return { - setImageBlock: - (attrs) => - ({ commands }) => - commands.insertContent({ - type: this.name, - attrs: { src: attrs.src }, - }), - setImageBlockAt: - (attrs) => - ({ commands }) => - commands.insertContentAt(attrs.pos, { - type: this.name, - attrs: { src: attrs.src }, - }), setImageUpload: - (props: { file?: File; pos?: number; event: "insert" | "replace" | "drop" }) => + (props: { file?: File; pos?: number; event: "insert" | "drop" }) => ({ commands }) => { const fileId = uuidv4(); - if (props?.file && props?.event === "drop") { - (this.editor.storage.imageBlock as UploadImageExtensionStorage).fileMap.set(fileId, { + if (props?.event === "drop" && props.file) { + (this.editor.storage.imageComponent as UploadImageExtensionStorage).fileMap.set(fileId, { file: props.file, event: props.event, }); - } else if (props.event !== "drop") { - (this.editor.storage.imageBlock as UploadImageExtensionStorage).fileMap.set(fileId, { + } else if (props.event === "insert") { + (this.editor.storage.imageComponent as UploadImageExtensionStorage).fileMap.set(fileId, { event: props.event, }); } @@ -115,6 +96,12 @@ export const ImageBlock = ({ "data-file": props?.file ? `data-file="${props.file}"` : "", }; + if (props.pos) { + return commands.insertContentAt(props.pos, { + type: this.name, + attrs: attributes, + }); + } return commands.insertContent({ type: this.name, attrs: attributes, @@ -134,4 +121,4 @@ export const ImageBlock = ({ inline: true, }); -export default ImageBlock; +export default CustomImageComponent; diff --git a/packages/editor/src/core/extensions/image-upload/image-upload.ts b/packages/editor/src/core/extensions/image-upload/image-upload.ts index f3913bc4212..141eb2d2824 100644 --- a/packages/editor/src/core/extensions/image-upload/image-upload.ts +++ b/packages/editor/src/core/extensions/image-upload/image-upload.ts @@ -3,30 +3,11 @@ import { v4 as uuidv4 } from "uuid"; import { DeleteImage, RestoreImage, UploadImage } from "@/types"; import { ImageUpload as ImageUploadComponent } from "./view/image-upload"; -declare module "@tiptap/core" { - interface Commands { - imageUpload: { - setImageUpload: ({ - file, - pos, - event, - }: { - file?: File; - pos?: number; - event: "insert" | "replace" | "drop"; - }) => ReturnType; - uploadImage: (file: File) => () => Promise | undefined; - restoreImage: (assetUrlWithWorkspaceId: string) => Promise; - deleteImage: (assetUrlWithWorkspaceId: string) => Promise; - }; - } -} - export interface UploadImageExtensionStorage { fileMap: Map; } -export type UploadEntity = ({ event: "insert" } | { event: "replace" } | { event: "drop"; file: File }) & { +export type UploadEntity = ({ event: "insert" } | { event: "drop"; file: File }) & { pos?: number; }; @@ -91,16 +72,16 @@ export const ImageUpload = ({ addCommands() { return { setImageUpload: - (props: { file?: File; pos?: number; event: "insert" | "replace" | "drop" }) => + (props: { file?: File; pos?: number; event: "insert" | "drop" }) => ({ commands }) => { const fileId = uuidv4(); if (props?.file && props?.event === "drop") { - (this.editor.storage.imageBlock as UploadImageExtensionStorage).fileMap.set(fileId, { + (this.editor.storage.imageComponent as UploadImageExtensionStorage).fileMap.set(fileId, { file: props.file, event: props.event, }); } else if (props.event !== "drop") { - (this.editor.storage.imageBlock as UploadImageExtensionStorage).fileMap.set(fileId, { + (this.editor.storage.imageComponent as UploadImageExtensionStorage).fileMap.set(fileId, { event: props.event, }); } diff --git a/packages/editor/src/core/extensions/image-upload/view/image-upload.tsx b/packages/editor/src/core/extensions/image-upload/view/image-upload.tsx index 7914f7129d4..6a2eb066b9c 100644 --- a/packages/editor/src/core/extensions/image-upload/view/image-upload.tsx +++ b/packages/editor/src/core/extensions/image-upload/view/image-upload.tsx @@ -24,7 +24,7 @@ export const ImageUpload: React.FC = ({ getPos, editor, node, const [isUploaded, setIsUploaded] = useState(!!node.attrs.src); const id = node.attrs.id as string; - const editorStorage = editor.storage.imageBlock as UploadImageExtensionStorage | undefined; + const editorStorage = editor.storage.imageComponent as UploadImageExtensionStorage | undefined; const getUploadEntity = useCallback( (): UploadEntity | undefined => editorStorage?.fileMap.get(id), diff --git a/packages/editor/src/core/extensions/image/image-block-without-props.tsx b/packages/editor/src/core/extensions/image/image-component-without-props.tsx similarity index 58% rename from packages/editor/src/core/extensions/image/image-block-without-props.tsx rename to packages/editor/src/core/extensions/image/image-component-without-props.tsx index 7feb1b09ac0..ceb6665eca8 100644 --- a/packages/editor/src/core/extensions/image/image-block-without-props.tsx +++ b/packages/editor/src/core/extensions/image/image-component-without-props.tsx @@ -1,24 +1,10 @@ -import { mergeAttributes, Range } from "@tiptap/core"; +import { mergeAttributes } from "@tiptap/core"; import { Image } from "@tiptap/extension-image"; import { UploadImageExtensionStorage } from "../image-upload"; -declare module "@tiptap/core" { - interface Commands { - imageBlock: { - setImageBlock: (attributes: { src: string; width?: number; height?: number }) => ReturnType; - setImageBlockAt: (attributes: { - src: string; - pos: number | Range; - width?: number; - height?: number; - }) => ReturnType; - }; - } -} - -export const ImageBlockWithoutProps = () => +export const CustomImageComponentWithoutProps = () => Image.extend<{}, UploadImageExtensionStorage>({ - name: "imageBlock", + name: "imageComponent", group: "inline", draggable: true, @@ -49,13 +35,13 @@ export const ImageBlockWithoutProps = () => parseHTML() { return [ { - tag: "image-block", + tag: "image-component", }, ]; }, renderHTML({ HTMLAttributes }) { - return ["image-block", mergeAttributes(HTMLAttributes)]; + return ["image-component", mergeAttributes(HTMLAttributes)]; }, addStorage() { @@ -67,4 +53,4 @@ export const ImageBlockWithoutProps = () => inline: true, }); -export default ImageBlockWithoutProps; +export default CustomImageComponentWithoutProps; diff --git a/packages/editor/src/styles/drag-drop.css b/packages/editor/src/styles/drag-drop.css index 848dc7fe48f..72d190f32f2 100644 --- a/packages/editor/src/styles/drag-drop.css +++ b/packages/editor/src/styles/drag-drop.css @@ -64,7 +64,7 @@ pointer-events: none; } - &.node-imageUpload { + &.node-imageComponent { --horizontal-offset: 0px; &::after { @@ -104,7 +104,7 @@ ol > li:nth-child(n + 100).ProseMirror-selectednode:not(.dragging)::after { margin-left: -35px; } -.ProseMirror node-imageBlock { +.ProseMirror node-imageComponent { transition: filter 0.1s ease-in-out; cursor: pointer; From 4b61f9b541271f3baf71ec018106179bc7688a70 Mon Sep 17 00:00:00 2001 From: Palanikannan M Date: Thu, 12 Sep 2024 17:04:11 +0530 Subject: [PATCH 12/29] fix: resize image real time sync --- packages/editor/src/core/extensions/extensions.tsx | 4 ---- .../image-block/components/image-block-view.tsx | 7 +++++-- .../src/core/extensions/image-block/image-block.ts | 10 +++++----- .../core/extensions/image-upload/image-upload.ts | 13 ++----------- .../extensions/image-upload/view/image-upload.tsx | 8 ++++---- .../src/core/hooks/use-collaborative-editor.ts | 1 - packages/editor/src/core/hooks/use-editor.ts | 13 +++++-------- 7 files changed, 21 insertions(+), 35 deletions(-) diff --git a/packages/editor/src/core/extensions/extensions.tsx b/packages/editor/src/core/extensions/extensions.tsx index 13093bfa33e..7aa1942e2ea 100644 --- a/packages/editor/src/core/extensions/extensions.tsx +++ b/packages/editor/src/core/extensions/extensions.tsx @@ -46,7 +46,6 @@ type TArguments = { }; placeholder?: string | ((isFocused: boolean, value: string) => string); tabIndex?: number; - provider?: HocuspocusProvider | null; }; export const CoreEditorExtensions = ({ @@ -55,7 +54,6 @@ export const CoreEditorExtensions = ({ mentionConfig, placeholder, tabIndex, - provider, }: TArguments) => [ StarterKit.configure({ bulletList: { @@ -113,8 +111,6 @@ export const CoreEditorExtensions = ({ restoreFile, uploadFile, cancelUploadImage, - }).configure({ - clientId: provider?.document?.clientID, }), TiptapUnderline, TextStyle, diff --git a/packages/editor/src/core/extensions/image-block/components/image-block-view.tsx b/packages/editor/src/core/extensions/image-block/components/image-block-view.tsx index 87db97debe0..edc75702f13 100644 --- a/packages/editor/src/core/extensions/image-block/components/image-block-view.tsx +++ b/packages/editor/src/core/extensions/image-block/components/image-block-view.tsx @@ -18,7 +18,7 @@ interface ImageBlockViewProps { const MIN_SIZE = 100; -export const ImageBlockView: React.FC = (props) => { +export const ImageComponent: React.FC = (props) => { const { node, updateAttributes } = props; const { src, width, height } = node.attrs; @@ -32,6 +32,9 @@ export const ImageBlockView: React.FC = (props) => { useEffect(() => { if (imageRef.current) { + // Set the size of the image for realtime resizing whenever width or height changes + setSize({ width, height }); + const img = imageRef.current; img.onload = () => { aspectRatio.current = img.naturalWidth / img.naturalHeight; @@ -125,4 +128,4 @@ export const ImageBlockView: React.FC = (props) => { ); }; -export default ImageBlockView; +export default ImageComponent; diff --git a/packages/editor/src/core/extensions/image-block/image-block.ts b/packages/editor/src/core/extensions/image-block/image-block.ts index 316c876f725..8466a847aa8 100644 --- a/packages/editor/src/core/extensions/image-block/image-block.ts +++ b/packages/editor/src/core/extensions/image-block/image-block.ts @@ -5,7 +5,7 @@ import { v4 as uuidv4 } from "uuid"; import { UploadImage, DeleteImage, RestoreImage } from "@/types"; import { UploadImageExtensionStorage } from "../image-upload"; -import { ImageUpload } from "../image-upload/view"; +import { CustomImage } from "../image-upload/view"; declare module "@tiptap/core" { interface Commands { @@ -18,9 +18,6 @@ declare module "@tiptap/core" { export const CustomImageComponent = ({ uploadFile, - // deleteFile, - // restoreFile, - // cancelUploadImage, }: { uploadFile: UploadImage; deleteFile: DeleteImage; @@ -79,6 +76,8 @@ export const CustomImageComponent = ({ setImageUpload: (props: { file?: File; pos?: number; event: "insert" | "drop" }) => ({ commands }) => { + // generate a unique id for the image to keep track of dropped + // files' file data const fileId = uuidv4(); if (props?.event === "drop" && props.file) { (this.editor.storage.imageComponent as UploadImageExtensionStorage).fileMap.set(fileId, { @@ -90,6 +89,7 @@ export const CustomImageComponent = ({ event: props.event, }); } + const attributes = { "data-type": this.name, id: fileId, @@ -115,7 +115,7 @@ export const CustomImageComponent = ({ }, addNodeView() { - return ReactNodeViewRenderer(ImageUpload); + return ReactNodeViewRenderer(CustomImage); }, }).configure({ inline: true, diff --git a/packages/editor/src/core/extensions/image-upload/image-upload.ts b/packages/editor/src/core/extensions/image-upload/image-upload.ts index 141eb2d2824..e13f1f286e5 100644 --- a/packages/editor/src/core/extensions/image-upload/image-upload.ts +++ b/packages/editor/src/core/extensions/image-upload/image-upload.ts @@ -1,7 +1,7 @@ import { mergeAttributes, Node, ReactNodeViewRenderer } from "@tiptap/react"; import { v4 as uuidv4 } from "uuid"; import { DeleteImage, RestoreImage, UploadImage } from "@/types"; -import { ImageUpload as ImageUploadComponent } from "./view/image-upload"; +import { CustomImage } from "./view/image-upload"; export interface UploadImageExtensionStorage { fileMap: Map; @@ -13,9 +13,6 @@ export type UploadEntity = ({ event: "insert" } | { event: "drop"; file: File }) export const ImageUpload = ({ uploadFile, - // deleteFile, - // restoreFile, - // cancelUploadImage, }: { uploadFile: UploadImage; deleteFile: DeleteImage; @@ -93,17 +90,11 @@ export const ImageUpload = ({ const fileUrl = await uploadFile(file); return fileUrl; }, - // restoreImage: (assetUrlWithWorkspaceId: string) => () => { - // restoreFile(assetUrlWithWorkspaceId); - // }, - // deleteImage: (assetUrlWithWorkspaceId: string) => () => { - // deleteFile(assetUrlWithWorkspaceId); - // }, }; }, addNodeView() { - return ReactNodeViewRenderer(ImageUploadComponent); + return ReactNodeViewRenderer(CustomImage); }, }); diff --git a/packages/editor/src/core/extensions/image-upload/view/image-upload.tsx b/packages/editor/src/core/extensions/image-upload/view/image-upload.tsx index 6a2eb066b9c..df8c2dbb72b 100644 --- a/packages/editor/src/core/extensions/image-upload/view/image-upload.tsx +++ b/packages/editor/src/core/extensions/image-upload/view/image-upload.tsx @@ -1,7 +1,7 @@ import React, { useCallback, useEffect, useRef, useState } from "react"; import { Node as ProsemirrorNode } from "@tiptap/pm/model"; import { Editor, NodeViewWrapper } from "@tiptap/react"; -import ImageBlockView from "@/extensions/image-block/components/image-block-view"; +import ImageComponent from "@/extensions/image-block/components/image-block-view"; import { UploadImageExtensionStorage, UploadEntity } from "../image-upload"; import { ImageUploader } from "./image-uploader"; @@ -18,7 +18,7 @@ interface ImageUploadProps { updateAttributes: (attrs: Record) => void; } -export const ImageUpload: React.FC = ({ getPos, editor, node, updateAttributes }) => { +export const CustomImage: React.FC = ({ getPos, editor, node, updateAttributes }) => { const fileInputRef = useRef(null); const hasTriggeredFilePickerRef = useRef(false); const [isUploaded, setIsUploaded] = useState(!!node.attrs.src); @@ -86,7 +86,7 @@ export const ImageUpload: React.FC = ({ getPos, editor, node,
{isUploaded ? ( - + ) : ( )} @@ -95,4 +95,4 @@ export const ImageUpload: React.FC = ({ getPos, editor, node, ); }; -export default ImageUpload; +export default CustomImage; diff --git a/packages/editor/src/core/hooks/use-collaborative-editor.ts b/packages/editor/src/core/hooks/use-collaborative-editor.ts index a16480d5fad..35456068301 100644 --- a/packages/editor/src/core/hooks/use-collaborative-editor.ts +++ b/packages/editor/src/core/hooks/use-collaborative-editor.ts @@ -72,7 +72,6 @@ export const useCollaborativeEditor = (props: TCollaborativeEditorProps) => { handleEditorReady, forwardedRef, mentionHandler, - provider, extensions: [ SideMenuExtension({ aiEnabled: !disabledExtensions?.includes("ai"), diff --git a/packages/editor/src/core/hooks/use-editor.ts b/packages/editor/src/core/hooks/use-editor.ts index 273465442ab..6de95557c87 100644 --- a/packages/editor/src/core/hooks/use-editor.ts +++ b/packages/editor/src/core/hooks/use-editor.ts @@ -15,7 +15,6 @@ import { IMarking, scrollSummary } from "@/helpers/scroll-to-node"; import { CoreEditorProps } from "@/props"; // types import { EditorRefApi, IMentionHighlight, IMentionSuggestion, TEditorCommands, TFileHandler } from "@/types"; -import { HocuspocusProvider } from "@hocuspocus/provider"; export interface CustomEditorProps { editorClassName: string; @@ -37,7 +36,6 @@ export interface CustomEditorProps { // undefined when prop is not passed, null if intentionally passed to stop // swr syncing value?: string | null | undefined; - provider?: HocuspocusProvider | null; } export const useEditor = (props: CustomEditorProps) => { @@ -56,7 +54,6 @@ export const useEditor = (props: CustomEditorProps) => { placeholder, tabIndex, value, - provider, } = props; // states const [savedSelection, setSavedSelection] = useState(null); @@ -70,8 +67,8 @@ export const useEditor = (props: CustomEditorProps) => { }), ...editorProps, }, - shouldRerenderOnTransaction: false, immediatelyRender: true, + shouldRerenderOnTransaction: false, onContentError: (error) => { console.error("Error rendering content:", error); }, @@ -90,7 +87,6 @@ export const useEditor = (props: CustomEditorProps) => { }, placeholder, tabIndex, - provider, }), ...extensions, ], @@ -100,6 +96,7 @@ export const useEditor = (props: CustomEditorProps) => { onUpdate: ({ editor }) => onChange?.(editor.getJSON(), editor.getHTML()), onDestroy: () => handleEditorReady?.(false), }); + // Update the ref whenever savedSelection changes useEffect(() => { savedSelectionRef.current = savedSelection; @@ -140,7 +137,7 @@ export const useEditor = (props: CustomEditorProps) => { } }, executeMenuItemCommand: (itemKey: TEditorCommands) => { - const editorItems = getEditorMenuItems(editorRef.current, fileHandler.upload); + const editorItems = getEditorMenuItems(editorRef.current); const getEditorMenuItem = (itemKey: TEditorCommands) => editorItems.find((item) => item.key === itemKey); @@ -156,7 +153,7 @@ export const useEditor = (props: CustomEditorProps) => { } }, isMenuItemActive: (itemName: TEditorCommands): boolean => { - const editorItems = getEditorMenuItems(editorRef.current, fileHandler.upload); + const editorItems = getEditorMenuItems(editorRef.current); const getEditorMenuItem = (itemName: TEditorCommands) => editorItems.find((item) => item.key === itemName); const item = getEditorMenuItem(itemName); @@ -250,7 +247,7 @@ export const useEditor = (props: CustomEditorProps) => { words: editorRef.current?.storage?.characterCount?.words?.() ?? 0, }, }), - [editorRef, savedSelection, fileHandler.upload] + [editorRef, savedSelection] ); if (!editor) { From b200b3382ab28e41d037f6a0401f8cfdb7fdd6c3 Mon Sep 17 00:00:00 2001 From: Palanikannan M Date: Thu, 12 Sep 2024 21:18:45 +0530 Subject: [PATCH 13/29] fix: drag drop menu --- .../components/image-block-view.tsx | 38 +++---- .../components/image-loader.tsx | 0 .../custom-image.ts} | 30 +++--- .../image-upload}/image-upload.tsx | 17 ++- .../image-upload}/image-uploader.tsx | 5 +- .../image-upload}/index.tsx | 0 .../custom-image-component/index.ts | 1 + .../editor/src/core/extensions/extensions.tsx | 10 +- .../src/core/extensions/image-block/index.ts | 1 - .../extensions/image-upload/image-upload.ts | 101 ------------------ .../src/core/extensions/image-upload/index.ts | 1 - .../image/image-component-without-props.tsx | 3 +- .../editor/src/core/extensions/side-menu.tsx | 37 +------ .../editor/src/core/plugins/drag-handle.ts | 23 +++- 14 files changed, 82 insertions(+), 185 deletions(-) rename packages/editor/src/core/extensions/{image-block => custom-image-component}/components/image-block-view.tsx (79%) rename packages/editor/src/core/extensions/{image-block => custom-image-component}/components/image-loader.tsx (100%) rename packages/editor/src/core/extensions/{image-block/image-block.ts => custom-image-component/custom-image.ts} (85%) rename packages/editor/src/core/extensions/{image-upload/view => custom-image-component/image-upload}/image-upload.tsx (83%) rename packages/editor/src/core/extensions/{image-upload/view => custom-image-component/image-upload}/image-uploader.tsx (86%) rename packages/editor/src/core/extensions/{image-upload/view => custom-image-component/image-upload}/index.tsx (100%) create mode 100644 packages/editor/src/core/extensions/custom-image-component/index.ts delete mode 100644 packages/editor/src/core/extensions/image-block/index.ts delete mode 100644 packages/editor/src/core/extensions/image-upload/image-upload.ts delete mode 100644 packages/editor/src/core/extensions/image-upload/index.ts diff --git a/packages/editor/src/core/extensions/image-block/components/image-block-view.tsx b/packages/editor/src/core/extensions/custom-image-component/components/image-block-view.tsx similarity index 79% rename from packages/editor/src/core/extensions/image-block/components/image-block-view.tsx rename to packages/editor/src/core/extensions/custom-image-component/components/image-block-view.tsx index edc75702f13..87364f71e17 100644 --- a/packages/editor/src/core/extensions/image-block/components/image-block-view.tsx +++ b/packages/editor/src/core/extensions/custom-image-component/components/image-block-view.tsx @@ -14,40 +14,33 @@ interface ImageBlockViewProps { }; }; updateAttributes: (attrs: Record) => void; + selected: boolean; } -const MIN_SIZE = 100; +const MIN_SIZE = 200; export const ImageComponent: React.FC = (props) => { - const { node, updateAttributes } = props; + const { node, updateAttributes, selected } = props; const { src, width, height } = node.attrs; - const [size, setSize] = useState({ width, height }); + const [size, setSize] = useState({ width: width || "35%", height: height || "auto" }); const [isSelected, setIsSelected] = useState(false); + const [isLoading, setIsLoading] = useState(true); + const containerRef = useRef(null); const imageRef = useRef(null); const isResizing = useRef(false); const aspectRatio = useRef(1); - const [isLoading, setIsLoading] = useState(true); useEffect(() => { if (imageRef.current) { - // Set the size of the image for realtime resizing whenever width or height changes - setSize({ width, height }); - const img = imageRef.current; img.onload = () => { aspectRatio.current = img.naturalWidth / img.naturalHeight; - if (width === "35%" && height === "auto") { - const containerWidth = containerRef.current?.offsetWidth || 0; - const newWidth = Math.max(containerWidth * 0.35, MIN_SIZE); - const newHeight = newWidth / aspectRatio.current; - setSize({ width: `${newWidth}px`, height: `${newHeight}px` }); - } setIsLoading(false); }; } - }, [src, width, height]); + }, [src]); const handleResizeStart = useCallback((e: React.MouseEvent | React.TouchEvent) => { e.preventDefault(); @@ -56,6 +49,10 @@ export const ImageComponent: React.FC = (props) => { setIsSelected(true); }, []); + useEffect(() => { + setIsSelected(selected); + }, [selected]); + const handleResize = useCallback((e: MouseEvent | TouchEvent) => { if (!isResizing.current || !containerRef.current) return; @@ -102,16 +99,19 @@ export const ImageComponent: React.FC = (props) => { }, [handleResize, handleResizeEnd]); return ( -
- {isLoading ? : null} +
+ {isLoading && } diff --git a/packages/editor/src/core/extensions/image-block/components/image-loader.tsx b/packages/editor/src/core/extensions/custom-image-component/components/image-loader.tsx similarity index 100% rename from packages/editor/src/core/extensions/image-block/components/image-loader.tsx rename to packages/editor/src/core/extensions/custom-image-component/components/image-loader.tsx diff --git a/packages/editor/src/core/extensions/image-block/image-block.ts b/packages/editor/src/core/extensions/custom-image-component/custom-image.ts similarity index 85% rename from packages/editor/src/core/extensions/image-block/image-block.ts rename to packages/editor/src/core/extensions/custom-image-component/custom-image.ts index 8466a847aa8..b0530c9e927 100644 --- a/packages/editor/src/core/extensions/image-block/image-block.ts +++ b/packages/editor/src/core/extensions/custom-image-component/custom-image.ts @@ -2,10 +2,10 @@ import { mergeAttributes } from "@tiptap/core"; import { Image } from "@tiptap/extension-image"; import { ReactNodeViewRenderer } from "@tiptap/react"; import { v4 as uuidv4 } from "uuid"; -import { UploadImage, DeleteImage, RestoreImage } from "@/types"; -import { UploadImageExtensionStorage } from "../image-upload"; -import { CustomImage } from "../image-upload/view"; +import { TFileHandler } from "@/types"; + +import { CustomImage } from "./image-upload"; declare module "@tiptap/core" { interface Commands { @@ -16,18 +16,19 @@ declare module "@tiptap/core" { } } -export const CustomImageComponent = ({ - uploadFile, -}: { - uploadFile: UploadImage; - deleteFile: DeleteImage; - restoreFile: RestoreImage; - cancelUploadImage?: () => void; -}) => - Image.extend<{}, UploadImageExtensionStorage>({ +export interface UploadImageExtensionStorage { + fileMap: Map; +} + +export type UploadEntity = { event: "insert" } | { event: "drop"; file: File }; + +export const CustomImageComponent = (props: TFileHandler) => { + const { upload } = props; + + return Image.extend<{}, UploadImageExtensionStorage>({ name: "imageComponent", + selectable: true, group: "inline", - draggable: true, addAttributes() { return { @@ -108,7 +109,7 @@ export const CustomImageComponent = ({ }); }, uploadImage: (file: File) => async () => { - const fileUrl = await uploadFile(file); + const fileUrl = await upload(file); return fileUrl; }, }; @@ -120,5 +121,6 @@ export const CustomImageComponent = ({ }).configure({ inline: true, }); +}; export default CustomImageComponent; diff --git a/packages/editor/src/core/extensions/image-upload/view/image-upload.tsx b/packages/editor/src/core/extensions/custom-image-component/image-upload/image-upload.tsx similarity index 83% rename from packages/editor/src/core/extensions/image-upload/view/image-upload.tsx rename to packages/editor/src/core/extensions/custom-image-component/image-upload/image-upload.tsx index df8c2dbb72b..41bb9d6aeeb 100644 --- a/packages/editor/src/core/extensions/image-upload/view/image-upload.tsx +++ b/packages/editor/src/core/extensions/custom-image-component/image-upload/image-upload.tsx @@ -1,8 +1,8 @@ import React, { useCallback, useEffect, useRef, useState } from "react"; import { Node as ProsemirrorNode } from "@tiptap/pm/model"; import { Editor, NodeViewWrapper } from "@tiptap/react"; -import ImageComponent from "@/extensions/image-block/components/image-block-view"; -import { UploadImageExtensionStorage, UploadEntity } from "../image-upload"; +import { UploadEntity, UploadImageExtensionStorage } from "@/extensions/custom-image-component"; +import ImageComponent from "@/extensions/custom-image-component/components/image-block-view"; import { ImageUploader } from "./image-uploader"; interface ImageUploadProps { @@ -16,9 +16,12 @@ interface ImageUploadProps { }; }; updateAttributes: (attrs: Record) => void; + selected: boolean; } -export const CustomImage: React.FC = ({ getPos, editor, node, updateAttributes }) => { +export const CustomImage = (props: ImageUploadProps) => { + const { getPos, editor, node, updateAttributes, selected } = props; + const fileInputRef = useRef(null); const hasTriggeredFilePickerRef = useRef(false); const [isUploaded, setIsUploaded] = useState(!!node.attrs.src); @@ -86,7 +89,13 @@ export const CustomImage: React.FC = ({ getPos, editor, node,
{isUploaded ? ( - + ) : ( )} diff --git a/packages/editor/src/core/extensions/image-upload/view/image-uploader.tsx b/packages/editor/src/core/extensions/custom-image-component/image-upload/image-uploader.tsx similarity index 86% rename from packages/editor/src/core/extensions/image-upload/view/image-uploader.tsx rename to packages/editor/src/core/extensions/custom-image-component/image-upload/image-uploader.tsx index 7ec16918b44..b7b7cf20e3c 100644 --- a/packages/editor/src/core/extensions/image-upload/view/image-uploader.tsx +++ b/packages/editor/src/core/extensions/custom-image-component/image-upload/image-uploader.tsx @@ -47,7 +47,7 @@ export const ImageUploader = ({ return (
{ localRef.current = element; assignRef(fileInputRef, element); assignRef(internalRef as RefType, element); }} + hidden type="file" accept=".jpg,.jpeg,.png,.webp" onChange={onFileChange} diff --git a/packages/editor/src/core/extensions/image-upload/view/index.tsx b/packages/editor/src/core/extensions/custom-image-component/image-upload/index.tsx similarity index 100% rename from packages/editor/src/core/extensions/image-upload/view/index.tsx rename to packages/editor/src/core/extensions/custom-image-component/image-upload/index.tsx diff --git a/packages/editor/src/core/extensions/custom-image-component/index.ts b/packages/editor/src/core/extensions/custom-image-component/index.ts new file mode 100644 index 00000000000..f72f24fd8f3 --- /dev/null +++ b/packages/editor/src/core/extensions/custom-image-component/index.ts @@ -0,0 +1 @@ +export * from "./custom-image"; diff --git a/packages/editor/src/core/extensions/extensions.tsx b/packages/editor/src/core/extensions/extensions.tsx index 7aa1942e2ea..411f7359c67 100644 --- a/packages/editor/src/core/extensions/extensions.tsx +++ b/packages/editor/src/core/extensions/extensions.tsx @@ -30,7 +30,7 @@ import { import { isValidHttpUrl } from "@/helpers/common"; // types import { DeleteImage, IMentionHighlight, IMentionSuggestion, RestoreImage, UploadImage } from "@/types"; -import { CustomImageComponent } from "./image-block"; +import { CustomImageComponent } from "./custom-image-component"; type TArguments = { enableHistory: boolean; @@ -107,10 +107,10 @@ export const CoreEditorExtensions = ({ }, }), CustomImageComponent({ - deleteFile, - restoreFile, - uploadFile, - cancelUploadImage, + delete: deleteFile, + restore: restoreFile, + upload: uploadFile, + cancel: cancelUploadImage ?? (() => {}), }), TiptapUnderline, TextStyle, diff --git a/packages/editor/src/core/extensions/image-block/index.ts b/packages/editor/src/core/extensions/image-block/index.ts deleted file mode 100644 index 3f622202102..00000000000 --- a/packages/editor/src/core/extensions/image-block/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from "./image-block"; diff --git a/packages/editor/src/core/extensions/image-upload/image-upload.ts b/packages/editor/src/core/extensions/image-upload/image-upload.ts deleted file mode 100644 index e13f1f286e5..00000000000 --- a/packages/editor/src/core/extensions/image-upload/image-upload.ts +++ /dev/null @@ -1,101 +0,0 @@ -import { mergeAttributes, Node, ReactNodeViewRenderer } from "@tiptap/react"; -import { v4 as uuidv4 } from "uuid"; -import { DeleteImage, RestoreImage, UploadImage } from "@/types"; -import { CustomImage } from "./view/image-upload"; - -export interface UploadImageExtensionStorage { - fileMap: Map; -} - -export type UploadEntity = ({ event: "insert" } | { event: "drop"; file: File }) & { - pos?: number; -}; - -export const ImageUpload = ({ - uploadFile, -}: { - uploadFile: UploadImage; - deleteFile: DeleteImage; - restoreFile: RestoreImage; - cancelUploadImage?: () => void; -}) => - Node.create({ - name: "imageUpload", - - isolating: true, - - defining: true, - - group: "block", - - draggable: true, - - selectable: true, - - inline: false, - - addAttributes() { - return { - ["data-type"]: { - default: this.name, - }, - ["data-file"]: { - default: null, - }, - ["id"]: { - default: null, - }, - }; - }, - - parseHTML() { - return [ - { - tag: "image-upload", - }, - ]; - }, - - renderHTML({ HTMLAttributes }) { - return ["image-upload", mergeAttributes(HTMLAttributes)]; - }, - - addStorage() { - return { - fileMap: new Map(), - }; - }, - - addCommands() { - return { - setImageUpload: - (props: { file?: File; pos?: number; event: "insert" | "drop" }) => - ({ commands }) => { - const fileId = uuidv4(); - if (props?.file && props?.event === "drop") { - (this.editor.storage.imageComponent as UploadImageExtensionStorage).fileMap.set(fileId, { - file: props.file, - event: props.event, - }); - } else if (props.event !== "drop") { - (this.editor.storage.imageComponent as UploadImageExtensionStorage).fileMap.set(fileId, { - event: props.event, - }); - } - return commands.insertContent( - `` - ); - }, - uploadImage: (file: File) => async () => { - const fileUrl = await uploadFile(file); - return fileUrl; - }, - }; - }, - - addNodeView() { - return ReactNodeViewRenderer(CustomImage); - }, - }); - -export default ImageUpload; diff --git a/packages/editor/src/core/extensions/image-upload/index.ts b/packages/editor/src/core/extensions/image-upload/index.ts deleted file mode 100644 index 0d0b9c26381..00000000000 --- a/packages/editor/src/core/extensions/image-upload/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from "./image-upload"; diff --git a/packages/editor/src/core/extensions/image/image-component-without-props.tsx b/packages/editor/src/core/extensions/image/image-component-without-props.tsx index ceb6665eca8..e3eedff06c5 100644 --- a/packages/editor/src/core/extensions/image/image-component-without-props.tsx +++ b/packages/editor/src/core/extensions/image/image-component-without-props.tsx @@ -1,12 +1,13 @@ import { mergeAttributes } from "@tiptap/core"; import { Image } from "@tiptap/extension-image"; -import { UploadImageExtensionStorage } from "../image-upload"; +import { UploadImageExtensionStorage } from "@/extensions/custom-image-component"; export const CustomImageComponentWithoutProps = () => Image.extend<{}, UploadImageExtensionStorage>({ name: "imageComponent", group: "inline", draggable: true, + selectable: true, addAttributes() { return { diff --git a/packages/editor/src/core/extensions/side-menu.tsx b/packages/editor/src/core/extensions/side-menu.tsx index 75577c74fc6..038d679b185 100644 --- a/packages/editor/src/core/extensions/side-menu.tsx +++ b/packages/editor/src/core/extensions/side-menu.tsx @@ -3,7 +3,7 @@ import { Plugin, PluginKey } from "@tiptap/pm/state"; import { EditorView } from "@tiptap/pm/view"; // plugins import { AIHandlePlugin } from "@/plugins/ai-handle"; -import { DragHandlePlugin } from "@/plugins/drag-handle"; +import { DragHandlePlugin, nodeDOMAtCoords } from "@/plugins/drag-handle"; type Props = { aiEnabled: boolean; @@ -59,41 +59,6 @@ const absoluteRect = (node: Element) => { }; }; -const nodeDOMAtCoords = (coords: { x: number; y: number }) => { - const elements = document.elementsFromPoint(coords.x, coords.y); - const generalSelectors = [ - "li", - "p:not(:first-child)", - ".code-block", - "blockquote", - "img", - "h1, h2, h3, h4, h5, h6", - "[data-type=horizontalRule]", - ".table-wrapper", - ".issue-embed", - ].join(", "); - - for (const elem of elements) { - if (elem.matches("p:first-child") && elem.parentElement?.matches(".ProseMirror")) { - return elem; - } - - // if the element is a

tag that is the first child of a td or th - if ( - (elem.matches("td > p:first-child") || elem.matches("th > p:first-child")) && - elem?.textContent?.trim() !== "" - ) { - return elem; // Return only if p tag is not empty in td or th - } - - // apply general selector - if (elem.matches(generalSelectors)) { - return elem; - } - } - return null; -}; - const SideMenu = (options: SideMenuPluginProps) => { const { handlesConfig } = options; const editorSideMenu: HTMLDivElement | null = document.createElement("div"); diff --git a/packages/editor/src/core/plugins/drag-handle.ts b/packages/editor/src/core/plugins/drag-handle.ts index f8b258e8ad1..d260b284968 100644 --- a/packages/editor/src/core/plugins/drag-handle.ts +++ b/packages/editor/src/core/plugins/drag-handle.ts @@ -30,7 +30,7 @@ const createDragHandleElement = (): HTMLElement => { return dragHandleElement; }; -const nodeDOMAtCoords = (coords: { x: number; y: number }) => { +export const nodeDOMAtCoords = (coords: { x: number; y: number }) => { const elements = document.elementsFromPoint(coords.x, coords.y); const generalSelectors = [ "li", @@ -42,13 +42,34 @@ const nodeDOMAtCoords = (coords: { x: number; y: number }) => { "[data-type=horizontalRule]", ".table-wrapper", ".issue-embed", + ".image-upload-component", ].join(", "); + const hasNestedImg = (el: Element): boolean => { + if (el.tagName.toLowerCase() === "img") return true; + // @ts-expect-error todo + for (const child of el.children) { + if (hasNestedImg(child)) return true; + } + return false; + }; + for (const elem of elements) { + const elemHasNestedImg = hasNestedImg(elem); if (elem.matches("p:first-child") && elem.parentElement?.matches(".ProseMirror")) { return elem; } + // if the element is a

tag and has a nested img i.e. the new image + // component + if (elem.matches("p") && elemHasNestedImg) { + return null; + } + + if (elem.matches("div") && elemHasNestedImg) { + return elem; + } + // if the element is a

tag that is the first child of a td or th if ( (elem.matches("td > p:first-child") || elem.matches("th > p:first-child")) && From 1e014d7c0e4d1723441825454bf4d84978b7fdf1 Mon Sep 17 00:00:00 2001 From: Palanikannan M Date: Fri, 13 Sep 2024 14:11:10 +0530 Subject: [PATCH 14/29] feat: custom image component editor --- .../src/ce/extensions/document-extensions.tsx | 7 +- .../components/editors/rich-text/editor.tsx | 2 +- .../src/core/components/menus/menu-items.ts | 9 +- .../src/core/extensions/core-without-props.ts | 2 + .../components/image-block-view.tsx | 135 ++++++++++++++++++ .../components/image-loader.tsx | 3 + .../custom-image-component/custom-image.ts | 132 +++++++++++++++++ .../image-upload/image-upload.tsx | 113 +++++++++++++++ .../image-upload/image-uploader.tsx | 91 ++++++++++++ .../image-upload/index.tsx | 1 + .../custom-image-component/index.ts | 1 + packages/editor/src/core/extensions/drop.tsx | 62 +++++--- .../editor/src/core/extensions/extensions.tsx | 12 +- .../image/image-component-without-props.tsx | 57 ++++++++ .../editor/src/core/extensions/side-menu.tsx | 37 +---- .../src/core/extensions/slash-commands.tsx | 17 ++- .../core/hooks/use-collaborative-editor.ts | 1 - packages/editor/src/core/hooks/use-editor.ts | 37 +++-- .../editor/src/core/hooks/use-file-upload.ts | 112 +++++++++++++++ .../editor/src/core/plugins/drag-handle.ts | 23 ++- packages/editor/src/styles/drag-drop.css | 12 +- packages/editor/src/styles/editor.css | 8 +- packages/editor/tsup.config.ts | 2 +- 23 files changed, 776 insertions(+), 100 deletions(-) create mode 100644 packages/editor/src/core/extensions/custom-image-component/components/image-block-view.tsx create mode 100644 packages/editor/src/core/extensions/custom-image-component/components/image-loader.tsx create mode 100644 packages/editor/src/core/extensions/custom-image-component/custom-image.ts create mode 100644 packages/editor/src/core/extensions/custom-image-component/image-upload/image-upload.tsx create mode 100644 packages/editor/src/core/extensions/custom-image-component/image-upload/image-uploader.tsx create mode 100644 packages/editor/src/core/extensions/custom-image-component/image-upload/index.tsx create mode 100644 packages/editor/src/core/extensions/custom-image-component/index.ts create mode 100644 packages/editor/src/core/extensions/image/image-component-without-props.tsx create mode 100644 packages/editor/src/core/hooks/use-file-upload.ts diff --git a/packages/editor/src/ce/extensions/document-extensions.tsx b/packages/editor/src/ce/extensions/document-extensions.tsx index bf2937fcd63..6bb58021324 100644 --- a/packages/editor/src/ce/extensions/document-extensions.tsx +++ b/packages/editor/src/ce/extensions/document-extensions.tsx @@ -4,20 +4,19 @@ import { SlashCommand } from "@/extensions"; // plane editor types import { TIssueEmbedConfig } from "@/plane-editor/types"; // types -import { TExtensions, TFileHandler, TUserDetails } from "@/types"; +import { TExtensions, TUserDetails } from "@/types"; type Props = { disabledExtensions?: TExtensions[]; - fileHandler: TFileHandler; issueEmbedConfig: TIssueEmbedConfig | undefined; provider: HocuspocusProvider; userDetails: TUserDetails; }; export const DocumentEditorAdditionalExtensions = (props: Props) => { - const { fileHandler } = props; + const {} = props; - const extensions: Extensions = [SlashCommand(fileHandler.upload)]; + const extensions: Extensions = [SlashCommand()]; return extensions; }; diff --git a/packages/editor/src/core/components/editors/rich-text/editor.tsx b/packages/editor/src/core/components/editors/rich-text/editor.tsx index 28204237275..b1f9eed7d0f 100644 --- a/packages/editor/src/core/components/editors/rich-text/editor.tsx +++ b/packages/editor/src/core/components/editors/rich-text/editor.tsx @@ -11,7 +11,7 @@ const RichTextEditor = (props: IRichTextEditor) => { const { dragDropEnabled, fileHandler } = props; const getExtensions = useCallback(() => { - const extensions = [SlashCommand(fileHandler.upload)]; + const extensions = [SlashCommand()]; extensions.push( SideMenuExtension({ diff --git a/packages/editor/src/core/components/menus/menu-items.ts b/packages/editor/src/core/components/menus/menu-items.ts index 60db11704d4..63d76117ab4 100644 --- a/packages/editor/src/core/components/menus/menu-items.ts +++ b/packages/editor/src/core/components/menus/menu-items.ts @@ -189,16 +189,17 @@ export const TableItem = (editor: Editor): EditorMenuItem => ({ icon: TableIcon, }); -export const ImageItem = (editor: Editor, uploadFile: UploadImage) => +export const ImageItem = (editor: Editor) => ({ key: "image", name: "Image", isActive: () => editor?.isActive("image"), - command: (savedSelection: Selection | null) => insertImageCommand(editor, uploadFile, savedSelection), + command: (savedSelection: Selection | null) => + editor?.commands.setImageUpload({ event: "insert", pos: savedSelection?.from }), icon: ImageIcon, }) as const; -export function getEditorMenuItems(editor: Editor | null, uploadFile: UploadImage) { +export function getEditorMenuItems(editor: Editor | null) { if (!editor) { return []; } @@ -220,6 +221,6 @@ export function getEditorMenuItems(editor: Editor | null, uploadFile: UploadImag NumberedListItem(editor), QuoteItem(editor), TableItem(editor), - ImageItem(editor, uploadFile), + ImageItem(editor), ]; } diff --git a/packages/editor/src/core/extensions/core-without-props.ts b/packages/editor/src/core/extensions/core-without-props.ts index c0f066c3ff9..1cedd513966 100644 --- a/packages/editor/src/core/extensions/core-without-props.ts +++ b/packages/editor/src/core/extensions/core-without-props.ts @@ -11,6 +11,7 @@ import { CustomCodeInlineExtension } from "./code-inline"; import { CustomLinkExtension } from "./custom-link"; import { CustomHorizontalRule } from "./horizontal-rule"; import { ImageExtensionWithoutProps } from "./image"; +import { CustomImageComponentWithoutProps } from "./image/image-component-without-props"; import { IssueWidgetWithoutProps } from "./issue-embed/issue-embed-without-props"; import { CustomMentionWithoutProps } from "./mentions/mentions-without-props"; import { CustomQuoteExtension } from "./quote"; @@ -61,6 +62,7 @@ export const CoreEditorExtensionsWithoutProps = [ class: "rounded-md", }, }), + CustomImageComponentWithoutProps(), TiptapUnderline, TextStyle, TaskList.configure({ diff --git a/packages/editor/src/core/extensions/custom-image-component/components/image-block-view.tsx b/packages/editor/src/core/extensions/custom-image-component/components/image-block-view.tsx new file mode 100644 index 00000000000..09697f9eebf --- /dev/null +++ b/packages/editor/src/core/extensions/custom-image-component/components/image-block-view.tsx @@ -0,0 +1,135 @@ +import React, { useRef, useState, useCallback, useLayoutEffect, useEffect } from "react"; +import { Node as ProsemirrorNode } from "@tiptap/pm/model"; +import { NodeSelection } from "@tiptap/pm/state"; +import { Editor } from "@tiptap/react"; +import { ImageShimmer } from "./image-loader"; + +interface ImageBlockViewProps { + editor: Editor; + getPos: () => number; + node: ProsemirrorNode & { + attrs: { + src: string; + width: string; + height: string; + }; + }; + updateAttributes: (attrs: Record) => void; + selected: boolean; +} + +const MIN_SIZE = 100; + +export const ImageComponent: React.FC = (props) => { + const { node, updateAttributes, selected, getPos, editor } = props; + const { src, width, height } = node.attrs; + + const [size, setSize] = useState({ width: width || "35%", height: height || "auto" }); + // const [isSelected, setIsSelected] = useState(false); + const [isLoading, setIsLoading] = useState(true); + + const containerRef = useRef(null); + const imageRef = useRef(null); + const isResizing = useRef(false); + const aspectRatio = useRef(1); + + useLayoutEffect(() => { + if (imageRef.current) { + const img = imageRef.current; + img.onload = () => { + aspectRatio.current = img.naturalWidth / img.naturalHeight; + const initialWidth = Math.max(img.naturalWidth * 0.35, MIN_SIZE); + const initialHeight = initialWidth / aspectRatio.current; + setSize({ width: `${initialWidth}px`, height: `${initialHeight}px` }); + setIsLoading(false); + }; + } + }, [src]); + + const handleResizeStart = useCallback((e: React.MouseEvent | React.TouchEvent) => { + e.preventDefault(); + e.stopPropagation(); + isResizing.current = true; + }, []); + + useLayoutEffect(() => { + // for realtime resizing and undo/redo + setSize({ width, height }); + }, [width, height]); + + const handleResize = useCallback((e: MouseEvent | TouchEvent) => { + if (!isResizing.current || !containerRef.current) return; + + const containerRect = containerRef.current.getBoundingClientRect(); + const clientX = "touches" in e ? e.touches[0].clientX : e.clientX; + + const newWidth = Math.max(clientX - containerRect.left, MIN_SIZE); + const newHeight = newWidth / aspectRatio.current; + + setSize({ width: `${newWidth}px`, height: `${newHeight}px` }); + }, []); + + const handleResizeEnd = useCallback(() => { + if (isResizing.current) { + isResizing.current = false; + + updateAttributes(size); + } + }, [size, updateAttributes]); + + const handleMouseDown = useCallback( + (e: React.MouseEvent) => { + e.stopPropagation(); + const pos = getPos(); + const nodeSelection = NodeSelection.create(editor.state.doc, pos); + editor.view.dispatch(editor.state.tr.setSelection(nodeSelection)); + }, + [editor, getPos] + ); + + useLayoutEffect(() => { + const handleGlobalMouseMove = (e: MouseEvent) => handleResize(e); + const handleGlobalMouseUp = () => handleResizeEnd(); + + document.addEventListener("mousemove", handleGlobalMouseMove); + document.addEventListener("mouseup", handleGlobalMouseUp); + + return () => { + document.removeEventListener("mousemove", handleGlobalMouseMove); + document.removeEventListener("mouseup", handleGlobalMouseUp); + }; + }, [handleResize, handleResizeEnd]); + + return ( +

+ {isLoading && } + + {selected && ( + <> +
+
+ + )} +
+ ); +}; + +export default ImageComponent; diff --git a/packages/editor/src/core/extensions/custom-image-component/components/image-loader.tsx b/packages/editor/src/core/extensions/custom-image-component/components/image-loader.tsx new file mode 100644 index 00000000000..52c62493aaa --- /dev/null +++ b/packages/editor/src/core/extensions/custom-image-component/components/image-loader.tsx @@ -0,0 +1,3 @@ +export const ImageShimmer: React.FC<{ width: string; height: string }> = ({ width, height }) => ( +
+); diff --git a/packages/editor/src/core/extensions/custom-image-component/custom-image.ts b/packages/editor/src/core/extensions/custom-image-component/custom-image.ts new file mode 100644 index 00000000000..6d9f5cf8bd1 --- /dev/null +++ b/packages/editor/src/core/extensions/custom-image-component/custom-image.ts @@ -0,0 +1,132 @@ +import { mergeAttributes } from "@tiptap/core"; +import { Image } from "@tiptap/extension-image"; +import { ReactNodeViewRenderer } from "@tiptap/react"; +import { v4 as uuidv4 } from "uuid"; + +import { TFileHandler } from "@/types"; + +import { CustomImage } from "./image-upload"; +import { isFileValid } from "@/plugins/image"; + +declare module "@tiptap/core" { + interface Commands { + imageComponent: { + setImageUpload: ({ file, pos, event }: { file?: File; pos?: number; event: "insert" | "drop" }) => ReturnType; + uploadImage: (file: File) => () => Promise | undefined; + }; + } +} + +export interface UploadImageExtensionStorage { + fileMap: Map; +} + +export type UploadEntity = { event: "insert" } | { event: "drop"; file: File }; + +export const CustomImageComponent = (props: TFileHandler) => { + const { upload } = props; + + return Image.extend<{}, UploadImageExtensionStorage>({ + name: "imageComponent", + selectable: true, + group: "block", + atom: true, + draggable: true, + + addAttributes() { + return { + ...this.parent?.(), + width: { + default: "35%", + }, + src: { + default: null, + }, + height: { + default: "auto", + }, + ["data-type"]: { + default: this.name, + }, + ["data-file"]: { + default: null, + }, + ["id"]: { + default: null, + }, + }; + }, + + parseHTML() { + return [ + { + tag: "image-component", + }, + ]; + }, + + renderHTML({ HTMLAttributes }) { + return ["image-component", mergeAttributes(HTMLAttributes)]; + }, + + addStorage() { + return { + fileMap: new Map(), + }; + }, + + addCommands() { + return { + setImageUpload: + (props: { file?: File; pos?: number; event: "insert" | "drop" }) => + ({ commands }) => { + // Early return if there's an invalid file being dropped + if (props?.file && !isFileValid(props.file)) { + return false; + } + + // generate a unique id for the image to keep track of dropped + // files' file data + const fileId = uuidv4(); + if (props?.event === "drop" && props.file) { + (this.editor.storage.imageComponent as UploadImageExtensionStorage).fileMap.set(fileId, { + file: props.file, + event: props.event, + }); + } else if (props.event === "insert") { + (this.editor.storage.imageComponent as UploadImageExtensionStorage).fileMap.set(fileId, { + event: props.event, + }); + } + + const attributes = { + "data-type": this.name, + id: fileId, + "data-file": props.file ? `data-file="${props.file}"` : "", + }; + + if (props.pos) { + return commands.insertContentAt(props.pos, { + type: this.name, + attrs: attributes, + }); + } + return commands.insertContent({ + type: this.name, + attrs: attributes, + }); + }, + uploadImage: (file: File) => async () => { + const fileUrl = await upload(file); + return fileUrl; + }, + }; + }, + + addNodeView() { + return ReactNodeViewRenderer(CustomImage); + }, + }); +}; + +export default CustomImageComponent; diff --git a/packages/editor/src/core/extensions/custom-image-component/image-upload/image-upload.tsx b/packages/editor/src/core/extensions/custom-image-component/image-upload/image-upload.tsx new file mode 100644 index 00000000000..d79162749a7 --- /dev/null +++ b/packages/editor/src/core/extensions/custom-image-component/image-upload/image-upload.tsx @@ -0,0 +1,113 @@ +import React, { useCallback, useEffect, useRef, useState } from "react"; +import { Node as ProsemirrorNode } from "@tiptap/pm/model"; +import { Editor, NodeViewWrapper } from "@tiptap/react"; +import { UploadEntity, UploadImageExtensionStorage } from "@/extensions/custom-image-component"; +import ImageComponent from "@/extensions/custom-image-component/components/image-block-view"; +import { ImageUploader } from "./image-uploader"; + +interface ImageUploadProps { + getPos: () => number; + editor: Editor; + node: ProsemirrorNode & { + attrs: { + src: string; + width: string; + height: string; + }; + }; + updateAttributes: (attrs: Record) => void; + selected: boolean; +} + +export const CustomImage = (props: ImageUploadProps) => { + const { getPos, editor, node, updateAttributes, selected } = props; + + const fileInputRef = useRef(null); + const hasTriggeredFilePickerRef = useRef(false); + const [isUploaded, setIsUploaded] = useState(!!node.attrs.src); + + const id = node.attrs.id as string; + const editorStorage = editor.storage.imageComponent as UploadImageExtensionStorage | undefined; + + const getUploadEntity = useCallback( + (): UploadEntity | undefined => editorStorage?.fileMap.get(id), + [editorStorage, id] + ); + + const onUpload = useCallback( + (url: string) => { + if (url) { + setIsUploaded(true); + // Update the node view's src attribute + updateAttributes({ src: url }); + editorStorage?.fileMap.delete(id); + } + }, + [editorStorage?.fileMap, id, updateAttributes] + ); + + const uploadFile = useCallback( + async (file: File) => { + try { + const result = await editor.commands.uploadImage(file)(); + if (result) { + onUpload(result); + } + } catch (error) { + console.error("Error uploading file:", error); + // Handle error state here if needed + } + }, + [editor.commands, onUpload] + ); + + useEffect(() => { + const uploadEntity = getUploadEntity(); + + if (uploadEntity) { + if (uploadEntity.event === "drop" && "file" in uploadEntity) { + uploadFile(uploadEntity.file); + } else if (uploadEntity.event === "insert" && fileInputRef.current && !hasTriggeredFilePickerRef.current) { + fileInputRef.current.click(); + hasTriggeredFilePickerRef.current = true; + } + } + }, [getUploadEntity, uploadFile]); + + useEffect(() => { + if (node.attrs.src) { + setIsUploaded(true); + } + }, [node.attrs]); + + const existingFile = React.useMemo(() => { + const entity = getUploadEntity(); + return entity && entity.event === "drop" ? entity.file : undefined; + }, [getUploadEntity]); + + return ( + +
+ {isUploaded ? ( + + ) : ( + + )} +
+
+ ); +}; + +export default CustomImage; diff --git a/packages/editor/src/core/extensions/custom-image-component/image-upload/image-uploader.tsx b/packages/editor/src/core/extensions/custom-image-component/image-upload/image-uploader.tsx new file mode 100644 index 00000000000..efdc1f789e5 --- /dev/null +++ b/packages/editor/src/core/extensions/custom-image-component/image-upload/image-uploader.tsx @@ -0,0 +1,91 @@ +import { ChangeEvent, useCallback, useEffect, useRef } from "react"; +import { Editor } from "@tiptap/core"; +import { ImageIcon } from "lucide-react"; +// helpers +import { cn } from "@/helpers/common"; +// hooks +import { useUploader, useFileUpload, useDropZone } from "@/hooks/use-file-upload"; +import { isFileValid } from "@/plugins/image"; + +type RefType = React.RefObject | ((instance: HTMLInputElement | null) => void); + +const assignRef = (ref: RefType, value: HTMLInputElement | null) => { + if (typeof ref === "function") { + ref(value); + } else if (ref && typeof ref === "object") { + (ref as React.MutableRefObject).current = value; + } +}; + +export const ImageUploader = (props: { + onUpload: (url: string) => void; + editor: Editor; + fileInputRef: RefType; + existingFile?: File; + selected: boolean; +}) => { + const { selected, onUpload, editor, fileInputRef, existingFile } = props; + const { loading, uploadFile } = useUploader({ onUpload, editor }); + const { handleUploadClick, ref: internalRef } = useFileUpload(); + const { draggedInside, onDrop, onDragEnter, onDragLeave } = useDropZone({ uploader: uploadFile }); + + const localRef = useRef(null); + + const onFileChange = useCallback( + (e: ChangeEvent) => { + const file = e.target.files?.[0]; + if (file) { + if (isFileValid(file)) { + uploadFile(file); + } + } + }, + [uploadFile] + ); + + useEffect(() => { + // no need to validate as the file is already validated before the drop onto + // the editor + if (existingFile) { + uploadFile(existingFile); + } + }, [existingFile, uploadFile]); + + return ( +
+ +
+ {loading ? "Uploading..." : draggedInside ? "Drop image here" : existingFile ? "Uploading..." : "Add an image"} +
+ { + localRef.current = element; + assignRef(fileInputRef, element); + assignRef(internalRef as RefType, element); + }} + hidden + type="file" + accept=".jpg,.jpeg,.png,.webp" + onChange={onFileChange} + /> +
+ ); +}; + +export default ImageUploader; diff --git a/packages/editor/src/core/extensions/custom-image-component/image-upload/index.tsx b/packages/editor/src/core/extensions/custom-image-component/image-upload/index.tsx new file mode 100644 index 00000000000..0d0b9c26381 --- /dev/null +++ b/packages/editor/src/core/extensions/custom-image-component/image-upload/index.tsx @@ -0,0 +1 @@ +export * from "./image-upload"; diff --git a/packages/editor/src/core/extensions/custom-image-component/index.ts b/packages/editor/src/core/extensions/custom-image-component/index.ts new file mode 100644 index 00000000000..f72f24fd8f3 --- /dev/null +++ b/packages/editor/src/core/extensions/custom-image-component/index.ts @@ -0,0 +1 @@ +export * from "./custom-image"; diff --git a/packages/editor/src/core/extensions/drop.tsx b/packages/editor/src/core/extensions/drop.tsx index d56f802d97a..943ab60d46f 100644 --- a/packages/editor/src/core/extensions/drop.tsx +++ b/packages/editor/src/core/extensions/drop.tsx @@ -1,11 +1,8 @@ import { Extension } from "@tiptap/core"; import { Plugin, PluginKey } from "prosemirror-state"; -// plugins -import { startImageUpload } from "@/plugins/image"; -// types -import { UploadImage } from "@/types"; +import { EditorView } from "prosemirror-view"; -export const DropHandlerExtension = (uploadFile: UploadImage) => +export const DropHandlerExtension = () => Extension.create({ name: "dropHandler", priority: 1000, @@ -15,28 +12,51 @@ export const DropHandlerExtension = (uploadFile: UploadImage) => new Plugin({ key: new PluginKey("drop-handler-plugin"), props: { - handlePaste: (view, event) => { - if (event.clipboardData && event.clipboardData.files && event.clipboardData.files[0]) { + handlePaste: (view: EditorView, event: ClipboardEvent) => { + if (event.clipboardData && event.clipboardData.files && event.clipboardData.files.length > 0) { event.preventDefault(); - const file = event.clipboardData.files[0]; - const pos = view.state.selection.from; - startImageUpload(this.editor, file, view, pos, uploadFile); - return true; + const files = Array.from(event.clipboardData.files); + const imageFiles = files.filter((file) => file.type.startsWith("image")); + + if (imageFiles.length > 0) { + const pos = view.state.selection.from; + imageFiles.forEach((file, index) => { + this.editor + .chain() + .focus() + .setImageUpload({ file, pos: pos + index, event: "drop" }) + .run(); + }); + return true; + } } return false; }, - handleDrop: (view, event, _slice, moved) => { - if (!moved && event.dataTransfer && event.dataTransfer.files && event.dataTransfer.files[0]) { + handleDrop: (view: EditorView, event: DragEvent, _slice: any, moved: boolean) => { + if (!moved && event.dataTransfer && event.dataTransfer.files && event.dataTransfer.files.length > 0) { event.preventDefault(); - const file = event.dataTransfer.files[0]; - const coordinates = view.posAtCoords({ - left: event.clientX, - top: event.clientY, - }); - if (coordinates) { - startImageUpload(this.editor, file, view, coordinates.pos - 1, uploadFile); + const files = Array.from(event.dataTransfer.files); + const imageFiles = files.filter((file) => file.type.startsWith("image")); + + if (imageFiles.length > 0) { + const coordinates = view.posAtCoords({ + left: event.clientX, + top: event.clientY, + }); + + if (coordinates) { + imageFiles.forEach((file, index) => { + setTimeout(() => { + this.editor + .chain() + .focus() + .setImageUpload({ file, pos: coordinates.pos + index, event: "drop" }) + .run(); + }, index * 100); // Slight delay between insertions + }); + } + return true; } - return true; } return false; }, diff --git a/packages/editor/src/core/extensions/extensions.tsx b/packages/editor/src/core/extensions/extensions.tsx index 823754a9317..411f7359c67 100644 --- a/packages/editor/src/core/extensions/extensions.tsx +++ b/packages/editor/src/core/extensions/extensions.tsx @@ -1,3 +1,4 @@ +import { HocuspocusProvider } from "@hocuspocus/provider"; import CharacterCount from "@tiptap/extension-character-count"; import Placeholder from "@tiptap/extension-placeholder"; import TaskItem from "@tiptap/extension-task-item"; @@ -29,6 +30,7 @@ import { import { isValidHttpUrl } from "@/helpers/common"; // types import { DeleteImage, IMentionHighlight, IMentionSuggestion, RestoreImage, UploadImage } from "@/types"; +import { CustomImageComponent } from "./custom-image-component"; type TArguments = { enableHistory: boolean; @@ -79,7 +81,7 @@ export const CoreEditorExtensions = ({ ...(enableHistory ? {} : { history: false }), }), CustomQuoteExtension, - DropHandlerExtension(uploadFile), + DropHandlerExtension(), CustomHorizontalRule.configure({ HTMLAttributes: { class: "my-4 border-custom-border-400", @@ -104,6 +106,12 @@ export const CoreEditorExtensions = ({ class: "rounded-md", }, }), + CustomImageComponent({ + delete: deleteFile, + restore: restoreFile, + upload: uploadFile, + cancel: cancelUploadImage ?? (() => {}), + }), TiptapUnderline, TextStyle, TaskList.configure({ @@ -142,7 +150,7 @@ export const CoreEditorExtensions = ({ placeholder: ({ editor, node }) => { if (node.type.name === "heading") return `Heading ${node.attrs.level}`; - if (editor.storage.image.uploadInProgress) return ""; + // if (editor.storage.image.uploadInProgress) return ""; const shouldHidePlaceholder = editor.isActive("table") || editor.isActive("codeBlock") || editor.isActive("image"); diff --git a/packages/editor/src/core/extensions/image/image-component-without-props.tsx b/packages/editor/src/core/extensions/image/image-component-without-props.tsx new file mode 100644 index 00000000000..e3eedff06c5 --- /dev/null +++ b/packages/editor/src/core/extensions/image/image-component-without-props.tsx @@ -0,0 +1,57 @@ +import { mergeAttributes } from "@tiptap/core"; +import { Image } from "@tiptap/extension-image"; +import { UploadImageExtensionStorage } from "@/extensions/custom-image-component"; + +export const CustomImageComponentWithoutProps = () => + Image.extend<{}, UploadImageExtensionStorage>({ + name: "imageComponent", + group: "inline", + draggable: true, + selectable: true, + + addAttributes() { + return { + ...this.parent?.(), + width: { + default: "35%", + }, + src: { + default: null, + }, + height: { + default: "auto", + }, + ["data-type"]: { + default: this.name, + }, + ["data-file"]: { + default: null, + }, + ["id"]: { + default: null, + }, + }; + }, + + parseHTML() { + return [ + { + tag: "image-component", + }, + ]; + }, + + renderHTML({ HTMLAttributes }) { + return ["image-component", mergeAttributes(HTMLAttributes)]; + }, + + addStorage() { + return { + fileMap: new Map(), + }; + }, + }).configure({ + inline: true, + }); + +export default CustomImageComponentWithoutProps; diff --git a/packages/editor/src/core/extensions/side-menu.tsx b/packages/editor/src/core/extensions/side-menu.tsx index 75577c74fc6..038d679b185 100644 --- a/packages/editor/src/core/extensions/side-menu.tsx +++ b/packages/editor/src/core/extensions/side-menu.tsx @@ -3,7 +3,7 @@ import { Plugin, PluginKey } from "@tiptap/pm/state"; import { EditorView } from "@tiptap/pm/view"; // plugins import { AIHandlePlugin } from "@/plugins/ai-handle"; -import { DragHandlePlugin } from "@/plugins/drag-handle"; +import { DragHandlePlugin, nodeDOMAtCoords } from "@/plugins/drag-handle"; type Props = { aiEnabled: boolean; @@ -59,41 +59,6 @@ const absoluteRect = (node: Element) => { }; }; -const nodeDOMAtCoords = (coords: { x: number; y: number }) => { - const elements = document.elementsFromPoint(coords.x, coords.y); - const generalSelectors = [ - "li", - "p:not(:first-child)", - ".code-block", - "blockquote", - "img", - "h1, h2, h3, h4, h5, h6", - "[data-type=horizontalRule]", - ".table-wrapper", - ".issue-embed", - ].join(", "); - - for (const elem of elements) { - if (elem.matches("p:first-child") && elem.parentElement?.matches(".ProseMirror")) { - return elem; - } - - // if the element is a

tag that is the first child of a td or th - if ( - (elem.matches("td > p:first-child") || elem.matches("th > p:first-child")) && - elem?.textContent?.trim() !== "" - ) { - return elem; // Return only if p tag is not empty in td or th - } - - // apply general selector - if (elem.matches(generalSelectors)) { - return elem; - } - } - return null; -}; - const SideMenu = (options: SideMenuPluginProps) => { const { handlesConfig } = options; const editorSideMenu: HTMLDivElement | null = document.createElement("div"); diff --git a/packages/editor/src/core/extensions/slash-commands.tsx b/packages/editor/src/core/extensions/slash-commands.tsx index 78aa379576b..3b1789781cf 100644 --- a/packages/editor/src/core/extensions/slash-commands.tsx +++ b/packages/editor/src/core/extensions/slash-commands.tsx @@ -28,7 +28,6 @@ import { toggleBulletList, toggleOrderedList, toggleTaskList, - insertImageCommand, toggleHeadingOne, toggleHeadingTwo, toggleHeadingThree, @@ -37,7 +36,7 @@ import { toggleHeadingSix, } from "@/helpers/editor-commands"; // types -import { CommandProps, ISlashCommandItem, UploadImage } from "@/types"; +import { CommandProps, ISlashCommandItem } from "@/types"; interface CommandItemProps { key: string; @@ -63,7 +62,7 @@ const Command = Extension.create({ const { selection } = editor.state; const parentNode = selection.$from.node(selection.$from.depth); - const blockType = parentNode?.type?.name; + const blockType = parentNode.type.name; if (blockType === "codeBlock") { return false; @@ -89,7 +88,7 @@ const Command = Extension.create({ }); const getSuggestionItems = - (uploadFile: UploadImage, additionalOptions?: Array) => + (additionalOptions?: Array) => ({ query }: { query: string }) => { let slashCommands: ISlashCommandItem[] = [ { @@ -224,11 +223,11 @@ const getSuggestionItems = { key: "image", title: "Image", - description: "Upload an image from your computer.", - searchTerms: ["img", "photo", "picture", "media"], icon: , + description: "Insert an image", + searchTerms: ["img", "photo", "picture", "media", "upload"], command: ({ editor, range }: CommandProps) => { - insertImageCommand(editor, uploadFile, null, range); + editor.chain().focus().deleteRange(range).setImageUpload({ event: "insert" }).run(); }, }, { @@ -415,10 +414,10 @@ const renderItems = () => { }; }; -export const SlashCommand = (uploadFile: UploadImage, additionalOptions?: Array) => +export const SlashCommand = (additionalOptions?: Array) => Command.configure({ suggestion: { - items: getSuggestionItems(uploadFile, additionalOptions), + items: getSuggestionItems(additionalOptions), render: renderItems, }, }); diff --git a/packages/editor/src/core/hooks/use-collaborative-editor.ts b/packages/editor/src/core/hooks/use-collaborative-editor.ts index eba56b099b8..35456068301 100644 --- a/packages/editor/src/core/hooks/use-collaborative-editor.ts +++ b/packages/editor/src/core/hooks/use-collaborative-editor.ts @@ -83,7 +83,6 @@ export const useCollaborativeEditor = (props: TCollaborativeEditorProps) => { ...(extensions ?? []), ...DocumentEditorAdditionalExtensions({ disabledExtensions, - fileHandler, issueEmbedConfig: embedHandler?.issue, provider, userDetails: user, diff --git a/packages/editor/src/core/hooks/use-editor.ts b/packages/editor/src/core/hooks/use-editor.ts index c12e612fe17..6de95557c87 100644 --- a/packages/editor/src/core/hooks/use-editor.ts +++ b/packages/editor/src/core/hooks/use-editor.ts @@ -67,6 +67,11 @@ export const useEditor = (props: CustomEditorProps) => { }), ...editorProps, }, + immediatelyRender: true, + shouldRerenderOnTransaction: false, + onContentError: (error) => { + console.error("Error rendering content:", error); + }, extensions: [ ...CoreEditorExtensions({ enableHistory, @@ -91,6 +96,7 @@ export const useEditor = (props: CustomEditorProps) => { onUpdate: ({ editor }) => onChange?.(editor.getJSON(), editor.getHTML()), onDestroy: () => handleEditorReady?.(false), }); + // Update the ref whenever savedSelection changes useEffect(() => { savedSelectionRef.current = savedSelection; @@ -131,7 +137,7 @@ export const useEditor = (props: CustomEditorProps) => { } }, executeMenuItemCommand: (itemKey: TEditorCommands) => { - const editorItems = getEditorMenuItems(editorRef.current, fileHandler.upload); + const editorItems = getEditorMenuItems(editorRef.current); const getEditorMenuItem = (itemKey: TEditorCommands) => editorItems.find((item) => item.key === itemKey); @@ -147,7 +153,7 @@ export const useEditor = (props: CustomEditorProps) => { } }, isMenuItemActive: (itemName: TEditorCommands): boolean => { - const editorItems = getEditorMenuItems(editorRef.current, fileHandler.upload); + const editorItems = getEditorMenuItems(editorRef.current); const getEditorMenuItem = (itemName: TEditorCommands) => editorItems.find((item) => item.key === itemName); const item = getEditorMenuItem(itemName); @@ -214,31 +220,34 @@ export const useEditor = (props: CustomEditorProps) => { } }); const selection = nodesArray.join(""); - console.log(selection); return selection; }, insertText: (contentHTML, insertOnNextLine) => { - if (!editor) return; + if (!editorRef.current) return; // get selection - const { from, to, empty } = editor.state.selection; + const { from, to, empty } = editorRef.current.state.selection; if (empty) return; if (insertOnNextLine) { // move cursor to the end of the selection and insert a new line - editor.chain().focus().setTextSelection(to).insertContent("
").insertContent(contentHTML).run(); + editorRef.current + .chain() + .focus() + .setTextSelection(to) + .insertContent("
") + .insertContent(contentHTML) + .run(); } else { // replace selected text with the content provided - editor.chain().focus().deleteRange({ from, to }).insertContent(contentHTML).run(); + editorRef.current.chain().focus().deleteRange({ from, to }).insertContent(contentHTML).run(); } }, - getDocumentInfo: () => { - return { - characters: editorRef?.current?.storage?.characterCount?.characters?.() ?? 0, - paragraphs: getParagraphCount(editorRef?.current?.state), - words: editorRef?.current?.storage?.characterCount?.words?.() ?? 0, - }; + documentInfo: { + characters: editorRef.current?.storage?.characterCount?.characters?.() ?? 0, + paragraphs: getParagraphCount(editorRef.current?.state), + words: editorRef.current?.storage?.characterCount?.words?.() ?? 0, }, }), - [editorRef, savedSelection, fileHandler.upload] + [editorRef, savedSelection] ); if (!editor) { diff --git a/packages/editor/src/core/hooks/use-file-upload.ts b/packages/editor/src/core/hooks/use-file-upload.ts new file mode 100644 index 00000000000..7d6a806ed14 --- /dev/null +++ b/packages/editor/src/core/hooks/use-file-upload.ts @@ -0,0 +1,112 @@ +import { DragEvent, useCallback, useEffect, useRef, useState } from "react"; +import { Editor } from "@tiptap/core"; +import { isFileValid } from "@/plugins/image"; + +export const useUploader = ({ onUpload, editor }: { onUpload: (url: string) => void; editor: Editor }) => { + const [loading, setLoading] = useState(false); + + const uploadFile = useCallback( + async (file: File) => { + setLoading(true); + try { + const url = await editor?.commands.uploadImage(file); + + if (!url) { + throw new Error("Something went wrong while uploading the image"); + } + onUpload(url); + } catch (errPayload: any) { + console.log(errPayload); + const error = errPayload?.response?.data?.error || "Something went wrong"; + console.error(error); + } + setLoading(false); + }, + [onUpload, editor] + ); + + return { loading, uploadFile }; +}; + +export const useFileUpload = () => { + const fileInput = useRef(null); + + const handleUploadClick = useCallback(() => { + fileInput.current?.click(); + }, []); + + return { ref: fileInput, handleUploadClick }; +}; + +export const useDropZone = ({ uploader }: { uploader: (file: File) => void }) => { + const [isDragging, setIsDragging] = useState(false); + const [draggedInside, setDraggedInside] = useState(false); + const [errorMessage, setErrorMessage] = useState(null); + + useEffect(() => { + const dragStartHandler = () => { + setIsDragging(true); + }; + + const dragEndHandler = () => { + setIsDragging(false); + }; + + document.body.addEventListener("dragstart", dragStartHandler); + document.body.addEventListener("dragend", dragEndHandler); + + return () => { + document.body.removeEventListener("dragstart", dragStartHandler); + document.body.removeEventListener("dragend", dragEndHandler); + }; + }, []); + + const onDrop = useCallback( + (e: DragEvent) => { + setDraggedInside(false); + setErrorMessage(null); + 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) { + const isValid = isFileValid(file); + if (isValid) { + uploader(file); + } + } + }, + [uploader] + ); + + const onDragEnter = () => { + setDraggedInside(true); + }; + + const onDragLeave = () => { + setDraggedInside(false); + }; + + return { isDragging, draggedInside, onDragEnter, onDragLeave, onDrop }; +}; diff --git a/packages/editor/src/core/plugins/drag-handle.ts b/packages/editor/src/core/plugins/drag-handle.ts index f8b258e8ad1..d260b284968 100644 --- a/packages/editor/src/core/plugins/drag-handle.ts +++ b/packages/editor/src/core/plugins/drag-handle.ts @@ -30,7 +30,7 @@ const createDragHandleElement = (): HTMLElement => { return dragHandleElement; }; -const nodeDOMAtCoords = (coords: { x: number; y: number }) => { +export const nodeDOMAtCoords = (coords: { x: number; y: number }) => { const elements = document.elementsFromPoint(coords.x, coords.y); const generalSelectors = [ "li", @@ -42,13 +42,34 @@ const nodeDOMAtCoords = (coords: { x: number; y: number }) => { "[data-type=horizontalRule]", ".table-wrapper", ".issue-embed", + ".image-upload-component", ].join(", "); + const hasNestedImg = (el: Element): boolean => { + if (el.tagName.toLowerCase() === "img") return true; + // @ts-expect-error todo + for (const child of el.children) { + if (hasNestedImg(child)) return true; + } + return false; + }; + for (const elem of elements) { + const elemHasNestedImg = hasNestedImg(elem); if (elem.matches("p:first-child") && elem.parentElement?.matches(".ProseMirror")) { return elem; } + // if the element is a

tag and has a nested img i.e. the new image + // component + if (elem.matches("p") && elemHasNestedImg) { + return null; + } + + if (elem.matches("div") && elemHasNestedImg) { + return elem; + } + // if the element is a

tag that is the first child of a td or th if ( (elem.matches("td > p:first-child") || elem.matches("th > p:first-child")) && diff --git a/packages/editor/src/styles/drag-drop.css b/packages/editor/src/styles/drag-drop.css index 3bea5dcf256..72d190f32f2 100644 --- a/packages/editor/src/styles/drag-drop.css +++ b/packages/editor/src/styles/drag-drop.css @@ -39,7 +39,7 @@ } /* end ai handle */ -.ProseMirror:not(.dragging) .ProseMirror-selectednode { +.ProseMirror:not(.dragging) .ProseMirror-selectednode:not(.node-imageBlock) { position: relative; cursor: grab; outline: none !important; @@ -63,6 +63,14 @@ border-radius: 4px; pointer-events: none; } + + &.node-imageComponent { + --horizontal-offset: 0px; + + &::after { + background-color: rgba(var(--color-background-100), 0.2); + } + } } /* for targeting the task list items */ @@ -96,7 +104,7 @@ ol > li:nth-child(n + 100).ProseMirror-selectednode:not(.dragging)::after { margin-left: -35px; } -.ProseMirror img { +.ProseMirror node-imageComponent { transition: filter 0.1s ease-in-out; cursor: pointer; diff --git a/packages/editor/src/styles/editor.css b/packages/editor/src/styles/editor.css index b27db3c6a41..f1a0e26739a 100644 --- a/packages/editor/src/styles/editor.css +++ b/packages/editor/src/styles/editor.css @@ -123,7 +123,7 @@ /* Custom image styles */ .ProseMirror img { transition: filter 0.1s ease-in-out; - margin-top: 8px; + margin-top: 0 !important; margin-bottom: 0; &:hover { @@ -170,13 +170,13 @@ ul[data-type="taskList"] li > label input[type="checkbox"]:hover { background-color: rgba(var(--color-background-80)) !important; } -ul[data-type="taskList"] li > label input[type="checkbox"][checked] { +ul[data-type="taskList"] li > label input[type="checkbox"]:checked { background-color: rgba(var(--color-primary-100)) !important; border-color: rgba(var(--color-primary-100)) !important; color: white !important; } -ul[data-type="taskList"] li > label input[type="checkbox"][checked]:hover { +ul[data-type="taskList"] li > label input[type="checkbox"]:checked:hover { background-color: rgba(var(--color-primary-300)) !important; border-color: rgba(var(--color-primary-300)) !important; } @@ -229,7 +229,7 @@ ul[data-type="taskList"] li > label input[type="checkbox"] { clip-path: polygon(14% 44%, 0 65%, 50% 100%, 100% 16%, 80% 0%, 43% 62%); } - &[checked]::before { + &:checked::before { transform: scale(1) translate(-50%, -50%); } } diff --git a/packages/editor/tsup.config.ts b/packages/editor/tsup.config.ts index 98a37e6705e..c378c0b2b2d 100644 --- a/packages/editor/tsup.config.ts +++ b/packages/editor/tsup.config.ts @@ -4,7 +4,7 @@ export default defineConfig((options: Options) => ({ entry: ["src/index.ts", "src/lib.ts"], format: ["cjs", "esm"], dts: true, - clean: true, + clean: false, external: ["react"], injectStyle: true, ...options, From c0fb56c71b1d05663509aefacb717c5169b3e417 Mon Sep 17 00:00:00 2001 From: Palanikannan M Date: Fri, 13 Sep 2024 14:23:14 +0530 Subject: [PATCH 15/29] fix: reverted back styles --- packages/editor/src/styles/editor.css | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/editor/src/styles/editor.css b/packages/editor/src/styles/editor.css index f1a0e26739a..b27db3c6a41 100644 --- a/packages/editor/src/styles/editor.css +++ b/packages/editor/src/styles/editor.css @@ -123,7 +123,7 @@ /* Custom image styles */ .ProseMirror img { transition: filter 0.1s ease-in-out; - margin-top: 0 !important; + margin-top: 8px; margin-bottom: 0; &:hover { @@ -170,13 +170,13 @@ ul[data-type="taskList"] li > label input[type="checkbox"]:hover { background-color: rgba(var(--color-background-80)) !important; } -ul[data-type="taskList"] li > label input[type="checkbox"]:checked { +ul[data-type="taskList"] li > label input[type="checkbox"][checked] { background-color: rgba(var(--color-primary-100)) !important; border-color: rgba(var(--color-primary-100)) !important; color: white !important; } -ul[data-type="taskList"] li > label input[type="checkbox"]:checked:hover { +ul[data-type="taskList"] li > label input[type="checkbox"][checked]:hover { background-color: rgba(var(--color-primary-300)) !important; border-color: rgba(var(--color-primary-300)) !important; } @@ -229,7 +229,7 @@ ul[data-type="taskList"] li > label input[type="checkbox"] { clip-path: polygon(14% 44%, 0 65%, 50% 100%, 100% 16%, 80% 0%, 43% 62%); } - &:checked::before { + &[checked]::before { transform: scale(1) translate(-50%, -50%); } } From 2502d5144d7828641d98aecb6df8b006959bbc12 Mon Sep 17 00:00:00 2001 From: Palanikannan M Date: Fri, 13 Sep 2024 14:24:55 +0530 Subject: [PATCH 16/29] fix: reverted back document info changes --- packages/editor/src/core/hooks/use-editor.ts | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/packages/editor/src/core/hooks/use-editor.ts b/packages/editor/src/core/hooks/use-editor.ts index 6de95557c87..769e17a3547 100644 --- a/packages/editor/src/core/hooks/use-editor.ts +++ b/packages/editor/src/core/hooks/use-editor.ts @@ -241,10 +241,12 @@ export const useEditor = (props: CustomEditorProps) => { editorRef.current.chain().focus().deleteRange({ from, to }).insertContent(contentHTML).run(); } }, - documentInfo: { - characters: editorRef.current?.storage?.characterCount?.characters?.() ?? 0, - paragraphs: getParagraphCount(editorRef.current?.state), - words: editorRef.current?.storage?.characterCount?.words?.() ?? 0, + getDocumentInfo: () => { + return { + characters: editorRef?.current?.storage?.characterCount?.characters?.() ?? 0, + paragraphs: getParagraphCount(editorRef?.current?.state), + words: editorRef?.current?.storage?.characterCount?.words?.() ?? 0, + }; }, }), [editorRef, savedSelection] From 47cf78e4f66350275492355a683814305472c7ee Mon Sep 17 00:00:00 2001 From: Palanikannan M Date: Fri, 13 Sep 2024 14:34:52 +0530 Subject: [PATCH 17/29] fix: css image css --- .../custom-image-component/components/image-block-view.tsx | 3 +-- packages/editor/src/styles/editor.css | 2 +- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/packages/editor/src/core/extensions/custom-image-component/components/image-block-view.tsx b/packages/editor/src/core/extensions/custom-image-component/components/image-block-view.tsx index 6fadb88ff70..801c1342166 100644 --- a/packages/editor/src/core/extensions/custom-image-component/components/image-block-view.tsx +++ b/packages/editor/src/core/extensions/custom-image-component/components/image-block-view.tsx @@ -1,4 +1,4 @@ -import React, { useRef, useState, useCallback, useLayoutEffect, useEffect } from "react"; +import React, { useRef, useState, useCallback, useLayoutEffect } from "react"; import { Node as ProsemirrorNode } from "@tiptap/pm/model"; import { NodeSelection } from "@tiptap/pm/state"; import { Editor } from "@tiptap/react"; @@ -25,7 +25,6 @@ export const ImageComponent: React.FC = (props) => { const { src, width, height } = node.attrs; const [size, setSize] = useState({ width: width || "35%", height: height || "auto" }); - // const [isSelected, setIsSelected] = useState(false); const [isLoading, setIsLoading] = useState(true); const containerRef = useRef(null); diff --git a/packages/editor/src/styles/editor.css b/packages/editor/src/styles/editor.css index b27db3c6a41..98f46db9abe 100644 --- a/packages/editor/src/styles/editor.css +++ b/packages/editor/src/styles/editor.css @@ -123,7 +123,7 @@ /* Custom image styles */ .ProseMirror img { transition: filter 0.1s ease-in-out; - margin-top: 8px; + margin-top: 0 !important; margin-bottom: 0; &:hover { From 2583038b0a11aed3cabdf3dbf3c2c87c900dddb2 Mon Sep 17 00:00:00 2001 From: Aaryan Khandelwal Date: Fri, 13 Sep 2024 15:12:10 +0530 Subject: [PATCH 18/29] style: image selected and hover states --- .../components/image-block-view.tsx | 33 +++++++++++-------- 1 file changed, 19 insertions(+), 14 deletions(-) diff --git a/packages/editor/src/core/extensions/custom-image-component/components/image-block-view.tsx b/packages/editor/src/core/extensions/custom-image-component/components/image-block-view.tsx index 801c1342166..1f8c7e54c20 100644 --- a/packages/editor/src/core/extensions/custom-image-component/components/image-block-view.tsx +++ b/packages/editor/src/core/extensions/custom-image-component/components/image-block-view.tsx @@ -2,6 +2,9 @@ import React, { useRef, useState, useCallback, useLayoutEffect } from "react"; import { Node as ProsemirrorNode } from "@tiptap/pm/model"; import { NodeSelection } from "@tiptap/pm/state"; import { Editor } from "@tiptap/react"; +// helpers +import { cn } from "@/helpers/common"; +// components import { ImageShimmer } from "./image-loader"; interface ImageBlockViewProps { @@ -101,31 +104,33 @@ export const ImageComponent: React.FC = (props) => { return (

{isLoading && } - {selected && ( - <> -
-
- - )} + {selected &&
} + <> +
+
+
); }; From f8d85fd6333f7f8d4db25803aa1cdd438fe06f07 Mon Sep 17 00:00:00 2001 From: Aaryan Khandelwal Date: Fri, 13 Sep 2024 15:50:26 +0530 Subject: [PATCH 19/29] refactor: custom image extension folder structure --- .../components/image-loader.tsx | 3 - .../image-upload/index.tsx | 1 - .../custom-image-component/index.ts | 1 - .../components/image-block.tsx} | 28 ++------- .../components/image-node.tsx} | 22 +++---- .../components}/image-uploader.tsx | 5 +- .../custom-image/components/index.ts | 3 + .../custom-image.ts | 15 +++-- .../src/core/extensions/custom-image/index.ts | 3 + .../custom-image/read-only-custom-image.ts | 60 +++++++++++++++++++ .../editor/src/core/extensions/extensions.tsx | 4 +- .../image/image-component-without-props.tsx | 2 +- packages/editor/src/core/extensions/index.ts | 1 + .../core/extensions/read-only-extensions.tsx | 2 + space/helpers/string.helper.ts | 4 +- web/helpers/string.helper.ts | 4 +- 16 files changed, 104 insertions(+), 54 deletions(-) delete mode 100644 packages/editor/src/core/extensions/custom-image-component/components/image-loader.tsx delete mode 100644 packages/editor/src/core/extensions/custom-image-component/image-upload/index.tsx delete mode 100644 packages/editor/src/core/extensions/custom-image-component/index.ts rename packages/editor/src/core/extensions/{custom-image-component/components/image-block-view.tsx => custom-image/components/image-block.tsx} (84%) rename packages/editor/src/core/extensions/{custom-image-component/image-upload/image-upload.tsx => custom-image/components/image-node.tsx} (87%) rename packages/editor/src/core/extensions/{custom-image-component/image-upload => custom-image/components}/image-uploader.tsx (97%) create mode 100644 packages/editor/src/core/extensions/custom-image/components/index.ts rename packages/editor/src/core/extensions/{custom-image-component => custom-image}/custom-image.ts (93%) create mode 100644 packages/editor/src/core/extensions/custom-image/index.ts create mode 100644 packages/editor/src/core/extensions/custom-image/read-only-custom-image.ts diff --git a/packages/editor/src/core/extensions/custom-image-component/components/image-loader.tsx b/packages/editor/src/core/extensions/custom-image-component/components/image-loader.tsx deleted file mode 100644 index 52c62493aaa..00000000000 --- a/packages/editor/src/core/extensions/custom-image-component/components/image-loader.tsx +++ /dev/null @@ -1,3 +0,0 @@ -export const ImageShimmer: React.FC<{ width: string; height: string }> = ({ width, height }) => ( -
-); diff --git a/packages/editor/src/core/extensions/custom-image-component/image-upload/index.tsx b/packages/editor/src/core/extensions/custom-image-component/image-upload/index.tsx deleted file mode 100644 index 0d0b9c26381..00000000000 --- a/packages/editor/src/core/extensions/custom-image-component/image-upload/index.tsx +++ /dev/null @@ -1 +0,0 @@ -export * from "./image-upload"; diff --git a/packages/editor/src/core/extensions/custom-image-component/index.ts b/packages/editor/src/core/extensions/custom-image-component/index.ts deleted file mode 100644 index f72f24fd8f3..00000000000 --- a/packages/editor/src/core/extensions/custom-image-component/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from "./custom-image"; diff --git a/packages/editor/src/core/extensions/custom-image-component/components/image-block-view.tsx b/packages/editor/src/core/extensions/custom-image/components/image-block.tsx similarity index 84% rename from packages/editor/src/core/extensions/custom-image-component/components/image-block-view.tsx rename to packages/editor/src/core/extensions/custom-image/components/image-block.tsx index 1f8c7e54c20..9de98dfae9e 100644 --- a/packages/editor/src/core/extensions/custom-image-component/components/image-block-view.tsx +++ b/packages/editor/src/core/extensions/custom-image/components/image-block.tsx @@ -1,29 +1,13 @@ import React, { useRef, useState, useCallback, useLayoutEffect } from "react"; -import { Node as ProsemirrorNode } from "@tiptap/pm/model"; import { NodeSelection } from "@tiptap/pm/state"; -import { Editor } from "@tiptap/react"; +// extensions +import { CustomImageNodeViewProps } from "@/extensions/custom-image"; // helpers import { cn } from "@/helpers/common"; -// components -import { ImageShimmer } from "./image-loader"; - -interface ImageBlockViewProps { - editor: Editor; - getPos: () => number; - node: ProsemirrorNode & { - attrs: { - src: string; - width: string; - height: string; - }; - }; - updateAttributes: (attrs: Record) => void; - selected: boolean; -} const MIN_SIZE = 100; -export const ImageComponent: React.FC = (props) => { +export const CustomImageBlock: React.FC = (props) => { const { node, updateAttributes, selected, getPos, editor } = props; const { src, width, height } = node.attrs; @@ -111,7 +95,7 @@ export const ImageComponent: React.FC = (props) => { height: size.height, }} > - {isLoading && } + {isLoading &&
} = (props) => { height: size.height, }} /> - {selected &&
} + {editor.isEditable && selected &&
} <>
= (props) => {
); }; - -export default ImageComponent; diff --git a/packages/editor/src/core/extensions/custom-image-component/image-upload/image-upload.tsx b/packages/editor/src/core/extensions/custom-image/components/image-node.tsx similarity index 87% rename from packages/editor/src/core/extensions/custom-image-component/image-upload/image-upload.tsx rename to packages/editor/src/core/extensions/custom-image/components/image-node.tsx index d79162749a7..b65d890f56b 100644 --- a/packages/editor/src/core/extensions/custom-image-component/image-upload/image-upload.tsx +++ b/packages/editor/src/core/extensions/custom-image/components/image-node.tsx @@ -1,11 +1,15 @@ import React, { useCallback, useEffect, useRef, useState } from "react"; import { Node as ProsemirrorNode } from "@tiptap/pm/model"; import { Editor, NodeViewWrapper } from "@tiptap/react"; -import { UploadEntity, UploadImageExtensionStorage } from "@/extensions/custom-image-component"; -import ImageComponent from "@/extensions/custom-image-component/components/image-block-view"; -import { ImageUploader } from "./image-uploader"; +// extensions +import { + CustomImageBlock, + CustomImageUploader, + UploadEntity, + UploadImageExtensionStorage, +} from "@/extensions/custom-image"; -interface ImageUploadProps { +export type CustomImageNodeViewProps = { getPos: () => number; editor: Editor; node: ProsemirrorNode & { @@ -17,9 +21,9 @@ interface ImageUploadProps { }; updateAttributes: (attrs: Record) => void; selected: boolean; -} +}; -export const CustomImage = (props: ImageUploadProps) => { +export const CustomImageNode = (props: CustomImageNodeViewProps) => { const { getPos, editor, node, updateAttributes, selected } = props; const fileInputRef = useRef(null); @@ -89,7 +93,7 @@ export const CustomImage = (props: ImageUploadProps) => {
{isUploaded ? ( - { selected={selected} /> ) : ( - { ); }; - -export default CustomImage; diff --git a/packages/editor/src/core/extensions/custom-image-component/image-upload/image-uploader.tsx b/packages/editor/src/core/extensions/custom-image/components/image-uploader.tsx similarity index 97% rename from packages/editor/src/core/extensions/custom-image-component/image-upload/image-uploader.tsx rename to packages/editor/src/core/extensions/custom-image/components/image-uploader.tsx index efdc1f789e5..d288630c637 100644 --- a/packages/editor/src/core/extensions/custom-image-component/image-upload/image-uploader.tsx +++ b/packages/editor/src/core/extensions/custom-image/components/image-uploader.tsx @@ -5,6 +5,7 @@ import { ImageIcon } from "lucide-react"; import { cn } from "@/helpers/common"; // hooks import { useUploader, useFileUpload, useDropZone } from "@/hooks/use-file-upload"; +// plugins import { isFileValid } from "@/plugins/image"; type RefType = React.RefObject | ((instance: HTMLInputElement | null) => void); @@ -17,7 +18,7 @@ const assignRef = (ref: RefType, value: HTMLInputElement | null) => { } }; -export const ImageUploader = (props: { +export const CustomImageUploader = (props: { onUpload: (url: string) => void; editor: Editor; fileInputRef: RefType; @@ -87,5 +88,3 @@ export const ImageUploader = (props: {
); }; - -export default ImageUploader; diff --git a/packages/editor/src/core/extensions/custom-image/components/index.ts b/packages/editor/src/core/extensions/custom-image/components/index.ts new file mode 100644 index 00000000000..d16be13c869 --- /dev/null +++ b/packages/editor/src/core/extensions/custom-image/components/index.ts @@ -0,0 +1,3 @@ +export * from "./image-block"; +export * from "./image-node"; +export * from "./image-uploader"; diff --git a/packages/editor/src/core/extensions/custom-image-component/custom-image.ts b/packages/editor/src/core/extensions/custom-image/custom-image.ts similarity index 93% rename from packages/editor/src/core/extensions/custom-image-component/custom-image.ts rename to packages/editor/src/core/extensions/custom-image/custom-image.ts index 6d9f5cf8bd1..4f0b96e7e8f 100644 --- a/packages/editor/src/core/extensions/custom-image-component/custom-image.ts +++ b/packages/editor/src/core/extensions/custom-image/custom-image.ts @@ -2,11 +2,12 @@ import { mergeAttributes } from "@tiptap/core"; import { Image } from "@tiptap/extension-image"; import { ReactNodeViewRenderer } from "@tiptap/react"; import { v4 as uuidv4 } from "uuid"; - -import { TFileHandler } from "@/types"; - -import { CustomImage } from "./image-upload"; +// extensions +import { CustomImageNode } from "@/extensions/custom-image"; +// plugins import { isFileValid } from "@/plugins/image"; +// types +import { TFileHandler } from "@/types"; declare module "@tiptap/core" { interface Commands { @@ -23,7 +24,7 @@ export interface UploadImageExtensionStorage { export type UploadEntity = { event: "insert" } | { event: "drop"; file: File }; -export const CustomImageComponent = (props: TFileHandler) => { +export const CustomImageExtension = (props: TFileHandler) => { const { upload } = props; return Image.extend<{}, UploadImageExtensionStorage>({ @@ -124,9 +125,7 @@ export const CustomImageComponent = (props: TFileHandler) => { }, addNodeView() { - return ReactNodeViewRenderer(CustomImage); + return ReactNodeViewRenderer(CustomImageNode); }, }); }; - -export default CustomImageComponent; diff --git a/packages/editor/src/core/extensions/custom-image/index.ts b/packages/editor/src/core/extensions/custom-image/index.ts new file mode 100644 index 00000000000..de2bb38789d --- /dev/null +++ b/packages/editor/src/core/extensions/custom-image/index.ts @@ -0,0 +1,3 @@ +export * from "./components"; +export * from "./custom-image"; +export * from "./read-only-custom-image"; diff --git a/packages/editor/src/core/extensions/custom-image/read-only-custom-image.ts b/packages/editor/src/core/extensions/custom-image/read-only-custom-image.ts new file mode 100644 index 00000000000..51235eeee78 --- /dev/null +++ b/packages/editor/src/core/extensions/custom-image/read-only-custom-image.ts @@ -0,0 +1,60 @@ +import { mergeAttributes } from "@tiptap/core"; +import { Image } from "@tiptap/extension-image"; +import { ReactNodeViewRenderer } from "@tiptap/react"; +// components +import { CustomImageNode, UploadImageExtensionStorage } from "@/extensions/custom-image"; + +export const CustomReadOnlyImageExtension = () => + Image.extend<{}, UploadImageExtensionStorage>({ + name: "imageComponent", + selectable: true, + group: "block", + atom: true, + draggable: true, + + addAttributes() { + return { + ...this.parent?.(), + width: { + default: "35%", + }, + src: { + default: null, + }, + height: { + default: "auto", + }, + ["data-type"]: { + default: this.name, + }, + ["data-file"]: { + default: null, + }, + ["id"]: { + default: null, + }, + }; + }, + + parseHTML() { + return [ + { + tag: "image-component", + }, + ]; + }, + + renderHTML({ HTMLAttributes }) { + return ["image-component", mergeAttributes(HTMLAttributes)]; + }, + + addStorage() { + return { + fileMap: new Map(), + }; + }, + + addNodeView() { + return ReactNodeViewRenderer(CustomImageNode); + }, + }); diff --git a/packages/editor/src/core/extensions/extensions.tsx b/packages/editor/src/core/extensions/extensions.tsx index 411f7359c67..41406b79543 100644 --- a/packages/editor/src/core/extensions/extensions.tsx +++ b/packages/editor/src/core/extensions/extensions.tsx @@ -13,6 +13,7 @@ import { CustomCodeInlineExtension, CustomCodeMarkPlugin, CustomHorizontalRule, + CustomImageExtension, CustomKeymap, CustomLinkExtension, CustomMention, @@ -30,7 +31,6 @@ import { import { isValidHttpUrl } from "@/helpers/common"; // types import { DeleteImage, IMentionHighlight, IMentionSuggestion, RestoreImage, UploadImage } from "@/types"; -import { CustomImageComponent } from "./custom-image-component"; type TArguments = { enableHistory: boolean; @@ -106,7 +106,7 @@ export const CoreEditorExtensions = ({ class: "rounded-md", }, }), - CustomImageComponent({ + CustomImageExtension({ delete: deleteFile, restore: restoreFile, upload: uploadFile, diff --git a/packages/editor/src/core/extensions/image/image-component-without-props.tsx b/packages/editor/src/core/extensions/image/image-component-without-props.tsx index e3eedff06c5..dfe58d4adcf 100644 --- a/packages/editor/src/core/extensions/image/image-component-without-props.tsx +++ b/packages/editor/src/core/extensions/image/image-component-without-props.tsx @@ -1,6 +1,6 @@ import { mergeAttributes } from "@tiptap/core"; import { Image } from "@tiptap/extension-image"; -import { UploadImageExtensionStorage } from "@/extensions/custom-image-component"; +import { UploadImageExtensionStorage } from "@/extensions/custom-image"; export const CustomImageComponentWithoutProps = () => Image.extend<{}, UploadImageExtensionStorage>({ diff --git a/packages/editor/src/core/extensions/index.ts b/packages/editor/src/core/extensions/index.ts index 41f8189b0b7..658dd2f7997 100644 --- a/packages/editor/src/core/extensions/index.ts +++ b/packages/editor/src/core/extensions/index.ts @@ -1,5 +1,6 @@ export * from "./code"; export * from "./code-inline"; +export * from "./custom-image"; export * from "./custom-link"; export * from "./custom-list-keymap"; export * from "./image"; diff --git a/packages/editor/src/core/extensions/read-only-extensions.tsx b/packages/editor/src/core/extensions/read-only-extensions.tsx index 68e3f7d6888..2898b6cdc03 100644 --- a/packages/editor/src/core/extensions/read-only-extensions.tsx +++ b/packages/editor/src/core/extensions/read-only-extensions.tsx @@ -19,6 +19,7 @@ import { TableRow, Table, CustomMention, + CustomReadOnlyImageExtension, } from "@/extensions"; // helpers import { isValidHttpUrl } from "@/helpers/common"; @@ -74,6 +75,7 @@ export const CoreReadOnlyEditorExtensions = (mentionConfig: { class: "rounded-md", }, }), + CustomReadOnlyImageExtension(), TiptapUnderline, TextStyle, TaskList.configure({ diff --git a/space/helpers/string.helper.ts b/space/helpers/string.helper.ts index 6c60e4bb9f0..5c704c44c36 100644 --- a/space/helpers/string.helper.ts +++ b/space/helpers/string.helper.ts @@ -69,7 +69,9 @@ export const isCommentEmpty = (comment: string | undefined): boolean => { // return true if comment is undefined if (!comment) return true; return ( - comment?.trim() === "" || comment === "

" || isEmptyHtmlString(comment ?? "", ["img", "mention-component"]) + comment?.trim() === "" || + comment === "

" || + isEmptyHtmlString(comment ?? "", ["img", "mention-component", "image-component"]) ); }; diff --git a/web/helpers/string.helper.ts b/web/helpers/string.helper.ts index 00d601c74e0..1182feeb0a7 100644 --- a/web/helpers/string.helper.ts +++ b/web/helpers/string.helper.ts @@ -249,7 +249,9 @@ export const isCommentEmpty = (comment: string | undefined): boolean => { // return true if comment is undefined if (!comment) return true; return ( - comment?.trim() === "" || comment === "

" || isEmptyHtmlString(comment ?? "", ["img", "mention-component"]) + comment?.trim() === "" || + comment === "

" || + isEmptyHtmlString(comment ?? "", ["img", "mention-component", "image-component"]) ); }; From 5a9548680f1b8233ea3f9cfdb2440c00f5c06a3c Mon Sep 17 00:00:00 2001 From: Aaryan Khandelwal Date: Fri, 13 Sep 2024 16:14:30 +0530 Subject: [PATCH 20/29] style: read-only image --- .../custom-image/components/image-block.tsx | 17 ++++++++++------- .../custom-image/read-only-custom-image.ts | 4 ++-- packages/editor/src/styles/editor.css | 19 +++++++++++-------- 3 files changed, 23 insertions(+), 17 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 9de98dfae9e..5b0163239b6 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 @@ -101,6 +101,7 @@ export const CustomImageBlock: React.FC = (props) => { src={src} className={cn("block rounded-md", { hidden: isLoading, + "read-only-image": !editor.isEditable, })} style={{ width: size.width, @@ -108,13 +109,15 @@ export const CustomImageBlock: React.FC = (props) => { }} /> {editor.isEditable && selected &&
} - <> -
-
- + {editor.isEditable && ( + <> +
+
+ + )}
); }; diff --git a/packages/editor/src/core/extensions/custom-image/read-only-custom-image.ts b/packages/editor/src/core/extensions/custom-image/read-only-custom-image.ts index 51235eeee78..7ffd4fba8b2 100644 --- a/packages/editor/src/core/extensions/custom-image/read-only-custom-image.ts +++ b/packages/editor/src/core/extensions/custom-image/read-only-custom-image.ts @@ -7,10 +7,10 @@ import { CustomImageNode, UploadImageExtensionStorage } from "@/extensions/custo export const CustomReadOnlyImageExtension = () => Image.extend<{}, UploadImageExtensionStorage>({ name: "imageComponent", - selectable: true, + selectable: false, group: "block", atom: true, - draggable: true, + draggable: false, addAttributes() { return { diff --git a/packages/editor/src/styles/editor.css b/packages/editor/src/styles/editor.css index 98f46db9abe..6d606bfeab3 100644 --- a/packages/editor/src/styles/editor.css +++ b/packages/editor/src/styles/editor.css @@ -122,18 +122,21 @@ /* Custom image styles */ .ProseMirror img { - transition: filter 0.1s ease-in-out; margin-top: 0 !important; margin-bottom: 0; - &:hover { - cursor: pointer; - filter: brightness(90%); - } + &:not(.read-only-image) { + transition: filter 0.1s ease-in-out; + + &:hover { + cursor: pointer; + filter: brightness(90%); + } - &.ProseMirror-selectednode { - outline: 3px solid rgba(var(--color-primary-100)); - filter: brightness(90%); + &.ProseMirror-selectednode { + outline: 3px solid rgba(var(--color-primary-100)); + filter: brightness(90%); + } } } From 2e1745e9058a04bcdca3fe733c52aa1a8f3813fe Mon Sep 17 00:00:00 2001 From: Palanikannan M Date: Fri, 13 Sep 2024 16:25:33 +0530 Subject: [PATCH 21/29] chore: remove file handler --- .../editor/src/core/components/editors/rich-text/editor.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/editor/src/core/components/editors/rich-text/editor.tsx b/packages/editor/src/core/components/editors/rich-text/editor.tsx index b1f9eed7d0f..fe4d2d51373 100644 --- a/packages/editor/src/core/components/editors/rich-text/editor.tsx +++ b/packages/editor/src/core/components/editors/rich-text/editor.tsx @@ -8,7 +8,7 @@ import { SideMenuExtension, SlashCommand } from "@/extensions"; import { EditorRefApi, IRichTextEditor } from "@/types"; const RichTextEditor = (props: IRichTextEditor) => { - const { dragDropEnabled, fileHandler } = props; + const { dragDropEnabled } = props; const getExtensions = useCallback(() => { const extensions = [SlashCommand()]; @@ -21,7 +21,7 @@ const RichTextEditor = (props: IRichTextEditor) => { ); return extensions; - }, [dragDropEnabled, fileHandler.upload]); + }, [dragDropEnabled]); return ( From fa5cfd5fc3720a8800e6336ad628ed5a1bda9f36 Mon Sep 17 00:00:00 2001 From: Palanikannan M Date: Fri, 13 Sep 2024 21:09:08 +0530 Subject: [PATCH 22/29] fix: fixed multi time file opener --- .../custom-image/components/image-node.tsx | 4 ++++ .../core/extensions/custom-image/custom-image.ts | 13 ++++--------- .../custom-image/read-only-custom-image.ts | 6 ------ 3 files changed, 8 insertions(+), 15 deletions(-) diff --git a/packages/editor/src/core/extensions/custom-image/components/image-node.tsx b/packages/editor/src/core/extensions/custom-image/components/image-node.tsx index b65d890f56b..17aa0b90c4d 100644 --- a/packages/editor/src/core/extensions/custom-image/components/image-node.tsx +++ b/packages/editor/src/core/extensions/custom-image/components/image-node.tsx @@ -72,8 +72,12 @@ export const CustomImageNode = (props: CustomImageNodeViewProps) => { if (uploadEntity.event === "drop" && "file" in uploadEntity) { uploadFile(uploadEntity.file); } else if (uploadEntity.event === "insert" && fileInputRef.current && !hasTriggeredFilePickerRef.current) { + const entity = editorStorage?.fileMap.get(id); + if (entity && entity.hasOpenedFileInputOnce) return; fileInputRef.current.click(); hasTriggeredFilePickerRef.current = true; + if (!entity) return; + editorStorage?.fileMap.set(id, { ...entity, hasOpenedFileInputOnce: true }); } } }, [getUploadEntity, uploadFile]); diff --git a/packages/editor/src/core/extensions/custom-image/custom-image.ts b/packages/editor/src/core/extensions/custom-image/custom-image.ts index 4f0b96e7e8f..ee942c1bd75 100644 --- a/packages/editor/src/core/extensions/custom-image/custom-image.ts +++ b/packages/editor/src/core/extensions/custom-image/custom-image.ts @@ -22,7 +22,7 @@ export interface UploadImageExtensionStorage { fileMap: Map; } -export type UploadEntity = { event: "insert" } | { event: "drop"; file: File }; +export type UploadEntity = ({ event: "insert" } | { event: "drop"; file: File }) & { hasOpenedFileInputOnce: boolean }; export const CustomImageExtension = (props: TFileHandler) => { const { upload } = props; @@ -46,12 +46,6 @@ export const CustomImageExtension = (props: TFileHandler) => { height: { default: "auto", }, - ["data-type"]: { - default: this.name, - }, - ["data-file"]: { - default: null, - }, ["id"]: { default: null, }, @@ -63,6 +57,9 @@ export const CustomImageExtension = (props: TFileHandler) => { { tag: "image-component", }, + { + tag: "img", + }, ]; }, @@ -101,9 +98,7 @@ export const CustomImageExtension = (props: TFileHandler) => { } const attributes = { - "data-type": this.name, id: fileId, - "data-file": props.file ? `data-file="${props.file}"` : "", }; if (props.pos) { diff --git a/packages/editor/src/core/extensions/custom-image/read-only-custom-image.ts b/packages/editor/src/core/extensions/custom-image/read-only-custom-image.ts index 7ffd4fba8b2..eb75de63c85 100644 --- a/packages/editor/src/core/extensions/custom-image/read-only-custom-image.ts +++ b/packages/editor/src/core/extensions/custom-image/read-only-custom-image.ts @@ -24,12 +24,6 @@ export const CustomReadOnlyImageExtension = () => height: { default: "auto", }, - ["data-type"]: { - default: this.name, - }, - ["data-file"]: { - default: null, - }, ["id"]: { default: null, }, From 78b1c50a7d180ce36bbab001f3c14d0822b7209c Mon Sep 17 00:00:00 2001 From: Palanikannan M Date: Fri, 13 Sep 2024 21:10:27 +0530 Subject: [PATCH 23/29] fix: editor readonly content set properly --- .../image/image-component-without-props.tsx | 16 ++++++---------- packages/editor/src/core/hooks/use-editor.ts | 2 +- .../src/core/hooks/use-read-only-editor.ts | 4 ++-- 3 files changed, 9 insertions(+), 13 deletions(-) diff --git a/packages/editor/src/core/extensions/image/image-component-without-props.tsx b/packages/editor/src/core/extensions/image/image-component-without-props.tsx index dfe58d4adcf..de27ba98ee4 100644 --- a/packages/editor/src/core/extensions/image/image-component-without-props.tsx +++ b/packages/editor/src/core/extensions/image/image-component-without-props.tsx @@ -5,9 +5,10 @@ import { UploadImageExtensionStorage } from "@/extensions/custom-image"; export const CustomImageComponentWithoutProps = () => Image.extend<{}, UploadImageExtensionStorage>({ name: "imageComponent", - group: "inline", - draggable: true, selectable: true, + group: "block", + atom: true, + draggable: true, addAttributes() { return { @@ -21,12 +22,6 @@ export const CustomImageComponentWithoutProps = () => height: { default: "auto", }, - ["data-type"]: { - default: this.name, - }, - ["data-file"]: { - default: null, - }, ["id"]: { default: null, }, @@ -38,6 +33,9 @@ export const CustomImageComponentWithoutProps = () => { tag: "image-component", }, + { + tag: "img", + }, ]; }, @@ -50,8 +48,6 @@ export const CustomImageComponentWithoutProps = () => fileMap: new Map(), }; }, - }).configure({ - inline: true, }); export default CustomImageComponentWithoutProps; diff --git a/packages/editor/src/core/hooks/use-editor.ts b/packages/editor/src/core/hooks/use-editor.ts index 769e17a3547..15b5031849e 100644 --- a/packages/editor/src/core/hooks/use-editor.ts +++ b/packages/editor/src/core/hooks/use-editor.ts @@ -129,7 +129,7 @@ export const useEditor = (props: CustomEditorProps) => { editorRef.current?.commands.clearContent(emitUpdate); }, setEditorValue: (content: string) => { - editorRef.current?.commands.setContent(content); + editorRef.current?.commands.setContent(content, false, { preserveWhitespace: "full" }); }, setEditorValueAtCursorPosition: (content: string) => { if (savedSelection) { 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 b6081a51032..3ee7f8e9075 100644 --- a/packages/editor/src/core/hooks/use-read-only-editor.ts +++ b/packages/editor/src/core/hooks/use-read-only-editor.ts @@ -58,7 +58,7 @@ export const useReadOnlyEditor = ({ // for syncing swr data on tab refocus etc useEffect(() => { if (initialValue === null || initialValue === undefined) return; - if (editor && !editor.isDestroyed) editor?.commands.setContent(initialValue); + if (editor && !editor.isDestroyed) editor?.commands.setContent(initialValue, false, { preserveWhitespace: "full" }); }, [editor, initialValue]); const editorRef: MutableRefObject = useRef(null); @@ -68,7 +68,7 @@ export const useReadOnlyEditor = ({ editorRef.current?.commands.clearContent(); }, setEditorValue: (content: string) => { - editorRef.current?.commands.setContent(content); + editorRef.current?.commands.setContent(content, false, { preserveWhitespace: "full" }); }, getMarkDown: (): string => { const markdownOutput = editorRef.current?.storage.markdown.getMarkdown(); From 8b9418d8facba9fde8f968cc6a3dc14995de668d Mon Sep 17 00:00:00 2001 From: Palanikannan M Date: Mon, 16 Sep 2024 12:25:48 +0530 Subject: [PATCH 24/29] fix: old images not rendered as new ones --- .../components/editors/editor-content.tsx | 3 - .../src/core/components/menus/block-menu.tsx | 3 +- .../src/core/components/menus/menu-items.ts | 3 +- .../extensions/custom-image/custom-image.ts | 44 ++++++++-- .../src/core/extensions/image/extension.tsx | 28 +++--- .../image/image-component-without-props.tsx | 12 ++- .../image/image-extension-without-props.tsx | 7 ++ .../core/extensions/image/image-resize.tsx | 85 ------------------- .../editor/src/core/extensions/image/index.ts | 1 - .../core/extensions/image/read-only-image.tsx | 6 ++ .../src/core/helpers/editor-commands.ts | 20 ----- packages/editor/src/core/hooks/use-editor.ts | 5 -- .../src/core/plugins/image/delete-image.ts | 14 +-- .../src/core/plugins/image/restore-image.ts | 21 ++--- packages/editor/src/index.ts | 1 - packages/editor/src/styles/drag-drop.css | 6 +- packages/editor/src/styles/editor.css | 20 ----- 17 files changed, 99 insertions(+), 180 deletions(-) delete mode 100644 packages/editor/src/core/extensions/image/image-resize.tsx diff --git a/packages/editor/src/core/components/editors/editor-content.tsx b/packages/editor/src/core/components/editors/editor-content.tsx index 691bc1002b7..b05457f2e63 100644 --- a/packages/editor/src/core/components/editors/editor-content.tsx +++ b/packages/editor/src/core/components/editors/editor-content.tsx @@ -1,7 +1,5 @@ import { FC, ReactNode } from "react"; import { Editor, EditorContent } from "@tiptap/react"; -// extensions -import { ImageResizer } from "@/extensions/image"; interface EditorContentProps { children?: ReactNode; @@ -16,7 +14,6 @@ export const EditorContentWrapper: FC = (props) => { return (
editor?.chain().focus(undefined, { scrollIntoView: false }).run()}> - {editor?.isActive("image") && editor?.isEditable && } {children}
); diff --git a/packages/editor/src/core/components/menus/block-menu.tsx b/packages/editor/src/core/components/menus/block-menu.tsx index a90e39ca22b..e1715b5f783 100644 --- a/packages/editor/src/core/components/menus/block-menu.tsx +++ b/packages/editor/src/core/components/menus/block-menu.tsx @@ -101,7 +101,8 @@ export const BlockMenu = (props: BlockMenuProps) => { icon: Copy, key: "duplicate", label: "Duplicate", - isDisabled: editor.state.selection.content().content.firstChild?.type.name === "image", + isDisabled: + editor.state.selection.content().content.firstChild?.type.name === "image" || editor.isActive("imageComponent"), onClick: (e) => { e.preventDefault(); e.stopPropagation(); diff --git a/packages/editor/src/core/components/menus/menu-items.ts b/packages/editor/src/core/components/menus/menu-items.ts index 63d76117ab4..b60196beacd 100644 --- a/packages/editor/src/core/components/menus/menu-items.ts +++ b/packages/editor/src/core/components/menus/menu-items.ts @@ -23,7 +23,6 @@ import { } from "lucide-react"; // helpers import { - insertImageCommand, insertTableCommand, setText, toggleBlockquote, @@ -43,7 +42,7 @@ import { toggleUnderline, } from "@/helpers/editor-commands"; // types -import { TEditorCommands, UploadImage } from "@/types"; +import { TEditorCommands } from "@/types"; export interface EditorMenuItem { key: TEditorCommands; diff --git a/packages/editor/src/core/extensions/custom-image/custom-image.ts b/packages/editor/src/core/extensions/custom-image/custom-image.ts index ee942c1bd75..d84f443aa76 100644 --- a/packages/editor/src/core/extensions/custom-image/custom-image.ts +++ b/packages/editor/src/core/extensions/custom-image/custom-image.ts @@ -5,9 +5,11 @@ import { v4 as uuidv4 } from "uuid"; // extensions import { CustomImageNode } from "@/extensions/custom-image"; // plugins -import { isFileValid } from "@/plugins/image"; +import { TrackImageDeletionPlugin, TrackImageRestorationPlugin, isFileValid } from "@/plugins/image"; // types import { TFileHandler } from "@/types"; +// helpers +import { insertEmptyParagraphAtNodeBoundaries } from "@/helpers/insert-empty-paragraph-at-node-boundary"; declare module "@tiptap/core" { interface Commands { @@ -22,10 +24,10 @@ export interface UploadImageExtensionStorage { fileMap: Map; } -export type UploadEntity = ({ event: "insert" } | { event: "drop"; file: File }) & { hasOpenedFileInputOnce: boolean }; +export type UploadEntity = ({ event: "insert" } | { event: "drop"; file: File }) & { hasOpenedFileInputOnce?: boolean }; export const CustomImageExtension = (props: TFileHandler) => { - const { upload } = props; + const { upload, delete: deleteImage, restore: restoreImage } = props; return Image.extend<{}, UploadImageExtensionStorage>({ name: "imageComponent", @@ -57,9 +59,6 @@ export const CustomImageExtension = (props: TFileHandler) => { { tag: "image-component", }, - { - tag: "img", - }, ]; }, @@ -67,9 +66,42 @@ export const CustomImageExtension = (props: TFileHandler) => { return ["image-component", mergeAttributes(HTMLAttributes)]; }, + onCreate(this) { + const imageSources = new Set(); + this.editor.state.doc.descendants((node) => { + if (node.type.name === this.name) { + imageSources.add(node.attrs.src); + } + }); + imageSources.forEach(async (src) => { + try { + const assetUrlWithWorkspaceId = new URL(src).pathname.substring(1); + console.log("assetUrlWithWorkspaceId restore ", this.name, assetUrlWithWorkspaceId); + await restoreImage(assetUrlWithWorkspaceId); + } catch (error) { + console.error("Error restoring image: ", error); + } + }); + }, + + addKeyboardShortcuts() { + return { + ArrowDown: insertEmptyParagraphAtNodeBoundaries("down", this.name), + ArrowUp: insertEmptyParagraphAtNodeBoundaries("up", this.name), + }; + }, + + addProseMirrorPlugins() { + return [ + TrackImageDeletionPlugin(this.editor, deleteImage, this.name), + TrackImageRestorationPlugin(this.editor, restoreImage, this.name), + ]; + }, + addStorage() { return { fileMap: new Map(), + deletedImageSet: new Map(), }; }, diff --git a/packages/editor/src/core/extensions/image/extension.tsx b/packages/editor/src/core/extensions/image/extension.tsx index 98961b7f0f1..2ed19f32655 100644 --- a/packages/editor/src/core/extensions/image/extension.tsx +++ b/packages/editor/src/core/extensions/image/extension.tsx @@ -1,43 +1,40 @@ import ImageExt from "@tiptap/extension-image"; +import { ReactNodeViewRenderer } from "@tiptap/react"; // helpers import { insertEmptyParagraphAtNodeBoundaries } from "@/helpers/insert-empty-paragraph-at-node-boundary"; // plugins -import { - IMAGE_NODE_TYPE, - ImageExtensionStorage, - TrackImageDeletionPlugin, - TrackImageRestorationPlugin, - UploadImagesPlugin, -} from "@/plugins/image"; +import { ImageExtensionStorage, TrackImageDeletionPlugin, TrackImageRestorationPlugin } from "@/plugins/image"; // types import { DeleteImage, RestoreImage } from "@/types"; +// extensions +import { CustomImageNode } from "@/extensions"; export const ImageExtension = (deleteImage: DeleteImage, restoreImage: RestoreImage, cancelUploadImage?: () => void) => ImageExt.extend({ addKeyboardShortcuts() { return { - ArrowDown: insertEmptyParagraphAtNodeBoundaries("down", "image"), - ArrowUp: insertEmptyParagraphAtNodeBoundaries("up", "image"), + ArrowDown: insertEmptyParagraphAtNodeBoundaries("down", this.name), + ArrowUp: insertEmptyParagraphAtNodeBoundaries("up", this.name), }; }, addProseMirrorPlugins() { return [ - UploadImagesPlugin(this.editor, cancelUploadImage), - TrackImageDeletionPlugin(this.editor, deleteImage), - TrackImageRestorationPlugin(this.editor, restoreImage), + TrackImageDeletionPlugin(this.editor, deleteImage, this.name), + TrackImageRestorationPlugin(this.editor, restoreImage, this.name), ]; }, onCreate(this) { const imageSources = new Set(); this.editor.state.doc.descendants((node) => { - if (node.type.name === IMAGE_NODE_TYPE) { + if (node.type.name === this.name) { imageSources.add(node.attrs.src); } }); imageSources.forEach(async (src) => { try { const assetUrlWithWorkspaceId = new URL(src).pathname.substring(1); + console.log("assetUrlWithWorkspaceId restore ", this.name, assetUrlWithWorkspaceId); await restoreImage(assetUrlWithWorkspaceId); } catch (error) { console.error("Error restoring image: ", error); @@ -64,4 +61,9 @@ export const ImageExtension = (deleteImage: DeleteImage, restoreImage: RestoreIm }, }; }, + + // render custom image node + addNodeView() { + return ReactNodeViewRenderer(CustomImageNode); + }, }); diff --git a/packages/editor/src/core/extensions/image/image-component-without-props.tsx b/packages/editor/src/core/extensions/image/image-component-without-props.tsx index de27ba98ee4..770a336e711 100644 --- a/packages/editor/src/core/extensions/image/image-component-without-props.tsx +++ b/packages/editor/src/core/extensions/image/image-component-without-props.tsx @@ -1,6 +1,8 @@ import { mergeAttributes } from "@tiptap/core"; +import { ReactNodeViewRenderer } from "@tiptap/react"; import { Image } from "@tiptap/extension-image"; -import { UploadImageExtensionStorage } from "@/extensions/custom-image"; +// extensions +import { CustomImageNode, UploadImageExtensionStorage } from "@/extensions"; export const CustomImageComponentWithoutProps = () => Image.extend<{}, UploadImageExtensionStorage>({ @@ -33,9 +35,6 @@ export const CustomImageComponentWithoutProps = () => { tag: "image-component", }, - { - tag: "img", - }, ]; }, @@ -46,8 +45,13 @@ export const CustomImageComponentWithoutProps = () => addStorage() { return { fileMap: new Map(), + deletedImageSet: new Map(), }; }, + + addNodeView() { + return ReactNodeViewRenderer(CustomImageNode); + }, }); export default CustomImageComponentWithoutProps; diff --git a/packages/editor/src/core/extensions/image/image-extension-without-props.tsx b/packages/editor/src/core/extensions/image/image-extension-without-props.tsx index 0d505000c7e..bd9ca3c820b 100644 --- a/packages/editor/src/core/extensions/image/image-extension-without-props.tsx +++ b/packages/editor/src/core/extensions/image/image-extension-without-props.tsx @@ -1,4 +1,7 @@ import ImageExt from "@tiptap/extension-image"; +import { ReactNodeViewRenderer } from "@tiptap/react"; +// extensions +import { CustomImageNode } from "@/extensions"; export const ImageExtensionWithoutProps = () => ImageExt.extend({ @@ -13,4 +16,8 @@ export const ImageExtensionWithoutProps = () => }, }; }, + + addNodeView() { + return ReactNodeViewRenderer(CustomImageNode); + }, }); diff --git a/packages/editor/src/core/extensions/image/image-resize.tsx b/packages/editor/src/core/extensions/image/image-resize.tsx deleted file mode 100644 index 334c402bc9f..00000000000 --- a/packages/editor/src/core/extensions/image/image-resize.tsx +++ /dev/null @@ -1,85 +0,0 @@ -import { useState } from "react"; -import { Editor } from "@tiptap/react"; -import Moveable from "react-moveable"; - -type Props = { - editor: Editor; - id: string; -}; - -const getImageElement = (editorId: string): HTMLImageElement | null => - document.querySelector(`#editor-container-${editorId} .ProseMirror-selectednode`); - -export const ImageResizer = (props: Props) => { - const { editor, id } = props; - // states - const [aspectRatio, setAspectRatio] = useState(1); - - const updateMediaSize = () => { - const imageElement = getImageElement(id); - - if (!imageElement) return; - - const selection = editor.state.selection; - - // Use the style width/height if available, otherwise fall back to the element's natural width/height - const width = imageElement.style.width - ? Number(imageElement.style.width.replace("px", "")) - : imageElement.getAttribute("width"); - const height = imageElement.style.height - ? Number(imageElement.style.height.replace("px", "")) - : imageElement.getAttribute("height"); - - editor.commands.setImage({ - src: imageElement.src, - width: width, - height: height, - } as any); - editor.commands.setNodeSelection(selection.from); - }; - - return ( - { - const imageElement = getImageElement(id); - if (imageElement) { - const originalWidth = Number(imageElement.width); - const originalHeight = Number(imageElement.height); - setAspectRatio(originalWidth / originalHeight); - } - }} - onResize={({ target, width, height, delta }) => { - if (delta[0] || delta[1]) { - let newWidth, newHeight; - if (delta[0]) { - // Width change detected - newWidth = Math.max(width, 100); - newHeight = newWidth / aspectRatio; - } else if (delta[1]) { - // Height change detected - newHeight = Math.max(height, 100); - newWidth = newHeight * aspectRatio; - } - target.style.width = `${newWidth}px`; - target.style.height = `${newHeight}px`; - } - }} - onResizeEnd={() => { - updateMediaSize(); - }} - scalable - renderDirections={["se"]} - onScale={({ target, transform }) => { - target.style.transform = transform; - }} - /> - ); -}; diff --git a/packages/editor/src/core/extensions/image/index.ts b/packages/editor/src/core/extensions/image/index.ts index 3e2f7518dd3..9c7dc65d783 100644 --- a/packages/editor/src/core/extensions/image/index.ts +++ b/packages/editor/src/core/extensions/image/index.ts @@ -1,4 +1,3 @@ export * from "./extension"; export * from "./image-extension-without-props"; -export * from "./image-resize"; export * from "./read-only-image"; diff --git a/packages/editor/src/core/extensions/image/read-only-image.tsx b/packages/editor/src/core/extensions/image/read-only-image.tsx index 8112eba4ec5..1605174b325 100644 --- a/packages/editor/src/core/extensions/image/read-only-image.tsx +++ b/packages/editor/src/core/extensions/image/read-only-image.tsx @@ -1,4 +1,7 @@ import Image from "@tiptap/extension-image"; +import { ReactNodeViewRenderer } from "@tiptap/react"; +// extensions +import { CustomImageNode } from "@/extensions"; export const ReadOnlyImageExtension = Image.extend({ addAttributes() { @@ -12,4 +15,7 @@ export const ReadOnlyImageExtension = Image.extend({ }, }; }, + addNodeView() { + return ReactNodeViewRenderer(CustomImageNode); + }, }); diff --git a/packages/editor/src/core/helpers/editor-commands.ts b/packages/editor/src/core/helpers/editor-commands.ts index eebb69f0a6c..7cf3e8d1f17 100644 --- a/packages/editor/src/core/helpers/editor-commands.ts +++ b/packages/editor/src/core/helpers/editor-commands.ts @@ -156,23 +156,3 @@ export const insertImageCommand = ( }; input.click(); }; - -export const insertImageNewCommand = (editor: Editor, saveSelection?: Selection | null, range?: Range) => { - // if (range) editor.chain().focus().deleteRange(range).setImageUpload(saveSelection).run(); - // const input = document.createElement("input"); - // input.type = "file"; - // input.accept = ".jpeg, .jpg, .png, .webp"; - // input.onchange = async () => { - // if (input.files?.length) { - // const file = input.files[0]; - // const pos = saveSelection?.anchor ?? editor.view.state.selection.from; - // const url = await (editor?.commands.uploadImage(file) as unknown as Promise); - // if (!url) { - // throw new Error("Something went wrong while uploading the image"); - // } - // editor.chain().setImageBlock({ src: url }).focus().run(); - // // startImageUpload(editor, file, editor.view, pos, uploadFile); - // } - // }; - // input.click(); -}; diff --git a/packages/editor/src/core/hooks/use-editor.ts b/packages/editor/src/core/hooks/use-editor.ts index 15b5031849e..0542418e5d5 100644 --- a/packages/editor/src/core/hooks/use-editor.ts +++ b/packages/editor/src/core/hooks/use-editor.ts @@ -67,11 +67,6 @@ export const useEditor = (props: CustomEditorProps) => { }), ...editorProps, }, - immediatelyRender: true, - shouldRerenderOnTransaction: false, - onContentError: (error) => { - console.error("Error rendering content:", error); - }, extensions: [ ...CoreEditorExtensions({ enableHistory, diff --git a/packages/editor/src/core/plugins/image/delete-image.ts b/packages/editor/src/core/plugins/image/delete-image.ts index 8dc1bf07229..769b6f24588 100644 --- a/packages/editor/src/core/plugins/image/delete-image.ts +++ b/packages/editor/src/core/plugins/image/delete-image.ts @@ -1,17 +1,17 @@ import { Editor } from "@tiptap/core"; -import { EditorState, Plugin, Transaction } from "@tiptap/pm/state"; +import { EditorState, Plugin, PluginKey, Transaction } from "@tiptap/pm/state"; // plugins -import { IMAGE_NODE_TYPE, deleteKey, type ImageNode } from "@/plugins/image"; +import { type ImageNode } from "@/plugins/image"; // types import { DeleteImage } from "@/types"; -export const TrackImageDeletionPlugin = (editor: Editor, deleteImage: DeleteImage): Plugin => +export const TrackImageDeletionPlugin = (editor: Editor, deleteImage: DeleteImage, nodeType: string): Plugin => new Plugin({ - key: deleteKey, + key: new PluginKey(`delete-${nodeType}`), appendTransaction: (transactions: readonly Transaction[], oldState: EditorState, newState: EditorState) => { const newImageSources = new Set(); newState.doc.descendants((node) => { - if (node.type.name === IMAGE_NODE_TYPE) { + if (node.type.name === nodeType) { newImageSources.add(node.attrs.src); } }); @@ -25,7 +25,7 @@ export const TrackImageDeletionPlugin = (editor: Editor, deleteImage: DeleteImag // iterate through all the nodes in the old state oldState.doc.descendants((oldNode) => { // if the node is not an image, then return as no point in checking - if (oldNode.type.name !== IMAGE_NODE_TYPE) return; + if (oldNode.type.name !== nodeType) return; // Check if the node has been deleted or replaced if (!newImageSources.has(oldNode.attrs.src)) { @@ -35,7 +35,7 @@ export const TrackImageDeletionPlugin = (editor: Editor, deleteImage: DeleteImag removedImages.forEach(async (node) => { const src = node.attrs.src; - editor.storage.image.deletedImageSet.set(src, true); + editor.storage[nodeType].deletedImageSet.set(src, true); await onNodeDeleted(src, deleteImage); }); }); diff --git a/packages/editor/src/core/plugins/image/restore-image.ts b/packages/editor/src/core/plugins/image/restore-image.ts index 036df9b8870..cbcd3353225 100644 --- a/packages/editor/src/core/plugins/image/restore-image.ts +++ b/packages/editor/src/core/plugins/image/restore-image.ts @@ -1,17 +1,17 @@ import { Editor } from "@tiptap/core"; -import { EditorState, Plugin, Transaction } from "@tiptap/pm/state"; +import { EditorState, Plugin, PluginKey, Transaction } from "@tiptap/pm/state"; // plugins -import { IMAGE_NODE_TYPE, ImageNode, restoreKey } from "@/plugins/image"; +import { ImageNode } from "@/plugins/image"; // types import { RestoreImage } from "@/types"; -export const TrackImageRestorationPlugin = (editor: Editor, restoreImage: RestoreImage): Plugin => +export const TrackImageRestorationPlugin = (editor: Editor, restoreImage: RestoreImage, nodeType: string): Plugin => new Plugin({ - key: restoreKey, + key: new PluginKey(`restore-${nodeType}`), appendTransaction: (transactions: readonly Transaction[], oldState: EditorState, newState: EditorState) => { const oldImageSources = new Set(); oldState.doc.descendants((node) => { - if (node.type.name === IMAGE_NODE_TYPE) { + if (node.type.name === nodeType) { oldImageSources.add(node.attrs.src); } }); @@ -22,20 +22,21 @@ export const TrackImageRestorationPlugin = (editor: Editor, restoreImage: Restor const addedImages: ImageNode[] = []; newState.doc.descendants((node, pos) => { - if (node.type.name !== IMAGE_NODE_TYPE) return; + if (node.type.name !== nodeType) return; if (pos < 0 || pos > newState.doc.content.size) return; if (oldImageSources.has(node.attrs.src)) return; addedImages.push(node as ImageNode); }); addedImages.forEach(async (image) => { - const wasDeleted = editor.storage.image.deletedImageSet.get(image.attrs.src); + const src = image.attrs.src; + const wasDeleted = editor.storage[nodeType].deletedImageSet.get(src); if (wasDeleted === undefined) { - editor.storage.image.deletedImageSet.set(image.attrs.src, false); + editor.storage[nodeType].deletedImageSet.set(src, false); } else if (wasDeleted === true) { try { - await onNodeRestored(image.attrs.src, restoreImage); - editor.storage.image.deletedImageSet.set(image.attrs.src, false); + await onNodeRestored(src, restoreImage); + editor.storage[nodeType].deletedImageSet.set(src, false); } catch (error) { console.error("Error restoring image: ", error); } diff --git a/packages/editor/src/index.ts b/packages/editor/src/index.ts index 15ec7913752..fc9fe1ac603 100644 --- a/packages/editor/src/index.ts +++ b/packages/editor/src/index.ts @@ -23,7 +23,6 @@ export * from "@/helpers/common"; export * from "@/helpers/editor-commands"; export * from "@/helpers/yjs"; export * from "@/extensions/table/table"; -export { startImageUpload } from "@/plugins/image"; // components export * from "@/components/menus"; diff --git a/packages/editor/src/styles/drag-drop.css b/packages/editor/src/styles/drag-drop.css index 72d190f32f2..9fb8a2c36aa 100644 --- a/packages/editor/src/styles/drag-drop.css +++ b/packages/editor/src/styles/drag-drop.css @@ -39,7 +39,7 @@ } /* end ai handle */ -.ProseMirror:not(.dragging) .ProseMirror-selectednode:not(.node-imageBlock) { +.ProseMirror:not(.dragging) .ProseMirror-selectednode:not(.node-imageComponent):not(.node-image) { position: relative; cursor: grab; outline: none !important; @@ -64,7 +64,8 @@ pointer-events: none; } - &.node-imageComponent { + &.node-imageComponent, + &.node-image { --horizontal-offset: 0px; &::after { @@ -104,6 +105,7 @@ ol > li:nth-child(n + 100).ProseMirror-selectednode:not(.dragging)::after { margin-left: -35px; } +.ProseMirror node-image, .ProseMirror node-imageComponent { transition: filter 0.1s ease-in-out; cursor: pointer; diff --git a/packages/editor/src/styles/editor.css b/packages/editor/src/styles/editor.css index 6d606bfeab3..0c79fdfc60c 100644 --- a/packages/editor/src/styles/editor.css +++ b/packages/editor/src/styles/editor.css @@ -264,26 +264,6 @@ ul[data-type="taskList"] li[data-checked="true"] > div > p { transition: opacity 0.2s ease-out; } -.img-placeholder { - position: relative; - width: 35%; - margin-top: 0 !important; - margin-bottom: 0 !important; - - &::before { - content: ""; - box-sizing: border-box; - position: absolute; - top: 50%; - left: 45%; - width: 20px; - height: 20px; - border-radius: 50%; - border: 3px solid rgba(var(--color-text-200)); - border-top-color: rgba(var(--color-text-800)); - animation: spinning 0.6s linear infinite; - } -} @keyframes spinning { to { From e297f7f98f658098250dc11583f83b32f58a9063 Mon Sep 17 00:00:00 2001 From: Palanikannan M Date: Mon, 16 Sep 2024 12:51:41 +0530 Subject: [PATCH 25/29] fix: drop upload fixed --- .../extensions/custom-image/components/image-node.tsx | 11 ++++++++--- .../src/core/extensions/custom-image/custom-image.ts | 2 +- .../extensions/custom-image/read-only-custom-image.ts | 2 +- .../image/image-component-without-props.tsx | 2 +- packages/editor/src/core/hooks/use-file-upload.ts | 7 ++++--- 5 files changed, 15 insertions(+), 9 deletions(-) diff --git a/packages/editor/src/core/extensions/custom-image/components/image-node.tsx b/packages/editor/src/core/extensions/custom-image/components/image-node.tsx index 17aa0b90c4d..639036e2fe9 100644 --- a/packages/editor/src/core/extensions/custom-image/components/image-node.tsx +++ b/packages/editor/src/core/extensions/custom-image/components/image-node.tsx @@ -53,10 +53,15 @@ export const CustomImageNode = (props: CustomImageNodeViewProps) => { const uploadFile = useCallback( async (file: File) => { try { - const result = await editor.commands.uploadImage(file)(); - if (result) { - onUpload(result); + // @ts-expect-error - TODO: fix typings, and don't remove await from + // here for now + const url: string = await editor?.commands.uploadImage(file); + console.log("url drop", url); + + if (!url) { + throw new Error("Something went wrong while uploading the image"); } + onUpload(url); } catch (error) { console.error("Error uploading file:", error); // Handle error state here if needed diff --git a/packages/editor/src/core/extensions/custom-image/custom-image.ts b/packages/editor/src/core/extensions/custom-image/custom-image.ts index d84f443aa76..86c0c22285c 100644 --- a/packages/editor/src/core/extensions/custom-image/custom-image.ts +++ b/packages/editor/src/core/extensions/custom-image/custom-image.ts @@ -29,7 +29,7 @@ export type UploadEntity = ({ event: "insert" } | { event: "drop"; file: File }) export const CustomImageExtension = (props: TFileHandler) => { const { upload, delete: deleteImage, restore: restoreImage } = props; - return Image.extend<{}, UploadImageExtensionStorage>({ + return Image.extend, UploadImageExtensionStorage>({ name: "imageComponent", selectable: true, group: "block", diff --git a/packages/editor/src/core/extensions/custom-image/read-only-custom-image.ts b/packages/editor/src/core/extensions/custom-image/read-only-custom-image.ts index eb75de63c85..4f9c7d3cd48 100644 --- a/packages/editor/src/core/extensions/custom-image/read-only-custom-image.ts +++ b/packages/editor/src/core/extensions/custom-image/read-only-custom-image.ts @@ -5,7 +5,7 @@ import { ReactNodeViewRenderer } from "@tiptap/react"; import { CustomImageNode, UploadImageExtensionStorage } from "@/extensions/custom-image"; export const CustomReadOnlyImageExtension = () => - Image.extend<{}, UploadImageExtensionStorage>({ + Image.extend, UploadImageExtensionStorage>({ name: "imageComponent", selectable: false, group: "block", diff --git a/packages/editor/src/core/extensions/image/image-component-without-props.tsx b/packages/editor/src/core/extensions/image/image-component-without-props.tsx index 770a336e711..2141f6445d3 100644 --- a/packages/editor/src/core/extensions/image/image-component-without-props.tsx +++ b/packages/editor/src/core/extensions/image/image-component-without-props.tsx @@ -5,7 +5,7 @@ import { Image } from "@tiptap/extension-image"; import { CustomImageNode, UploadImageExtensionStorage } from "@/extensions"; export const CustomImageComponentWithoutProps = () => - Image.extend<{}, UploadImageExtensionStorage>({ + Image.extend, UploadImageExtensionStorage>({ name: "imageComponent", selectable: true, group: "block", diff --git a/packages/editor/src/core/hooks/use-file-upload.ts b/packages/editor/src/core/hooks/use-file-upload.ts index 7d6a806ed14..c0f6af2c269 100644 --- a/packages/editor/src/core/hooks/use-file-upload.ts +++ b/packages/editor/src/core/hooks/use-file-upload.ts @@ -9,7 +9,10 @@ export const useUploader = ({ onUpload, editor }: { onUpload: (url: string) => v async (file: File) => { setLoading(true); try { - const url = await editor?.commands.uploadImage(file); + // @ts-expect-error - TODO: fix typings, and don't remove await from + // here for now + const url: string = await editor?.commands.uploadImage(file); + console.log("url upload", url); if (!url) { throw new Error("Something went wrong while uploading the image"); @@ -41,7 +44,6 @@ export const useFileUpload = () => { export const useDropZone = ({ uploader }: { uploader: (file: File) => void }) => { const [isDragging, setIsDragging] = useState(false); const [draggedInside, setDraggedInside] = useState(false); - const [errorMessage, setErrorMessage] = useState(null); useEffect(() => { const dragStartHandler = () => { @@ -64,7 +66,6 @@ export const useDropZone = ({ uploader }: { uploader: (file: File) => void }) => const onDrop = useCallback( (e: DragEvent) => { setDraggedInside(false); - setErrorMessage(null); if (e.dataTransfer.files.length === 0) { return; } From 5c27a726bbcb935a3d6da5819ecb73bd30446b21 Mon Sep 17 00:00:00 2001 From: Palanikannan M Date: Mon, 16 Sep 2024 13:15:06 +0530 Subject: [PATCH 26/29] chore: remove console logs --- .../custom-image/components/image-node.tsx | 1 - .../core/extensions/custom-image/custom-image.ts | 1 - .../editor/src/core/extensions/image/extension.tsx | 1 - packages/editor/src/core/hooks/use-file-upload.ts | 2 -- .../src/core/plugins/image/utils/validate-file.ts | 14 ++++++++++---- 5 files changed, 10 insertions(+), 9 deletions(-) diff --git a/packages/editor/src/core/extensions/custom-image/components/image-node.tsx b/packages/editor/src/core/extensions/custom-image/components/image-node.tsx index 639036e2fe9..bcd7a6f1b89 100644 --- a/packages/editor/src/core/extensions/custom-image/components/image-node.tsx +++ b/packages/editor/src/core/extensions/custom-image/components/image-node.tsx @@ -56,7 +56,6 @@ export const CustomImageNode = (props: CustomImageNodeViewProps) => { // @ts-expect-error - TODO: fix typings, and don't remove await from // here for now const url: string = await editor?.commands.uploadImage(file); - console.log("url drop", url); if (!url) { throw new Error("Something went wrong while uploading the image"); diff --git a/packages/editor/src/core/extensions/custom-image/custom-image.ts b/packages/editor/src/core/extensions/custom-image/custom-image.ts index 86c0c22285c..73786e621f0 100644 --- a/packages/editor/src/core/extensions/custom-image/custom-image.ts +++ b/packages/editor/src/core/extensions/custom-image/custom-image.ts @@ -76,7 +76,6 @@ export const CustomImageExtension = (props: TFileHandler) => { imageSources.forEach(async (src) => { try { const assetUrlWithWorkspaceId = new URL(src).pathname.substring(1); - console.log("assetUrlWithWorkspaceId restore ", this.name, assetUrlWithWorkspaceId); await restoreImage(assetUrlWithWorkspaceId); } catch (error) { console.error("Error restoring image: ", error); diff --git a/packages/editor/src/core/extensions/image/extension.tsx b/packages/editor/src/core/extensions/image/extension.tsx index 2ed19f32655..1f15846a1a1 100644 --- a/packages/editor/src/core/extensions/image/extension.tsx +++ b/packages/editor/src/core/extensions/image/extension.tsx @@ -34,7 +34,6 @@ export const ImageExtension = (deleteImage: DeleteImage, restoreImage: RestoreIm imageSources.forEach(async (src) => { try { const assetUrlWithWorkspaceId = new URL(src).pathname.substring(1); - console.log("assetUrlWithWorkspaceId restore ", this.name, assetUrlWithWorkspaceId); await restoreImage(assetUrlWithWorkspaceId); } catch (error) { console.error("Error restoring image: ", error); diff --git a/packages/editor/src/core/hooks/use-file-upload.ts b/packages/editor/src/core/hooks/use-file-upload.ts index c0f6af2c269..3aea2d155cb 100644 --- a/packages/editor/src/core/hooks/use-file-upload.ts +++ b/packages/editor/src/core/hooks/use-file-upload.ts @@ -12,7 +12,6 @@ export const useUploader = ({ onUpload, editor }: { onUpload: (url: string) => v // @ts-expect-error - TODO: fix typings, and don't remove await from // here for now const url: string = await editor?.commands.uploadImage(file); - console.log("url upload", url); if (!url) { throw new Error("Something went wrong while uploading the image"); @@ -40,7 +39,6 @@ export const useFileUpload = () => { return { ref: fileInput, handleUploadClick }; }; - export const useDropZone = ({ uploader }: { uploader: (file: File) => void }) => { const [isDragging, setIsDragging] = useState(false); const [draggedInside, setDraggedInside] = useState(false); diff --git a/packages/editor/src/core/plugins/image/utils/validate-file.ts b/packages/editor/src/core/plugins/image/utils/validate-file.ts index b79ca6683e2..c86e99335fe 100644 --- a/packages/editor/src/core/plugins/image/utils/validate-file.ts +++ b/packages/editor/src/core/plugins/image/utils/validate-file.ts @@ -1,17 +1,23 @@ -export function isFileValid(file: File): boolean { +export function isFileValid(file: File, showAlert = true): boolean { if (!file) { - alert("No file selected. Please select a file to upload."); + if (showAlert) { + alert("No file selected. Please select a file to upload."); + } return false; } const allowedTypes = ["image/jpeg", "image/jpg", "image/png", "image/webp"]; if (!allowedTypes.includes(file.type)) { - alert("Invalid file type. Please select a JPEG, JPG, PNG, or WEBP image file."); + if (showAlert) { + alert("Invalid file type. Please select a JPEG, JPG, PNG, or WEBP image file."); + } return false; } if (file.size > 5 * 1024 * 1024) { - alert("File size too large. Please select a file smaller than 5MB."); + if (showAlert) { + alert("File size too large. Please select a file smaller than 5MB."); + } return false; } From da47e2bc4742644c1548ca5531dfeec75fca4669 Mon Sep 17 00:00:00 2001 From: Palanikannan M Date: Mon, 16 Sep 2024 15:47:07 +0530 Subject: [PATCH 27/29] fix: src of image node as dependency --- .../src/core/extensions/custom-image/components/image-node.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/editor/src/core/extensions/custom-image/components/image-node.tsx b/packages/editor/src/core/extensions/custom-image/components/image-node.tsx index f13ab1db7a1..4786d8f997a 100644 --- a/packages/editor/src/core/extensions/custom-image/components/image-node.tsx +++ b/packages/editor/src/core/extensions/custom-image/components/image-node.tsx @@ -89,7 +89,7 @@ export const CustomImageNode = (props: CustomImageNodeViewProps) => { if (node.attrs.src) { setIsUploaded(true); } - }, [node.attrs]); + }, [node.attrs.src]); const existingFile = React.useMemo(() => { const entity = getUploadEntity(); From 889dbb5a67f5c2fcf760f6c0c0bb2b52394c1f1c Mon Sep 17 00:00:00 2001 From: Palanikannan M Date: Mon, 16 Sep 2024 15:48:13 +0530 Subject: [PATCH 28/29] fix: helper library build fix --- packages/helpers/package.json | 13 +++++++++++-- packages/helpers/tsconfig.json | 8 ++++++++ yarn.lock | 16 ++++++++++++++-- 3 files changed, 33 insertions(+), 4 deletions(-) create mode 100644 packages/helpers/tsconfig.json diff --git a/packages/helpers/package.json b/packages/helpers/package.json index 736459f9824..9cc5c44c249 100644 --- a/packages/helpers/package.json +++ b/packages/helpers/package.json @@ -2,12 +2,21 @@ "name": "@plane/helpers", "version": "0.22.0", "description": "Helper functions shared across multiple apps internally", - "main": "index.ts", "private": true, + "main": "./dist/index.js", + "module": "./dist/index.mjs", + "types": "./dist/index.d.ts", + "files": [ + "dist/**" + ], + "scripts": { + "build": "tsup ./index.ts --format esm,cjs --dts --external react --minify" + }, "devDependencies": { "@types/node": "^22.5.4", "@types/react": "^18.3.5", - "typescript": "^5.6.2" + "typescript": "^5.6.2", + "tsup": "^7.2.0" }, "dependencies": { "react": "^18.3.1" diff --git a/packages/helpers/tsconfig.json b/packages/helpers/tsconfig.json new file mode 100644 index 00000000000..08a82e90799 --- /dev/null +++ b/packages/helpers/tsconfig.json @@ -0,0 +1,8 @@ +{ + "extends": "tsconfig/react-library.json", + "compilerOptions": { + "jsx": "react" + }, + "include": ["."], + "exclude": ["dist", "build", "node_modules"] +} diff --git a/yarn.lock b/yarn.lock index c3bec8f79e0..694c95d6c17 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4020,6 +4020,13 @@ dependencies: undici-types "~6.19.2" +"@types/node@^22.5.4": + version "22.5.5" + resolved "https://registry.yarnpkg.com/@types/node/-/node-22.5.5.tgz#52f939dd0f65fc552a4ad0b392f3c466cc5d7a44" + integrity sha512-Xjs4y5UPO/CLdzpgR6GirZJx36yScjh73+2NlLlkFRSoQN8B0DpfXPdZGnvVmLRLOsqDpOfTNv7D9trgGhmOIA== + dependencies: + undici-types "~6.19.2" + "@types/nprogress@^0.2.0": version "0.2.3" resolved "https://registry.yarnpkg.com/@types/nprogress/-/nprogress-0.2.3.tgz#b2150b054a13622fabcba12cf6f0b54c48b14287" @@ -4092,7 +4099,7 @@ dependencies: "@types/react" "*" -"@types/react@*", "@types/react@18.2.48", "@types/react@^16.8.0 || ^17.0.0 || ^18.0.0", "@types/react@^18.2.42", "@types/react@^18.2.48": +"@types/react@*", "@types/react@18.2.48", "@types/react@^16.8.0 || ^17.0.0 || ^18.0.0", "@types/react@^18.2.42", "@types/react@^18.2.48", "@types/react@^18.3.5": version "18.2.48" resolved "https://registry.yarnpkg.com/@types/react/-/react-18.2.48.tgz#11df5664642d0bd879c1f58bc1d37205b064e8f1" integrity sha512-qboRCl6Ie70DQQG9hhNREz81jqC1cs9EVNcjQ1AU+jH6NFfSAhVVbrrY/+nSF+Bsk4AOwm9Qa61InvMCyV+H3w== @@ -12445,6 +12452,11 @@ typescript@5.4.5: resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.4.5.tgz#42ccef2c571fdbd0f6718b1d1f5e6e5ef006f611" integrity sha512-vcI4UpRgg81oIRUFwR0WSIHKt11nJ7SAVlYNIu+QpqeyXP+gpQJy/Z4+F0aGxSE4MqwjyXvW/TzgkLAx2AGHwQ== +typescript@^5.6.2: + version "5.6.2" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.6.2.tgz#d1de67b6bef77c41823f822df8f0b3bcff60a5a0" + integrity sha512-NW8ByodCSNCwZeghjN3o+JX5OFH0Ojg6sadjEKY4huZ52TqbJTJnDo5+Tw98lSy63NZvi4n+ez5m2u5d4PkZyw== + uc.micro@^2.0.0, uc.micro@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/uc.micro/-/uc.micro-2.1.0.tgz#f8d3f7d0ec4c3dea35a7e3c8efa4cb8b45c9e7ee" @@ -13167,4 +13179,4 @@ zeed-dom@^0.10.9: zxcvbn@^4.4.2: version "4.4.2" resolved "https://registry.yarnpkg.com/zxcvbn/-/zxcvbn-4.4.2.tgz#28ec17cf09743edcab056ddd8b1b06262cc73c30" - integrity sha512-Bq0B+ixT/DMyG8kgX2xWcI5jUvCwqrMxSFam7m0lAf78nf04hv6lNCsyLYdyYTrCVMqNDY/206K7eExYCeSyUQ== \ No newline at end of file + integrity sha512-Bq0B+ixT/DMyG8kgX2xWcI5jUvCwqrMxSFam7m0lAf78nf04hv6lNCsyLYdyYTrCVMqNDY/206K7eExYCeSyUQ== From 1330a0ff49129076a3ddbea0d77c3a18cbd9333a Mon Sep 17 00:00:00 2001 From: Palanikannan M Date: Mon, 16 Sep 2024 18:19:23 +0530 Subject: [PATCH 29/29] fix: improved reflow/layout and fixed resizing --- .../custom-image/components/image-block.tsx | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 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 5b0163239b6..22a793a9580 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 @@ -15,6 +15,7 @@ export const CustomImageBlock: React.FC = (props) => { const [isLoading, setIsLoading] = useState(true); const containerRef = useRef(null); + const containerRect = useRef(null); const imageRef = useRef(null); const isResizing = useRef(false); const aspectRatio = useRef(1); @@ -23,10 +24,12 @@ export const CustomImageBlock: React.FC = (props) => { if (imageRef.current) { const img = imageRef.current; img.onload = () => { - aspectRatio.current = img.naturalWidth / img.naturalHeight; - const initialWidth = Math.max(img.naturalWidth * 0.35, MIN_SIZE); - const initialHeight = initialWidth / aspectRatio.current; - setSize({ width: `${initialWidth}px`, height: `${initialHeight}px` }); + if (node.attrs.width === "35%" && node.attrs.height === "auto") { + aspectRatio.current = img.naturalWidth / img.naturalHeight; + const initialWidth = Math.max(img.naturalWidth * 0.35, MIN_SIZE); + const initialHeight = initialWidth / aspectRatio.current; + setSize({ width: `${initialWidth}px`, height: `${initialHeight}px` }); + } setIsLoading(false); }; } @@ -36,6 +39,9 @@ export const CustomImageBlock: React.FC = (props) => { e.preventDefault(); e.stopPropagation(); isResizing.current = true; + if (containerRef.current) { + containerRect.current = containerRef.current.getBoundingClientRect(); + } }, []); useLayoutEffect(() => { @@ -44,12 +50,11 @@ export const CustomImageBlock: React.FC = (props) => { }, [width, height]); const handleResize = useCallback((e: MouseEvent | TouchEvent) => { - if (!isResizing.current || !containerRef.current) return; + if (!isResizing.current || !containerRef.current || !containerRect.current) return; - const containerRect = containerRef.current.getBoundingClientRect(); const clientX = "touches" in e ? e.touches[0].clientX : e.clientX; - const newWidth = Math.max(clientX - containerRect.left, MIN_SIZE); + const newWidth = Math.max(clientX - containerRect.current.left, MIN_SIZE); const newHeight = newWidth / aspectRatio.current; setSize({ width: `${newWidth}px`, height: `${newHeight}px` });