<output id="qn6qe"></output>

    1. <output id="qn6qe"><tt id="qn6qe"></tt></output>
    2. <strike id="qn6qe"></strike>

      亚洲 日本 欧洲 欧美 视频,日韩中文字幕有码av,一本一道av中文字幕无码,国产线播放免费人成视频播放,人妻少妇偷人无码视频,日夜啪啪一区二区三区,国产尤物精品自在拍视频首页,久热这里只有精品12

      從零實現(xiàn)富文本編輯器#3-基于Delta的線性數(shù)據(jù)結(jié)構(gòu)模型

      數(shù)據(jù)模型的設計是編輯器的核心基礎,其直接影響了選區(qū)模型、DOM模型、狀態(tài)管理等模塊的設計。例如在quill中的選區(qū)模型是index + len的表達,而slate中則是anchor + focus的表達,這些都是基于數(shù)據(jù)模型的設計而來的。因此我們從零實現(xiàn)的富文本編輯器就需要從數(shù)據(jù)模型的設計開始,之后就可以逐步實現(xiàn)其他模塊。

      從零實現(xiàn)富文本編輯器項目的相關(guān)文章:

      Delta

      在先前的架構(gòu)設計中我們已經(jīng)提到了,我們實現(xiàn)的扁平數(shù)據(jù)結(jié)構(gòu)且獨立分包設計,無論是在編輯器中操作,還是在服務端數(shù)據(jù)解析,都可以更加方便地處理。相比較來說,嵌套的數(shù)據(jù)結(jié)構(gòu)能夠更好地對齊DOM表達,然而這樣對于數(shù)據(jù)的操作卻變得更加復雜。

      因此在數(shù)據(jù)結(jié)構(gòu)的設計上,我們是基于quilldelta結(jié)構(gòu)進行了改造。最主要的部分是將其改造為immutable的實現(xiàn),在編輯器中實際是維護的狀態(tài)而不是本身的delta結(jié)構(gòu)。并且精簡了整個數(shù)據(jù)模型的表達,將復雜的insertAttribute類型縮減,那么其操作邏輯的復雜度也會降低。

      delta是一種簡潔而功能強大的格式,用于描述文檔內(nèi)容及其變化。該格式基于JSON,不僅便于閱讀,同時也易于機器解析。通過使用delta描述的內(nèi)容,可以準確地描述任何富文本文檔的內(nèi)容和格式信息,避免了HTML中常見的歧義和復雜性。

      delta由一系列操作組成,這些操作描述了對文檔進行的更改。常見的操作包括insertdeleteretain。需要注意的是,這些操作不依賴于具體的索引位置,其總是描述當前索引位置的更改,并且可以通過retain來移動指針位置。

      delta既可以表示整個文檔,也可以表示對文檔進行的更改。那么在這里我們將delta的主要類對象及相關(guān)操作邏輯進行描述,特別是在編輯器中實際應用場景,以及主要的改造和相關(guān)類型聲明。

      insert

      insert方法是將數(shù)據(jù)插入到delta的操作,這就是delta。當描述整個文檔內(nèi)容時,整個數(shù)據(jù)的內(nèi)容應該全部都是insert。首個參數(shù)是要插入的文本內(nèi)容,第二個參數(shù)是可選的屬性對象,用于描述文本的格式信息。

      const delta = new Delta();
      delta.insert("123").insert("567", { a: "1" });
      // [{"insert":"123"},{"insert":"567","attributes":{"a":"1"}}]
      

      原始的insert參數(shù)是可以對象類型的Embed結(jié)構(gòu),這種結(jié)構(gòu)可以表達ImageVideoMention等非文本結(jié)構(gòu)的數(shù)據(jù),而屬性AttributeMap參數(shù)是Record<string, unknown>類型,這樣用來描述復雜屬性值。

      在這里我們將其精簡了,insert參數(shù)僅支持string類型,而具體的schema則在編輯器的初始化時定義,格式信息則收歸于Attrs中描述。而AttributeMap則改為Record<string, string>類型,并且可以避免諸如CloneDeepisEqual等對于復雜數(shù)據(jù)結(jié)構(gòu)的實現(xiàn)。

      其實在EtherPad中就是將Attribute就是[string, string]類型,在這里我們也是使用了類似的設計。在這種基礎結(jié)構(gòu)設計下,我們更推薦將屬性值扁平地放置于attributes屬性中,而不是使用單個屬性值作為key,將所有屬性值嵌套地放置于value中。

      export interface AttributeMap {
        [key: string]: string;
      }
      

      delta整個名字通常會用于描述變更,那么除了描述整個文檔內(nèi)容外,當然還可以描述文檔內(nèi)容的變更。不過應用變更的內(nèi)容需要用到compose,這個方法的描述我們在后邊再看。

      const delta1 = new Delta().insert("123");
      const delta2 = new Delta().insert("456");
      delta1.compose(delta2); // [{"insert":"456123"}]
      

      delete

      delete方法描述了刪除內(nèi)容的長度,由于上述的定義我們現(xiàn)在的內(nèi)容全部都是文本,在原始的數(shù)據(jù)定義中嵌入Embed的長度為1

      const delta = new Delta().insert("123");
      delta.compose(new Delta().delete(1)); // [{"insert":"23"}]
      

      其實這里是比較有趣的事情,通過delete來描述變更時,無法得知究竟刪除了哪些內(nèi)容。那么這種情況下,進行invert的時候就需要額外的數(shù)據(jù)來構(gòu)造insert操作。類似的場景在OT-JSON中,內(nèi)容描述是直接寫入op的,因此可以直接根據(jù)op來進行invert操作。

      const delta = new Delta().insert("123");
      const del = new Delta().delete(1);
      const invert1 = del.invert(delta); // [{"insert":"1"}]
      const delta2 = delta.compose(del); // [{"insert":"23"}]
      delta2.compose(invert1); // [{"insert":"123"}]
      

      retain

      retain方法描述了保留內(nèi)容的長度,換句話說,這個操作可以用來移動指針。

      const delta = new Delta().insert("123");
      delta.compose(new Delta().retain(1).insert("a")); // [{"insert":"1a23"}]
      

      同時retain操作也可以用于修改內(nèi)容的屬性值,此外如果想刪除某個屬性,只需要將value設置為""即可。

      const delta = new Delta().insert("123");
      const d2 = new Delta().retain(1).retain(1, { "a": "1" });
      const d3 = delta.compose(d2); // [{"insert":"1"},{"insert":"2","attributes":{"a":"1"}},{"insert":"3"}]
      d3.compose(new Delta().retain(1).retain(1, { "a": "" })); // [{"insert":"123"}]
      

      push

      push方法是上述的insertdeleteretain依賴的基礎方法,主要實現(xiàn)是將內(nèi)容推入到delta維護的數(shù)組中。這里的實現(xiàn)非常重要部分是op的合并,當屬性值相同時,則需要將其合并為單個op

      const delta = new Delta();
      delta.push({ insert: "123" }).push({ insert: "456" }); // [{"insert": "123456"}]
      

      當然這里不僅僅是insert操作會合并,對于deleteretain操作也是一樣的。這里的合并操作是基于opattributes屬性值,如果attributes屬性值不同,則會被視為不同的op,不會自動合并。

      const delta = new Delta();
      delta.push({ delete: 1 }).push({ delete: 1 }); // [{"delete": 2}]
      const delta2 = new Delta();
      delta2.push({ retain: 1 }).push({ retain: 1 }); // [{"retain": 2}]
      const delta3 = new Delta();
      delta3.push({ retain: 1 }).push({ retain: 1, attributes: { a: "1"} }); // [{"retain": 1}, {"retain": 1, "attributes": {"a": "1"}}]
      

      slice

      slice方法是用于截取delta的操作,這個方法是基于oplength屬性值進行截取的。

      const delta = new Delta().insert("123").insert("456", {a: "1"});
      delta.slice(2, 4); // [{"insert":"3"},{"insert":"4","attributes":{"a":"1"}}]
      

      eachLine

      eachLine方法用于按行迭代整個delta,我們的整體數(shù)據(jù)結(jié)構(gòu)是線性的,但是編輯器DOM需要按行來劃分內(nèi)容,因此基于\n來劃分行就是比較常規(guī)的操作了。

      這個方法對于編輯器的初始化非常重要,而當初始化完畢后,我們的變更就需要基于狀態(tài)來實現(xiàn),而不是每次都需要經(jīng)過該方法。在這里我們也對其進行了改造,原始的eachLine方法是不會攜帶\n節(jié)點。

      const delta = new Delta().insert("123\n456\n789");
      delta.eachLine((line, attributes) => {
        console.log(line, attributes);
      });
      // [{insert:"123"},{insert:"\n"}] {}
      // [{insert:"456"},{insert:"\n"}] {}
      // [{insert:"789"},{insert:"\n"}] {}
      

      diff

      diff方法用于對比兩個delta之間的差異,這個方法實際上是基于純文本的myers diff來實現(xiàn)。通過將delta轉(zhuǎn)換為純文本,在diff過后不斷挑選較短的操作部分來實現(xiàn)delta之間的diff

      其實在我們的實現(xiàn)中完全可以將diff方法獨立出來,這里唯一引用了外部的fast-diff依賴。在quilldiff是必要的,因為其完全是非受控的輸入方式,文本的輸入依賴于對DOM文本的diff來實現(xiàn),而我們的輸入是依賴beforeinput事件的半受控輸入,因此并不強依賴diff

      const delta1 = new Delta().insert("123");
      const delta2 = new Delta().insert("126");
      delta1.diff(delta2); // [{"retain":2},{"insert":"6"},{"delete":1}]
      

      chop

      chop方法用于裁剪末尾的retain操作,當存在末尾的retain且沒有屬性操作時,其本身是沒有意義的,因此可以調(diào)用該方法檢查并移除。

      const delta = new Delta().insert("123").retain(1);
      delta.chop(); // [{"insert":"123"}]
      

      compose

      compose方法可以將兩個delta合并為一個delta,具體來說則是將Bdelta操作應用到Adelta上,此時返回的是一個新的delta對象。當然在我們的實現(xiàn)中,繼承了原始Delta類重寫了compose方法,做到了immutable

      compose在編輯器中的應用場景非常多,例如輸入事件、內(nèi)容粘貼、歷史操作等場景中,類似于編輯器的apply方法,相當于應用內(nèi)容變更。

      const delta1 = new Delta().insert("123");
      const delta2 = new Delta().retain(1).delete(1);
      delta1.compose(delta2); // [{"insert":"13"}]
      

      invert

      invert方法是將delta的操作進行反轉(zhuǎn),這個方法在歷史操作中非常重要,因為本身undo就是需要將當前的操作進行反轉(zhuǎn)。此外,在實現(xiàn)OTlocal-cs中,invert也是非常重要的方法。

      值得關(guān)注的是,上邊也提到了delete操作和retain操作在本身執(zhí)行時是不會記錄原始內(nèi)容的,因此在invert是需要原始的delta作為數(shù)據(jù)源來進行操作,注意這里的delta是最初的delta,而不是invert后的delta

      const delta = new Delta().insert("123");
      const del = new Delta().delete(1);
      const invert1 = del.invert(delta); // [{"insert":"1"}]
      const delta2 = delta.compose(del); // [{"insert":"23"}]
      delta2.compose(invert1); // [{"insert":"123"}]
      

      concat

      concat方法可以連接兩個delta到新的delta中。這個操作與compose不同,compose是將B的操作應用到A上,而concat則是將B的操作追加到A的操作后。

      const delta1 = new Delta().insert("123");
      const delta2 = new Delta().insert("456");
      delta1.compose(delta2); // [{"insert":"456123"}]
      delta1.concat(delta2); // [{"insert":"123456"}]
      

      transform

      transform方法是實現(xiàn)操作OT協(xié)同的基礎,即使不實現(xiàn)協(xié)同編輯,在編輯器中的歷史操作模塊中也會需要這部分實現(xiàn)。假設我們現(xiàn)在有用戶A[uid:1]和用戶B[uid:2],此時我們以uid定義優(yōu)先級,則A的操作優(yōu)先級高于B,且當前的文檔內(nèi)容為12

      如果是在協(xié)同中的話,b'=a.t(b)的意思是,假設ab都是從相同的draft分支出來的,那么b'就是假設a已經(jīng)應用了,此時b需要在a的基礎上變換出b'才能直接應用,我們也可以理解為transform解決了a操作對b操作造成的影響。

      那么我們假設A12后的位置插入了A字符,B12后的位置插入了B字符。如果進行協(xié)同操作,那么兩者相當于同時在同一個位置插入了字符,如果不進行操作變換而直接應用的話,兩者的數(shù)據(jù)就會出現(xiàn)沖突,A的數(shù)據(jù)是12BA,而B的數(shù)據(jù)是12AB,因此就需要先轉(zhuǎn)換再應用。

      // User A
      const base = new Delta().insert("12");
      const delta = new Delta().retain(2).insert("A");
      let current = base.compose(delta); // 12A
      // Accept Remote B
      const remote = new Delta().retain(2).insert("B");
      // ob1=OT(oa, ob)
      const remote2 = delta.transform(remote, true); // [{"retain":3},{"insert":"B"}] 
      current = current.compose(remote2); // 12AB
      
      // User B
      const base = new Delta().insert("12");
      const delta = new Delta().retain(2).insert("B");
      let current = base.compose(delta); // 12B
      // Accept Remote A
      const remote = new Delta().retain(2).insert("A");
      // oa2=OT(ob, oa)
      const remote2 = delta.transform(remote, false); // [{"retain":2},{"insert":"A"}] 
      current = current.compose(remote2); // 12AB
      

      transformPosition

      transformPosition方法用于將指定的位置進行轉(zhuǎn)換,這個方法的主要場景是編輯器中的選區(qū)/光標的位置變換,例如光標此時在1后面,構(gòu)造delta1之前增加了內(nèi)容的話,那么光標就需要跟隨移動。

      const delta = new Delta().retain(5).insert("a");
      delta.transformPosition(4); // 4
      delta.transformPosition(5); // 6
      

      OpIterator

      OpIterator類定義一個迭代器,用于迭代delta中的op操作。迭代器大量用于diffcomposetransform等方法中,需要注意的是該迭代器調(diào)用next時不會跨越op,即使傳遞的length大于當前op的長度。

      const delta = new Delta()
        .insert("Hello", { bold: "true" })
        .insert(" World", { italic: "true" });
        .retain(3);
      iter.next(2); // { insert: "He", attributes: { bold: "true" } }
      iter.next(10); // { insert: "llo", attributes: { bold: "true" } }
      

      EtherPad

      EtherPad同樣是非常優(yōu)秀的協(xié)同編輯器,其內(nèi)置實現(xiàn)的數(shù)據(jù)結(jié)構(gòu)同樣是線性的,文檔整體描述稱為ClientVars,數(shù)據(jù)結(jié)構(gòu)變更被稱為ChangeSet。協(xié)同算法的實現(xiàn)是EasySync,且其文檔中對如何進行服務端調(diào)度也有較為詳細的描述。

      ClientVars/Changeset同樣是一種基于JSON的數(shù)據(jù)格式,用于描述文檔的內(nèi)容及其變更。但是其并沒有像Delta那么清晰的表達,JSON結(jié)構(gòu)主要是AttributePool內(nèi),而對于文本內(nèi)容的表達則是純文本的結(jié)構(gòu)。

      文檔描述

      文檔內(nèi)容是使用ClientVars的數(shù)據(jù)結(jié)構(gòu)表示的,其中包含了三個部分,apool文本屬性池、text文本內(nèi)容、attribs屬性描述。在下面的例子中,我們描述了標題、加粗、斜體、純文本的內(nèi)容,那么這其中的內(nèi)容如下所示。

      ({
        initialAttributedText: {
          text: "short description\n*Heading1\ntext\n*Heading2\nbold italic\nplain text\n\n",
          attribs: "*0|1+i*0*1*2*3+1*0|2+e*0*4*2*3+1*0|1+9*0*5+4*0+1*0*6+6*0|2+c|1+1",
        },
        apool: {
          numToAttrib: {
            "0": ["author", "a.XYe86foM7oYgmpuu"],
            "1": ["heading", "h1"],
            "2": ["insertorder", "first"],
            "3": ["lmkr", "1"],
            "4": ["heading", "h2"],
            "5": ["bold", "true"],
            "6": ["italic", "true"],
          },
          nextNum: 7,
        },
      });
      

      對于這個內(nèi)容直接看上去是比較復雜的,當然實際上也是比較復雜的。apool是一個屬性池,所有對于文本內(nèi)容的裝飾都是在這里存儲的,也就是其中的numToAttrib屬性存儲的[string, string]值,nextNum則是下個要放置的索引。text則是純文本的內(nèi)容,相當于此時文檔的純文本內(nèi)容。attribs則是根據(jù)text的純文本內(nèi)容,并且取得apool中的屬性值,相當于裝飾文本內(nèi)容的編碼。

      因此attribs需要單獨拿出來解析,*n表示取第n個屬性應用到文本上,通常需要配合|n+n屬性使用,|n表示影響n行,僅對于\n屬性需要使用該屬性,+n則表示取出n個字符數(shù),相當于retain操作,不僅可以移動指針,還可以用來持有屬性變更。特別需要注意的是|m不會單獨出現(xiàn),其總是與+n一同表達,表示這n個字符中存在m個換行,且最后一個應用的字符必然是\n

      此外,EasySync里面的數(shù)字大都是36進制的,因此這里的+i/+e等都不是特殊的符號,需要用0-9數(shù)字來表示0-9的字符,而10-35則是表示a-z,例如+i就是i - a = 8 => 8 + 10 = 18

      • *0表示取出author的屬性,|1+i表示將其應用到了i長度即18,字符為short description\n,由于其包含\n則定義|1
      • *0*1*2*3表示取出前4個屬性,+1表示將其應用1個字符,即*字符,在EasySync中行首的該字符承載了行屬性,而非放置\n中。
      • *0表示取出author的屬性,|2+e表示應用了e長度即14,字符為Heading1\ntext\n,其包含兩個\n則定義|2
      • *0*4*2*3表示取出相關(guān)屬性,+1表示將其應用1個字符,即*字符表示行屬性內(nèi)容。
      • *0|1+9表示取出author的屬性,+9表示將其應用9個字符,即Heading2\n,末尾是\n則定義|1
      • *0*5+4表示取出加粗等屬性,應用4個字符,即bold
      • *0+1表示取出author的屬性,應用1個字符即空格。
      • *0*6+6表示取出斜體等屬性,應用6個字符,即italic
      • *0|2+c表示取出相關(guān)屬性,應用12個字符即\nplain text\n,存在兩個\n則定義|2
      • |1+1表示末尾的\n屬性,在EasySync中行末的該字符需要單獨定義。

      變更描述

      OT操作變換的基準原子實現(xiàn)就是insertdeleteretain三種操作,那么ChangeSet的內(nèi)容描述自然也是類似,但是數(shù)據(jù)的變更描述并非像delta的結(jié)構(gòu)那么清晰,而是特別設計了一套數(shù)據(jù)結(jié)構(gòu)描述。

      文檔在最開始創(chuàng)建或是導入的時候是初始的ClientVars,而此后每次對于文檔內(nèi)容的修改則是會生成ChangeSet。針對于上述的三種操作對應了三種符號,=表示retain+表示insert-表示delete,這三種符號的組合就可以描述文檔內(nèi)容的變更,除此之外還有額外的定義:

      • Z: 首字母為MagicNumber,表示為符號位。
      • :N: 文檔原始內(nèi)容長度為N
      • >N: 最終文檔長度會比原始文檔長度長N
      • <N: 最終文檔長度會比原始文檔長度短N
      • +N: 實際執(zhí)行操作,表示插入了N個字符。
      • -N: 實際實際操作,表示操作刪除了N個字符。
      • =N: 實際執(zhí)行操作,表示操作保留了N個字符,移動指針或應用屬性。
      • |N: 表示影響了N行,與上述文檔描述一致需要與+/-/=N使用,操作的長度包含N\n,且末尾操作必須是\n。文檔最末尾的\n需要表示的話,則必須要用|1=1表示。
      • *I: 表示應用屬性,Iapool中的索引值,在一個+=|之前可以有任意數(shù)量的*操作。
      • $: 表示結(jié)束符號,用于標記Operation部分的結(jié)束。
      • char bank: 用于存儲insert操作具體的字符內(nèi)容,在執(zhí)行插入操作時按序取用。

      同樣是上述的例子,現(xiàn)在的文檔中已經(jīng)存在exist text\n\n的文本內(nèi)容,緊接著將上述的內(nèi)容粘貼到文檔中,那么發(fā)生的User ChangeSet的變更描述如下:

      ({
        changeset:
          "Z:c>1t|1=b*0|1+i*0*1*2*3+1*0|2+e*0*4*2*3+1*0|1+9*0+5*0*5+6*0|1+1*0+a$short description\n*Heading1\ntext\n*Heading2\nbold italic\nplain text",
        apool: {
          numToAttrib: {
            "0": ["author", "a.XYe86foM7oYgmpuu"],
            "1": ["heading", "h1"],
            "2": ["insertorder", "first"],
            "3": ["lmkr", "1"],
            "4": ["heading", "h2"],
            "5": ["italic", "true"],
          },
          nextNum: 6,
        },
      });
      
      • Z表示MagicNumber,即符號位。
      • c表示文檔原始內(nèi)容長度為12,即exist text\n\n內(nèi)容長度。
      • >1t表示最終文檔會比原始內(nèi)容長度多1t36進制轉(zhuǎn)換1t64,具體為char bank索引。
      • |1=b表示移動指針長度為b,轉(zhuǎn)換長度為11,文本內(nèi)容為exist text\n,末尾為\n定義|1
      • *0|1+i表示從apool中取出0屬性,應用i轉(zhuǎn)換長度為18,文本內(nèi)容為short description\n,末尾為\n定義|1
      • *0*1*2*3+1表示取出4個屬性,應用為1,文本內(nèi)容為*,具體則是行屬性的起始標記。
      • *0|2+e表示取出0屬性,應用e轉(zhuǎn)換長度為14,文本內(nèi)容為Heading1\ntext\n,末尾為\n且包含兩個\n定義|2
      • *0*4*2*3+1表示取出4個屬性,應用為1,文本內(nèi)容為*,同樣是行屬性的起始標記。
      • *0|1+9表示取出0屬性,應用長度為9,文本內(nèi)容為Heading2\n,末尾為\n定義|1
      • *0+5表示取出0屬性,應用長度為5,文本內(nèi)容為bold
      • *0*5+6表示取出斜體等屬性,應用長度為6,文本內(nèi)容為italic
      • *0|1+1表示取出0屬性,應用長度為1,末尾為\n則定義|1,文本內(nèi)容為\n
      • *0+a表示取出0屬性,應用長度為a10,文本內(nèi)容為plain text
      • $表示結(jié)束符號,后續(xù)的內(nèi)容符號則為char bank,最末尾的\n通常不需要表示,即使表示也需要|1=1單獨表示。

      Slate

      slate的數(shù)據(jù)結(jié)構(gòu)以及選區(qū)的設計幾乎完全對齊了DOM結(jié)構(gòu),且數(shù)據(jù)結(jié)構(gòu)設計并未獨立出來,同樣基于JSON的結(jié)構(gòu),非常類似于低代碼的結(jié)構(gòu)設計。操作變換是直接在slate的核心模塊Transform中實現(xiàn),且位置相關(guān)操作變換的實現(xiàn)分散在PointPath對象中。

      [
        {
          type: "paragraph",
          children: [
            { text: "This is editable " },
            { text: "rich", bold: true },
            { text: " text." },
          ],
        },
        { type: "block-quote", children: [{ text: "A wise quote." }] },
      ];
      

      Operation

      同樣是基于OT實現(xiàn)操作變換算法,線性的數(shù)據(jù)結(jié)構(gòu)僅需要insertdeleteretain三種基本操作即可實現(xiàn),而在slate中則實現(xiàn)了9種原子操作來描述變更,這其中包含了文本處理、節(jié)點處理、選區(qū)變換的操作等。

      • insert_node: 插入節(jié)點。
      • insert_text: 插入文本。
      • merge_node: 合并節(jié)點。
      • move_node: 移動節(jié)點。
      • remove_node: 移除節(jié)點。
      • remove_text: 移除文本。
      • set_node: 設置節(jié)點。
      • set_selection: 設置選區(qū)。
      • split_node: 分割節(jié)點。

      實際上僅實現(xiàn)應用還好,其相對應的inverttransform則會更加復雜。在slate中的inverse相關(guān)操作在operation.ts中實現(xiàn),與位置相關(guān)的transformpath.tspoint.ts中有相關(guān)實現(xiàn)。

      而實際上這些操作通常都不會在編輯器中直接調(diào)用,slate針對這些最基礎的操作進行了封裝,實現(xiàn)了Transforms模塊。在這個模塊中實現(xiàn)了諸多具體的操作,例如insertNodesliftNodesmergeNodesmoveNodesremoveNodes等等,這里的操作就遠不止9種類型了。

      • insertFragment: 在指定的位置插入節(jié)點的片段。
      • insertNodes: 在指定的位置插入節(jié)點。
      • removeNodes: 在文檔中指定的位置刪除節(jié)點。
      • mergeNodes: 在某個節(jié)點與同級的前節(jié)點合并。
      • splitNodes: 在某個節(jié)點中的指定位置分割節(jié)點。
      • wrapNodes: 在某個節(jié)點中的指定位置包裹一層節(jié)點。
      • unwrapNodes: 在某個節(jié)點中的指定位置解除一層包裹節(jié)點。
      • setNodes: 在某個節(jié)點中的指定位置設置節(jié)點屬性。
      • unsetNodes: 在某個節(jié)點中的指定位置取消節(jié)點屬性。
      • liftNodes: 在某個節(jié)點中的指定位置提升一層節(jié)點。
      • moveNodes: 在文檔中的指定位置移動節(jié)點。
      • collapse: 將選區(qū)折疊為插入符。
      • select: 主動設置選區(qū)位置。
      • deselect: 取消選區(qū)位置。
      • move: 移動選區(qū)位置。
      • setPoint: 設置選區(qū)的單側(cè)位置。
      • setSelection: 設置新選區(qū)位置。
      • delete: 刪除選區(qū)內(nèi)容。
      • insertText: 在選區(qū)位置插入文本。
      • transform: 在編輯器上immutable地執(zhí)行op

      OT-JSON

      類似的,在OT-JSON(json0)中實現(xiàn)了11種操作,富文本場景中SubType仍然需要擴展,那自然就需要更多的操作來描述變更。因此,實際上以JSON嵌套的數(shù)據(jù)格式來描述內(nèi)容變更,要比線形的操作復雜得多。

      slate中是自行封裝了編輯器的基礎op,如果其本身是在OT-JSON的基礎上封裝Transforms的話,對于實現(xiàn)OT的協(xié)同會更方便一些,ShareDB等協(xié)同框架都是要參考OTTypes的定義的。當然,基于CRDT實現(xiàn)的協(xié)同看起來更加容易處理。

      • {p:[path], na:x}: 在指定的路徑[path]值上加x數(shù)值。
      • {p:[path,idx], li:obj}: 在列表[path]的索引idx前插入對象obj
      • {p:[path,idx], ld:obj}: 從列表[path]的索引idx中刪除對象obj
      • {p:[path,idx], ld:before, li:after}: 用對象after替換列表[path]中索引idx的對象before
      • {p:[path,idx1], lm:idx2}: 將列表[path]中索引idx1的對象移動到索引idx2處。
      • {p:[path,key], oi:obj}: 向路徑[path]中的對象添加鍵key和對象obj
      • {p:[path,key], od:obj}: 從路徑[path]中的對象中刪除鍵key和值obj
      • {p:[path,key], od:before, oi:after}: 用對象after替換路徑[path]中鍵key的對象before
      • {p:[path], t:subtype, o:subtypeOp}: 對路徑[path]中的對象應用類型為t的子操作o,子類型操作。
      • {p:[path,offset], si:s}: 在路徑[path]的字符串的偏移量offset處插入字符串s,內(nèi)部使用子類型。
      • {p:[path,offset], sd:s}: 從路徑[path]的字符串的偏移量offset處刪除字符串s,內(nèi)部使用子類型。

      總結(jié)

      數(shù)據(jù)結(jié)構(gòu)的設計是非常重要的,對于編輯器來說,數(shù)據(jù)結(jié)構(gòu)的設計直接影響著選區(qū)模型、DOM模型、狀態(tài)管理等模塊的設計。在這里我們聊到了很多的數(shù)據(jù)結(jié)構(gòu)設計,DeltaChangeset的線性結(jié)構(gòu),Slate的嵌套結(jié)構(gòu),每種數(shù)據(jù)都有著各自的設計與考量。

      那么在選定好了數(shù)據(jù)結(jié)構(gòu)后,就可以在此基礎上實現(xiàn)編輯器的各個模塊。我們接下來會從數(shù)據(jù)模型出發(fā),設計選區(qū)模型的表示,然后在此基礎上實現(xiàn)瀏覽器選區(qū)與編輯器選區(qū)模型的同步。通過選區(qū)模型作為操作的目標,來實現(xiàn)編輯器的基礎操作,例如插入、刪除、格式化等操作。

      每日一題

      參考

      posted @ 2025-04-22 10:42  WindRunnerMax  閱讀(1047)  評論(2)    收藏  舉報
      ?Copyright    @Blog    @WindRunnerMax
      主站蜘蛛池模板: 老男人久久青草av高清| 亚洲各类熟女们中文字幕| 久久99精品九九九久久婷婷| 国产精品美女乱子伦高| 麻豆精产国品一二三产| 国产精品亚洲中文字幕| 久热这里只国产精品视频| 国产福利社区一区二区| 久久精品网站免费观看| 欧美男男作爱videos可播放| 国产精品乱一区二区三区| 日日摸夜夜添狠狠添欧美| 91福利视频一区二区| 国产成人午夜在线视频极速观看 | 丰满高跟丝袜老熟女久久| 中文字幕午夜福利片午夜福利片97 | 国精品无码一区二区三区在线看| 国产永久免费高清在线观看| 吉隆县| 四虎影视一区二区精品| 97在线观看视频免费| 国产一区二区三区免费观看| 成人午夜大片免费看爽爽爽| 狠狠爱五月丁香亚洲综| 樱花草视频www日本韩国| 在线看高清中文字幕一区| 谢通门县| 激情综合五月| 亚洲日韩一区二区| 亚洲av无在线播放中文| 亚洲日韩VA无码中文字幕| 国产一区二区三区免费观看| 久草热8精品视频在线观看| 国产精品中文一区二区| 国产爽视频一区二区三区| 欧美浓毛大泬视频| 无码丰满人妻熟妇区| 久久天天躁夜夜躁狠狠| 特级做a爰片毛片免费看无码| 亚洲欧美中文字幕5发布| 美女内射福利大全在线看|