proseMirror guide 翻譯
自己的整理
plugin / node view / decoration 的差異
plugin 用來改變編輯器的文檔,可以加一些按鍵綁定、修改編輯器的狀態... node view 為特定節點擴展自定義的結構 decoration 將文檔添加視覺效果
介紹
ProseMirror 提供了能完成文字編輯器的一系列工具與概念。
PM 的主要原則是讓 code 能有完全對於 document 的掌控,能完全知道發生什麼事。document 不是 html,是指一個自訂的資料結構,這個結構可以規範什麼樣的 DOM 可以包含在結構中,所有的更新都經過一個特定的點,讓你可以觀察與回應。
core module 的設計是以模組化與客製化的能力為主要考量。
有 4 個重要的 module,進行任何編輯都需要這些。還有一些額外的插件,但可以忽略,如果你只需要做一些簡單的功能。或者你也可以自己做一個相關插件取代。
重要的 module 包含:
-
prosemirror-model 定義用來描述編輯器內容的文件模型,是 state 的一部分。
-
prosemirror-state 提供描述整個編輯器狀態的資料結構,包括選取範圍與狀態改變的變更機制(transaction system)。
-
prosemirror-view 實現了可以顯示 editor state 的 ui 介面,使用 html editable element,並處理使用者互動。
-
prosemirror-transform 提供可以修改 document 的功能,並且可以被紀錄與重複,這是 state module 的 transaction system 的基礎,也是有這些功能,使協作功能、復原功能可以實現
transactions
當使用者與 view 互動時,會產生 state transaction,代表在更改 state 時,並不是在內部更改 document 的狀態,而是將每個改變都 create 一個 transaction。transaction 就是描述與原 state 的差異,可以被 state apply 成一個新的 state。
預設的上述的機制都發生在幕後。可以寫 plugin 或是 設定自己的 view 來 hook。 例如:dispatchTransaction,是 EditorView 可傳入的參數,dispatchTransaction 是一個 callback,在 transaction 被創建時觸發。
每一次 state 的更新,都透過 EditorView.updateState,每一個編輯更新都由 dispatch transaction 觸發。
plugins
plugin 透過不同的方式來擴充編輯器與編輯器狀態。有些特別簡單,像是 keymap plugin。有些則是涉入較多,像是 history plugin。因為 plugin 在創立 state 時註冊,因為 plugin 可以取得 state transaction。
commands
大部分的編輯動作都是一個 command。 prosemirror command 提供一系列基本的編輯 command,也包含一些基本的操作功能包(baseKeymap),像是 delete, enter 等。
content
整個編輯器的 document state 是存在 EditorState.doc 裡。這是一個唯讀的資料結構,用多層級的 node 表示 document,類似於 DOM 結構。
Documents
Structure
PM document 是一個 class Node。 class Node 有 property content 是 class Fragment 而 Fragment 有 property content 是 [NODE]
Fragment 是代表 子 node 的集合
node 與 fragment 都是 persistent 的特性。這意味著我們不該 mutate 它。
如果是這樣一段 html
<p>This is <strong>strong text with <em>emphasis</em> </strong></p>
在 DOM Tree 裡概念上是這樣
{
type: 'p',
content: [
{
type: 'text',
text: 'This is',
},
{
type: 'strong',
content: [
{
type: 'text',
text: 'strong text with'
},
{
type: 'em',
content: [
{
type: 'text',
text: 'emphasis'
}
]
}
]
}
]
}
但在 ProseMirror 概念是這樣
{
type: 'p',
content: [
{
type: 'text',
text: 'This is',
},
{
type: 'text',
text: 'strong text with',
mark: [{ type: 'strong' }]
},
{
type: 'text',
text: 'emphasis',
mark: [{ type: 'strong' }, { type: 'em' }]
},
]
}
所以實際上,ProseMirror 把 inline 元素 flatten 了,ProseMirror document 實際上是 block 元素的樹狀結構。
這更符合我們傾向於思考和處理此類文本的方式。它允許我們使用字元偏移量而不是樹中的路徑來表示段落中的位置,並且可以更輕鬆地執行拆分或更改內容樣式等操作,而無需執行笨拙的樹操作。
Document transformations
steps
更新 docuement 這個行為,可以分解成多個 step,通常不需要直接用到 step,但知道如何實作是有用的。
steps 的例子:用 replaceStep 取代 document 中的一塊。
step 可以被 apply 進 document,建立一個新的 document
console.log(myDoc.toString()); // p('hello');
// A step that deletes the content between position 3 and 5
let step = new ReplaceStep(3, 5, Slice.empty);
let result = step.apply(mydoc);
console.log(result.doc.toString()); // p('heo')
apply step 是一個相對直觀的行為,意思是,是有可能會報錯的。例如:如果嘗試刪除 opening token(<p> 標籤),可能會導致 token 不平衡。這也是為什麼 apply 會回傳一個 result object。result 會在成功時回傳 doc,失敗時回傳 error message。
通常會習慣使用 Transform 提供的一系列 helper function 處理 step,這樣就不用顧慮太多細節。
Transform
一個編輯動作,可能會產生一個或多個 step。 如果要操作一系列的 step,最方便的方式是使用 Transform。
Transform 這個 class 就是用來建立與追蹤一系列 step 的抽象
let tr = new Transform(myDoc);
tr.delete(5, 7); // delete between position 5 and 7
tr.split(5); // split the parent node at position 5
console.log(tr.doc.toString()); // The modeified document
console.log(tr.steps.length); // 2
transform 大部分的 method 都會回傳自己,也就是可以把方法 chain 起來。
tr.delete(5, 7).split(5)
Mapping
當改變 document,每個 postion index 其實也會改變。例如插入一個字元或是刪除字元,都會導致整個 doc 的 index 與之前不一樣。
通常會需要在文字的變更中,保留位置。ex: selection boundaries。 為了這個需求,proseMirror 的 step 提供 map, 可以透過比較改變前後的 index 變化。
let step = new ReplaceStep(4, 6, Slice.empty); // delete 4-5
let map = step.getMap();
console.log(map.map(8)); // -> 6 (原本是 8 變成 6)
console.log(map.map(2)); // -> 2 (原本是 2 不變)
Tramsform 有 mapping 這個 property 紀錄了一系列 steps 的 maps。
let tr = new Transform(myDoc);
tr.split(10) // split a node, +2 tokens at 10
tr.delete(2, 5) // -3 tokens at 2
console.log(tr.mapping.map(15)) // -> 14
console.log(tr.mapping.map(6)) // -> 3
console.log(tr.mapping.map(10)) // -> 9
某些時候,並不是很清楚原始的 index 要 map 到哪一個位置。例如:前面例子 index 10,究竟應該把 10 放在分割的後面或者是前面。
如果其實 index 10 應該在前面,可以這樣做 tr.mapping.map(10, -1)
將各種 step 定義的小而直觀的原因是,讓這樣子的 mapping 紀錄可以實現。甚至可以反轉 step。
Rebasing
當透過 step 或是 position map 實作較複雜的行為,例如:追蹤變更紀錄,或是在多人協作中增加其他功能,可能會使用到 rebase steps。
太抽象了,有實作這段,再來說明
The editor state
編輯器要記得狀態有哪些,如何文章的資料結構(document)外,還有 current selection、目前 mark 的狀態。 ProseMirror state 有 3 個主要的狀態就是 doc, selection, storeMarks。
plugin 也需要儲存 state。undo history 也需要記得歷史紀錄。這就是為什麼 editorState 裡,也有儲存目前 active 的 plugin。這些 plugin 也可以儲存 plugin 自己的 state。
Selection
PM 支援多種不同的 selection。所有的 selection 都是繼承 Class Selection 的實例。editor state 裡的 selection 也是 immutable 的,要改變 selection,要建一個新的 selection object,與一個新的 state。
.from:selection 當中,文本前端的那個 point
.to:selection 當中,文本後端的那個 point
.anchor:selection 當中,一開始釘下的 point,不會動的那個 point
.head:selection 當中,在移動的 point
Transaction
在一般正常編輯的時候,newState 會根據舊的 state 產生。
state 可以 apply transaction 來產生新的 state。
Transaction 繼承自 Transform,transaction 會追蹤 selection 與其他有狀態的元件,Transaction 實際上被歸類在 prosemirror-state,像是 store marks 的改變,也會追蹤。
我覺得可以說 Transaction 是針對編輯器有加上了一些特化功能的 Transform。
所以 Transaction 有一系列修該編輯器相關狀態的 method。setStoreMarks、ensureMarks