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

作者: MarxJiao | 来源:发表于2017-08-17 16:04 被阅读0次

    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 (
                <div className="basic">
                    基础编辑器
                    <div className="editor">
                        <Editor
                            editorState={this.state.editorState}
                            onChange={this.onChange}/>
                    </div>
                </div>
            )
        }
    }
    

    这里的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 (
                <div className="basic">
                    <button onClick={() => {this.toggleInlineStyle('BOLD')}}>Bold</button>
                    <div className="editor">
                        <Editor
                            editorState={this.state.editorState}
                            onChange={this.onChange}/>
                    </div>
                </div>
            )
        }
    }
    

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

    
    // ...
    const styleMap = {
        'RED': {
            color: 'red'
        }
    }
    
    class MyEditor extends React.Component {
        // ...
        render() {
            return (
                <div className="basic">
                    <button onClick={() => {this.toggleInlineStyle('BOLD')}}>Bold</button>
                    <!-- 点击之后会在styleMap里�查找「RED」对应的样式 -->
                    <button onClick={() => {this.toggleInlineStyle('RED')}}>Red</button>
                    <div className="editor">
                        <Editor
                            customStyleMap={styleMap}
                            editorState={this.state.editorState}
                            onChange={this.onChange}/>
                    </div>
                </div>
            )
        }
    }
    

    ��在线示例

    块级样式

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

    html标签 block类型
    <h1/> header-one
    <h2/> header-two
    <h3/> header-three
    <h4/> header-four
    <h5/> header-five
    <h6/> header-six
    <blockquote/> blockquote
    <pre/> code-block
    <figure/> atomic
    <li/> unordered-list-item,ordered-list-item
    <div/> 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 (
                <div className="basic">
                    <button onClick={() => {this.toggleBlockType('header-one')}}>H1</button>
                    <button onClick={() => {this.toggleBlockType('blockquote')}}>blockquote</button>
                    <div className="editor">
                        <Editor
                            blockStyleFn={getBlockStyle}
                            editorState={this.state.editorState}
                            onChange={this.onChange}/>
                    </div>
                </div>
            )
        }
    }
    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 (
            <img
                style={{height: '300px', width: 'auto'}}
                src={props.blockProps.src}
                alt="图片"/>
        )
    }
    
    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:

    <Editor
        editorState={this.state.editorState}
        onChange={this.onChange}
        blockRendererFn={myBlockRenderer}/>
    

    在线示例

    示例代码

    Decorator

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

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

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

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

    const HandleSpan = (props) => {
        return (
            <span
                style={styles.handle}
                data-offset-key={props.offsetKey}
                >
                {props.children}
            </span>
        );
    };
    const HashtagSpan = (props) => {
        return (
            <span
                style={styles.hashtag}
                data-offset-key={props.offsetKey}
                >
                {props.children}
            </span>
        );
    };
    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 (
                <div style={styles.root}>
                    <div style={styles.editor} onClick={this.focus}>
                        <Editor
                            editorState={this.state.editorState}
                            onChange={this.onChange}
                        />
                    </div>
                </div>
            );
        }
    }
    

    在线示例

    示例源码

    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 (
            <a href={url}>
                {props.children}
            </a>
        );
    };
    

    新建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 (
                <div>
                    链接编辑器
                    <div className="tools">
                        <Input value={this.state.url} onChange={this.urlChange}></Input>
                        <Button className="addlink" onClick={this.addLink}>addLink</Button>
                    </div>
                    <div className="editor">
                        <Editor
                            editorState={this.state.editorState}
                            onChange={this.onChange}
                            ref="editor"/>
                    </div>
                </div>
            )
        }
    }
    
    

    在线示例

    示例代码

    总结

    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开发富文本编辑器

        本文链接:https://www.haomeiwen.com/subject/qkhwrxtx.html