Rabin Nepal
Rabin Nepal

Reputation: 1

Images are not pasted at the correct position between text after custom paste handling

I am implementing a rich text editor using Quill.js and want to handle custom paste functionality. My goal is to allow users to paste images (from clipboard or HTML content) exactly where the cursor is positioned, including between text blocks.

However, the images either:

  1. Appear at the wrong position.

  2. Overwrite some text content or don't respect their intended placement.

How can I ensure that pasted images are inserted at the exact cursor position while preserving the layout of surrounding content?

What I’ve Tried

Here’s the code I’m using for the paste handler function. It processes pasted images, either from the clipboard or embedded in HTML, and uploads them using an API.

 const handlePaste = async (event) => {
    event.preventDefault();

    const clipboard = event.clipboardData || window.clipboardData;
    const quill = quillRef.current?.getEditor();

    if (!quill) {
      console.error("Quill editor instance not found");
      return;
    }

    let range = quill.getSelection();

    if (!range) {
      range = { index: quill.getLength(), length: 0 };
    }

    const html = clipboard.getData("text/html");
    const text = clipboard.getData("text/plain");

    if (html) {
      const parser = new DOMParser();
      const doc = parser.parseFromString(html, "text/html");
      const nodes = Array.from(doc.body.childNodes);

      for (const node of nodes) {
        if (node.nodeName === "IMG") {
          const src = node.getAttribute("src");
          if (src?.startsWith("data:")) {
            const placeholderIndex = range.index;
            quill.insertEmbed(placeholderIndex, "image", "loading-placeholder");
            range.index += 1;

            uploadImage(src, quill, placeholderIndex);
          } else if (src?.startsWith("http")) {
            toast.warning("Please download and paste remote images directly.");
          }
        } else if (node.nodeType === Node.TEXT_NODE || node.nodeName === "P") {
          const textContent = node.textContent || "";
          quill.insertText(range.index, textContent);
          range.index += textContent.length;
        } else if (node.nodeName === "A") {
          const href = node.getAttribute("href");
          const linkText = node.textContent || href || "";
          quill.insertText(range.index, linkText, "link", href);
          range.index += linkText.length;
        }
      }
    } else if (text) {
      quill.insertText(range.index, text);
    }
  };

Here is uploadImage function:

const uploadImage = async (fileOrBase64, quill, placeholderIndex) => {
    try {
      const formData = new FormData();
      if (
        typeof fileOrBase64 === "string" &&
        fileOrBase64.startsWith("data:")
      ) {
        const binaryData = base64ToBinary(fileOrBase64);
        formData.append("file", new Blob([binaryData]), "image.png");
      } else if (fileOrBase64 instanceof File || fileOrBase64 instanceof Blob) {
        formData.append("file", fileOrBase64);
      }

      const response = await HttpClient.post(
        `${baseURL}/api/upload-image`,
        formData,
        { headers: { "Content-Type": "multipart/form-data" } }
      );

      const uploadedUrl = response.data.message[0];

      if (quill && placeholderIndex !== undefined) {
        quill.deleteText(placeholderIndex, 1);
        quill.insertEmbed(placeholderIndex, "image", uploadedUrl);
      } else {
        console.error("Quill instance or placeholder index is undefined");
      }
    } catch (error) {
      console.error("Image upload failed:", error);
      if (quill && placeholderIndex !== undefined) {
        quill.deleteText(placeholderIndex, 1);
      }
      toast.error("Image upload failed.");
    }
  };
useEffect(() => {
    const quill = quillRef.current?.getEditor();
    if (quill) {
      const customPasteHandler = async (event) => {
        event.preventDefault();

        const clipboard = event.clipboardData || window.clipboardData;
        const items = clipboard.items;
        let range = quill.getSelection() || { index: 0 };
        let pastedText = "";
        let pastedHtml = "";

        const base64ToBinary = (base64, filename, mimeType = "image/png") => {
          const byteString = atob(base64.split(",")[1]);
          const binaryData = new Uint8Array(byteString.length);
          for (let i = 0; i < byteString.length; i++) {
            binaryData[i] = byteString.charCodeAt(i);
          }
          return new File([binaryData], filename, { type: mimeType });
        };

        const uploadImage = async (file, quill, insertIndex) => {
          try {
            const formData = new FormData();
            formData.append("file", file);

            const response = await HttpClient.post(
              `${baseURL}/api/upload-image`,
              formData,
              { headers: { "Content-Type": "multipart/form-data" } }
            );

            const uploadedUrl = response.data.message[0];
            quill.insertEmbed(insertIndex, "image", uploadedUrl);
            range.index += 1;
          } catch (error) {
            console.error("Failed to upload image:", error);
            toast.error("Image upload failed.");
          }
        };

        for (let i = 0; i < items.length; i++) {
          const item = items[i];

          if (item.type.indexOf("image") !== -1) {
            const file = item.getAsFile();
            if (file) {
              await uploadImage(file, quill, range.index);
            }
          } else if (item.type === "text/html") {
            await new Promise((resolve) => {
              item.getAsString(async (html) => {
                const parser = new DOMParser();
                const doc = parser.parseFromString(html, "text/html");
                const images = doc.querySelectorAll("img");

                for (const img of images) {
                  const src = img.getAttribute("src");
                  if (src) {
                    if (src.startsWith("data:")) {
                      const mimeType =
                        src.match(/data:(.*?);base64,/)?.[1] || "image/png";
                      const file = base64ToBinary(
                        src,
                        "pasted-image.png",
                        mimeType
                      );
                      await uploadImage(file, quill, range.index);
                    }
                  }
                }

                pastedHtml = html.replace(/<img[^>]*>/gi, "");
                resolve(true);
              });
            });
          } else if (item.type === "text/plain") {
            await new Promise((resolve) => {
              item.getAsString((text) => {
                pastedText = text;
                resolve(true);
              });
            });
          }
        }

        if (pastedHtml) {
          quill.clipboard.dangerouslyPasteHTML(range.index, pastedHtml);
          range.index += pastedHtml.length;
        }

        if (pastedText) {
          quill.insertText(range.index, pastedText);
        }
      };

      const editorElement = quill.root;
      editorElement.addEventListener("paste", customPasteHandler);

      return () => {
        editorElement.removeEventListener("paste", customPasteHandler);
      };
    }
  }, []);

Here's my quill editor setup:

 <ReactQuill
    ref={quillRef}
    theme="snow"
    value={article.overview}
    onChange={(newValue) =>
      setArticle((prevstate) => ({
        ...prevstate,
        overview: newValue,
      }))
    }
    onPaste={handlePaste}
    className={"react-quill-article mb-10"}
    placeholder="Overview"
  />

Desired Behavior

I want the pasted images to:

  1. Appear at the exact position of the cursor or where they are pasted (between text blocks).

  2. Preserve the existing layout and not overwrite or misplace text or other content.

Upvotes: 0

Views: 17

Answers (0)

Related Questions