forked from Gitlink/forgeplus-react
278 lines
8.0 KiB
JavaScript
278 lines
8.0 KiB
JavaScript
/*
|
||
* @Description: quill 编辑器
|
||
* @Author: tangjiang
|
||
* @Github:
|
||
* @Date: 2019-12-18 08:49:30
|
||
* @LastEditors : tangjiang
|
||
* @LastEditTime : 2020-02-05 11:23:03
|
||
*/
|
||
import './index.scss';
|
||
import 'quill/dist/quill.core.css'; // 核心样式
|
||
import 'quill/dist/quill.snow.css'; // 有工具栏
|
||
import 'quill/dist/quill.bubble.css'; // 无工具栏
|
||
import 'katex/dist/katex.min.css'; // katex 表达式样式
|
||
import './font.css'
|
||
import React, { useState, useRef, useEffect } from 'react';
|
||
import Quill from 'quill';
|
||
import katex from 'katex';
|
||
import deepEqual from './deepEqual.js'
|
||
import { fetchUploadImage } from '../../services/ojService.js';
|
||
import { getImageUrl } from 'educoder'
|
||
import ImageBlot from './ImageBlot';
|
||
import FillBlot from './FillBlot';
|
||
import LinkBlot from './link-blot'
|
||
var Size = Quill.import('attributors/style/size');
|
||
Size.whitelist = ['14px', '16px', '18px', '20px', false];
|
||
var fonts = ['Microsoft-YaHei', 'SimSun', 'SimHei', 'KaiTi', 'FangSong'];
|
||
var Font = Quill.import('formats/font');
|
||
Font.whitelist = fonts; //将字体加入到白名单
|
||
window.Quill = Quill;
|
||
window.katex = katex;
|
||
Quill.register(ImageBlot);
|
||
Quill.register(Size);
|
||
Quill.register(LinkBlot);
|
||
Quill.register(Font, true);
|
||
// Quill.register({'modules/toolbar': Toolbar});
|
||
Quill.register({
|
||
'formats/fill': FillBlot
|
||
});
|
||
// Quill.register(Color);
|
||
|
||
|
||
function QuillForEditor({
|
||
placeholder,
|
||
readOnly,
|
||
autoFocus = false,
|
||
options,
|
||
value,
|
||
imgAttrs = {}, // 指定图片的宽高
|
||
style = {},
|
||
wrapStyle = {},
|
||
showUploadImage,
|
||
onContentChange,
|
||
addFill, // 点击填空成功的回调
|
||
deleteFill // 删除填空,返回删除的下标
|
||
}) {
|
||
// toolbar 默认值
|
||
const defaultConfig = [
|
||
'bold', 'italic', 'underline',
|
||
{ size: ['14px', '16px', '18px', '20px'] },
|
||
{ align: [] }, { list: 'ordered' }, { list: 'bullet' }, // 列表
|
||
{ script: 'sub' }, { script: 'super' },
|
||
{ 'color': [] }, { 'background': [] },
|
||
{ 'font': [] },
|
||
{ header: [1, 2, 3, 4, 5, false] },
|
||
'blockquote', 'code-block',
|
||
'link', 'image', 'video',
|
||
'formula',
|
||
'clean'
|
||
];
|
||
|
||
const editorRef = useRef(null);
|
||
// quill 实例
|
||
const [quill, setQuill] = useState(null);
|
||
const [selection, setSelection] = useState(null);
|
||
const [fillCount, setFillCount] = useState(0);
|
||
const [quillCtx, setQuillCtx] = useState({});
|
||
|
||
// 文本内容变化时
|
||
const handleOnChange = content => {
|
||
onContentChange && onContentChange(content, quill);
|
||
};
|
||
|
||
const renderOptions = options || defaultConfig;
|
||
|
||
const bindings = {
|
||
tab: {
|
||
key: 9,
|
||
handler: function () { }
|
||
},
|
||
backspace: {
|
||
key: 'Backspace',
|
||
/**
|
||
* @param {*} range
|
||
* { index, // 删除元素的位置
|
||
* length // 删除元素的个数, 当删除一个时, length=0, 其它等于删除的元素的个数
|
||
* }
|
||
* @param {*} context 上下文
|
||
*/
|
||
handler: function (range, context) {
|
||
/**
|
||
* index: 删除元素的位置
|
||
* length: 删除元素的个数
|
||
*/
|
||
const { index, length } = range;
|
||
const _start = length === 0 ? index - 1 : index;
|
||
const _length = length || 1;
|
||
let delCtx = this.quill.getText(_start, _length); // 删除的元素
|
||
const reg = /▁/g;
|
||
const delArrs = delCtx.match(reg);
|
||
if (delArrs) {
|
||
const r = window.confirm('确定要删除吗?');
|
||
if (r) {
|
||
let leaveCtx; // 获取删除元素之前的内容
|
||
if (length === 0) {
|
||
leaveCtx = this.quill.getText(0, index - 1);
|
||
} else {
|
||
leaveCtx = this.quill.getText(0, index);
|
||
}
|
||
const leaveArrs = leaveCtx.match(reg);
|
||
const leaveLen = (leaveArrs || []).length;
|
||
let delIndexs = [];
|
||
// 获取删除元素的下标
|
||
delArrs.forEach((item, i) => {
|
||
leaveLen === 0 ? delIndexs.push(i) : delIndexs.push(leaveLen + i);
|
||
});
|
||
deleteFill && deleteFill(delIndexs); // 调用删除回调, 返回删除的元素下标[]
|
||
return true
|
||
} else {
|
||
return false;
|
||
}
|
||
}
|
||
return true;
|
||
}
|
||
}
|
||
};
|
||
// quill 配置信息
|
||
const quillOption = {
|
||
modules: {
|
||
toolbar: renderOptions,
|
||
keyboard: {
|
||
bindings: bindings
|
||
}
|
||
// toolbar: {
|
||
// container: renderOptions
|
||
// }
|
||
},
|
||
readOnly,
|
||
placeholder,
|
||
theme: readOnly ? 'bubble' : 'snow',
|
||
};
|
||
|
||
|
||
useEffect(() => {
|
||
|
||
const quillNode = document.createElement('div');
|
||
editorRef.current.appendChild(quillNode);
|
||
const _quill = new Quill(editorRef.current, quillOption);
|
||
|
||
setQuill(_quill);
|
||
// 处理图片上传功能
|
||
_quill.getModule('toolbar').addHandler('image', (e) => {
|
||
const input = document.createElement('input');
|
||
input.setAttribute('type', 'file');
|
||
input.setAttribute('accept', 'image/*');
|
||
input.click();
|
||
|
||
input.onchange = async (e) => {
|
||
const file = input.files[0]; // 获取文件信息
|
||
const formData = new FormData();
|
||
formData.append('file', file);
|
||
|
||
const range = _quill.getSelection(true);
|
||
let fileUrl = ''; // 保存上传成功后图片的url
|
||
// 上传文件
|
||
const result = await fetchUploadImage(formData);
|
||
// 获取上传图片的url
|
||
if (result.data && result.data.id) {
|
||
fileUrl = (`https://testforgeplus.trustie.net/api/attachments/${result.data.id}`);
|
||
}
|
||
// 根据id获取文件路径
|
||
const { width, height } = imgAttrs;
|
||
// console.log('上传图片的url:', fileUrl);
|
||
if (fileUrl) {
|
||
_quill.insertEmbed(range.index, 'image', {
|
||
url: fileUrl,
|
||
alt: '图片信息',
|
||
onClick: showUploadImage,
|
||
width,
|
||
height
|
||
});
|
||
}
|
||
}
|
||
});
|
||
// 处理填空
|
||
_quill.getModule('toolbar').addHandler('fill', (e) => {
|
||
// alert(1111);
|
||
setFillCount(fillCount + 1);
|
||
const range = _quill.getSelection(true);
|
||
_quill.insertText(range.index, '▁');
|
||
addFill && addFill(); // 调用添加回调
|
||
});
|
||
}, []);
|
||
|
||
// 设置值
|
||
useEffect(() => {
|
||
if (!quill) return
|
||
|
||
const previous = quill.getContents()
|
||
|
||
if (value && value.hasOwnProperty('ops')) {
|
||
const ops = value.ops || [];
|
||
ops.forEach((item, i) => {
|
||
if (item.insert['image']) {
|
||
item.insert['image'] = Object.assign({}, item.insert['image'], { style: { cursor: 'pointer' }, onclick: (url) => showUploadImage(url) });
|
||
}
|
||
});
|
||
}
|
||
|
||
const current = value
|
||
if (!deepEqual(previous, current)) {
|
||
setSelection(quill.getSelection())
|
||
if (typeof value === 'string' && value) {
|
||
// debugger
|
||
quill.clipboard.dangerouslyPasteHTML(value, 'api');
|
||
if (autoFocus) {
|
||
quill.focus();
|
||
} else {
|
||
quill.blur();
|
||
}
|
||
} else {
|
||
quill.setContents(value)
|
||
if (autoFocus) quill.focus();
|
||
}
|
||
}
|
||
}, [quill, value, setQuill, autoFocus]);
|
||
|
||
// 清除选择区域
|
||
useEffect(() => {
|
||
if (quill && selection) {
|
||
quill.setSelection(selection)
|
||
setSelection(null)
|
||
}
|
||
}, [quill, selection, setSelection]);
|
||
|
||
// 设置placeholder值
|
||
useEffect(() => {
|
||
if (!quill || !quill.root) return;
|
||
quill.root.dataset.placeholder = placeholder;
|
||
}, [quill, placeholder]);
|
||
|
||
// 处理内容变化
|
||
useEffect(() => {
|
||
if (!quill) return;
|
||
if (typeof handleOnChange !== 'function') return;
|
||
let handler;
|
||
quill.on(
|
||
'text-change',
|
||
(handler = (delta, oldDelta, source) => {
|
||
const _ctx = quill.getContents();
|
||
setQuillCtx(_ctx);
|
||
handleOnChange(quill.getContents()); // getContents: 检索编辑器内容
|
||
})
|
||
);
|
||
return () => {
|
||
quill.off('text-change', handler);
|
||
}
|
||
}, [quill, handleOnChange]);
|
||
|
||
// 返回结果
|
||
return (
|
||
<div className='quill_editor_for_react_area' style={wrapStyle}>
|
||
<div ref={editorRef} style={style}></div>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
export default QuillForEditor;
|