前言
在前端开发中, 富文本是一种常见的业务场景, 而本文要讲的就是富文本框架 quill.js 中的自定义工具栏的开发
介绍
Quill.js 是一个具备跨平台和跨浏览器反对的富文本编辑器。凭借其可扩大架构和富裕表现力的 API,能够齐全自定义它以满足个性化的需要。因为其模块化架构和富裕表现力的 API,能够从 Quill 外围开始,而后依据须要自定义其模块或将本人的扩大增加到这个富文本编辑器中。它提供了两个用于更改编辑器外观的主题,能够应用插件或笼罩其 CSS 样式表中的规定进一步自定义。Quill 还反对任何自定义内容和格局,因而能够增加嵌入式幻灯片、3D 模型等。
该富文本编辑器的 特点:
- 因为其 API 驱动的设计,无需像在其余文本编辑器中那样解析 HTML 或不同的 DOM 树;
- 跨平台和浏览器反对,疾速轻便;
- 通过其模块和富裕表现力的 API 齐全可定制;
- 能够将内容示意为 JSON,更易于解决和转换为其余格局;
- 提供两个主题以疾速轻松地更改编辑器的外观。
自定义工具栏的开发
本次的编辑器应用
react-quill
组件库, 他在quill.js
外层包装了一层react
组件, 使得开发者在 react 框架用应用更加敌对
相干链接: https://github.com/zenoamaro/…
应用:
import React, {useState} from 'react';
import ReactQuill from 'react-quill';
import 'react-quill/dist/quill.snow.css';
function App() {const [value, setValue] = useState('');
return <ReactQuill theme="snow" value={value} onChange={setValue} />;
}
自定义 toolbar
传递自定义
toolbar
的值
toolbar
中 自定义的按钮, 能够用 iconfont
的 svg
或者 class
, 这里为了不便, 咱们间接用文字
const CustomButton = () => <span className="iconfont">
find
</span>;
function App() {const [value, setValue] = useState('');
function insertStar() {// 点击自定义图标后的回调}
// 自定义的 toolbar, useCallback 重渲染会有显示问题
const CustomToolbar = useCallback(() => (
<div id="toolbar">
<select
className="ql-header"
defaultValue={''}
onChange={(e) => e.persist()}
>
<option value="1"></option>
<option value="2"></option>
<option selected></option>
</select>
<button className="ql-bold"></button>
<button className="ql-italic"></button>
<button className="ql-insertStar">
<CustomButton/>
</button>
</div>
), []);
// 间接申明会有显示问题
const modules = useMemo(() => ({
toolbar: {
container: '#toolbar',
handlers: {insertStar: insertStar,},
},
}), []);
return (<div>
<CustomToolbar/>
<ReactQuill theme="snow" value={value} modules={modules} onChange={setValue}/>
</div>)
}
通过此计划, 能够打造一个属于本人的工具栏了
然而也有一个毛病: 原有的 quill.js
工具栏 性能须要本人手写或者去官网 copy 下来
例子
首先咱们上在线例子: https://d1nrnh.csb.app/
当初能够自定义增加工具栏了, 那就开始咱们的开发之旅
本次的例子是一个 查找与替换性能 的工具栏开发
首先依据 自定义 toolbar
中的计划增加按钮, 因为下面曾经有了例子, 这里就疏忽掉自定义按钮的代码
次要构造
当初依据点击之后的回调, 显示如下的款式:
class FindModal extends React.Component {render(){return <div className={'find-modal'}>
<span className={'close'} onClick={this.props.closeFindModal}>x</span>
<Tabs defaultActiveKey="1" size={'small'}>
<TabPane tab={'查找'} key="1">
{this.renderSearch()}
</TabPane>
<TabPane tab={'替换'} key="2">
{this.renderSearch()}
<div className={'find-input-box replace-input'}>
<label>{'替换'}</label>
<Input onChange={this.replaceOnChange}/>
</div>
<div className={'replace-buttons'}>
<Button disabled={!indices.length} size={'small'} onClick={this.replaceAll}>
{'全副替换'}
</Button>
<Button
disabled={!indices.length}
size={'small'}
type={'primary'}
onClick={this.replace}
>
{'替换'}
</Button>
</div>
</TabPane>
</Tabs>
</div>
}
}
在内部应用 state
的 visible
管制即可:
visible ? (<FindModal/>) : null
搜寻栏的解决
这里咱们从用户的输出关键词开始动手: 当用户 输出搜寻关键词 时, 触发回调:
<Input
onChange={this.onChange}
value={searchKey}
/>
onChange
输出时的触发 _(这里咱们能够加上 debounce)_:
首先咱们保留输出的值, 将搜寻后果 indices
重置为空:
this.setState({
searchKey: value,
indices: [],});
通过 quill 获取所有文本格式:
const {getEditor} = this.props;
const quill = getEditor();
const totalText = quill.getText();
解析用户输出的词, 将其转换成正则 (留神这里要对用户输出本义, 防止一些关键词影响正则 )
之后则是是非大小写敏感: 应用 i
标记, g
示意全局匹配的意思(_不加上就只会匹配一次_):
function escapeRegExp(string) {return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
}
const re = new RegExp(escapeRegExp(searchKey), this.state.checked ? 'g' : 'gi');
之后咱们就要利用 totalText
和 re
进行循环正则匹配:
while ((match = re.exec(totalText)) !== null) {
// 指标文本在文档中的地位
let index = match.index;
// 计算 从最后到 index 有多少个非凡 insert
index = this.countSpecial(index, indices.length ? indices[indices.length - 1].index : 0);
// 来自于 formatText 的办法, 使其高亮, 第 0 个默认选中
quill.formatText(index, searchKey.length, 'SearchedString', true, 'api');
// 最初记录搜寻到的坐标
indices.push({index});
}
特殊字符问题
这里须要留神的是 countSpecial
办法
具体实现:
countSpecial = (index, lastIndex) => {const {getEditor} = this.props;
const quill = getEditor();
const delta = quill.getContents();
// 获取上一个节点到以后节点的 delta
const restDelta = delta.slice(lastIndex, index);
const initValue = this.specialArray.length
? this.specialArray[this.specialArray.length - 1]
: 0;
const num = restDelta.reduce((num, op) => {if (typeof op.insert === 'object') {return num + 1;}
return num;
}, initValue);
this.specialArray.push(num);
return index + num;
};
他的次要作用是用来计算编辑器中的 特殊字符 数量, 如 图片、emoji、附件 等等
这样做的起因在于, 通过 quill
的办法 quill.getText();
并不能齐全返回所有的显示, 他只能返回 文本 , 而像是 图片 这样的, 他是没有理论的文本, 然而却有着实在的占位符
像这些特殊符号只能通过 delta
的计划来获取 它是否存在, 而如果全局应用 delta
计划的话, 他就不能实现搜寻了;
举个例子
比方我当初输出一句新诗 但愿人长久,千里共婵娟。
, 其中 短暂
两个字应用了 加粗 的格局, 他显示的 delta
是这样的:
[{insert: '但愿人'},
{attributes: {bold: true}, insert: '短暂'},
{insert: ',千里共婵娟。\n'},
]
能够看到 delta
的文字是断裂的, 会被任意的格局所拆开;
所以当初应用的是这样一种 text
+ delta
组合的计划
搜寻完结
搜寻结束之后, 格局后果的坐标一次赋予对应格局, 同时记录以后选中的第 0
个搜寻关键词
if (indices.length) {this.currentIndex = indices[0].index;
// 使得 indices[0].index 到 length 的间隔的文本 增加 SearchedStringActive 格局
quill.formatText(indices[0].index, length, 'SearchedStringActive', true, Emitter.sources.API);
this.setState({
currentPosition: 0,
indices,
});
}
quill 格局
在下面搜寻性能中咱们应用了一个 API
: quill.formatText
这里咱们就来介绍一下他
在 quill.js
中咱们能够给他增加自定义的格局, 以这个 SearchedString
格局为例子:
quill.formatText(index, length, 'SearchedString', true, 'api');
想要让他起效咱们就要先创立文件 SearchedString.ts
(_应用 js 也没问题_):
import {Quill} from 'react-quill';
const Inline = Quill.import('blots/inline');
class SearchedStringBlot extends Inline {
static blotName: string;
static className: string;
static tagName: string;
}
SearchedStringBlot.blotName = 'SearchedString';
SearchedStringBlot.className = 'ql-searched-string';
SearchedStringBlot.tagName = 'div';
export default SearchedStringBlot;
在入口应用:
import SearchedStringBlot from './SearchedString'
Quill.register(SearchedStringBlot);
增加这样一个格局, 在咱们搜寻调用之后, 搜寻到的后果就会有对应的类名了:
在这里咱们还须要在 CSS 中增加对应的款式即可实现高亮性能:
.ql-searched-string {
// 这里须要保障权重, 防止查找的显示被背景色和字体色彩笼罩
background-color: #ffaf0f !important;
display: inline;
}
搜寻的选中
在搜寻结束之后, 默认选中的是第 0 个, 并且咱们还须要赋予另一个格局: SearchedStringActive
,
依照上述计划同样增加这个 formats
之后增加款式:
// 选中的规定权限须要大于 ql-searched-string 的规定, 并且要不一样的色彩和背景
.ql-searched-string-active {
display: inline;
.ql-searched-string {
background-color: #337eff !important;
color: #fff !important;
}
}
给咱们的输入框开端增加 上一个 和下一个 性能, 这里就间接用图标来做按钮, 两头显示以后索引和总数:
<Input
onChange={this.onChange}
value={searchKey}
suffix={
indices.length ? (<span className={'search-range'}>
<LeftOutlined onClick={this.leftClick} />
{currentPosition + 1} / {indices.length}
<RightOutlined onClick={this.rightClick} />
</span>
) : null
}
/>
点击事件
在点击 下一个 图标之后, 咱们只须要做四步:
- 革除上一个索引的款式
- 索引数加一, 并判断下一个是否存在, 如果不存在则赋值为 0
- 获取下一个的索引, 并增加高亮
- 查看下一个的地位是否在视窗中, 不在则滚动窗口
上述的数据获取起源都在于搜寻函数中的 indices
数组, 它标记着每一个搜寻后果的索引
和 下一个 事件相同的就是 上一个 事件了, 他的步骤和 下一个 步骤相似
视窗的查看
在点击之后咱们须要对以后高亮的索引地位进行判断, 依赖于 quill
和 原生的地位 API
来做出调整:
const scrollingContainer = quill.scrollingContainer;
const bounds = quill.getBounds(index + searchKey.length, 1);
// bounds.top + scrollingContainer.scrollTop 等于指标到最顶部的间隔
if (
bounds.top < 0 ||
bounds.top > scrollingContainer.scrollTop + scrollingContainer.offsetHeight
) {scrollingContainer.scrollTop = bounds.top - scrollingContainer.offsetHeight / 3;}
替换
在查找性能之后, 咱们就须要增加替换的性能
单个的替换是非常简单的, 只须要三步: 删除原有词, 增加新词, 从新搜寻:
quill.deleteText(this.currentIndex, searchKey.length, 'user');
quill.insertText(this.currentIndex, this.replaceKey, 'user');
this.search();
想要实现全副替换, 就不是循环单个替换了, 这样破费的性能较多, 甚至会产生卡顿, 对用户是否不敌对
目前我应用的计划是, 倒序删除:
let length = indices.length;
// 遍历 indices 尾部替换
while (length--) {
// 先删除再增加
quill.deleteText(indices[length].index, oldStringLen, 'user');
quill.insertText(indices[length].index, newString, 'user');
}
// 完结后从新搜寻
this.search();
总结
目前 quill
存在了两点问题:
- 不反对表格等格局, 须要降级到
2.0dev
版本, 然而此版本更改了很多货色 - 以后此仓库的人员称曾经进行保护了, 后续的更新保护是一个大问题
本文从单个工具栏的开发, 介绍了 quill
富文本编辑器的局部开发流程, 整个构造是很简略的, 根本也是都用了 quill
的官网 API
以后性能只波及到了 format
格局, 在下一篇文章中, 我讲持续讲述 table
modules
和 [email protected]
的开发
本文中例子的源码: 点击查看
援用
- https://juejin.cn/post/708404…