zoukankan      html  css  js  c++  java
  • 浅析富文本编辑器框架Slate.js

    浅析富文本编辑器框架Slate.js

    本文不是关于Slate.js使用入门的文章,如果还不了解该框架,建议先阅读下官方的文档:Slate官网文档

    关于Slate的一些特性
    • 不同于其他编辑器类的库,Slate并不提供譬如粗体、斜体、字体色等开箱即用的功能
    • Slate只是提供了一套自己定义的核心数据模型,以此一些操作数据和选区相关的API
    • 视图层的渲染和行为完全由开发者基于React定制

    从顶层设计上看,Slate的架构是典型的MVC模型,由自身定义数据模型(Model),暴露操作数据的方法(Controller),然后交由用户使用该数据在React中做渲染(View)

    虽然在实现简单的编辑器应用时这种方式显得有些繁冗,但在遇到需要对业务做较定制化的功能,如内嵌复杂表单、流程图等时,就能展现出极大的灵活性。而这类需求在使用其他编辑器的库时,经常是不可行的或者成本很高(往往要在源码层面进行改造)

    Slate的数据模型

    Slate.js数据模型的设计非常的“类DOM”,对于拥有Web基础的开发者降低了心智负担。下面从节点(Node)和选区(Selection)的设计上说明。

    type Node = Editor | Element | Text
    
    interface Editor {
        children: Node[]
    }
    
    interface Element {
        children: Node[]
        [key: string]: unknown
    }
    
    interface Text {
        text: string,
        [key: string]: unknown
    }
    

    Node作为最抽象的节点类型,包括以下三种类型:

    • Editor 编辑器的根节点类型

    • Element 具有children属性,可以作为其他Node的父节点;由传入的renderElement函数做自定义渲染

    • Text 包含文本信息;由renderLeaf函数做自定义渲染;在添加mark时,将文本打散成不同的Leaf(这个行为是由Slate执行的,下面的例子会讲)

    除了接口中定义的属性,也可以在节点中添加任意业务相关的属性值(如下面的例子)。

    一个基础的使用示例如下:

    const RichText = (props: any) => {
        // 创建Editor
        const editor = createEditor()
        // 初始值
        const [value, setValue] = useState([{
            type: 'paragraph',
            children: [{ text: "" }]
        }])
    
        // 自定义Element渲染
        const renderElement = (props) => {
            const { attributes, children, element } = props
    
            switch (element.type) {	// 根据Element中的type属性判断节点类型
                case "heading-one":
                    return <h1 {...attributes}>{children}</h1>;
                case "heading-two":
                    return <h2 {...attributes}>{children}</h2>;
                case "paragraph":
                    return <p {...attributes}>{children}</p>;
            }
        }
        // 自定义Leaf渲染
        const renderLeaf = (props) => {
            const { attributes, children, leaf } = props
        
            // 根据Text中的自定义属性判断样式类型
            if (leaf['background-color']) {
                children = <span style={{ backgroundColor: leaf['background-color']}}>
                    {children}
                </span>
            }
        
            if (leaf['font-color']) {
                children = <span style={{ color: leaf['font-color']}}>
                    {children}
                </span>
            }
        
            return <span {...attributes}>{children}</span>;
        }
    
        return <Slate editor={editor} value={value} onChange={(value) => setValue(value)}>
            <Editable
                renderElement={renderElement}
                renderLeaf={renderLeaf}/>
        </Slate>
    }
    

    假如我们在该编辑器中输入了两行文字,并选取一段文本添加颜色样式:

    则上图的文本内容所对应的数据结构:

    [
        {
            type: "paragraph",
            children: [
                {
                    text: "著名武术泰斗马保国。"
                }
            ]
        },
        {
            type: "paragraph",
            children: [
                {
                    text: "著名"
                },
                {
                    text: "篮球运动员",
                    'font-size': "rgba(139, 87, 42, 1)"
                },
                {
                    text: "蔡徐坤"
                }
            ]
        }
    ]
    

    该数组中最顶层的对象映射了Element元素(由renderElement渲染的),在其中的type字段中设为paragraph标志为默认的块级元素(当然也可以设为其他任何值,Slate.js并不关心type字段的含义)。下一层的叶子节点对象,包含了text字段,表示文本内容;也可能含有自定义的一些marks,例如上面的'font-size',用来在renderLeaf中依据marks来实现自定义的样式渲染。

    接下来再选取一段文字赋予一个背景色:

    添加背景色的选区是在上面带有font-size的Text节点中的,因此会被打散成三个Text,变为如下形式:

    [
        {
            type: "paragraph",
            children: [
                {
                    text: "著名武术泰斗马保国。"
                }
            ]
        },
        {
            type: "paragraph",
            children: [
                {
                    text: "著名"
                },
                {
                    text: "篮球",
                    'font-size': "rgba(139, 87, 42, 1)"
                },
                {
                    text: "运动",
                    'font-size': "rgba(139, 87, 42, 1)",
                    'background-color': "rgba(80, 227, 194, 1)"
                },
                {
                    text: "员",
                    'font-size': "rgba(139, 87, 42, 1)"
                },
                {
                    text: "蔡徐坤"
                }
            ]
        }
    ]
    
    光标和选区

    光标的定位由一个 Pathoffset 确定,Path 代表节点在文档中的位置,offset 则代表在节点中的偏移量:

    declare type Path = number[]
    
    interface Point {
        path: Path,
        offset: number
    }
    

    Paht 是一个number类型数组,包含的数值代表着从文档数据模型的根部到光标所在Text节点的路径offset 表示光标在Text节点上的偏移量

    如图中框中的节点所对应的Path就是[1, 0]

    选取的接口定义 Range 与原生selection的属性非常相似:

    interface Range {
        anchor: Point,
        focus: Point
    }
    

    锚点anchor代表选区的起始点,焦点focus代表选区的结束点;两者都为上述的Point类型。

    插件机制

    Slate提供了插件的机制允许我们覆盖编辑器原有的行为。除了直接使用slate-react和slate-history这些官方的插件,也可以自定义插件来对Slate编辑器进行拓展,而且实现方式非常简易:提供一个函数,该函数接收一个编辑器的实例editor对象,在其中重写实例对象上的方法,并返回editor实例。

    下面是个例子,加入在实现业务时有这么一个场景,需要在文本域中插入一些自定义的控件如按钮、下拉框等,并且都不可被编辑;而默认情况下在文本域中所有的dom元素都是contenteditable=true的状态,是能够被光标聚焦和编辑的。为了改变这种行为,可以自行实现一个插件:

    import { createEditor } from "slate"
    import { withReact } from "slate-react"
    import { withHistory } from "slate-history"
    
    const myCustomeEditor = (editor) => {
        const { isVoid } = editor	// editor原有的isVoid方法, 用以判断节点是否可编辑
        
        editor.isVoid = (element) => {	// 根据自定义的type字段将元素置为 不可编辑的
            return element.type === 'custome-element' ? true : isVoid(element)
        }
        
        return editor
    }
    
    // 创建了一个带有三个插件组合的Slate编辑器
    const eidotr = myCustomeEditor(withHistory(withReact(createEditor())))
    
  • 相关阅读:
    PHP+VUE实现前端和后端数据互通(宝塔面板)
    PHP上传图片
    GIT常用命令
    基于Postman中的报错
    VUE项目Eslint报错
    git配置:本地仓库提交到远程仓库
    mybatis基础
    Json验证数据
    Json 三种格式数据解析
    Ajax 实现数据异步传输
  • 原文地址:https://www.cnblogs.com/geek1116/p/14987973.html
Copyright © 2011-2022 走看看