slate富文本组件

376 阅读4分钟

本人已参与「新人创作礼」活动,一起开启掘金创作之路

一天一个奇怪需求又来了。

今天是需要在富文本添加一个自定义输入框,也就是需要自定义一个小工具,在富文本中就相当于自定义一个块级,查看了许多的开箱即用的富文本组件,都难以做到,要么就是社区太少,要么就是接口文档说明不够。

所以决定自己写一个富文本组件,查看了比较火的富文本框架,最后决定使用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的社区比较丰富。

以上代码可直接复制粘贴使用,对应图片需要自己下载,个人是到阿里下载的

效果图如下

image.png

image.png

目前短时间实现该基本功能,后期会添加上表情包,撤回等等功能