前言
近期产品提出了新的需求,想要将某类文章的上传权限下放到用户端,同时在管理端也能够实时编辑修改。到这里我明白富文本编辑器势在必行了。不过我司不可能提供大量的时间供我精挑细选,又是一个时间紧任务重。
实践
根据过往业务以及正在进行的业务调研,发现这些项目采用了开源富文本编辑器:QuillEditor。既然已经有了实践,我们只需要做好搬运工就可以了。
屋漏偏逢连夜雨,QuillEditor 在上线两周后,产品想要支持 Table,但是根据调研 QuillEditor 是无法支持的,因此最终经过系统的调研选择了 TinyMCE。
Quill 踩坑记录
在上传图片时,如何拦截并替换为 CDN 链接?✅
upload image: replace the base64 url with the url getting from sever ?
Updated Apr 24, 2022
参考
issue 2034
中 thumbs 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> ) }
Toolbar
在 hover
时, Tooltip
效果如何处理?✅
Button tooltips in the demo
Updated Nov 7, 2023
Feature Request: Tooltips for toolbar buttons
Updated Jun 30, 2022
结合
issue 650
和 issue 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; } } }
使用 clipboard
的 matchers
时,需要使用到 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]], }, };
但是实际调试时,却出现了:
定位代码时,发现是这一段:
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; };
Delta
的 concat
不支持 Promise
,真难搞。粘贴时,实现域外图片资源转换为 CDN
,怎么做?✅
parchment
quilljs • Updated 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 withQuill.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 应该怎么做?目前无法支持。❌
本来觉得
react-quill-v2
既然支持表格,那么应该是可以直接使用的,而不是需要安装额外的 NPM 包。但是看到了 codepen
中引入的 quill
版本竟然是 2.0.0-dev.2
,而 [email protected]
依赖的 quill
是 1.3.7
,版本对不上,那就无法使用了,而原生 Table
和 quill-better-table
都是在 quill^2
才支持。is there any plan for quilljs2.0
Updated Mar 19, 2024
TinyMCE 踩坑记录
菜单栏如何进行自定义,以及如何开始一个插件?
Toolbar 如何增加自定义图标?
如何拦截编辑器本身的粘贴功能,可以实现拦截图片并上传到图床吗?
中有提到过使用
postprocess
等方法进行拦截并上传图片到图床。自带的 placeholder
功能怎么使用?❌
官方示例中,
placeholder
是拿 textarea
原生实现的,针对 react-tinymce
来讲是无法实现的,只能自己写插件来实现了。当手动插入自定义元素时,有哪些坑要踩?
插入 div
或 p
元素并 enter
,前一行元素的 class
或 data-
也会继承到下一行。✅
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-*
等属性处理方法富文本编辑器
调研了市面上的富文本编辑器,总结归纳出了以下内容。
名称 | 自定义 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 做的二次开发,具有插件安装能力。 |
参考资料
- http://tinymce.ax-z.cn/ tinymce中文文档