Quill 是一种现代的 WYSIWYG 编辑器,具备兼容性和可扩展性。
https://github.com/quilljs/quill
安装
React 包装器:
npm i -S react-quill
图片上传
使用一个 type=file 的 input ,完整自定义图片上传的逻辑
<ReactQuill
theme="snow"
value={value || ''}
onChange={onChange}
onFocus={() => setFocus(true)}
onBlur={() => setFocus(false)}
modules={modules}
ref={reactQuillRef}
/>
<input
ref={uploadInputRef}
type="file"
accept="image/*"
style={{ display: 'none' }}
onChange={uploadInputChange}
/>
自己写 image 的 addHandler
const reactQuillRef = useRef<ReactQuill>(null);
//组件中定义选择图片的方法
const selectImage = () => {
if (uploadInputRef && uploadInputRef.current) {
uploadInputRef.current.click(); //点击modal的html结构中的input标签
}
};
useEffect(() => {
if (reactQuillRef.current) {
const quill = reactQuillRef.current.getEditor();
quill.getModule('toolbar').addHandler('image', selectImage);
}
});
- input 被模拟点击后,选择了图片,触发change,拿到上传的图
- 上传后,拿到服务器返回的地址。再反写到 quill 中
const uploadInputChange = async (e: any) => {
const url = await uploadFile(e.target.files[0]);
if (reactQuillRef.current) {
const quill = reactQuillRef.current.getEditor();
const range = quill.getSelection();
if (range && url) {
const cursorPosition = range.index; //获取当前光标位置
quill.insertEmbed(cursorPosition, 'image', url); //插入图片
quill.setSelection(cursorPosition + 1, 1); //光标位置加1
}
}
};
自定义按钮
在 modules - toolbar - container 中写一个自定义名称的按钮
export const modules = {
toolbar: {
container: [
// ... toolbar 各按钮
['hcustom'], // 自定义按钮
],
},
};
相当于给 quill 工具栏增加了一个 .ql-hcustom 按钮,给这个按钮绑定点击事件即可:
useEffect(() => {
let hCustomButton: HTMLButtonElement | null = null;
if (wrapperRef.current) {
hCustomButton = wrapperRef.current.querySelector('.ql-hcustom');
if (hCustomButton) {
hCustomButton.addEventListener('click', handleHCustomButtonClick);
}
}
return () => {
if (hCustomButton) {
hCustomButton.removeEventListener('click', handleHCustomButtonClick);
}
};
});
给这个按钮 :after 设置图标
.ql-hcustom:after {
content: 'Ω';
}
汉化
汉化: https://www.jianshu.com/p/e0469b50f2b3
.ql-snow .ql-tooltip[data-mode='link']::before {
content: '\94FE\63A5';
}
.ql-snow .ql-tooltip.ql-editing a.ql-action::after {
content: '\786E\5B9A';
}
.ql-snow .ql-picker.ql-size .ql-picker-label[data-value='small']::before,
.ql-snow .ql-picker.ql-size .ql-picker-item[data-value='small']::before {
content: '\5C0F\53F7\5B57\4F53';
}
.ql-snow .ql-picker.ql-size .ql-picker-label::before,
.ql-snow .ql-picker.ql-size .ql-picker-item::before {
content: '\6B63\5E38\5927\5C0F';
}
.ql-snow .ql-picker.ql-size .ql-picker-label[data-value='large']::before,
.ql-snow .ql-picker.ql-size .ql-picker-item[data-value='large']::before {
content: '\5927\53F7\5B57\4F53';
}
.ql-snow .ql-picker.ql-size .ql-picker-label[data-value='huge']::before,
.ql-snow .ql-picker.ql-size .ql-picker-item[data-value='huge']::before {
content: '\8D85\5927\5B57\4F53';
}
.ql-snow .ql-picker.ql-header .ql-picker-label[data-value='1']::before,
.ql-snow .ql-picker.ql-header .ql-picker-item[data-value='1']::before {
content: '\6807\9898\0020\0031';
}
.ql-snow .ql-picker.ql-header .ql-picker-label[data-value='2']::before,
.ql-snow .ql-picker.ql-header .ql-picker-item[data-value='2']::before {
content: '\6807\9898\0020\0032';
}
.ql-snow .ql-picker.ql-header .ql-picker-label[data-value='3']::before,
.ql-snow .ql-picker.ql-header .ql-picker-item[data-value='3']::before {
content: '\6807\9898\0020\0033';
}
.ql-snow .ql-picker.ql-header .ql-picker-label[data-value='4']::before,
.ql-snow .ql-picker.ql-header .ql-picker-item[data-value='4']::before {
content: '\6807\9898\0020\0034';
}
.ql-snow .ql-picker.ql-header .ql-picker-label[data-value='5']::before,
.ql-snow .ql-picker.ql-header .ql-picker-item[data-value='5']::before {
content: '\6807\9898\0020\0035';
}
.ql-snow .ql-picker.ql-header .ql-picker-label[data-value='6']::before,
.ql-snow .ql-picker.ql-header .ql-picker-item[data-value='6']::before {
content: '\6807\9898\0020\0036';
}
.ql-snow .ql-picker.ql-header .ql-picker-label::before,
.ql-snow .ql-picker.ql-header .ql-picker-item::before {
content: '\666E\901A\6587\672C';
}
.ql-snow .ql-tooltip::before {
content: '\8BBF\95EE\94FE\63A5';
}
.ql-snow .ql-tooltip a.ql-action::after {
content: '\7F16\8F91';
}
.ql-snow .ql-tooltip a.ql-remove::before {
content: '\79FB\9664\000A';
}
.ql-snow .ql-tooltip[data-mode='video']::before {
content: '\89C6\9891\94FE\63A5';
}
css 渲染
渲染时也使用同一套 css ,保持管理端和用户端一致:
<link href="/static/libs/quill/quill.snow.css" rel="stylesheet">
渲染时,使用 ql-snow 和 ql-editor 包裹:
<div class="post-content ql-snow">
<div class="ql-editor">
<?= HtmlPurifier::process($post->content); ?>
</div>
</div>