淺析富文本編輯器框架Slate.js
- 2021 年 7 月 8 日
- 筆記
- javascript, React
淺析富文本編輯器框架Slate.js
本文不是關於Slate.js使用入門的文章,如果還不了解該框架,建議先閱讀下官方的文檔:Slate官網文檔
關於Slate的一些特性
- 不同於其他編輯器類的庫,Slate並不提供譬如粗體、斜體、字體色等開箱即用的功能
- Slate只是提供了一套自己定義的核心數據模型,以此一些操作數據和選區相關的API
- 視圖層的渲染和行為完全由開發者基於React定製
從頂層設計上看,Slate的架構是典型的MVC模型,由自身定義數據模型(Model),暴露操作數據的方法(Controller),然後交由用戶使用該數據在React中做渲染(View)
雖然在實現簡單的編輯器應用時這種方式顯得有些繁冗,但在遇到需要對業務做較定製化的功能,如內嵌複雜表單、流程圖等時,就能展現出極大的靈活性。而這類需求在使用其他編輯器的庫時,經常是不可行的或者成本很高(往往要在源碼層面進行改造)
Slate的數據模型
Slate.js數據模型的設計非常的「類DOM」,對於擁有Web基礎的開發者降低了心智負擔。下面從節點(Node)和選區(Selection)的設計上說明。
type Node = Editor | Element | Text
interface Editor {
children: Node[]
}
interface Element {
children: Node[]
[key: string]: unknown
}
interface Text {
text: string,
[key: string]: unknown
}
Node
作為最抽象的節點類型,包括以下三種類型:
-
Editor
編輯器的根節點類型 -
Element
具有children屬性,可以作為其他Node的父節點;由傳入的renderElement函數做自定義渲染 -
Text
包含文本信息;由renderLeaf函數做自定義渲染;在添加mark時,將文本打散成不同的Leaf(這個行為是由Slate執行的,下面的例子會講)
除了接口中定義的屬性,也可以在節點中添加任意業務相關的屬性值(如下面的例子)。
一個基礎的使用示例如下:
const RichText = (props: any) => {
// 創建Editor
const editor = createEditor()
// 初始值
const [value, setValue] = useState([{
type: 'paragraph',
children: [{ text: "" }]
}])
// 自定義Element渲染
const renderElement = (props) => {
const { attributes, children, element } = props
switch (element.type) { // 根據Element中的type屬性判斷節點類型
case "heading-one":
return <h1 {...attributes}>{children}</h1>;
case "heading-two":
return <h2 {...attributes}>{children}</h2>;
case "paragraph":
return <p {...attributes}>{children}</p>;
}
}
// 自定義Leaf渲染
const renderLeaf = (props) => {
const { attributes, children, leaf } = props
// 根據Text中的自定義屬性判斷樣式類型
if (leaf['background-color']) {
children = <span style={{ backgroundColor: leaf['background-color']}}>
{children}
</span>
}
if (leaf['font-color']) {
children = <span style={{ color: leaf['font-color']}}>
{children}
</span>
}
return <span {...attributes}>{children}</span>;
}
return <Slate editor={editor} value={value} onChange={(value) => setValue(value)}>
<Editable
renderElement={renderElement}
renderLeaf={renderLeaf}/>
</Slate>
}
假如我們在該編輯器中輸入了兩行文字,並選取一段文本添加顏色樣式:
則上圖的文本內容所對應的數據結構:
[
{
type: "paragraph",
children: [
{
text: "著名武術泰斗馬保國。"
}
]
},
{
type: "paragraph",
children: [
{
text: "著名"
},
{
text: "籃球運動員",
'font-size': "rgba(139, 87, 42, 1)"
},
{
text: "蔡徐坤"
}
]
}
]
該數組中最頂層的對象映射了Element元素(由renderElement渲染的),在其中的type字段中設為paragraph標誌為默認的塊級元素(當然也可以設為其他任何值,Slate.js並不關心type字段的含義)。下一層的葉子節點對象,包含了text字段,表示文本內容;也可能含有自定義的一些marks,例如上面的’font-size’,用來在renderLeaf中依據marks來實現自定義的樣式渲染。
接下來再選取一段文字賦予一個背景色:
添加背景色的選區是在上面帶有font-size的Text節點中的,因此會被打散成三個Text,變為如下形式:
[
{
type: "paragraph",
children: [
{
text: "著名武術泰斗馬保國。"
}
]
},
{
type: "paragraph",
children: [
{
text: "著名"
},
{
text: "籃球",
'font-size': "rgba(139, 87, 42, 1)"
},
{
text: "運動",
'font-size': "rgba(139, 87, 42, 1)",
'background-color': "rgba(80, 227, 194, 1)"
},
{
text: "員",
'font-size': "rgba(139, 87, 42, 1)"
},
{
text: "蔡徐坤"
}
]
}
]
光標和選區
光標的定位由一個 Path
和 offset
確定,Path
代表節點在文檔中的位置,offset
則代表在節點中的偏移量:
declare type Path = number[]
interface Point {
path: Path,
offset: number
}
Paht
是一個number類型數組,包含的數值代表着從文檔數據模型的根部到光標所在Text節點的路徑;offset
表示光標在Text節點上的偏移量。
如圖中框中的節點所對應的
Path
就是[1, 0]
選取的接口定義 Range
與原生selection的屬性非常相似:
interface Range {
anchor: Point,
focus: Point
}
錨點anchor代表選區的起始點,焦點focus代表選區的結束點;兩者都為上述的Point類型。
插件機制
Slate提供了插件的機制允許我們覆蓋編輯器原有的行為。除了直接使用slate-react和slate-history這些官方的插件,也可以自定義插件來對Slate編輯器進行拓展,而且實現方式非常簡易:提供一個函數,該函數接收一個編輯器的實例editor對象,在其中重寫實例對象上的方法,並返回editor實例。
下面是個例子,加入在實現業務時有這麼一個場景,需要在文本域中插入一些自定義的控件如按鈕、下拉框等,並且都不可被編輯;而默認情況下在文本域中所有的dom元素都是contenteditable=true
的狀態,是能夠被光標聚焦和編輯的。為了改變這種行為,可以自行實現一個插件:
import { createEditor } from "slate"
import { withReact } from "slate-react"
import { withHistory } from "slate-history"
const myCustomeEditor = (editor) => {
const { isVoid } = editor // editor原有的isVoid方法, 用以判斷節點是否可編輯
editor.isVoid = (element) => { // 根據自定義的type字段將元素置為 不可編輯的
return element.type === 'custome-element' ? true : isVoid(element)
}
return editor
}
// 創建了一個帶有三個插件組合的Slate編輯器
const eidotr = myCustomeEditor(withHistory(withReact(createEditor())))