Copyright © 2008 ~ 2024 MUKI space* / omegaBook theme All Rights Reserved.

前言

先前曾分享過使用 react-quill 套件並客製化工具列按鈕的方法,詳見:客製化 Quill 編輯器,並搭配 ant design 製作所見即所得編輯器

然而,react-quill 套件已逾兩年未更新,評估後決定改用官方 Quill 套件。本文將分享如何在 React 中使用官方 Quill 編輯器,並新增「上傳圖片至圖床」功能。

Quill 預設的上傳圖片功能會將圖片轉為 base64 格式,但近期專案需求改為將圖片上傳至圖床,再將網址插入編輯器中。因此有類似需求的朋友,可以參考看看唷。

ps. 是說,現在的工程師似乎不再稱呼提供圖片服務的空間為「圖床」,最近跟一些 2000 年後出生的工程師說「圖床」,他們都不懂這是啥 😂...,但我也忘記他們怎麼稱呼它了 XXD。整個感受到時代的隔閡,嗚嗚 QAQ

安裝與使用

▼ 使用 npm 安裝 Quill React,我寫這篇文章時安裝的版本是 v2.0.2

$ npm install quill --save

我將編輯器拆成兩個架構,一個是編輯器本身,一個是工具列:

  • Editor.js(x):編輯器主體
  • EditorQuill.js(x):工具列,等同於 Editor.js(x) 的子元件

▼ 在 Editor.js(x) 加入編輯器的語法,Quill 內建兩種 Theme:snow 以及 bubble,載入對應的 css 檔案,並將 theme 設定為 snow / bubble 即可。

import React, { useState, useRef } from "react";
import EditorQuill from "./EditorQuill";
import "quill/dist/quill.snow.css";

export const Editor = () => {
  const quillRef = useRef();

  const handleChange = (content, delta, source, editor) => {
    // 使用 setTimeout 取得增加 style 樣式的 HTML
    // 我將編輯器的語法用 console 印出來,使用時只要處理這段 HtmlContent 即可
    setTimeout(() => {
      console.log("HtmlContent", editor.root.innerHTML);
    }, 20);
  };

  return (
    <div className="mb-24">
      <EditorQuill ref={quillRef} onTextChange={handleChange} />
    </div>
  );
};

export default Editor;

▼ 可以在 toolbarOptions 裡面設定自己想要的工具列,另外加了 uploadImage 的功能,用來上傳圖片至圖床。

import React, { forwardRef, useEffect, useLayoutEffect, useRef } from "react";
import Quill from "quill";
import "./styles.css";

const EditorQuill = forwardRef(
  ({ readOnly, defaultValue, onTextChange, onSelectionChange }, ref) => {
    const containerRef = useRef(null);
    const defaultValueRef = useRef(defaultValue);
    const onTextChangeRef = useRef(onTextChange);
    const onSelectionChangeRef = useRef(onSelectionChange);

    useLayoutEffect(() => {
      onTextChangeRef.current = onTextChange;
      onSelectionChangeRef.current = onSelectionChange;
    });

    useEffect(() => {
      ref.current?.enable(!readOnly);
    }, [ref, readOnly]);

    useEffect(() => {
      const container = containerRef.current;
      const editorContainer = container.appendChild(
        container.ownerDocument.createElement("div")
      );
      const toolbarOptions = {
        container: [
          [{ header: [1, 2, 3] }],
          [{ size: ["small", false, "large", "huge"] }],
          ["bold", "italic", "underline", "strike", { align: [] }],
          [{ list: "ordered" }, { list: "bullet" }],
          [{ color: [] }, { background: [] }],
          // 新增了一個 uploadImage 的功能
          ["link", { uploadImage: true }, "video"],
          ["clean"],
        ],
        handlers: {
          uploadImage: function () {
            let fileInput = document.createElement("input");
            fileInput.setAttribute("type", "file");
            fileInput.setAttribute("accept", "image/*");
            fileInput.click();

            fileInput.addEventListener("change", () => {
              const file = fileInput.files[0];
              const reader = new FileReader();
              reader.onloadend = async () => {
                let formData = new FormData();
                formData.append("files[0]", file, file.name);
                try {
                  // 上傳圖片的 API Demo
                  // const res = await uploadImageAPI(formData);
                  // const imageUrl = res.data.urls;
                  // imageUrl 是上傳後取得的圖片網址
                  const imageUrl = "https://muki.tw/images/cover.jpg";
                  let range = this.quill.getSelection();
                  quill.insertEmbed(range.index, "image", imageUrl);
                  // 取得剛插入的圖片並設置樣式 max-width = 100%
                  setTimeout(() => {
                    let img = this.quill.root.querySelector(
                      `img[src="${imageUrl}"]`
                    );
                    if (img) {
                      img.style.maxWidth = "100%";
                    }
                  }, 10);
                } catch (error) {
                  console.error("Response error:", error);
                }
              };
              reader.readAsArrayBuffer(file);
            });
          },
        },
      };

      const quill = new Quill(editorContainer, {
        theme: "snow",
        placeholder: "請輸入內容...",
        modules: {
          toolbar: toolbarOptions,
        },
      });

      ref.current = quill;

      if (defaultValueRef.current) {
        quill.setContents(defaultValueRef.current);
      }

      quill.on(Quill.events.TEXT_CHANGE, (delta, oldDelta, source) => {
        onTextChangeRef.current?.(quill.getContents(), delta, source, quill);
      });

      quill.on(Quill.events.SELECTION_CHANGE, (...args) => {
        onSelectionChangeRef.current?.(...args);
      });

      return () => {
        ref.current = null;
        container.innerHTML = "";
      };
    }, [ref]);

    return <div ref={containerRef}></div>;
  }
);

EditorQuill.displayName = "EditorQuill";

export default EditorQuill;

▼ 最後幫 uploadImage 加上一個圖案,我先寫一個 U 示意

.ql-uploadImage::before {
  content: "U";
}

▼ 點選「U」可以上傳圖片,因為沒有實際串接 API,所以我先用固定的圖片網址

▼ 切到 console 面板可以看到 HtmlContent 的變化,我們最後就是要將這些 HTML 上傳到對應的位置即可

用 HTML 客製化你的工具列

因為之前有寫過同樣的內容,所以這邊就不贅述。

雖然 Quill 和 react-quill 套件在呼叫上有一些不同,但使用 HTML 客製化的部分大同小異,有需要使用 HTML 客製化工具列的朋友,可以先移步之前的文章參考修改:用 HTML 客製化你的工具列

線上範例

我也有把這一份程式碼放在 codesandbox.io,有興趣的朋友可以拉下來參考:codesandbox 線上範例

歡迎給我點鼓勵,讓我知道你來過 :)

Subscribe
Notify of
guest

0 則留言
Inline Feedbacks
View all comments