React 富文本编辑器实践

React 富文本编辑器实践

Tags
React.js
JavaScript
Published
July 22, 2022
Author

前言


近期产品提出了新的需求,想要将某类文章的上传权限下放到用户端,同时在管理端也能够实时编辑修改。到这里我明白富文本编辑器势在必行了。不过我司不可能提供大量的时间供我精挑细选,又是一个时间紧任务重。
 

实践


根据过往业务以及正在进行的业务调研,发现这些项目采用了开源富文本编辑器:QuillEditor。既然已经有了实践,我们只需要做好搬运工就可以了。
屋漏偏逢连夜雨,QuillEditor 在上线两周后,产品想要支持 Table,但是根据调研 QuillEditor 是无法支持的,因此最终经过系统的调研选择了 TinyMCE。

Quill 踩坑记录

丁医后台 vue-quill 实现效果
丁医后台 vue-quill 实现效果
在上传图片时,如何拦截并替换为 CDN 链接?✅
upload image: replace the base64 url with the url getting from sever ?
Updated Apr 24, 2022
参考 issue 2034thumbs up 最多的 comment:
// Add handler config to your toolbar: modules: { toolbar: { handlers: { image: this.imageHandler }, ... // Implement imageHandler function somewhere imageHandler() { const input = document.createElement('input'); input.setAttribute('type', 'file'); input.setAttribute('accept', 'image/*'); input.click(); input.onchange = async function() { const file = input.files[0]; console.log('User trying to uplaod this:', file); const id = await uploadFile(file); // I'm using react, so whatever upload function const range = this.quill.getSelection(); const link = `${ROOT_URL}/file/${id}`; // this part the image is inserted // by 'image' option below, you just have to put src(link) of img here. this.quill.insertEmbed(range.index, 'image', link); }.bind(this); // react thing }
当页面存在多个 Quill 时,会出现 innerHTML() is undefined 错误,是什么导致的?✅
Multiple quill toolbars on the same page
Updated Aug 2, 2017
参考 issue 249 ,发现是在自定义 Toolbar 时,设置的 ID 相同导致的,处理办法如下:
// import Quill <QuillEditor id="xxx1_editor" placeholder="please type xxx1" /> // Quill /* * Custom toolbar component */ const CustomToolbar = (props) => ( <div id={`${props.id}_toolbar`} className="quill_toolbar" style={{ borderBottom: 'none' }}> <select className="ql-header" defaultValue={false} onChange={(e) => e.persist()}> <option value="3" /> <option value={false} /> </select> <button className="ql-bold" data-tooltip="粗体" /> <button className="ql-italic" data-tooltip="斜体" /> <button className="ql-underline" data-tooltip="下划线" /> <button className="ql-strike" data-tooltip="删除线" /> <select className="ql-align" data-tooltip="对齐方式" /> <button className="ql-blockquote" data-tooltip="引用" /> <button className="ql-list" value="ordered" data-tooltip="有序列表" /> <button className="ql-list" value="bullet" data-tooltip="无序列表" /> <button className="ql-link" data-tooltip="链接" /> <button className="ql-image" data-tooltip="图片" /> <button className="ql-clean" data-tooltip="清除格式" /> </div> ); const QuillEditor = (props) => { const { id } = props //... const modules = { toolbar: { container: `#${id}_toolbar` } } //... return ( <div className="text-editor"> <CustomToolbar id={id} /> <ReactQuill id={id} value={value} onChange={handleChange} placeholder={placeholder} modules={modules} formats={formats} theme={'snow'} /> </div> ) }
Toolbarhover 时, Tooltip 效果如何处理?✅
Button tooltips in the demo
Updated Nov 7, 2023
Feature Request: Tooltips for toolbar buttons
Updated Jun 30, 2022
结合 issue 650issue 235 ,总结出了一份最佳 CSS 实现:
.quill_toolbar { border-bottom: none !important; [data-tooltip]::after { position: relative; z-index: 50; font-family: 'PingFangSC-Regular', 'PingFang SC', 'Micro'; top: 0%; left: 100%; color: #aaa; background-color: #fff; visibility: hidden; white-space: nowrap; content: attr(data-tooltip); } [data-tooltip]:hover::after { visibility: visible; transition: visibility 0s 0.8s; } .ql-picker.ql-header { .ql-picker-label::before, .ql-picker-item::before { content: '正文' !important; } .ql-picker-label[data-value="3"]::before, .ql-picker-item[data-value="3"]::before { content: '子标题' !important; } } }
使用 clipboardmatchers 时,需要使用到 new Delta() ,Delta 从哪里来?⭕
Delta not defined
Updated Oct 10, 2023
// 安装后出现 react-quill 无法找到的问题,不知道为什么。 import Delta from 'quill-delta';
另外,使用 Matchers 是想实现粘贴时图片复制的功能:
// Custom Img Matcher async function customMatcherImg(node, delta) { if (node.src) { const res = await replaceImg({ urls: [node.src] }); if (res.success) { delta.forEach((op) => { if (op.insert.image) { // eslint-disable-next-line no-param-reassign op.insert.image = res.results.items[0].newUrl; } }); return delta; } } return delta; } // Modules const modules = { toolbar: { container: `#${id}_toolbar`, handlers: { image: imageHandler, }, }, clipboard: { matchVisual: false, matchers: [['IMG', customMatcherImg]], }, };
但是实际调试时,却出现了:
notion image
定位代码时,发现是这一段:
Delta.prototype.concat = function (other) { var delta = new Delta(this.ops.slice()); if (other.ops.length > 0) { delta.push(other.ops[0]); delta.ops = delta.ops.concat(other.ops.slice(1)); } return delta; };
Deltaconcat 不支持 Promise ,真难搞。
粘贴时,实现域外图片资源转换为 CDN ,怎么做?✅
parchment
quilljsUpdated Apr 20, 2024
掘金的这边文章给了我很大的启发,使用 Quill.register 可以做到很多意想不到的事情。
register Registers a module, theme, or format(s), making them available to be added to an editor. Can later be retrieved with Quill.import. Use the path prefix of 'formats/''modules/', or 'themes/' for registering formats, modules or themes, respectively. For formats specifically there is a shorthand to just pass in the format directly and the path will be autogenerated. Will overwrite existing definitions with the same path.
  • Methods
    • Quill.register(format: Attributor | BlotDefinintion, supressWarning: Boolean = false) Quill.register(path: String, def: any, supressWarning: Boolean = false) Quill.register(defs: { [String]: any }, supressWarning: Boolean = false)
  • Examples
    • var Module = Quill.import('core/module'); class CustomModule extends Module {} Quill.register('modules/custom-module', CustomModule); Quill.register({ 'formats/custom-format': CustomFormat, 'modules/custom-module-a': CustomModuleA, 'modules/custom-module-b': CustomModuleB, }); Quill.register(CustomFormat); // You cannot do Quill.register(CustomModuleA); as CustomModuleA is not a format
具体实现:
const BlockEmbed = Quill.import('blots/block/embed'); class ImageBlot extends BlockEmbed { static create(value) { const node = super.create(); // 当图片地址不为 DXYCDN 时 if (value.url && !value.url.startsWith('https://img1.dxycdn.com')) { let url = value.url; switch (true) { case value.url.startsWith('base64'): url = value.url.split('base64,')[1]; break; default: break; } replaceImg({ urls: [url] }) .then((res) => { if (res.success) { node.setAttribute('src', res.results.items[0].newUrl); } else { node.setAttribute('src', url); } }) .catch(() => { node.setAttribute('src', url); }); } else { node.setAttribute('src', value.url); } return node; } static value(node) { const url = node.getAttribute('src'); return { url, }; } } ImageBlot.blotName = 'image'; ImageBlot.tagName = 'img'; Quill.register(ImageBlot);
既然 react-quill-v1 不支持 Table ,那 v2 应该怎么做?目前无法支持。
Quill 2.0 Table Demo
notion image
本来觉得 react-quill-v2 既然支持表格,那么应该是可以直接使用的,而不是需要安装额外的 NPM 包。但是看到了 codepen 中引入的 quill 版本竟然是 2.0.0-dev.2 ,而 [email protected] 依赖的 quill1.3.7 ,版本对不上,那就无法使用了,而原生 Tablequill-better-table 都是在 quill^2 才支持。
is there any plan for quilljs2.0
Updated Mar 19, 2024

TinyMCE 踩坑记录

如何使用免费版进行编辑器创建?
菜单栏如何进行自定义,以及如何开始一个插件?
AddOnCallback Type Misuse
Updated Nov 16, 2022
notion image
Toolbar 如何增加自定义图标?
如何拦截编辑器本身的粘贴功能,可以实现拦截图片并上传到图床吗?
中有提到过使用 postprocess 等方法进行拦截并上传图片到图床。
自带的 placeholder 功能怎么使用?❌ 
如何统计输入的字数并进行展示?✅
当手动插入自定义元素时,有哪些坑要踩?
插入 divp 元素并 enter ,前一行元素的 classdata- 也会继承到下一行。✅
setup: (editor) => { editor.on('keydown', (e) => { if (e.key === 'Enter') { if (editor.dom.hasClass(editor.selection.getNode(), 'video-container')) { editor.insertContent('<p></p>'); return false; } else { return true; } } }); },
处理换行时继承 class 以及 data-* 等属性处理方法
⚠️ 在网上的众多示例中, 替换原本的标签使用的是 editor.selection.setContent ,这会导致将代码插入到 <p></p> 标签内,而不是标签外。

富文本编辑器


调研了市面上的富文本编辑器,总结归纳出了以下内容。
名称
自定义 Toolbar
现成 UI
Table 支持
粘贴大量内容时性能
说明
/
/
/
[email protected] 是 2019 年发布的, beta 版 2020 年就开始了,维护性很差。
配置复杂,无法满足快速上线需求。👎
世界排名第一位的富文本编辑器。 存在收费版本和免费版本,免费版本由社区维护并开源。Demo 1、在社区版及GitHub能下载到的部分都是开源且可免费商用的(包括官方封装的vue版react版等等)
draft-js
👎
1. draft-js 在突然粘贴大量内容时,会有较长时间的 js 处理、html 渲染操作,不适用于大量文本内容需求。 2. Draft.js 的硬伤在于性能和体验,根源在于它底层的设计和富文本的描述 schema。 3. 知乎 PC 端的回答问题采用的是此编辑器。
基于 ProseMirror 做的二次开发,具有插件安装能力。

参考资料


  1. draft-js
    facebookarchiveUpdated May 2, 2023
  1. react-draft-wysiwyg
    jpuriUpdated May 2, 2023
  1. react-quill
    zenoamaroUpdated May 1, 2023
  1. slate
    ianstormtaylorUpdated May 3, 2023
  1. jodit
    xdanUpdated May 2, 2023
  1. UEditor
  1. Top 5 rich-text React components
  1. draftJs与基于ContentEditable的富文本编辑器的比较
  1. vue-quill-editor富文本编辑器使用及配置更改
  1. 主流的开源「富文本编辑器」都有什么缺陷?
  1. Custom Editing Area losing focus in React 16
    Updated Aug 15, 2022
  1. Error: The given range isn't in document
    Updated Aug 22, 2022
  1. Quill 中文文档
  1. quill-better-table:赋予quill富文本编辑器强大的表格编辑功能!
  1. Best Open Source Rich Text Editors
  1. http://tinymce.ax-z.cn/ tinymce中文文档
  1. ckeditor vs prosemirror vs tinymce