天天看點

kind富文本編輯器_富文本編輯器Prosemirror - 入門

一、了解prosemirror

prosemirorr本身并不是一個開箱即用的編輯器,而是通過一系列子產品配合搭建起來的一個富文本編輯器,核心的子產品主要有以下四個

  • prosemirror-model :負責prosemirror的内容結構。定義了編輯器的文檔模型,用于描述編輯器内容的資料結構,并實作了對編輯器内容的一原子的操作。實作了一套索引系統,用于處理位置資訊。同時提供了從DOM -> ProsemirrNode的Parser以及反向的Serilizer。
  • prosemirror-transfrom :負責對編輯内容的修改操作。文檔的改動由step實作。transform基于step封裝了一系列對内容進行操作的API。step執行了對文檔内容的操作,通過StepMap記錄了改動的資訊,可用于追溯位置的變化。
  • prosemirror-state :負責描述整個編輯器的狀态。包括文檔内容,選區資訊,所有的節點類型以及并基于Transform實作了Transaction,Transaction主要增加了對選區的管理以及狀态記錄。同時提供了強大的插件系統,實作使用者對狀态更新流程幹預的可能性。
  • prosemirror-view :負責視圖的渲染,實作了從state到視圖的渲染。監聽或者劫持使用者的操作并修正,并建立相應的對state的改動,最終比對dom與state的決定最終渲染結果。

這四個子產品實作了prosemirror的核心功能,但諸如什麼按鍵有什麼行為的規定,都需要由使用方自己實作,好在官方提供了prosemirror-commands、prosemirror-history、prosemirror-keymap等庫來幫助我們更友善地去實作一個編輯器。

二、節點定義

prosemirror中渲染出的節點必須在Schema中有相應的定義,而Scheme中的可以定義的節點分為兩種,

node

mark

Node

node即為正常意義上的節點,分為塊級或者是行内。我們可以定義一個節點在prosemirror中表現:

paragraph: {
    group: "block", 
    content: "text*",
    parseDOM: [{tag: 'p'}],
    toDOM(node) { return ['p', 0] }
}
           

如上述定義了'paragraph'節點:group表示其為block組;content表示其可以包含任意文字;parseDOM表示其解析

<p>xxx</p>

;toDOM表示其渲染為

<p>xxx</p>

,子節點從0的位置開始插入,更多的配置可以檢視NodeSpec,content是一個類正則字元串,可以使用node的名稱或者是group名。

Mark

Mark是一種比較特殊的Schema類型,他的表現不同于Node,Mark主要作用于行内節點,用于給行内元素增加樣式或者附加其他資訊,他不像Node會占據文檔位置,更像是一種對文檔描述的補充。

bold: {
    parseDOM: [{tag: 'strong'}],
    toDOM(node) { return ['strong', 0]}
}
           

如上定義bold類型的Mark,解析以及渲染時候的DOM。更多定義參考:MarkSpec

Attributes

Node以及Mark都可以附加一些資訊,但需要在初始化是定義好支援的屬性

paragraph: {
    group: "block", 
    content: "text*",
    attrs: {
        align: {
            default: 'left'
        }
    }
}
           

如上定義了一個attrs中包含align的paragraph節點,可以用作單純的資訊存儲,也可以配合toDOM修改渲染輸出。

三、文檔結構

内容

prosemiror的内容被描述成一棵樹,他的特征屬性(isBlock、isInline等)由我們所定義的節點特征來确定

kind富文本編輯器_富文本編輯器Prosemirror - 入門

Node的content由一個Fragment表示,Fragment的content則是由Node組成的數組,prosemirror通過這樣嵌套形成的樹形結構來描述一個文檔。

索引

prosemirror實作了一套索引系統用于表示文檔中某個位置,主要分為兩種:

第一種是比較像是通路DOM,利用content的數組的特性去通路節點,把文檔當成一棵樹去周遊。

第二種是強大的索引系統,把文檔打平後的索引,prosemirror文檔中的任何位置,都可以用一個唯一的整數表示。

對于正常的DOM,它是樹形的結構

kind富文本編輯器_富文本編輯器Prosemirror - 入門

