forgeplus-react-v2/src/common/quillForEditor/index.js

274 lines
7.6 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import './index.scss'
import 'quill/dist/quill.core.css' // 核心样式
import 'quill/dist/quill.snow.css' // 有工具栏
import 'quill/dist/quill.bubble.css' // 无工具栏
import './font.css'
import React, { useState, useRef, useEffect } from 'react'
import Quill from 'quill'
import katex from 'katex'
import { fetchUploadImage } from '../../services/ojService.js'
import { getImageUrl } from 'educoder'
import ImageBlot from './ImageBlot'
import FillBlot from './FillBlot'
import LinkBlot from './link-blot'
import mediator from '../mediator'
import { defaultQuillOpt } from '../components/custom-editor'
let Size = Quill.import('attributors/style/size')
Size.whitelist = ['14px', '16px', '18px', '20px', false]
let fonts = ['Microsoft-YaHei', 'SimSun', 'SimHei', 'KaiTi', 'FangSong']
let 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(FillBlot)
const FillStr = '▁'
const FillReg = new RegExp(FillStr, 'g')
const BRSTR = '<p><br></p>'
//hack
function quillSetValue(el, value) {
if (value && value.hasOwnProperty('ops')) {
el.setContents(value)
} else {
if (value) {
let rs = value
if (rs.endsWith(BRSTR)) {
rs += BRSTR
}
el.clipboard.dangerouslyPasteHTML(rs);
}
}
}
const BUTTONTIP = {
'bold': '加粗',
'strike': '删除线',
'italic': '斜体',
'underline': '下划线',
'ordered': '有序列表',
'bullet': '无序列表',
'color': '字体颜色',
'background': '背景色',
'sub': '下标',
'super': '上标',
'image': '上传图片',
'code-block': '代码块',
'formula': '公式',
'clean': '清除格式',
'fill': '插入填空项',
'align': '对齐',
'header1': '标题一',
'header2': '标题二',
'header3': '标题三',
'header4': '标题四',
'header5': '标题五',
'header6': '标题六'
}
function insertAfter(newNode, referenceNode) {
referenceNode.parentNode.insertBefore(newNode, referenceNode.nextSibling);
}
function initButtonTip(el) {
const container = el.getModule('toolbar').container
const btns = container.querySelectorAll('.ql-formats > button,.ql-formats>span')
Array.from(btns).forEach(btn => {
let value = btn.getAttribute('value')
let cls = btn.classList[0].replace('ql-', '')
btn.setAttribute('title', BUTTONTIP[value || cls] || BUTTONTIP[`${cls}${value}`])
if (cls === 'fill') {
let span = document.createElement('span')
span.setAttribute('class', 'fill-tip')
span.innerText = '点击插入填空项'
span.addEventListener('click', () => { btn.click() })
insertAfter(span, btn)
}
})
}
export default ({
defaultValue,
placeholder,
readOnly,
autoFocus = false,
options,
value,
imgAttrs = {}, // 指定图片的宽高
style = {},
wrapStyle = {},
showUploadImage,
onContentChange,
className,
onAddFill,
onDeleteFill
}) => {
const editorRef = useRef(null)
// quill 实例
const [quill, setQuill] = useState(null)
const bindings = {
backspace: {
key: 'Backspace',
/**
* @param {*} range
* { index, // 删除元素的位置
* length // 删除元素的个数, 当删除一个时, length=0 其它等于删除的元素的个数
* }
* @param {*} context 上下文
*/
handler: function (range, context) {
const [index, delArrs] = getFillInfo(range, this.quill)
if (delArrs) {
if (window.confirm('确定要删除这个填空吗?')) {
onDeleteFill && onDeleteFill(index, delArrs.length) // 调用删除回调, 返回删除的元素下标[]
return true
} else {
return false
}
}
return true
}
}
}
const quillOption = {
modules: {
toolbar: options || defaultQuillOpt,
'syntax': false,
keyboard: {
bindings: bindings
}
},
readOnly,
placeholder,
theme: readOnly ? 'bubble' : 'snow',
}
//返回当前选中文件之前填空的索引,和选中的文本内有多少个填空
function getFillInfo(range, quill) {
const { index, length } = range
const start = length === 0 ? index - 1 : index
const selectionText = quill.getText(start, Math.max(1, length))
const beforeSelectionText = quill.getText(0, start)
return [(beforeSelectionText.match(FillReg) || []).length, selectionText.match(FillReg)]
}
useEffect(() => {
const quillInstance = new Quill(editorRef.current, quillOption)
setQuill(quillInstance)
// 处理图片上传功能
quillInstance.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 = quillInstance.getSelection(true)
let fileUrl = '' // 保存上传成功后图片的url
const result = await fetchUploadImage(formData)
// 获取上传图片的url
if (result.data && result.data.id) {
fileUrl = getImageUrl(`api/attachments/${result.data.id}`)
}
const { width, height } = imgAttrs
if (fileUrl) {
let imgOption = {
url: fileUrl,
alt: '图片信息',
width,
height
}
quillInstance.insertEmbed(range.index, 'image', imgOption)
}
}
})
quillInstance.getModule('toolbar').addHandler('fill', (e) => {
const range = quillInstance.getSelection(true)
const [fillIndex, _] = getFillInfo(range, quillInstance)
quillInstance.insertEmbed(range.index, 'fill', { "data-index": fillIndex, text: FillStr })
quillInstance.setSelection(quillInstance.getLength(), 0, 'api')
onAddFill && onAddFill(fillIndex) // 调用添加回调
})
initButtonTip(quillInstance)
quillSetValue(quillInstance, value)
if (autoFocus) {
quillInstance.focus()
}
function onPreview(url) {
if (showUploadImage) {
showUploadImage(url)
}
}
let unsub = mediator.subscribe('on-preview-image', onPreview)
return () => {
unsub()
}
}, [])
useEffect(() => {
if (quill) {
if (readOnly === true) {
quill.enable(false)
} else {
quill.enable(true)
}
}
}, [quill, readOnly])
useEffect(() => {
quillSetValue(quill, defaultValue)
}, [quill, defaultValue])
useEffect(() => {
if (quill) {
quill.root.dataset.placeholder = placeholder;
}
}, [quill, placeholder])
useEffect(() => {
if (quill) {
autoFocus ? quill.focus() : quill.blur()
}
}, [quill, autoFocus])
useEffect(() => {
if (quill && onContentChange) {
function onChangeHandler(delta, oldDelta, source) {
let html = editorRef.current.children[0].innerHTML
let text = quill.getText()
if (html === '<p><br></p>') html = ''
if (onContentChange) {
onContentChange(quill.getContents(), html, { quill, text, delta, oldDelta, source })
}
}
quill.on('text-change', onChangeHandler)
return () => {
quill.off('text-change', onChangeHandler)
}
}
}, [quill, onContentChange])
// 返回结果
return (
<div className={`quill_editor_for_react_area ${className} `} style={wrapStyle}>
<div ref={editorRef} style={style}></div>
</div>
)
}