使用draft.js開發(fā)富文本編輯器

Draft.js是Facebook開源的開發(fā)React富文本編輯器開發(fā)框架。和其它富文本編輯器不同厨剪,draft.js并不是一個開箱即用的富文本編輯器估灿,而是一個提供了一系列開發(fā)富文本編輯器的工具棉磨。本文通過開發(fā)一些簡單的富文本編輯器叁温,來介紹draft.js提供的各種能力再悼。

draft.js解決的問題

  1. 統(tǒng)一html標(biāo)簽contenteditable="true",在編輯內(nèi)容時膝但,不同瀏覽器下產(chǎn)生不同dom結(jié)構(gòu)的問題;
  2. 給html的改變賦予onChange時的監(jiān)聽能力;
  3. 使用不可變的數(shù)據(jù)結(jié)構(gòu)冲九,每次修改都生成新的狀態(tài),保證里歷史記錄的可回溯;
  4. 可以結(jié)構(gòu)化存儲富文本內(nèi)容跟束,而不需要保存html片段莺奸。

不可變的數(shù)據(jù)結(jié)構(gòu)

這里要介紹下不可變的數(shù)據(jù),draft.js使用immutable.js提供的數(shù)據(jù)結(jié)構(gòu)冀宴。draft.js中所有的數(shù)據(jù)都是不可變的灭贷。每次修改都會新建數(shù)據(jù),并且內(nèi)存中會保存原來的狀態(tài)花鹅,方便回到上一步氧腰,這里很符合react的單向數(shù)據(jù)流的設(shè)計思路。

Editor組件

Draft.js提供了一個Editor組件刨肃。Editor組件是內(nèi)容呈現(xiàn)的載體古拴。我們先看一個基礎(chǔ)編輯器。在線示例

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">
                基礎(chǔ)編輯器
                <div className="editor">
                    <Editor
                        editorState={this.state.editorState}
                        onChange={this.onChange}/>
                </div>
            </div>
        )
    }
}

這里的Editor組件接收2個props:editorState是整個編輯器的狀態(tài)真友,類似?文本框的value黄痪;onChange監(jiān)聽狀態(tài)改變并把新的狀態(tài)傳給對應(yīng)的函數(shù)。?初始化的時候我們使用了EditorState提供的createEmpty方法盔然,?根據(jù)語意我們很容易知道這個是生成一個沒有內(nèi)容的EditorState對象桅打。

富文本樣式

提到富文本編輯器,當(dāng)然避免不了各種豐富的樣式愈案。富文本樣式包含兩種挺尾,行內(nèi)樣式和塊級樣式。行內(nèi)樣式是在段落中?某些字段上添加的樣式站绪,如??粗體遭铺、斜體、文字加下劃線等等恢准。塊級樣式是在整個段落上加的樣式魂挂,如段落縮進、有序列表馁筐、無需列表等涂召。Draft.js提供了?RichUtils模塊?來?處理?富文本樣式。

行內(nèi)樣式

?RichUtils.toggleInlineStyle方法可以切換光標(biāo)?所在位置的行內(nèi)樣式敏沉。該函數(shù)接收2個參數(shù)果正。第一個是editorState炎码,在editorState中已經(jīng)包含了光標(biāo)選中內(nèi)容的信息。第二個參數(shù)是樣式名舱卡,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來自定義?行內(nèi)樣式。


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

class MyEditor extends React.Component {
    // ...
    render() {
        return (
            <div className="basic">
                <button onClick={() => {this.toggleInlineStyle('BOLD')}}>Bold</button>
                <!-- 點擊之后會在styleMap里?查找「RED」對應(yīng)的樣式 -->
                <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矫钓。下面是一些標(biāo)簽對應(yīng)的樣式名

html標(biāo)簽 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對應(yīng)的類型。

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對應(yīng)的contentBlock概龄。contentBlock.getType()可以獲取到當(dāng)前contentBlock對應(yīng)的類型。

自定義組件渲染

除了上定義的contentBlock類型對應(yīng)的標(biāo)簽之外饲握,Draft.js還提供了自定義組件渲染功能私杜。實現(xiàn)起來非常簡單。自定義一個渲染函數(shù)救欧,之后把這個函數(shù)傳個Editor組件blockRendererFn這個prop就行衰粹。

先自定義渲染函數(shù)和組件:


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,  // 這里設(shè)置自定義的組件可不可以編輯笆怠,因為是圖片铝耻,這里選擇不可編輯
            // 這里的props在自定義的組件中需要用this.props.blockProps來訪問
            props: {
                src: matches[2],,
            }
        };
    }
}

