Gitlab React-quill:https://github.com/zenoamaro/react-quill
中文文档 Quill:http://doc.quilljs.cn/1409381
官网 Quill:https://quilljs.com/docs/quickstart/
Gitlab Delta:https://github.com/quilljs/delta
需求:
坑点:
一开始的需求是focus点击变成只读状态,onBlur后变成可写状态并调接口保存,查阅文档后用了编辑器自带的
readOnly字段,在没加图片上传的imageHandler的时候还是好好的,但是一加就陷入了一直调保存接口死循环,中间有试图绕过但又中了其他坑最后和产品商量放弃了onBlur去保存采用按钮手动保存
这个问题的使用场景是在配合antd form表单时出现,研究了很久发现不配合form的时候也会出现,然后一点点试发现只要是handler里的使用方法都会出现这个问题不只是imageHandler,官方文档的介绍也很少,最后是在React Quill 富文本编辑器中图片上传服务端找到了解决方案,使用useMemo包裹
const modules = React.useMemo(() => ({
toolbar: {
container: toolbarContainer,
handlers: {
image: imageHandler
}
},
}), []);
需要用该方法对编辑器做初始化,但使用时出现了undefined的错误,因为网上没找着具体的函数使用说明所以就以为第一个参数是传内容值,后来点进源码发现第一个参数要传quill的实例,第二个参数才是内容值
reactQuillRef?.current?.setEditorContents?.(
reactQuillRef?.current?.getEditor(),
data,
);
因为要在imageHandler里调上传接口所以需要获取编辑器的数据,但ref为空了,后来定位发现是ref的使用有问题
// 正确
let reactQuillRef: any = useRef(null);
<ReactQuill
ref={reactQuillRef}
/>
// 错误
let reactQuillRef: any = useRef(null);
<ReactQuill
ref={e=>{reactQuillRef = e}}
/>
原本想粘贴图片的时候去获取光标的位置然后粘在光标处,但始终拿不到光标定位,可能是因为粘贴的时候调用了上传图片的接口然后函数中执行了input.click方法导致失焦,后来直接写死了个最大值然后它就自动定位到末尾了
quill.insertEmbed(9999, 'image', link)
在配合form编辑时初始化表单使用form.setFields发现先触发了粘贴的函数,然后报错导致整个页面挂了,一点点调试后发现用插件的函数setEditorContents也同样会触发,但是在readOnly=true时是正常的,最后实在没有找到方法只能在编辑的时候放弃使用粘贴的功能
Base64转文件流
粘贴的时候需要将base格式转为文件流,采用了以下博主的方法
convertBase64UrlToBlob = (urlData) => {
//去掉url的头,并转换为byte
const bytes = window.atob(urlData.split(',')[1]);
//处理异常,将ascii码小于0的转换为大于0
const ab = new ArrayBuffer(bytes.length);
const ia = new Uint8Array(ab);
ia.forEach((i, index) => {
ia[index] = bytes.charCodeAt(index);
});
return new Blob([ia], { type: urlData.split(',')[0].split(':')[1].split(';')[0] });
};
完整代码:
import React, {
forwardRef,
useImperativeHandle,
useRef,
} from 'react';
import 'react-quill/dist/quill.snow.css';
import ReactQuill from 'react-quill';
import styles from './index.less';
import { connect } from 'dva';
import Utils from '@/utils/utils';
const formats = [
'header',
'bold',
'italic',
'underline',
'strike',
'blockquote',
'code-block',
'list',
'bullet',
'indent',
'link',
'image',
'color',
'background',
'font',
'align',
'clean',
];
const Editor = (props: any) => {
const {
refInstance,
readOnly = false,
closeClipboardImg = false,
...otherProps
} = props;
let reactQuillRef: any = useRef(null);
// 上传图片
const imageHandler = async () => {
const input = document.createElement('input');
input.setAttribute('type', 'file');
input.setAttribute('accept', 'image/*');
input.setAttribute('multiple', 'multiple');
input.click();
input.onchange = async () => {
Array.from(input.files).forEach((item) => {
const formData = new FormData();
formData.append('files', item);
// 上传图片
props.dispatch({
type: 'attachment/addAttachment',
payload: formData,
callback: (url: any) => {
// 获取url
let quill = reactQuillRef?.current?.getEditor(); //获取到编辑器本身
const cursorPosition = quill.getSelection().index; //获取当前光标位置
const link = url;
quill.insertEmbed(cursorPosition, 'image', link); //插入图片
quill.setSelection(cursorPosition + 1); //光标位置加1
},
});
});
};
};
// 对外暴露
useImperativeHandle(refInstance, () => ({
focus: (param = {}) => {
reactQuillRef?.current?.focus?.();
},
getEditorContents: (param = {}) => {
return reactQuillRef?.current?.getEditorContents?.();
},
setEditorContents: (data: any) => {
reactQuillRef?.current?.setEditorContents?.(
reactQuillRef?.current?.getEditor(),
data,
);
},
moveMouseEnd: () => {
// 将光标移动到最后
let editor = reactQuillRef?.current?.getEditor();
editor.setSelection(9999, 0);
},
}));
// 粘贴图片
const customImgForPaste = async (node: any, delta: any) => {
delta.forEach((op: any) => {
let file = Utils.base64toFile(op?.insert?.image);
const formData = new FormData();
formData.append('files', file);
// 上传图片
props.dispatch({
type: 'attachment/addAttachment',
payload: formData,
callback: (url: any) => {
// 获取url
let quill = reactQuillRef?.current?.getEditor(); //获取到编辑器本身
const cursorPosition = 99999; //粘贴的时候失去焦点拿不到光标位置
const link = url;
quill.insertEmbed(cursorPosition, 'image', link); //插入图片
quill.setSelection(cursorPosition); //光标位置
},
});
});
};
const toolbar = React.useMemo(
() => ({
toolbar: {
container: [
[{ header: [1, 2, 3, 4, 5, 6, false] }],
['bold', 'italic', 'underline', 'strike', 'blockquote', 'code-block'],
[
{ list: 'ordered' },
{ list: 'bullet' },
{ indent: '-1' },
{ indent: '+1' },
],
['link', 'image'],
[{ color: [] }, { background: [] }, { align: [] }],
['clean'],
],
handlers: {
image: imageHandler,
},
},
clipboard: {
matchers: [
// 粘贴事件和setEditorContents有冲突报错,但在readOnly=true的时候可以正常使用
// 临时处理方式:需要setEditorContents的地方先将readOnly=true再使用,或者先去掉粘贴功能比如表单编辑的时候
closeClipboardImg ? [] : ['img', customImgForPaste],
],
},
// imageDrop: true,
}),
[],
);
const defaultProps = {
theme: 'snow',
modules: toolbar,
formats,
placeholder: '请输入',
className: styles.default_style,
};
const readOnlyProps = readOnly
? {
readOnly: true,
modules: {},
formats: {},
theme: '',
}
: {};
return (
<>
<ReactQuill
ref={reactQuillRef}
{...defaultProps}
{...otherProps}
{...readOnlyProps}
/>
</>
);
};
const Ed = connect()(Editor);
export default forwardRef((props, ref) => <Ed {...props} refInstance={ref} />);