MUKI AI Summary
在 React 中使用 Quill 編輯器,並增加上傳圖片至圖床的功能。react-quill 套件已久未更新,因此轉用官方 Quill 套件。Quill 預設將圖片轉為 base64,但專案需求改為上傳至圖床並插入網址。可在 toolbarOptions 中設定工具列,並增加 uploadImage 功能上傳圖片。
安裝 Quill React 使用 npm,編輯器分為 Editor.js(x) 和 EditorQuill.js(x) 兩部分。Editor.js(x) 載入 Quill 的 snow 或 bubble 主題樣式。EditorQuill.js(x) 設定工具列並新增上傳圖片功能,圖片上傳後插入編輯器,並設置樣式 max-width 為 100%。完整程式碼可在 codesandbox.io 參考。...
前言
先前曾分享過使用 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 線上範例