diff --git a/packages/editor/core/src/hooks/use-editor.tsx b/packages/editor/core/src/hooks/use-editor.tsx index e3ef76483..e432ec6a4 100644 --- a/packages/editor/core/src/hooks/use-editor.tsx +++ b/packages/editor/core/src/hooks/use-editor.tsx @@ -19,7 +19,7 @@ interface CustomEditorProps { uploadFile: UploadImage; restoreFile: RestoreImage; deleteFile: DeleteImage; - cancelUploadImage?: () => any; + cancelUploadImage?: () => void; initialValue: string; editorClassName: string; // undefined when prop is not passed, null if intentionally passed to stop @@ -56,7 +56,7 @@ export const useEditor = ({ }: CustomEditorProps) => { const editor = useCustomEditor({ editorProps: { - ...CoreEditorProps(uploadFile, editorClassName), + ...CoreEditorProps(editorClassName), ...editorProps, }, extensions: [ @@ -69,6 +69,7 @@ export const useEditor = ({ deleteFile, restoreFile, cancelUploadImage, + uploadFile, }, placeholder, }), @@ -89,19 +90,37 @@ export const useEditor = ({ }, }); - // for syncing swr data on tab refocus etc, can remove it once this is merged - // https://github.com/ueberdosis/tiptap/pull/4453 + const editorRef: MutableRefObject = useRef(null); + + const [savedSelection, setSavedSelection] = useState(null); + + // Inside your component or hook + const savedSelectionRef = useRef(savedSelection); + + // Update the ref whenever savedSelection changes + useEffect(() => { + savedSelectionRef.current = savedSelection; + }, [savedSelection]); + + // Effect for syncing SWR data useEffect(() => { // value is null when intentionally passed where syncing is not yet // supported and value is undefined when the data from swr is not populated if (value === null || value === undefined) return; - if (editor && !editor.isDestroyed) editor?.commands.setContent(value); + if (editor && !editor.isDestroyed && !editor.storage.image.uploadInProgress) { + editor.commands.setContent(value); + const currentSavedSelection = savedSelectionRef.current; + if (currentSavedSelection) { + editor.view.focus(); + const docLength = editor.state.doc.content.size; + const relativePosition = Math.min(currentSavedSelection.from, docLength - 1); + editor.commands.setTextSelection(relativePosition); + } else { + editor.commands.focus("end"); + } + } }, [editor, value, id]); - const editorRef: MutableRefObject = useRef(null); - - const [savedSelection, setSavedSelection] = useState(null); - useImperativeHandle( forwardedRef, () => ({ diff --git a/packages/editor/core/src/lib/editor-commands.ts b/packages/editor/core/src/lib/editor-commands.ts index e163b17dd..ce14502a7 100644 --- a/packages/editor/core/src/lib/editor-commands.ts +++ b/packages/editor/core/src/lib/editor-commands.ts @@ -126,7 +126,7 @@ export const insertImageCommand = ( if (input.files?.length) { const file = input.files[0]; const pos = savedSelection?.anchor ?? editor.view.state.selection.from; - startImageUpload(file, editor.view, pos, uploadFile); + startImageUpload(editor, file, editor.view, pos, uploadFile); } }; input.click(); diff --git a/packages/editor/core/src/ui/extensions/drop.tsx b/packages/editor/core/src/ui/extensions/drop.tsx new file mode 100644 index 000000000..8de48f9e0 --- /dev/null +++ b/packages/editor/core/src/ui/extensions/drop.tsx @@ -0,0 +1,45 @@ +import { Extension } from "@tiptap/core"; +import { Plugin, PluginKey } from "prosemirror-state"; +import { UploadImage } from "src/types/upload-image"; +import { startImageUpload } from "../plugins/upload-image"; + +export const DropHandlerExtension = (uploadFile: UploadImage) => + Extension.create({ + name: "dropHandler", + priority: 1000, + + addProseMirrorPlugins() { + return [ + new Plugin({ + key: new PluginKey("dropHandler"), + props: { + handlePaste: (view, event) => { + if (event.clipboardData && event.clipboardData.files && event.clipboardData.files[0]) { + event.preventDefault(); + const file = event.clipboardData.files[0]; + const pos = view.state.selection.from; + startImageUpload(this.editor, file, view, pos, uploadFile); + return true; + } + return false; + }, + handleDrop: (view, event, _slice, moved) => { + if (!moved && event.dataTransfer && event.dataTransfer.files && event.dataTransfer.files[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); + } + return true; + } + return false; + }, + }, + }), + ]; + }, + }); diff --git a/packages/editor/core/src/ui/extensions/image/index.tsx b/packages/editor/core/src/ui/extensions/image/index.tsx index 1431b7755..b85100fe5 100644 --- a/packages/editor/core/src/ui/extensions/image/index.tsx +++ b/packages/editor/core/src/ui/extensions/image/index.tsx @@ -18,7 +18,7 @@ interface ImageNode extends ProseMirrorNode { const deleteKey = new PluginKey("delete-image"); const IMAGE_NODE_TYPE = "image"; -export const ImageExtension = (deleteImage: DeleteImage, restoreFile: RestoreImage, cancelUploadImage?: () => any) => +export const ImageExtension = (deleteImage: DeleteImage, restoreFile: RestoreImage, cancelUploadImage?: () => void) => ImageExt.extend({ addKeyboardShortcuts() { return { @@ -28,7 +28,7 @@ export const ImageExtension = (deleteImage: DeleteImage, restoreFile: RestoreIma }, addProseMirrorPlugins() { return [ - UploadImagesPlugin(cancelUploadImage), + UploadImagesPlugin(this.editor, cancelUploadImage), new Plugin({ key: deleteKey, appendTransaction: (transactions: readonly Transaction[], oldState: EditorState, newState: EditorState) => { @@ -124,6 +124,7 @@ export const ImageExtension = (deleteImage: DeleteImage, restoreFile: RestoreIma addStorage() { return { images: new Map(), + uploadInProgress: false, }; }, diff --git a/packages/editor/core/src/ui/extensions/index.tsx b/packages/editor/core/src/ui/extensions/index.tsx index 0a9dd34ed..c5a5d5eb9 100644 --- a/packages/editor/core/src/ui/extensions/index.tsx +++ b/packages/editor/core/src/ui/extensions/index.tsx @@ -29,6 +29,8 @@ import { CustomCodeInlineExtension } from "src/ui/extensions/code-inline"; import { CustomTypographyExtension } from "src/ui/extensions/typography"; import { CustomHorizontalRule } from "src/ui/extensions/horizontal-rule/horizontal-rule"; import { CustomCodeMarkPlugin } from "./custom-code-inline/inline-code-plugin"; +import { UploadImage } from "src/types/upload-image"; +import { DropHandlerExtension } from "./drop"; type TArguments = { mentionConfig: { @@ -38,14 +40,15 @@ type TArguments = { fileConfig: { deleteFile: DeleteImage; restoreFile: RestoreImage; - cancelUploadImage?: () => any; + cancelUploadImage?: () => void; + uploadFile: UploadImage; }; placeholder?: string | ((isFocused: boolean) => string); }; export const CoreEditorExtensions = ({ mentionConfig, - fileConfig: { deleteFile, restoreFile, cancelUploadImage }, + fileConfig: { deleteFile, restoreFile, cancelUploadImage, uploadFile }, placeholder, }: TArguments) => [ StarterKit.configure({ @@ -73,10 +76,8 @@ export const CoreEditorExtensions = ({ width: 1, }, }), - // BulletList, - // OrderedList, - // ListItem, CustomQuoteExtension, + DropHandlerExtension(uploadFile), CustomHorizontalRule.configure({ HTMLAttributes: { class: "my-4 border-custom-border-400", diff --git a/packages/editor/core/src/ui/plugins/upload-image.tsx b/packages/editor/core/src/ui/plugins/upload-image.tsx index 3f6a40bf9..af56d5382 100644 --- a/packages/editor/core/src/ui/plugins/upload-image.tsx +++ b/packages/editor/core/src/ui/plugins/upload-image.tsx @@ -1,12 +1,22 @@ +import { Editor } from "@tiptap/core"; import { EditorState, Plugin, PluginKey } from "@tiptap/pm/state"; import { Decoration, DecorationSet, EditorView } from "@tiptap/pm/view"; import { UploadImage } from "src/types/upload-image"; const uploadKey = new PluginKey("upload-image"); -export const UploadImagesPlugin = (cancelUploadImage?: () => any) => - new Plugin({ +export const UploadImagesPlugin = (editor: Editor, cancelUploadImage?: () => void) => { + let currentView: EditorView | null = null; + return new Plugin({ key: uploadKey, + view(editorView) { + currentView = editorView; + return { + destroy() { + currentView = null; + }, + }; + }, state: { init() { return DecorationSet.empty; @@ -27,13 +37,17 @@ export const UploadImagesPlugin = (cancelUploadImage?: () => any) => // Create cancel button const cancelButton = document.createElement("button"); + cancelButton.type = "button"; cancelButton.style.position = "absolute"; cancelButton.style.right = "3px"; cancelButton.style.top = "3px"; cancelButton.setAttribute("class", "opacity-90 rounded-lg"); cancelButton.onclick = () => { - cancelUploadImage?.(); + if (currentView) { + cancelUploadImage?.(); + removePlaceholder(editor, currentView, id); + } }; // Create an SVG element from the SVG string @@ -59,6 +73,7 @@ export const UploadImagesPlugin = (cancelUploadImage?: () => any) => }, }, }); +}; function findPlaceholder(state: EditorState, id: {}) { const decos = uploadKey.getState(state); @@ -66,26 +81,38 @@ function findPlaceholder(state: EditorState, id: {}) { return found.length ? found[0].from : null; } -const removePlaceholder = (view: EditorView, id: {}) => { +const removePlaceholder = (editor: Editor, view: EditorView, id: {}) => { const removePlaceholderTr = view.state.tr.setMeta(uploadKey, { remove: { id }, }); view.dispatch(removePlaceholderTr); + editor.storage.image.uploadInProgress = false; }; -export async function startImageUpload(file: File, view: EditorView, pos: number, uploadFile: UploadImage) { +export async function startImageUpload( + editor: Editor, + file: File, + view: EditorView, + pos: number, + uploadFile: UploadImage +) { + editor.storage.image.uploadInProgress = true; + if (!file) { alert("No file selected. Please select a file to upload."); + editor.storage.image.uploadInProgress = false; return; } if (!file.type.includes("image/")) { alert("Invalid file type. Please select an image file."); + editor.storage.image.uploadInProgress = false; return; } if (file.size > 5 * 1024 * 1024) { alert("File size too large. Please select a file smaller than 5MB."); + editor.storage.image.uploadInProgress = false; return; } @@ -110,7 +137,7 @@ export async function startImageUpload(file: File, view: EditorView, pos: number // Handle FileReader errors reader.onerror = (error) => { console.error("FileReader error: ", error); - removePlaceholder(view, id); + removePlaceholder(editor, view, id); return; }; @@ -121,7 +148,10 @@ export async function startImageUpload(file: File, view: EditorView, pos: number const { schema } = view.state; pos = findPlaceholder(view.state, id); - if (pos == null) return; + if (pos == null) { + editor.storage.image.uploadInProgress = false; + return; + } const imageSrc = typeof src === "object" ? reader.result : src; const node = schema.nodes.image.create({ src: imageSrc }); @@ -129,9 +159,9 @@ export async function startImageUpload(file: File, view: EditorView, pos: number view.dispatch(transaction); view.focus(); + editor.storage.image.uploadInProgress = false; } catch (error) { - console.error("Upload error: ", error); - removePlaceholder(view, id); + removePlaceholder(editor, view, id); } } diff --git a/packages/editor/core/src/ui/props.tsx b/packages/editor/core/src/ui/props.tsx index aa88fa042..3d46b5840 100644 --- a/packages/editor/core/src/ui/props.tsx +++ b/packages/editor/core/src/ui/props.tsx @@ -1,9 +1,7 @@ import { EditorProps } from "@tiptap/pm/view"; -import { cn, findTableAncestor } from "src/lib/utils"; -import { UploadImage } from "src/types/upload-image"; -import { startImageUpload } from "src/ui/plugins/upload-image"; +import { cn } from "src/lib/utils"; -export function CoreEditorProps(uploadFile: UploadImage, editorClassName: string): EditorProps { +export function CoreEditorProps(editorClassName: string): EditorProps { return { attributes: { class: cn( @@ -17,45 +15,12 @@ export function CoreEditorProps(uploadFile: UploadImage, editorClassName: string if (["ArrowUp", "ArrowDown", "Enter"].includes(event.key)) { const slashCommand = document.querySelector("#slash-command"); if (slashCommand) { + console.log("registered"); return true; } } }, }, - handlePaste: (view, event) => { - if (typeof window !== "undefined") { - const selection: any = window?.getSelection(); - if (selection.rangeCount !== 0) { - const range = selection.getRangeAt(0); - if (findTableAncestor(range.startContainer)) { - return; - } - } - } - if (event.clipboardData && event.clipboardData.files && event.clipboardData.files[0]) { - event.preventDefault(); - const file = event.clipboardData.files[0]; - const pos = view.state.selection.from; - startImageUpload(file, view, pos, uploadFile); - return true; - } - return false; - }, - handleDrop: (view, event, _slice, moved) => { - if (!moved && event.dataTransfer && event.dataTransfer.files && event.dataTransfer.files[0]) { - event.preventDefault(); - const file = event.dataTransfer.files[0]; - const coordinates = view.posAtCoords({ - left: event.clientX, - top: event.clientY, - }); - if (coordinates) { - startImageUpload(file, view, coordinates.pos - 1, uploadFile); - } - return true; - } - return false; - }, transformPastedHTML(html) { return html.replace(//g, ""); },