在前段時間的工做中,我遇到了一個在桌面端和移動端進行圖文混排編輯的需求。雖然若是隻須要編輯純文本和圖片,不必定要使用富文本編輯器來實現。可是爲了之後方便擴展,好比文本會有樣式要求,我仍是用 Draft.js 實現了一個功能較基礎的富文本編輯器。react
我將代碼開源在了這個項目 draft-editor 中,也能夠在這裏在線預覽。本文中我將介紹一下一些關於 Draft.js 的基礎知識,並由此擴展到如何在 Draft.js 編輯器中插入圖片功能的實現。git
Draft.js 是 Facebook 推出的用於 React 的富文本編輯器框架,初始化一個最基本的 Draft.js 的代碼以下:github
import React from ‘react’; import ReactDOM from ‘react-dom’; import {Editor, EditorState} from ‘draft-js’; class MyEditor extends React.Component { constructor(props) { super(props); this.state = {editorState: EditorState.createEmpty()}; this.onChange = (editorState) => this.setState({editorState}); } render() { return ( <Editor editorState={this.state.editorState} onChange={this.onChange} /> ); } }
能夠在這裏查看。這裏給 Editor
傳入一個 editorState
屬性,並綁定一個 onChange
事件,當發生編輯操做時,返回一個新的 editorState
。這樣咱們就獲得了一個能夠進行基本的文本操做的編輯器了。npm
在說明什麼是 EditorState
及 Draft.js 對於數據的存儲方式以前,須要簡略介紹一下 Immutables.js。數組
Draft.js 是利用 Immutable.js來保存數據的,正如其名,這是一種不可變的數據結構。對於一個 Immutable 的對象,你沒法修改它自己,若想修改其值,只會返回一個新的修改後的對象。將這一點應用在編輯器上,用戶的每一次修改都會生成一個最新的狀態快照,就很容易實現撤銷功能了。數據結構
在 Draft.js 的使用過程當中,可能會遇到下面這些數據結構。app
Map
相似於 js 中的對象,用 .set()
和 .get()
方法進行寫和讀。框架
const Immutable = require(‘immutable’); const framework = Immutable.Map({ name: 'React', age: 6 }); const newFramework = client.set('name', 'Vue'); console.log(framework.get('name'));
OrderedMap
混合了 object
和 array
的特色。經過使用 orderedMap.get(‘key’)
和orderedMap.set(‘key’, newValue)
這兩個方法,能夠將它當成一個普通的 object
來使用。但和 Map
的不一樣點在於其中的 key 是按照被加入時的順序排序的。dom
Record
也相似於 Map
,但有一些不一樣之處。編輯器
record
一旦被初始化,就不能再添加新的 key 了record
實例添加默認值還有一點,immutable 的對象,提供了 toJS()
方法,可將其轉成普通的 js 對象,這一方法在想查看其內部內容時很是有用。
Immutable.js 參考文章: Immutable Data with Immutable.js | Jscrambler Blog
在建立基本的編輯器的時候,咱們用到了 EditorState
。 EditorState
是編輯器最頂層的狀態對象,它是一個 Immutable Record 對象,保存了編輯器中所有的狀態信息,包括文本狀態、選中狀態等。
調用 editorState.toJS()
可將 immutable record 轉換成一個普通的 object,打印出來以下:
簡單地來看下其中的部份內容:
currentContent
是一個 ContentState
對象,存放的是當前編輯器中的內容selection
中是當前選中的狀態redoStack
和 undoStack
就是撤銷/重作棧,它是一個數組,存放的是 ContentState
類型的編輯器狀態decorator
會尋找特定的模式,並用特定的組件渲染出來既然編輯器中的內容是存儲在一個 ContentState
對象中,那麼這個 ContentState
又是什麼?
ContentState
也是一個 Immutable Record 對象,其中保存了編輯器裏的所有內容和渲染先後的兩個選中狀態。能夠經過 EditorState.getCurrentContent()
來獲取當前的 ContentState
,一樣調用 .toJS()
後將它打印出來看下:
blockMap
和 entityMap
裏放置的就是編輯器中的 block
和 entity
,它們是構建 Draft 編輯器的磚瓦。
一個 ContentBlock
表示一個編輯器內容中的一個獨立的 block,即視覺上獨立的一塊。
如下圖的編輯器做爲一個例子,圖中的四個紅框標出的部分都是 block。在平時閱讀文章時,內容是以段落爲單位的,在編輯器中每一個段落就是一個 block,如第一個和最後一個紅框中的文字內容。第二個紅框中是一張圖片,它也是一個 block,但顯示方式不一樣於普通的 block,爲了自定義它的顯示方式還須要額外作一些工做,後面會加以詳細說明。
還有一點須要稍做說明,第三個紅框中雖然是空白,但它也是一個 block,只不過其中的文本爲空而已。
此時,輸出一下 convertToRaw(currentContent)
,看看其中的內容。注意這裏的輸出結構與上面的 currentContent.toJS()
略有所區別,這裏只有 blocks
和 entityMap
這兩項。
能夠看到 blocks
這個數組中依次存放了各個 block
的信息,每個 block
都是一個 contentBlock
對象。
每一個 contentBlock
都有以下的幾個屬性值:
key
: 標識出這是哪個 blocktype
: 這是何種類型的 blocktext
: 其中的文字Draft.js 中 block
的 type
有 unstyled,paragraph,header-one,atomic …… 等值,在 Draft.js 的文檔中 atomic
類型對應的是 <figure />
元素,咱們也選取了它來實現插入圖片的功能。
圖中的這些 block 的除了第三個 key = 「1u22q」 的 block 的 type 值是 atomic
外,其他的值都是 「unstyled」
。再仔細看下這個 atomic
類型的 block:
除了 key
,text
,type
等值以外,在 entityRanges
這一項中保存它保存了使用到的 entity
的信息:offset 和 length 肯定了 entity
在 block 中的範圍,而 key 則能讓咱們去取出對應的 entity
。
回到上面的打印出的 contentState
的內容,除了 blocks
數組外還有一個 entityMap
對象。它是以 entity
的 key
做爲鍵值的對象,裏面保存了圖片、連接等種類的 entity
信息,從中就可得到 blocks
所須要的 entity
。
entityMap: { 0: { type: "image", mutability: "IMUTABLE", data: {} } }
以上介紹了 Draft.js 是如何對編輯器中的數據進行存儲的,接下來會從代碼實現的角度來講明插入圖片是如何實現的。
插入圖片有着這樣的流程:首先爲圖片建立一個 entity
,而後建立一個帶有這個 entity
的新 EditorState
,而後更新便可。如下是關鍵部分的代碼:
import { AtomicBlockUtils } from 'draft.js'; // ... const editorState = this.state.editorState; const contentState = editorState.getCurrentContent(); // 使用 `contentState.createEntity` 建立一個 `entity`,指定其 `type` 爲 `image` const contentStateWithEntity = contentState.createEntity( ‘image’, ‘IMMUTABLE’, { src } ); // 獲取新建立的 `entity` 的 key const entityKey = contentStateWithEntity.getLastCreatedEntityKey(); // 用 `EditorState.set()` 來創建一個帶有這個 `entity` 的新的 EditorState const newEditorState = EditorState.set( editorState, { currentContent: contentStateWithEntity }, ‘create-entity’ ); // 利用`AtomicBlockUtils.insertAtomicBlock` 來插入一個新的 `block` this.setState({ editorState: AtomicBlockUtils.insertAtomicBlock(newEditorState, entityKey, ‘ ‘) },
上面咱們已經見到了,一張圖片是做爲一個 atomic
類型的 block 插入的。Draft.js 提供了blockRendererFn
讓咱們能夠自定義 ContentBlock
的渲染方式,給它傳入一個函數後,由該函數來判斷這個 block 的 type
是什麼,而後決定如何渲染。
如下的這段代碼來自 Draft.js 的官方文檔,展現瞭如何處理一個 type 爲 atomic
的 ContentBlock
。
function myBlockRenderer(contentBlock) { const type = contentBlock.getType(); if (type === 'atomic') { return { component: MediaComponent, editable: false, props: { foo: 'bar', }, }; } } // Then... import {Editor} from 'draft-js'; class EditorWithMedia extends React.Component { ... render() { return <Editor ... blockRendererFn={myBlockRenderer} />; } }
能夠看到這裏傳遞了一個 props
:
component: MediaComponent, props: { foo: 'bar', },
結果等同於 <MediaComponent foo='bar' />
,能夠利用這裏的 props
傳入所須要的其餘數據。
這裏咱們就能夠定義一個本身的 MediaComponent
來決定展示方式。由於不論是圖片仍是視頻等其它的媒體類型,它們的 type
都是 atomic
。在 MediaComponent
裏就須要經過 entity
的 type
來肯定其種類。
const entity = props.contentState.getEntity(props.block.getEntityAt(0)); const { src } = entity.getData(); // 取出圖片的地址 const type = entity.getType(); // 判斷 entity 的 type 的
當 entity
的 type
是咱們自定義的 image
時就能夠返回 <Image />
組件了。
<Image src={src} /> // 自定義的圖片組件 <Image />
既然已經插入了圖片,那麼如何刪除它呢?固然咱們能夠按鍵盤上的 Backspace 鍵來刪除。也能夠在圖片的右上角加入一個 「X」 的圖標,點擊後刪除該圖片,實現方式以下。
deleteImage = (block) => { const editorState = this.state.editorState; const contentState = editorState.getCurrentContent(); const key = block.getKey(); const selection = editorState.getSelection(); const selectionOfAtomicBlock = selection.merge({ anchorKey: key, anchorOffset: 0, focusKey: key, focusOffset: block.getLength(), }); // 重寫 entity 數據,將其從 block 中移除,防止這個 entity 還被其它的 block 引用 const contentStateWithoutEntity = Modifier.applyEntity(contentState, selectionOfAtomicBlock, null); const editorStateWithoutEntity = EditorState.push(editorState, contentStateWithoutEntity, ‘apply-entity’); // 移除 block const contentStateWithoutBlock = Modifier.removeRange(contentStateWithoutEntity, selectionOfAtomicBlock, ‘backward’); const newEditorState = EditorState.push(editorStateWithoutEntity, contentStateWithoutBlock, ‘remove-range’,); this.onChange(newEditorState); }
至此,對圖片的相關操做就完成了。
在本文中,介紹了 Draft.js 的基本功能,它是如何進行數據的存儲的,及 EditorState
、ContentState
、ContentBlock
、Entity
等對象間的關係。並以此爲基礎說明了如何在編輯器中對圖片進行操做。
固然關於 Draft.js 還有不少內容沒有在本文中說起,如修改行內文本的樣式,利用 decorators
來插入與渲染連接等等。這些就須要讀者探索下 Draft.js 的官方文檔和其餘人的分享並親自嘗試下了。
本文所基於的編輯器項目:draft-editorHow Draft.js Represents Rich Text Data
Building a Rich Text Editor with React and Draft.js, Part 2.4: Embedding Images
Draft.js 在知乎的實踐