简介
react-quilljs 毕竟是别人封装过的,对自己的需求不是很满足,例如国际化,禁用状态,数据I/O的预处理等等都要配置,还不如自己弄一个。
- 另外,查了下很多人使用的时候都是编辑器单独使用,而我需要将他嵌入表单之中,由于
toolbar使用的选择器id不能重复,所以这里用的是ref。 - 添加了图片模块
我在这里做了一个简易封装,稍作修改即可使用。
代码:
import { useDeepCompareEffect } from 'ahooks'
import Quill from 'quill'
import 'quill/dist/quill.snow.css'
import {
forwardRef,
memo,
useEffect,
useImperativeHandle,
useRef,
useState,
} from 'react'
import { ImageUpload } from './image.module'
import React from 'react'
Quill.register('modules/imageUpload', ImageUpload)
interface IProps {
defaultValue?: string
mode?: 'viewer' | 'editor'
}
export interface IOpenApiRichTextRefProps {
getEditor: () => Quill
getContent: () => string
setContent: (val: string) => void
clearContent: () => void
}
export const QUILL_EMPTY = JSON.stringify([
{
insert: '\n',
},
])
export default memo(
forwardRef((props: IProps, ref) => {
const { defaultValue = '', mode = 'editor' } = props
const isView = mode !== 'editor'
const editorRef = useRef()
const toolbarRef = useRef()
const [editor, setEditor] = useState<Quill>()
const [urlMap, setUrlMap] = useState<{ [k: string]: string }>({})
const [requestLoading, setRequestLoading] = useState(false)
const [id] = useState(`ql-toolbar-${randomId()}`)
const options = {
debug: false,
modules: {
debug: false,
toolbar: isView
? false
: {
container: `#${id}`,
},
imageUpload: {
notImage: () => {
message.error('请选择一个图片')
},
customUploader: async (
file: File,
insertUrl: (url: string) => void
) => {
// 自己的请求方法
const url = await UploadFn(file)
insertUrl(url)
},
},
},
placeholder: isView ? '' : '请输入',
theme: 'snow',
}
const setContentFn = async (val: string) => {
// 自定义检测json方法
if (!checkedJSON(val) || !val) {
editor?.setContents([{ insert: `${val}\n` }])
console.error('传入非json值,已展示默认格式')
} else {
const res = JSON.parse(val)
if (!Array.isArray(res)) return console.error('设置值需要数组')
// 在这里进行图片的预处理
// ...
editor?.setContents(res)
}
}
useImperativeHandle(ref, () => ({
getEditor: () => {
return editor
},
getContent: () => {
const rawData = editor?.getContents()?.ops
// const res = rawData?.map((a) => {
// if (a?.attributes?.alt === 'image') {
// return {
// ...a,
// insert: {
// // 图片后的处理
// // image: xxx,
// },
// }
// } else {
// return a
// }
// })
const res = rawData
return JSON.stringify(res)
},
setContent: setContentFn,
clearContent: () => {
editor?.setContents([{ insert: '\n' }])
},
}))
useEffect(() => {
if (editorRef.current && toolbarRef.current) {
const quill = new Quill(editorRef.current, options)
isView && quill.enable(false)
setEditor(quill)
}
}, [])
useDeepCompareEffect(() => {
if (editor) {
setContentFn(defaultValue)
}
}, [defaultValue, editor])
return (
<Spin spinning={requestLoading}>
<Box
style={{ height: '350px', width: '100%' }}
sx={{
'.ql-toolbar': {
borderTopLeftRadius: '5px',
borderTopRightRadius: '5px',
},
'.ql-container': {
height: '300px',
borderBottomLeftRadius: '5px',
borderBottomRightRadius: '5px',
},
}}
>
{isView ? (
<div ref={toolbarRef}></div>
) : (
<CustomToolbar id={id} ref={toolbarRef}></CustomToolbar>
)}
<div ref={editorRef}> </div>
</Box>
</Spin>
)
})
)
const CustomButton = () => <span className='octicon octicon-star' />
const CustomToolbar = React.forwardRef((props, ref) => {
// 在这里进行国际化
return (
<div ref={ref} id={props.id}>
<button className='ql-bold' />
<select
className='ql-header'
defaultValue={''}
onChange={(e) => e.persist()}
>
<option value='0'>正文</option>
<option value='1'>标题 1</option>
<option value='2'>标题 2</option>
<option value='3'>标题 3</option>
<option value='4'>标题 4</option>
<option value='5'>标题 5</option>
<option value='6'>标题 6</option>
</select>
{/* <select className='ql-size'>
<option value='small'>小号</option>
<option selected>中号</option>
<option value='large'>大号</option>
<option value='huge'>特大号</option>
</select> */}
<button className='ql-italic' />
<button className='ql-underline' />
<button className='ql-strike' />
<button className='ql-blockquote' />
<button className='ql-code-block' />
<button className='ql-list' value='ordered' />
<button className='ql-list' value='bullet' />
<select className='ql-color'>
<option value='black' selected>
black
</option>
<option value='red'>red</option>
<option value='green'>green</option>
<option value='blue'>blue</option>
<option value='orange'>orange</option>
<option value='violet'>violet</option>
<option value='#d0d1d2'></option>
</select>
<button className='ql-link' />
<button className='ql-image' />
{/* <button className='ql-video' /> */}
<button className='ql-script' value='sub'></button>
<button className='ql-script' value='super'></button>
<button className='ql-clean'></button>
<button className='ql-custom-chart'></button>
<button className='ql-insertStar'>
<CustomButton />
</button>
</div>
)
})
对图片模块进行了,修改便于预处理。加装了处理非图片格式的方法
模块来源于:quill-image-upload
/**
* Custom module for quilljs to allow user to drag images from their file system into the editor
* and paste images from clipboard (Works on Chrome, Firefox, Edge, not on Safari)
* @see https://quilljs.com/blog/building-a-custom-module/
*/
export class ImageUpload {
options: any
/**
* Instantiate the module given a quill instance and any options
* @param {Quill} quill
* @param {Object} options
*/
constructor(quill, options = {}) {
// save the quill reference
this.quill = quill
// save options
this.options = options
// listen for drop and paste events
this.quill
.getModule('toolbar')
?.addHandler('image', this.selectLocalImage.bind(this))
}
/**
* Select local image
*/
selectLocalImage() {
const input = document.createElement('input')
input.style.display = 'none'
input.id = 'newImage'
document.body.appendChild(input)
input.setAttribute('type', 'file')
input.click()
// Listen upload local image and save to server
input.onchange = () => {
const file = input.files[0]
// file type is only image.
if (/^image\//.test(file.type)) {
const checkBeforeSend =
this.options.checkBeforeSend || this.checkBeforeSend.bind(this)
checkBeforeSend(file, this.sendToServer.bind(this))
} else {
console.warn('You could only upload images.')
this.options?.notImage?.()
}
document.body.removeChild(input)
}
}
/**
* Check file before sending to the server
* @param {File} file
* @param {Function} next
*/
checkBeforeSend(file, next) {
next(file)
}
/**
* Send to server
* @param {File} file
*/
sendToServer(file: string | Blob) {
// Handle custom upload
if (this.options.customUploader) {
this.options.customUploader(file, (dataUrl) => {
this.insert(dataUrl)
})
} else {
const url = this.options.url,
method = this.options.method || 'POST',
name = this.options.name || 'image',
headers = this.options.headers || {},
callbackOK =
this.options.callbackOK || this.uploadImageCallbackOK.bind(this),
callbackKO =
this.options.callbackKO || this.uploadImageCallbackKO.bind(this)
if (url) {
const fd = new FormData()
fd.append(name, file)
if (this.options.csrf) {
// add CSRF
fd.append(this.options.csrf.token, this.options.csrf.hash)
}
const xhr = new XMLHttpRequest()
// init http query
xhr.open(method, url, true)
// add custom headers
for (var index in headers) {
xhr.setRequestHeader(index, headers[index])
}
// listen callback
xhr.onload = () => {
if (xhr.status === 200) {
callbackOK(JSON.parse(xhr.responseText), this.insert.bind(this))
} else {
callbackKO({
code: xhr.status,
type: xhr.statusText,
body: xhr.responseText,
})
}
}
if (this.options.withCredentials) {
xhr.withCredentials = true
}
xhr.send(fd)
} else {
const reader = new FileReader()
reader.onload = (event) => {
callbackOK(event.target.result, this.insert.bind(this))
}
reader.readAsDataURL(file)
}
}
}
/**
* Insert the image into the document at the current cursor position
* @param {String} dataUrl The base64-encoded image URI
*/
insert(dataUrl) {
const index =
(this.quill.getSelection() || {}).index || this.quill.getLength()
this.quill.insertEmbed(index, 'image', dataUrl, 'user')
// 设置attributes为image
this.quill.formatText(index, index, { alt: 'image' }, 'user')
}
/**
* callback on image upload succesfull
* @param {Any} response http response
*/
uploadImageCallbackOK(response, next) {
next(response)
}
/**
* callback on image upload failed
* @param {Any} error http error
*/
uploadImageCallbackKO(error) {
alert(error)
}
}