使用draft.js开发富文本编辑器

Draft.js是Facebook开源的开发React富文本编辑器开发框架。和其它富文本编辑器不同,draft.js并不是一个开箱即用的富文本编辑器,而是一个提供了一系列开发富文本编辑器的工具。本文通过开发一些简单的富文本编辑器,来介绍draft.js提供的各种能力。

draft.js解决的问题

  1. 统一html标签contenteditable="true",在编辑内容时,不同浏览器下产生不同dom结构的问题;
  2. 给html的改变赋予onChange时的监听能力;
  3. 使用不可变的数据结构,每次修改都生成新的状态,保证里历史记录的可回溯;
  4. 可以结构化存储富文本内容,而不需要保存html片段。

不可变的数据结构

这里要介绍下不可变的数据,draft.js使用immutable.js提供的数据结构。draft.js中所有的数据都是不可变的。每次修改都会新建数据,并且内存中会保存原来的状态,方便回到上一步,这里很符合react的单向数据流的设计思路。

Editor组件

Draft.js提供了一个Editor组件。Editor组件是内容呈现的载体。我们先看一个基础编辑器。在线示例

import React, {Component} from 'react';
import {Editor, EditorState} from 'draft-js';

export default class extends Component {
    constructor(props) {
        super(props);
        this.state = {
            editorState: EditorState.createEmpty()
        };
        this.onChange = editorState => {
            this.setState({editorState});
        };
    }
    render() {
        return (
            
基础编辑器
) } }

这里的Editor组件接收2个props:editorState是整个编辑器的状态,类似�文本框的valueonChange监听状态改变并把新的状态传给对应的函数。�初始化的时候我们使用了EditorState提供的createEmpty方法,�根据语意我们很容易知道这个是生成一个没有内容的EditorState对象。

富文本样式

提到富文本编辑器,当然避免不了各种丰富的样式。富文本样式包含两种,行内样式和块级样式。行内样式是在段落中�某些字段上添加的样式,如��粗体、斜体、文字加下划线等等。块级样式是在整个段落上加的样式,如段落缩进、有序列表、无需列表等。Draft.js提供了�RichUtils模块�来�处理�富文本样式。

行内样式

RichUtils.toggleInlineStyle方法可以切换光标�所在位置的行内样式。该函数接收2个参数。第一个是editorState,在editorState中已经包含了光标选中内容的信息。第二个参数是样式名,draft.js提供了'BOLD', 'ITALIC', 'UNDERLINE','CODE'这几个默认的样式名。

toggleInlineStyle(
    editorState: EditorState,
    inlineStyle: string
): EditorState

点击�「Bold」�按钮��使选中字体变粗的例子:

import React, {Component} from 'react';
import {Editor, EditorState, RichUtils} from 'draft-js';

export default class extends Component {
    constructor(props) {
        super(props);
        this.state = {
            editorState: EditorState.createEmpty()
        };
        this.onChange = editorState => {
            this.setState({editorState});
        };
        this.toggleInlineStyle = this.toggleInlineStyle.bind(this);
    }
    toggleInlineStyle(inlineStyle) {
        this.onChange(
            RichUtils.toggleInlineStyle(
                this.state.editorState,
                inlineStyle
            )
        );
    }
    render() {
        return (
            
) } }

除此之外还可以为Editor提供customStyleMapprop来自定义�行内样式。


// ...
const styleMap = {
    'RED': {
        color: 'red'
    }
}

class MyEditor extends React.Component {
    // ...
    render() {
        return (
            
) } }

��在线示例

块级样式

Draft.js的块级样式是写在css文件中的,�要使用默认�样式需要引用draft-js/dist/Draft.css。下面是一些标签对应的样式名

html标签 block类型

header-one

header-two

header-three