在prosemirror的索引系統中,把這棵樹打平了,規定:

  • 整個文檔的第一個節點前的位置為零。
  • 進入或離開不是葉節點(即可以包含其他内容)的節點視為一個token。是以,如果文檔以一個段落開頭,則該段落的内容開頭算作位置1。
  • 文本節點中的每個内容都使做是一個token。
  • 葉子節點(不能包含其他節點内容)也視作是一個token。

按照這個規則,想象我們有一個指針,從開頭0開始進入一個節點時索引加1,每越過一個文本内容加1,退出一個節點時也加1,通過這樣的形式,就可以描述文檔中的每個位置。

kind富文本編輯器_富文本編輯器Prosemirror - 入門

如上所示,nodeSize可以了解為我們的指針從進入到退出節點時走過的距離,是以對應

<p>

的nodeSize為5,

<blockquote>

的nodeSize為8,通過這樣的方式,我們就可以描述每個節點的位置以及大小。prosemirror中的很多操作都需要使用到這些資訊。

四、如何修改文檔

了解了上面的内容之後,我們對prosemirror的文檔結構有了一個大緻的認識,下一步我們來嘗試修改文檔的資料。我們來把官網的内容替換成Hello Prosemirror!。

kind富文本編輯器_富文本編輯器Prosemirror - 入門

分析

根據上面的分析,我們要做的可以是修改doc的content屬性或者是直接修改文字内容亦或是删除内容後再插入,我們選擇第一種方式來實作。

prosemirror中的資料更新實際上都是對state的修改,通過state提供的updateState的API接受一個新的state來更新state,編輯器執行個體view中幫我封裝好了這一步操作,對外暴露出來的API是dispatch。

上面我們說到修改文檔的操作是由

prosemirror-transform

來實作的,而

prosemirror-state

中的

tr

屬性繼承了transform,state又是作為

prosemirror-view

執行個體的一個屬性。

是以更新操作都可以通過view來實作,翻閱API文檔,看到replaceRangeWith這個API符合我們的需求,

kind富文本編輯器_富文本編輯器Prosemirror - 入門

from和to就是上文介紹到的索引數字,代表替換的位置,node即為新的節點,對目前的操作來說,替換的起點from為起始位置0,替換結束的位置為内容終點即為doc的content的大小,node則可以通過schema建立。

實作

根據上面的分析,實作節點替換的操作為:

const { dispatch, state } = view;
const { schema, tr, doc } = state;
const { paragraph } = schema.nodes;
const textNode = schema.text('Hello Prosemirror!');
const newParagraph = paragraph.create(undefined, textNode);
dispatch(tr.replaceRangeWith(0, doc.content.size, newParagraph));
           
kind富文本編輯器_富文本編輯器Prosemirror - 入門

這樣就實作了對内容的替換!

原理

是以從prosemirror的角度看,replaceRangeWith做了什麼操作呢?

上面說到transform對文檔的操作都是通過step去實作,是以這一步實際上建立了一個ReplaceStep去修改文檔。

step對文檔的修改不一定是成功的,結果由StepResult表示,如果失敗了會抛出一個TransformError,如果成功了,則會傳回新的文檔的内容,transform會把舊文檔内容儲存在docs屬性中,新的應用到doc屬性中,并把step儲存在steps屬性中,可以實作撤銷的操作。最後通過distpach更新到state。

kind富文本編輯器_富文本編輯器Prosemirror - 入門

是以,我們可以把state.tr可以看成是一個事務,每一個step可以看作是一次原子操作,通過dispatch送出事務并應用到state上生效,實作了對文檔的修改。

五、總結

通過上文,簡單的介紹prosemirror的一些概念,API使用以及文檔更新的原理,可以看到prosemirror通過對資料的抽象,可以把文檔的結構描述得很清晰。把對文檔的操作封裝得很好,隐藏了很多細節的東西,并提供了各種友善使用的API。

本文目前還隻是停留在對prosemirror粗淺的介紹,諸如文檔具體如何更新,視圖是怎麼去渲染的,使用者行為是怎麼捕獲并分析……還有很多内容值得研究,後續會有一系列文章來介紹它們。

歡迎指出錯誤或提出問題,互相交流,共同發展