Draft.js是Facebook開源的開發(fā)React富文本編輯器開發(fā)框架。和其它富文本編輯器不同厨剪,draft.js并不是一個開箱即用的富文本編輯器估灿,而是一個提供了一系列開發(fā)富文本編輯器的工具棉磨。本文通過開發(fā)一些簡單的富文本編輯器叁温,來介紹draft.js提供的各種能力再悼。
draft.js解決的問題
- 統(tǒng)一html標(biāo)簽contenteditable="true",在編輯內(nèi)容時膝但,不同瀏覽器下產(chǎn)生不同dom結(jié)構(gòu)的問題;
- 給html的改變賦予onChange時的監(jiān)聽能力;
- 使用不可變的數(shù)據(jù)結(jié)構(gòu)冲九,每次修改都生成新的狀態(tài),保證里歷史記錄的可回溯;
- 可以結(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
提供customStyleMap
prop來自定義?行內(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
Editor
的blockStyleFn
prop可以方便自定義樣式舍杜。
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();
// 我們假定這里圖片的文本格式為
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