之后只要在Editor上加blockRendererFn:

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

在線示例

示例代碼

Decorator

除了使用自定義樣式外,我們也可以使用自定義組件來渲染特定的內(nèi)容蹬刷。為了支持自定義富文本的靈活性瓢捉,Draft.js提供了一個decrator系統(tǒng)。Decorator基于掃描給定ContentBlock的內(nèi)容办成,找到滿足與定義的策略匹配的文本范圍泡态,然后使用指定的React組件呈現(xiàn)它們。

可以使用CompositeDecorator類定義所需的裝飾器行為迂卢。 此類允許你提供多個DraftDecorator對象某弦,并依次搜索每個策略的文本塊。

Decrator 保存在EditorState記錄中冷守。當(dāng)新建一個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) {
            // 這里可以根據(jù)contentBlock和contentState做一些判斷,根據(jù)判斷給出要使用對應(yīng)組件渲染的位置執(zhí)行callback
            // callback函數(shù)接收2個參數(shù)馆截,start組件包裹的起始位置充活,end組件的結(jié)束位置
            // 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

對于一些特殊情況蜂莉,我們需要在文本上附加一些額外的信息,比如超鏈接中混卵,超鏈接的文字和對應(yīng)的鏈接地址是不一樣的映穗,我們就需要對超鏈接文字附加上鏈接地址信息。這個時候就需要entity來實現(xiàn)了幕随。

contentState.createEntity可以新建entity蚁滋。

const contentState = editorState.getCurrentContent();
const contentStateWithEntity = contentState.createEntity(
    'LINK',
    'MUTABLE',
    {url: 'http://www.zombo.com'}
);

// 要把entity和內(nèi)容對應(yīng)上,我們需要知道entity的key值
const entityKey = contentStateWithEntity.getLastCreatedEntityKey();

contentState.createEntity接收三個參數(shù):

  • type: 指示了entity的類型赘淮,例如:'LINK'辕录、'MENTION'、'PHOTO'等梢卸。
  • mutability: 可變性走诞。不要將不可變性和immutable.js混淆,此屬性表示在編輯器中編輯文本范圍時蛤高,使用此Enity對象對應(yīng)的一系列文本的行為蚣旱。 這在下面更詳細地討論。
  • data: 一個包含了一些對于當(dāng)前enity可選數(shù)據(jù)的對象戴陡。例如塞绿,'LINK' enity包含了該鏈接的href值的數(shù)據(jù)對象。

mutability

IMMUTABLE

如果不移除文本上的entity猜欺,文本不能被改變位隶。當(dāng)文本改變時,entity自動移除开皿,當(dāng)刪除字符的時候整個entity連同上邊攜帶的文字也會被刪除涧黄。

MUTABLE

如果設(shè)置Mutability為MUTABLE,被加了enity的文字可以隨意編輯赋荆。比如超鏈接的文字是可以隨意編輯的笋妥,一般超鏈接的文字和鏈接的指向是沒有關(guān)系的。

SEGMENTED

設(shè)置為「SEGMENTED」的entity和設(shè)置為「IMMUTABLE」很類似窄潭,但是刪除行為有些不同春宣,比如一段帶有entity的英文文本(因為英文單詞間都有空格),按刪除鍵嫉你,只會刪除當(dāng)前光標(biāo)所在的單詞月帝,不會把當(dāng)前entity對應(yīng)的文本都刪除掉。

這里可以直觀體會三種entity的區(qū)別幽污。

我們使用RichUtils.toggleLink來管理entity和內(nèi)容嚷辅。

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

下面?通過一個?能夠編輯超鏈接的編輯器來了解entity的使用。

首先我們新建一個?Link組件來渲染超鏈接距误。

const Link = (props) => {
    // 這里通過contentState來獲取entity?簸搞,之后通過getData獲取entity中包含的數(shù)據(jù)
    const {url} = props.contentState.getEntity(props.entityKey).getData();
    return (
        <a href={url}>
            {props.children}
        </a>
    );
};

新建decorator扁位,這里面contentBlock.findEntityRanges接收2個函數(shù)作為參數(shù),如果第一個參數(shù)的函數(shù)執(zhí)行時?返回true趁俊,就會執(zhí)行第二個參數(shù)函數(shù)域仇,同時會?將匹配的?字符的起始位置和結(jié)束位置傳遞給第二個參數(shù)。

const decorator = new CompositeDecorator([
    {
        strategy: function (contentBlock, callback, contentState) {

            // 這個方法接收2個函數(shù)作為參數(shù)寺擂,如果第一個參數(shù)的函數(shù)執(zhí)行時?返回true暇务,就會執(zhí)行第二個參數(shù)函數(shù),同時會?將匹配的?字符的起始位置和結(jié)束位置傳遞給第二個參數(shù)沽讹。
            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設(shè)置到editorState上
        const newEditorState = EditorState.set(editorState, { currentContent: contentStateWithEntity });
        // 把entity和選中的內(nèi)容對應(yīng)
        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>
        )
    }
}

在線示例

示例代碼

總結(jié)

draft.js提供了很多豐富的功能般卑,還有自定義快捷鍵等功能本文沒有提及。在使用過程中爽雄,感覺主要難點在decorator和entity的理解上蝠检。希望本文能夠?qū)δ懔私鈊raft.js有所幫助。

開發(fā)了一些簡單的demo供參考:https://marxjiao.com/draft-demo/

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

相關(guān)鏈接

Draft.js官方文檔

Draft.js 在知乎的實踐

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末挚瘟,一起剝皮案震驚了整個濱河市叹谁,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌乘盖,老刑警劉巖焰檩,帶你破解...
    沈念sama閱讀 221,548評論 6 515
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異订框,居然都是意外死亡析苫,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,497評論 3 399
  • 文/潘曉璐 我一進店門穿扳,熙熙樓的掌柜王于貴愁眉苦臉地迎上來衩侥,“玉大人,你說我怎么就攤上這事矛物∶K溃” “怎么了?”我有些...
    開封第一講書人閱讀 167,990評論 0 360
  • 文/不壞的土叔 我叫張陵履羞,是天一觀的道長峦萎。 經(jīng)常有香客問我,道長忆首,這世上最難降的妖魔是什么爱榔? 我笑而不...
    開封第一講書人閱讀 59,618評論 1 296
  • 正文 為了忘掉前任,我火速辦了婚禮糙及,結(jié)果婚禮上详幽,老公的妹妹穿的比我還像新娘。我一直安慰自己丁鹉,他們只是感情好妒潭,可當(dāng)我...
    茶點故事閱讀 68,618評論 6 397
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著揣钦,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上赫粥,一...
    開封第一講書人閱讀 52,246評論 1 308
  • 那天梢薪,我揣著相機與錄音,去河邊找鬼宇姚。 笑死匈庭,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的浑劳。 我是一名探鬼主播阱持,決...
    沈念sama閱讀 40,819評論 3 421
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼魔熏!你這毒婦竟也來了衷咽?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,725評論 0 276
  • 序言:老撾萬榮一對情侶失蹤蒜绽,失蹤者是張志新(化名)和其女友劉穎镶骗,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體躲雅,經(jīng)...
    沈念sama閱讀 46,268評論 1 320
  • 正文 獨居荒郊野嶺守林人離奇死亡鼎姊,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 38,356評論 3 340
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了相赁。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片相寇。...
    茶點故事閱讀 40,488評論 1 352
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖噪生,靈堂內(nèi)的尸體忽然破棺而出裆赵,到底是詐尸還是另有隱情,我是刑警寧澤跺嗽,帶...
    沈念sama閱讀 36,181評論 5 350
  • 正文 年R本政府宣布战授,位于F島的核電站,受9級特大地震影響桨嫁,放射性物質(zhì)發(fā)生泄漏植兰。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,862評論 3 333
  • 文/蒙蒙 一璃吧、第九天 我趴在偏房一處隱蔽的房頂上張望楣导。 院中可真熱鬧,春花似錦畜挨、人聲如沸筒繁。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,331評論 0 24
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽毡咏。三九已至驮宴,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間呕缭,已是汗流浹背堵泽。 一陣腳步聲響...
    開封第一講書人閱讀 33,445評論 1 272
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留恢总,地道東北人迎罗。 一個月前我還...
    沈念sama閱讀 48,897評論 3 376
  • 正文 我出身青樓,卻偏偏與公主長得像片仿,于是被迫代替她去往敵國和親纹安。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 45,500評論 2 359

推薦閱讀更多精彩內(nèi)容