本人已参与「新人创作礼」活动,一起开启掘金创作之路
一天一个奇怪需求又来了。
今天是需要在富文本添加一个自定义输入框,也就是需要自定义一个小工具,在富文本中就相当于自定义一个块级,查看了许多的开箱即用的富文本组件,都难以做到,要么就是社区太少,要么就是接口文档说明不够。
所以决定自己写一个富文本组件,查看了比较火的富文本框架,最后决定使用slate.js该富文本开发框架。 废话不多说,直接上代码。
相关技术栈react、typescript、slate.js
分为三部分,toolbar.tsx,component.tsx,inport.tsx
inport.tsx(如果是.js,只需把变量名的类型去除就可以)
import React, { useMemo, useCallback, useState, useRef } from 'react'
import { createEditor, BaseEditor, Descendant, Transforms } from 'slate'
import { Slate, Editable, withReact, ReactEditor } from 'slate-react'
import {Tooblean} from './toolbar'//富文本的工具栏
import {CodeElement,DefaultElement,WeightOne,WeightTwo,Imagee,Leaf} from './components'//工具栏上面的自定义工具
import './index.less'//css样式
interface CustomElement { type?: any,children?:any, align?: string, url?: string }
interface CustomText extends CustomElement{ text?: string, bold?: boolean, italic?: boolean, underline?: boolean }
declare module 'slate' {
interface CustomTypes {
Editor: BaseEditor & ReactEditor
Element: CustomElement
Text: CustomText
}
}
const initialValue: any = [//初始化,刚开始的第一个块级
{
type: 'quote',
children: [{ text: ' ' }]
}
]
interface Props{
onChang:(e:any)=>void
}
export const SlateText = (props:Props) => {
const editor = useMemo(() => withReact(createEditor()), [])//editor接收Node
const [text, setText] = useState<any>()
const tt = useRef(null)//判断是否在该自定义块级中(个人需求,可以去除)
const rendeerElement = useCallback((props: any) => {//判断添加,渲染不同的块级组件
tt.current = props.element.type
console.log(props.element)
switch (props.element.type) {
case 'code':
return <CodeElement {...props} />
case 'weightone':
return <WeightOne {...props} />
case 'weighttwo':
return <WeightTwo {...props} />
case 'image':
return <Imagee {...props} />
default:
return <DefaultElement {...props} />
}
}, [])
const renderLeaf = useCallback((props: any) => {
return <Leaf {...props} />
}, [])
return (
<>
<div className='slate'>
<Slate editor={editor} value={initialValue} onChange={(value) => {props.onChang(value) }} >
<Tooblean>
</Tooblean>
<div className='slatecontent'>
<Editable
renderElement={rendeerElement}//自定义块级
renderLeaf={renderLeaf}//自定义叶子组件,也就是最颗粒,作用在文本上
spellCheck
autoFocus
onKeyDown={event => {//(个人需求,可以去除)
if (event.keyCode == 13 && tt.current == 'code') {
event.cancelable = true;
event.preventDefault();
event.stopPropagation();
Transforms.insertText(editor, ' \n')
return
}
}}
/>
</div>
</Slate>
</div>
</>
)
}
tooblae.tsx(富文本组件上方的工具栏)
import React, { useState} from 'react'
import { BaseEditor, Descendant, Transforms, Text, Node } from 'slate'
import { ReactEditor, useSlate } from 'slate-react'
interface CustomElement { type?: any,children?:any, align?: string, url?: string }
interface CustomText extends CustomElement{ text?: string, bold?: boolean, italic?: boolean, underline?: boolean }
declare module 'slate' {
interface CustomTypes {
Editor: BaseEditor & ReactEditor
Element: CustomElement
Text: CustomText
}
}
export const Tooblean = (props: any) => {
const editor = useSlate()//通过useslate可实时获取富文本的状态,比如判断当前是否加粗,当前的块级组件
const elephant = (descendant: Descendant[], styleString: string) => {//判断当前文本的状态,从而确定是附加样式
if (descendant.every((T: any, I: number) => {
return T.children.every((T_: any, I_: number) => T_[styleString])
})) {
return true
} else {
return false
}
}
const operate = (e: any, name: string, nameObj: CustomText) => {//自定义块级组件
if (Node.fragment(editor, editor.selection!)[0].children[0].text.length != 0) {
Transforms.setNodes(
editor,
nameObj,//渲染哪一个组件
{
match: (n: any) => {
return Text.isText(n)
},
split: true,//分组
}
)
} else {
editor.addMark(name, !elephant(Node.fragment(editor, editor.selection!), name))//添加叶子文节点
}
e.preventDefault();
e.stopPropagation();
}
const operateDom = (e: any, nameObj: CustomElement) => {//自定义叶子文节点
if (Node.fragment(editor, editor.selection!)[0].align == nameObj.align) {
Transforms.setNodes(
editor,
{ type: 'quote', align: 'left' }//对渲染的叶子文节点的选择
)
} else {
Transforms.setNodes(
editor,
nameObj
)
}
e.preventDefault();
e.stopPropagation();
}
const operateWeight = (e: any, nameObj: CustomElement) => {//文字的左对齐、右对齐、居中
if (Node.fragment(editor, editor.selection!)[0].type == nameObj.type) {
Transforms.setNodes(
editor,
{ type: 'quote' }
)
} else {
Transforms.setNodes(
editor,
nameObj
)
}
e.preventDefault();
e.stopPropagation();
}
const [toolbar, setToolbar] = useState([//自定义工具栏的工具
{
effect: 'bold',//加粗
src: './image/string.png',
src2: './image/string2.png',
click: (event: any) => {
operate(event, 'bold', { bold: !elephant(Node.fragment(editor, editor.selection!), 'bold') })
}
},
{
effect: 'italic',//斜体
src: './image/italic.png',
src2: './image/italic2.png',
click: (event: any) => {
operate(event, 'italic', { italic: !elephant(Node.fragment(editor, editor.selection!), 'italic') })
}
},
{
effect: 'underline',//下划线
src: './image/underline.png',
src2: './image/underline2.png',
click: (event: any) => {
operate(event, 'underline', { italic: !elephant(Node.fragment(editor, editor.selection!), 'underline') })
}
},
{
effect: 'hidden',//个人需求
src: './image/yingc.png',
src2: './image/yingc.png',
click: () => {
Transforms.insertNodes(
editor,
{ type: 'code', children: [{ text: '请输入隐藏内容,第一行为表头',}] },
)
Transforms.insertNodes(
editor,
{ type: 'quote', children: [{ text: '' }] },
)
}
},
{
effect: 'left',//文本左对齐
src: './image/left.png',
src2: './image/left2.png',
click: (event: any) => {
operateDom(event, { align: 'left' })
}
},
{
effect: 'right',//文本右对齐
src: './image/right.png',
src2: './image/right2.png',
click: (event: any) => {
operateDom(event, { align: 'right' })
}
},
{
effect: 'center',//文本居中
src: './image/center.png',
src2: './image/center2.png',
click: (event: any) => {
operateDom(event, { align: 'center' })
}
},
{
effect: 'justify',//文本左右对齐
src: './image/justify.png',
src2: './image/justify2.png',
click: (event: any) => {
operateDom(event, { align: 'justify' })
}
},
{
effect: 'weightone',//字体为h1
src: './image/H1-22.png',
src2: './image/H1-22 (1).png',
click: (event: any) => {
operateWeight(event, { type: 'weightone' })
}
},
{
effect: 'weighttwo',//字体为h2
src: './image/H2-22.png',
src2: './image/H2-22 (1).png',
click: (event: any) => {
operateWeight(event, { type: 'weighttwo' })
}
},
{
effect: 'image',//图片
src: './image/image.png',
src2: './image/image.png',
click: (event: any) => {
let input=document.createElement('input')//创建input type='file'
input.type='file'
input.onchange=(e:any)=>{//上传图片
let files=e.target.files
if(files){
if(files![0].type.indexOf('image')==-1)return
let reder=new FileReader()
reder.readAsDataURL(files[0])
reder.onload=(e)=>{//可以获取到上传的图片的base64,该地方写上传接口,通过返回的的src附件到下面的img即可
Transforms.insertNodes(
editor,
{
type: 'image',
children:[{text:''}],
url:e.target?.result//换成接口返回的数据
}
)
Transforms.insertNodes(
editor,
{
type:'quote',
children:[{text:''}]
}
)
}
}
}
input.click()//触发上传
input.remove()//删除input
}
}
])
const booleansrc = (data: string) => {//判断工具栏按钮图片状态
if (editor.selection) {
return Node.fragment(editor, editor.selection!)[0].children[0][data]
} else {
return false
}
}
const booleansrcTwo = (styleString: string) => {//判断工具栏按钮图片状态
if (editor.selection) {
return Node.fragment(editor, editor.selection!)[0].align == styleString ? true : false
} else {
return false
}
}
const booleansrcThree = (styleString: string) => {//判断工具栏按钮图片状态
if (editor.selection) {
return Node.fragment(editor, editor.selection!)[0].type == styleString ? true : false
} else {
return false
}
}
const srcImg = (i: number, t: any) => {
if (i < 4) {
return booleansrc(t.effect) ? require(`${t.src2}`) : require(`${t.src}`)
} else if (i < 8) {
return booleansrcTwo(t.effect) ? require(`${t.src2}`) : require(`${t.src}`)
} else if (i < 11) {
return booleansrcThree(t.effect) ? require(`${t.src2}`) : require(`${t.src}`)
}
}
return (
<div className='slatetop'>
{toolbar.map((T: any, I: number) =>
<img
className='img'
src={srcImg(I, T)}
onClick={(e) => {
let dom: any = document.querySelector(`#${T.effect}`)
dom.src = dom.src == require(`${T.src}`) ? require(`${T.src2}`) : require(`${T.src}`)
T.click(e)
}}
id={T.effect}
key={T.effect}
onTouchStart={(e) => { e.preventDefault() }}///防止点击工具栏的工具导致富文本失去焦点
onMouseDown={(e) => { e.preventDefault() }}//防止点击工具栏的工具导致富文本失去焦点
></img>
)}
</div>
)
}
compoment.tsx(通过props获取自定义内容,判断渲染)
import * as React from 'react'//工具栏渲染的自定义组件
export const CodeElement = (props: any) => {
return <p id='style' className='style'>{props.children}</p>
}
export const DefaultElement = (props: any) => {
return <p {...props.attributes} style={{ textAlign: props.element.align ? props.element.align : 'left' }}>{props.children}</p>
}
export const WeightOne = (props: any) => {
return <h1 {...props.attributes}>{props.children}</h1>
}
export const WeightTwo = (props: any) => {
return <h2 {...props.attributes}>{props.children}</h2>
}
export const Imagee = (props: any) => {
return (
<div {...props.attributes} style={{'margin':'4px 0 8px'}}>
<div style={{'height':'0px','display':'none'}}>
{props.children}
</div>
<div
style={{ 'position': 'relative' }}
>
<img
src='https://maoyetrpg-1254195378.cos.ap-guangzhou.myqcloud.com/resource/bf7fecb9fe8b6fb9b0a64f8a478cc518'
style={{
'display': 'block',
'maxWidth': '100%',
'maxHeight': '20em'
}}
/>
</div>
</div>
)
}
export const Leaf = (props: any) => {
return (
<span
{...props.attributes}
style={{
fontWeight: props.leaf.bold ? 'bold' : 'normal',
fontStyle: props.leaf.italic ? 'italic' : 'normal',
textDecoration: props.leaf.underline ? 'underline' : 'none'
}}
>
{props.children}
</span>
)
}
实现了富文本组件的基本功能,字体加粗、斜体、下划线等功能,个人觉得slate.js的社区比较丰富。
以上代码可直接复制粘贴使用,对应图片需要自己下载,个人是到阿里下载的
效果图如下
目前短时间实现该基本功能,后期会添加上表情包,撤回等等功能