header-four
header-five
header-six
blockquote
code-block
atomic
  • unordered-list-item,ordered-list-item
    unstyled

    可以使用RichUtils.toggleBlockType来改变block对应的类型。

    toggleBlockType(
        editorState: EditorState,
        blockType: string
    ): EditorState
    

    EditorblockStyleFnprop可以方便自定义样式。

    import 'draft-js/dist/Draft.css';
    import './index.css';
    import React, {Component} from 'react';
    import {Editor, EditorState, RichUtils} from 'draft-js';
    
    export default class extends Component {
        constructor(props) {
            super(props);
            this.state = {
                editorState: EditorState.createEmpty()
            };
            this.onChange = editorState => {
                this.setState({editorState});
            };
            this.toggleBlockType = this.toggleBlockType.bind(this);
        }
        toggleBlockType(blockType) {
            this.onChange(
                RichUtils.toggleBlockType(
                    this.state.editorState,
                    blockType
                )
            );
        }
        render() {
            return (
                
    ) } } function getBlockStyle(block) { switch (block.getType()) { case 'blockquote': return 'RichEditor-blockquote'; default: return null; } }

    在css文件中,可以自定义.RichEditor-blockquote的样式。

    .RichEditor-blockquote {
        border-left: 5px solid #eee;
        color: #666;
        font-family: 'Hoefler Text', 'Georgia', serif;
        font-style: italic;
        margin: 16px 0;
        padding: 10px 20px;
    }
    

    在线示例

    我们可以使用editorState.getCurrentContent()获取contentState对象,contentState.getBlockForKey(blockKey)可以获取到blockKey对应的contentBlockcontentBlock.getType()可以获取到当前contentBlock对应的类型。

    自定义组件渲染

    除了上定义的contentBlock类型对应的标签之外,Draft.js还提供了自定义组件渲染功能。实现起来非常简单。自定义一个渲染函数,之后把这个函数传个Editor组件blockRendererFn这个prop就行。

    先自定义渲染函数和组件:

    
    const ImgComponent = (props) => {
        return (
            图片
        )
    }
    
    function myBlockRenderer(contentBlock) {
        
        // 获取到contentBlock的文本信息,可以用contentBlock提供的其它方法获取到想要使用的信息
        const text = contentBlock.getText();
    
        // 我们假定这里图片的文本格式为![](htt://....)
        let matches = text.match(/\!\[(.*)\]\((http.*)\)/);
        if (matches) {
            return {
                component: ImgComponent,  // 指定组件
                editable: false,  // 这里设置自定义的组件可不可以编辑,因为是图片,这里选择不可编辑
                // 这里的props在自定义的组件中需要用this.props.blockProps来访问
                props: {
                    src: matches[2],,
                }
            };
        }
    }
    

    之后只要在Editor上加blockRendererFn:

    
    

    在线示例

    示例代码

    Decorator

    除了使用自定义样式外,我们也可以使用自定义组件来渲染特定的内容。为了支持自定义富文本的灵活性,Draft.js提供了一个decrator系统。Decorator基于扫描给定ContentBlock的内容,找到满足与定义的策略匹配的文本范围,然后使用指定的React组件呈现它们。

    可以使用CompositeDecorator类定义所需的装饰器行为。 此类允许你提供多个DraftDecorator对象,并依次搜索每个策略的文本块。

    Decrator 保存在EditorState记录中。当新建一个EditorState对象时,例如使用EditorState.createEmpty(),可以提供一个decorator。

    新建一个Decorator类似这个样子:

    const HandleSpan = (props) => {
        return (
            
                {props.children}
            
        );
    };
    const HashtagSpan = (props) => {
        return (
            
                {props.children}
            
        );
    };
    const compositeDecorator = new CompositeDecorator([
        {
            strategy: function (contentBlock, callback, contentState) {
                // 这里可以根据contentBlock和contentState做一些判断,根据判断给出要使用对应组件渲染的位置执行callback
                // callback函数接收2个参数,start组件包裹的起始位置,end组件的结束位置
                // callback(start, end);
            },
            component: HandleSpan
        },
        {
            strategy: function (contentBlock, callback, contentState) {},
            component: HashtagSpan
        }
    ]);
    
    export default  class extends React.Component {
        constructor() {
            super();
            this.state = {
                editorState: EditorState.createEmpty(compositeDecorator),
            };
            // ...
        }
        render() {
            return (
                
    ); } }

    在线示例

    示例源码

    Entity

    对于一些特殊情况,我们需要在文本上附加一些额外的信息,比如超链接中,超链接的文字和对应的链接地址是不一样的,我们就需要对超链接文字附加上链接地址信息。这个时候就需要entity来实现了。

    contentState.createEntity可以新建entity。

    const contentState = editorState.getCurrentContent();
    const contentStateWithEntity = contentState.createEntity(
        'LINK',
        'MUTABLE',
        {url: 'http://www.zombo.com'}
    );
    
    // 要把entity和内容对应上,我们需要知道entity的key值
    const entityKey = contentStateWithEntity.getLastCreatedEntityKey();
    

    contentState.createEntity接收三个参数:

    • type: 指示了entity的类型,例如:'LINK'、'MENTION'、'PHOTO'等。
    • mutability: 可变性。不要将不可变性和immutable.js混淆,此属性表示在编辑器中编辑文本范围时,使用此Enity对象对应的一系列文本的行为。 这在下面更详细地讨论。
    • data: 一个包含了一些对于当前enity可选数据的对象。例如,'LINK' enity包含了该链接的href值的数据对象。

    mutability

    IMMUTABLE

    如果不移除文本上的entity,文本不能被改变。当文本改变时,entity自动移除,当删除字符的时候整个entity连同上边携带的文字也会被删除。

    MUTABLE

    如果设置Mutability为MUTABLE,被加了enity的文字可以随意编辑。比如超链接的文字是可以随意编辑的,一般超链接的文字和链接的指向是没有关系的。

    SEGMENTED

    设置为「SEGMENTED」的entity和设置为「IMMUTABLE」很类似,但是删除行为有些不同,比如一段带有entity的英文文本(因为英文单词间都有空格),按删除键,只会删除当前光标所在的单词,不会把当前entity对应的文本都删除掉。

    这里可以直观体会三种entity的区别。

    我们使用RichUtils.toggleLink来管理entity和内容。

    toggleLink(
        editorState: EditorState,
        targetSelection: SelectionState,
        entityKey: string
    ): EditorState
    

    下面�通过一个�能够编辑超链接的编辑器来了解entity的使用。

    首先我们新建一个�Link组件来渲染超链接。

    const Link = (props) => {
        // 这里通过contentState来获取entity�,之后通过getData获取entity中包含的数据
        const {url} = props.contentState.getEntity(props.entityKey).getData();
        return (
            
                {props.children}
            
        );
    };
    

    新建decorator,这里面contentBlock.findEntityRanges接收2个函数作为参数,如果第一个参数的函数执行时�返回true,就会执行第二个参数函数,同时会�将匹配的�字符的起始位置和结束位置传递给第二个参数。

    const decorator = new CompositeDecorator([
        {
            strategy: function (contentBlock, callback, contentState) {
    
                // 这个方法接收2个函数作为参数,如果第一个参数的函数执行时�返回true,就会执行第二个参数函数,同时会�将匹配的�字符的起始位置和结束位置传递给第二个参数。
                contentBlock.findEntityRanges(
                    (character) => {
                        const entityKey = character.getEntity();
                        return (
                            entityKey !== null &&
                            contentState.getEntity(entityKey).getType() === 'LINK'
                        );
                    },
                    function () {
                        callback(...arguments);
                    }
                    
                );
            },
            component: Link
        }
    ]);
    

    下面来新建编辑器组件

    class LinkEditor extends Component {
        constructor(props) {
            super(props);
    
            this.state = {
                // 新建editor�时加入�上边建的decorator
                editorState: EditorState.createEmpty(decorator),
                url: ''
            };
            this.onChange = editorState => {
                this.setState({editorState});
            };
            this.addLink = this.addLink.bind(this);
            this.urlChange = this.urlChange.bind(this);
        }
    
        /**
         * 添加链接
         */
        addLink() {
            const {editorState, url} = this.state;
            // 获取contentState
            const contentState = editorState.getCurrentContent();
            // 在contentState上新建entity
            const contentStateWithEntity = contentState.createEntity(
                'LINK',
                'MUTABLE',
                {url}
            );
            // 获取到刚才新建的entity
            const entityKey = contentStateWithEntity.getLastCreatedEntityKey();
            // 把带有entity的contentState设置到editorState上
            const newEditorState = EditorState.set(editorState, { currentContent: contentStateWithEntity });
            // 把entity和选中的内容对应
            this.setState({
                editorState: RichUtils.toggleLink(
                    newEditorState,
                    newEditorState.getSelection(),
                    entityKey
                ),
                url: '',
                }, () => {
                setTimeout(() => this.refs.editor.focus(), 0);
            });
        }
    
        /**
         * 链接改变
         *
         * @param {Object} event 事件
         */
        urlChange(event) {
            const target = event.target;
            this.setState({
                url: target.value
            });
        }
    
        render() {
            return (
                
    链接编辑器
    ) } }

    在线示例

    示例代码

    总结

    draft.js提供了很多丰富的功能,还有自定义快捷键等功能本文没有提及。在使用过程中,感觉主要难点在decorator和entity的理解上。希望本文能够对你了解draft.js有所帮助。

    开发了一些简单的demo供参考:https://marxjiao.com/draft-demo/

    demo源码:https://github.com/MarxJiao/draft-demo

    相关链接

    Draft.js官方文档

    Draft.js 在知乎的实践

    你可能感兴趣的:(使用draft.js开发富文本编辑器)