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

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

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

      從零實現富文本編輯器#8-瀏覽器輸入模式的非受控DOM行為

      先前我們在選區模塊的基礎上,通過瀏覽器的組合事件來實現半受控的輸入模式,這是狀態同步的重要實現之一。在這里我們要關注于處理瀏覽器復雜DOM結構默認行為,以及兼容IME輸入法的各種輸入場景,相當于我們來Case By Case地處理輸入法和瀏覽器兼容的行為。

      從零實現富文本編輯器系列文章

      概述

      在整個編輯器系列最開始的時候,我們就提到了ContentEditable的可控性以及瀏覽器兼容性問題,特別是結合了React作為視圖層的模式下,狀態管理以及DOM的行為將變得更不可控,這里回顧一下常見的瀏覽器的兼容性問題:

      • 在空contenteditable編輯器的情況下,直接按下回車鍵,在Chrome中的表現是會插入<div><br></div>,而在FireFox(<60)中的表現是會插入<br>IE中的表現是會插入<p><br></p>
      • 在有文本的編輯器中,如果在文本中間插入回車例如123|123,在Chrome中的表現內容是123<div>123</div>,而在FireFox中的表現則是會將內容格式化為<div>123</div><div>123</div>
      • 同樣在有文本的編輯器中,如果在文本中間插入回車后再刪除回車,例如123|123->123123,在Chrome中的表現內容會恢復原本的123123,而在FireFox中的表現則是會變為<div>123123</div>
      • 在同時存在兩行文本的時候,如果同時選中兩行內容再執行("formatBlock", false, "P")命令,在Chrome中的表現是會將兩行內容包裹在同個<p>中,而在FireFox中的表現則是會將兩行內容分別包裹<p>標簽。
      • ...

      由于我們的編輯器輸入是依靠瀏覽器提供的組合事件,自然無法規避相關問題。編輯器設計的視圖結構是需要嚴格控制的,這樣我們才能根據一定的規則實現視圖與選區模式的同步。依照整體MVC架構的設計,當前編輯器的視圖結構設計如下:

      <div data-block="true" >
        <div data-node="true">
          <span data-leaf="true"><span data-string="true">inline</span></span>
          <span data-leaf="true"><span data-string="true">inline2</span></span>
        </div>
      </div>
      

      那么如果在ContentEdiable輸入時導致上述的結構被破壞,我們設計的編輯器同步模式便會出現問題。因此為了解決類似的問題,我們就需要實現臟DOM檢查,若是出現破壞性的節點結構,就需要嘗試修復DOM結構,甚至需要調度React來重新渲染嚴格的視圖結構。

      然而,如果每次輸入或者選區變化等時機都進行DOM檢查和修復,勢必會影響編輯器整體性能或者輸入流暢性,并且DOM檢查和修復的范圍也需要進行限制,否則同樣影響性能。因此在這里我們需要對瀏覽器的輸入模式進行歸類,針對不同的類型進行不同的DOM檢查和修復模式。

      行內節點

      DOM結構與Model結構的同步在非受控的React組件中變得復雜,這其實也就是部分編輯器選擇自繪選區的原因之一,可以以此避免非受控問題。那么非受控的行為造成的主要問題可以比較容易地復現出來,假設此時存在兩個節點,分別是inline類型和text類型的文本節點:

      inline|text
      

      此時我們的光標在inline后,假設schema中定義的inline規則是不會繼承前個節點的格式,那么接下來如果我們輸入內容例如1,此時文本就變成了inline|1text。這個操作是符合直覺的,然而當我們在上述的位置喚醒IME輸入中文內容時,這里的文本就變成了錯誤的內容。

      inline中文|中文text
      

      這里的差異可以比較容易地看出來,如果是輸入的英文或者數字,即不需要喚醒IME的受控輸入模式,1這個字符是會添加到text文本節點前。而喚醒IME輸入法的非受控輸入模式,則會導致輸入的內容不僅出現在text前,而且還會出現在inline節點的后面,這部分顯然是有問題的。

      這里究其原因還是在于非受控的IME問題,在輸入英文時我們的輸入在beforeinput事件中被阻止了默認行為,因此不會觸發瀏覽器默認行為的DOM變更。然而當前在喚醒IME的情況下,DOM的變更行為是無法被阻止的,因此此時屬于非受控的輸入,這樣就導致了問題。

      此時由于瀏覽器的默認行為,inline節點的內容會被輸入法插入“中文”的文本,這部分是瀏覽器對于輸入法的默認處理。而當我們輸入完成后,數據結構Model層的內容是會將文本放置于text前,這部分則是編輯器來控制的行為,這跟我們輸入非中文的表現是一致的,也是符合預期表現的。

      那么由于我們的immutable設計,再加上性能優化策略的memo以及useMemo的執行,即使在最終的文本節點渲染加入了臟DOM檢測也是不夠的,因為此時完全不會執行rerender。這就導致React原地復用了當前的DOM節點,因此造成了IME輸入的DOM變更和Model層的不一致。

      const onRef = (dom: HTMLSpanElement | null) => {
        if (props.children === dom.textContent) return void 0;
        const children = dom.childNodes;
        // If the text content is inconsistent due to the modification of the input
        // it needs to be corrected
        for (let i = 1; i < children.length; ++i) {
          const node = children[i];
          node && node.remove();
        }
        // Guaranteed to have only one text child
        if (isDOMText(dom.firstChild)) {
          dom.firstChild.nodeValue = props.children;
        }
      };
      

      而如果我們直接將leafReact.memo以及useMemo移除,這個問題自然是會消失,然而這樣就會導致編輯器的性能下降。因此我們就需要考慮盡可能檢查到臟DOM的情況,實際上如果是在input事件或者MutationObserver中處理輸入的純非受控情況,也需要處理臟DOM的問題。

      那么我們可以明顯的想到,當行狀態發生變更時,我們就直接檢查當前行的所有leaf節點,然后對比文本內容,如果存在不一致的情況則直接進行修正。如果直接使用querySelector的話顯然不夠優雅,我們可以借助WeakMap來映射葉子狀態到DOM結構,以此來快速定位到需要的節點。

      然后在行節點的狀態變更后,在處理副作用的時候檢查臟DOM節點,并且由于我們的行狀態也是immutable的,因此也不需要擔心性能問題。此時檢查的執行是O(N)的算法,而且檢查的范圍也會限制在發生rerender的行中,具體檢查節點的方法自然也跟上述onRef一致。

      const leaves = lineState.getLeaves();
      for (const leaf of leaves) {
        const dom = LEAF_TO_TEXT.get(leaf);
        if (!dom) continue;
        const text = leaf.getText();
        // 避免 React 非受控與 IME 造成的 DOM 內容問題
        if (text === dom.textContent) continue;
        editor.logger.debug("Correct Text Node", dom);
        const nodes = dom.childNodes;
        for (let i = 1; i < nodes.length; ++i) {
          const node = nodes[i];
          node && node.remove();
        }
        if (isDOMText(dom.firstChild)) {
          dom.firstChild.nodeValue = text;
        }
      }
      

      這里需要注意的是,臟節點的狀態檢查是需要在useLayoutEffect時機執行的,因為我們需要保證執行的順序是先校正DOM再更新選區。如果反過來的話就會導致一個問題,先更新的選區依然停留在臟節點上,此時再校正會由于DOM節點變化導致選區的丟失,表現是選區會在inline的最前方。

      leaf rerender -> line rerender -> line layout effect -> block layout effect
      

      此外,這里的實現在首次渲染并不需要檢查,此時不會存在臟節點的情況,因此初始化渲染的時候我們可以直接跳過檢查。以這種策略來處理臟DOM的問題,還可以避免部分其他可能存在的問題,零寬字符文本的內容暫時先不處理,如果再碰到類似的情況是需要額外的檢查的。

      其實換個角度想,這里的問題也可能是我們的選區策略是盡可能偏左側的查找,如果在這種情況將其校正到右側節點可能也可以解決問題。不過因為在空行的情況下我們的末尾\n節點并不會渲染,因此這樣的策略目前并不能徹底解決問題,而且這個處理方式也會使得編輯器的選區策略變得更加復雜。

      [inline|][text] => [inline][|text]
      

      這里還需要關注下ReactHooks調用時機,在下面的例子中,從真實DOM中得到onRef執行順序是最前的,因此在此時進行首次DOM檢查是合理的。而后續的Child LayoutEffect就類似于行DOM檢查,在修正過后在Parent LayoutEffect中更新選區是符合調度時機方案。

      Child onRef
      Child useLayoutEffect
      Parent useLayoutEffect
      Child useEffect
      Parent useEffect
      
      // https://playcode.io/react
      import React from 'react';
      const Child = () => {
        const [,forceUpdate] = React.useState({});
        const onRef = () => console.log("Child onRef");
        React.useEffect(() => console.log("Child useEffect"));
        React.useLayoutEffect(() => console.log("Child useLayoutEffect"));
        return <button ref={onRef} onClick={() => forceUpdate({})}>Update</button>
      }
      export function App(props) {
        React.useEffect(() => console.log("Parent useEffect"));
        React.useLayoutEffect(() => console.log("Parent useLayoutEffect"));
        return <Child></Child>;
      }
      

      包裝節點

      關于包裝節點的問題需要我們先聊一下這個模式的設計,現在實現的富文本編輯器是沒有塊結構的,因此實現任何具有嵌套的結構都是個復雜的問題。在這里我們原本就不會處理諸如表格類的嵌套結構,但是例如blockquote這種wrapper級結構我們是需要處理的。

      類似的結構還有list,但是list我們可以完全自己繪制,但是blockquote這種結構是需要具體組合才可以的。然而如果僅僅是blockquote還好,在inline節點上使用wrapper是更常見的實現,例如a標簽的包裝在編輯器的實現模式中就是很常規的行為。

      具體來說,在我們將文本分割為bolditalicinline節點時,會導致DOM節點被實際切割,此時如果嵌套<a>節點的話,就會導致hover后下劃線等效果出現切割。因此如果能夠將其wrapper在同一個<a>標簽的話,就不會出現這種問題。

      但是新的問題又來了,如果僅僅是單個key來實現渲染時嵌套并不是什么復雜問題,而同時存在多個需要wrapperkey則變成了令人費解的問題。如下面的例子中,如果將34單獨合并b,外層再包裹a似乎是合理的,但是將34先包裹a后再合并5b也是合理的,甚至有沒有辦法將67一并合并,因為其都存在b標簽。

      1 2 3  4  5 6  7 8 9 0
      a a ab ab b bc b c c c
      

      思來想去,我最終想到了個簡單的實現,對于需要wrapper的元素,如果其合并listkeyvalue全部相同的話,那么就作為同一個值來合并。那么這種情況下就變的簡單了很多,我們將其認為是一個組合值,而不是單獨的值,在大部分場景下是足夠的。

      1 2 3  4  5 6  7 8 9 0
      a a ab ab b bc b c c c
      12 34 5 6 7 890
      

      不過話又說回來,這種wrapper結構是比較特殊的場景下才會需要的,在某些操作例如縮進這個行為中,是無法判斷究竟是要縮進引用塊還是縮進其中的文字。這個問題在很多開源編輯器中都存在,特別是扁平化的數據結構設計例如Quill編輯器。

      其實也就是在沒有塊結構的情況下,對于類似的行為不好控制,而整體縮進這件事配合list在大型文檔中也是很合理的行為,因此這部分實現還是要等我們的塊結構編輯器實現才可以。當然,如果數據結構本身支持嵌套模式,例如Slate就可以實現。

      后續在wrap node實現的a標簽來實現輸入時,又出現了上述類似inline-code的臟DOM問題。以下面的DOM結構來看,看似并不會有什么問題,然而當光標放置于超鏈接這三個字后喚醒IME輸入中文時,會發現輸入“測試輸入”這幾個字會被放置于直屬div下,與a標簽平級。

      <div contenteditable>
        <a ><span>超鏈接</span></a>
        <span>文本</span>
      </div>
      
      <div contenteditable>
        <a ><span>超鏈接</span></a>
        測試輸入
        <span>文本</span>
      </div>
      

      在這種情況下我們先前實現的臟DOM檢測就失效了,因為檢查臟DOM的實現是基于data-leaf實現的。此時瀏覽器的輸入表現會導致我們無法正確檢查到這部分內容,除非直接拿data-node行節點來直接判斷,這樣的實現自然不夠好。

      說到這里,先前我發現飛書文檔的實現是a標簽渲染的leaf,而wrap的包裝實現是使用的span直接處理的,并且額外增加了樣式來實現hover效果。直接使用span包裹就不會出現上述問題,而內部的a標簽雖然會導致同樣的問題,但是在leaf下可以觸發臟DOM檢查。

      <div contenteditable>
        <span>
          <a ><span>超鏈接</span></a>
          測試輸入
        </span>
        <span>文本</span>
      </div>
      

      因此就可以在先前的臟DOM檢查基礎上解決了問題,而本質上類似的行為就是瀏覽器默認處理的結果,不同的瀏覽器處理結果可能都不一樣。目前看起來是瀏覽器認為a標簽的結構應該是屬于inline的實現,也就是類似我們的inline-code實現,理論上倒卻是并沒有什么問題,由此我們需要自己來處理這些非受控的問題。

      實際上Quill本身也會出現這個問題,同樣也是臟DOM的處理。而slate并不會出現這個問題,這里處理方案則是通過DOM規避了問題,在a標簽兩端放置額外的&nbsp節點,以此來避免這個問題。當然還引入了額外的問題,引入了新的節點,目前看起來轉移光標需要受控處理。

      <!-- https://github.com/ianstormtaylor/slate/blob/main/site/examples/ts/inlines.tsx -->
      <div contenteditable>
        <a 
          ><span contenteditable="false" style="font-size: 0">&nbsp;</span
          ><span>超鏈接測試輸入</span
          ><span contenteditable="false" style="font-size: 0">&nbsp;</span></a
        ><span>文本</span>
      </div>
      

      瀏覽器兼容性

      在后續瀏覽器的測試中,重新出現了上述提到的a標簽問題,此時并不是由于包裝節點引起的,因此問題變得復雜了很多,主要是各個瀏覽器的兼容性的問題。類似于行內代碼塊,本質上還是瀏覽器IME非受控導致的DOM變更問題,但是在瀏覽器表現差異很大,下面是最小的DEMO結構。

      <div contenteditable>
        <span data-leaf><a href="#"><span data-string>在[:]后輸入:</span></a></span><span data-leaf>非鏈接文本</span>
      </div>
      

      在上述示例的a標簽位置的最后的位置上輸入內容,主流的瀏覽器的表現是有差異的,甚至在不同版本的瀏覽器上表現還不一致:

      • Chrome中會在a標簽的同級位置插入文本類型的節點,效果類似于<a></a>"text"內容。
      • Firefox中會在a標簽內插入span類型的節點,效果類似于<a></a><span data-string>text</span>內容。
      • Safari中會將a標簽和span標簽交換位置,然后在a標簽上同級位置加入文本內容,類似<span><a></a>"text"</span>
      <!-- Chrome -->
      <span data-leaf="true">
        <a ><span data-string="true">超鏈接</span></a>
        "文本"
      </span>
      
      <!-- Firefox -->
       <span data-leaf="true">
        <a ><span data-string="true">超鏈接</span></a>
        <span data-string="true">文本</span>
      </span>
      
      <!-- Safari -->
       <span data-leaf="true">
        <span data-string="true">
          <a >超鏈接</a>
          "文本"
          ""
        </span>
      </span>
      

      因此我們的臟DOM檢查需要更細粒度地處理,僅僅對比文本內容顯然是不足以處理的,我們還需要檢查文本的內容節點結構是否準確。其實最開始我們是僅處理了Chrome下的情況,最簡單的辦法就是在leaf節點下僅允許存在單個節點,存在多個節點則說明是臟DOM

      for (let i = 1; i < nodes.length; ++i) {
        const node = nodes[i];
        node && node.remove();
      }
      

      但是后來發現在編輯時會把Embed節點移除,這里也就是因為我們錯誤地把組合的div節點當成了臟DOM,因此這里就需要更細粒度地處理了。然后考慮檢查節點的類型,如果是文本的節點類型再移除,那么就可以避免Embed節點被誤刪的問題。

      for (let i = 1; i < nodes.length; ++i) {
        const node = nodes[i];
        isDOMText(node) && node.remove();
      }
      

      雖然看起來是解決了問題,然而在后續就發現了FirefoxSafari下的問題。先來看Firefox的情況,這個節點并非文本類型的節點,在臟DOM檢查的時候就無法被移除掉,這依然無法處理Firefox下的臟DOM問題,因此我們需要進一步處理不同類型的節點。

      // data-leaf 節點內部僅應該存在非文本節點, 文本類型單節點, 嵌入類型雙節點
      for (let i = 1; i < nodes.length; ++i) {
        const node = nodes[i];
        // 雙節點情況下, 即 Void/Embed 節點類型時需要忽略該節點
        if (isHTMLElement(node) && node.hasAttribute(VOID_KEY)) {
          continue;
        }
        node.remove();
      }
      

      Safari的情況下就更加復雜,因為其會將a標簽和span標簽交換位置,這樣就導致了DOM結構性造成了破壞。這種情況下我們就必須要重新刷新DOM結構,這種情況下就需要更加復雜地處理,在這里我們加入forceUpdate以及TextNode節點的檢查。

      其實在飛書文檔中也是采用了類似的做法,飛書文檔的a標簽在喚醒IME輸入后,同樣會觸發臟DOM的檢查,然后飛書文檔會直接以行為基礎ReMount當前行的所有leaf節點,這樣就可以避免復雜的臟DOM檢查。我們這里實現更精細的leaf處理,主要是避免不必要的掛載。

      const LeafView: FC = () => {
        const { forceUpdate, index: renderKey } = useForceUpdate();
        LEAF_TO_REMOUNT.set(leafState, forceUpdate);
        return (<span key={renderKey}></span>);
      }
      
      if (isDOMText(dom.firstChild)) {
        // ...
      } else {
        const func = LEAF_TO_REMOUNT.get(leaf);
        func && func();
      }
      

      這里需要注意的是,我們還需要處理零寬字符類型的情況。當Embed節點前沒有任何節點,即位于行首時,輸入中文后同樣會導致IME的輸入內容被滯留在Embed節點的零寬字符上,這點與上述的inline節點是類似的,因此這部分也需要處理。

      const zeroNode = LEAF_TO_ZERO_TEXT.get(leaf);
      const isZeroNode = !!zeroNode;
      const textNode = isZeroNode ? zeroNode : LEAF_TO_TEXT.get(leaf);
      const text = isZeroNode ? ZERO_SYMBOL : leaf.getText();
      const nodes = textNode.childNodes;
      

      到這里,我們的臟DOM檢查已經能夠處理大部分情況了,整體的模式都是React在行DOM結構計算完成后,瀏覽器渲染前進行處理。針對于文本節點以及a標簽的檢查,需要檢查文本與狀態的關系,以及嚴格的DOM結構破壞后的需要直接Remount組件。

      // 文本節點內部僅應該存在一個文本節點, 需要移除額外節點
      for (let i = 1; i < nodes.length; ++i) {
        const node = nodes[i];
        node && node.remove();
      }
      // 如果文本內容不合法, 通常是由于輸入的臟 DOM, 需要糾正內容
      if (isDOMText(textNode.firstChild)) {
        // Case1: [inline-code][caret][text] IME 會導致模型/文本差異
        // Case3: 在單行僅存在 Embed 節點時, 在節點最前輸入會導致內容重復
        if (textNode.firstChild.nodeValue === text) return false;
        textNode.firstChild.nodeValue = text;
        } else {
        // Case2: Safari 下在 a 節點末尾輸入時, 會導致節點內外層交換
        const func = LEAF_TO_REMOUNT.get(leaf);
        func && func();
        if (process.env.NODE_ENV === "development") {
          console.log("Force Render Text Node", textNode);
        }
      }
      

      而針對于額外的文本節點,即本章節中重點提到的瀏覽器兼容性問題,我們需要嚴格地控制leaf節點下的DOM結構。如果僅存在單個文本節點的情況下,是符合設計的結構,而如果是存在多個節點,除了Void/Embed節點的情況外,則說明DOM結構被破壞了,這里我們就需要移除掉多余的節點。

      // data-leaf 節點內部僅應該存在非文本節點, 文本類型單節點, 嵌入類型雙節點
      for (let i = 1; i < nodes.length; ++i) {
        const node = nodes[i];
        // 雙節點情況下, 即 Void/Embed 節點類型時需要忽略該節點
        if (isHTMLElement(node) && node.hasAttribute(VOID_KEY)) {
          continue;
        }
        // Case1: Chrome a 標簽內的 IME 輸入會導致同級的額外文本節點類型插入
        // Case2: Firefox a 標簽內的 IME 輸入會導致同級的額外 data-string 節點類型插入
        node.remove();
      }
      

      樣式組合渲染

      由于我們的編輯器是以immutable提高渲染性能,因此在文本節點變更時若是需要存在連續的格式處理,例如inline-code的樣式實現,就會出現組件不重新渲染問題。具體表現是若是存在多個連續的code節點,最后一個節點長度為1,刪除最后這個節點時會導致前一個節點無法刷新樣式。

      [inline][c]|
      

      這個問題的原因是我們的className是在渲染leaf節點時動態計算的,具體的邏輯如下所示。如果前一個節點不存在或者前一個節點不是inline-code,則添加inline-code-start類屬性,類似的需要在最后一個節點加入inline-code-end類屬性。

      if (!prev || !prev.op.attributes || !prev.op.attributes[INLINE_CODE_KEY]) {
        context.classList.push(INLINE_CODE_START_CLASS);
      }
      context.classList.push("block-kit-inline-code");
      if (!next || !next.op.attributes || !next.op.attributes[INLINE_CODE_KEY]) {
        context.classList.push(INLINE_CODE_END_CLASS);
      }
      

      這個情況同樣類似于Dirty DOM的問題,由于刪除的節點長度為1,因此前一個節點的LeafState并沒有變更,因此不會觸發React的重新渲染。這里我們就需要在行節點渲染時進行糾正,這里的執行倒是不需要像上述檢查那樣同步執行,以異步的effect執行即可。

      /**
       * 編輯器行結構布局計算后異步調用
       */
      public didPaintLineState(lineState: LineState): void {
        for (let i = 0; i < leaves.length; i++) {
          if (!prev || !prev.op.attributes || !prev.op.attributes[INLINE_CODE_KEY]) {
            node && node.classList.add(INLINE_CODE_START_CLASS);
          }
          if (!next || !next.op.attributes || !next.op.attributes[INLINE_CODE_KEY]) {
            node && node.classList.add(INLINE_CODE_END_CLASS);
          }
        }
      }
      

      雖然看起來已經解決了問題,然而在React中還是存在一些問題,主要的原因此時的DOM處理是非受控的。類似于下面的例子,由于React在處理style屬性時,只會更新發生變化的樣式屬性,即使整體是新對象,但具體值與上次渲染時相同,因此React不會重新設置這個樣式屬性。

      // https://playcode.io/react
      import React from "react";
      export function App() {
        const el = React.useRef();
        const [, setState] = React.useState(1);
        const onClick = () => {
          el.current && (el.current.style.color = "blue");
        }
        console.log("Render App")
        return (
          <div>
            <div style={{ color:"red" }} ref={el}>Hello React.</div>
            <button onClick={onClick}>Color Button</button>
            <button onClick={() => setState(c => ++c)}>Rerender Button</button>
          </div>
        );
      }
      

      因此,在上述的didPaintLineState中我們主要是classList添加類屬性值,即使是LeafState發生了變更,React也不會重新設置類屬性值,因此這里我們還需要在didPaintLineState變更時刪除非必要的類屬性值。

      public didPaintLineState(lineState: LineState): void {
        for (let i = 0; i < leaves.length; i++) {
          if (!prev || !prev.op.attributes || !prev.op.attributes[INLINE_CODE_KEY]) {
            node && node.classList.add(INLINE_CODE_START_CLASS);
          } else {
            node && node.classList.remove(INLINE_CODE_START_CLASS);
          }
          if (!next || !next.op.attributes || !next.op.attributes[INLINE_CODE_KEY]) {
            node && node.classList.add(INLINE_CODE_END_CLASS);
          } else {
            node && node.classList.remove(INLINE_CODE_END_CLASS);
          }
        }
      }
      

      總結

      在先前我們實現了半受控的輸入模式,這個輸入模式同樣是目前大多數富文本編輯器的主流實現方式。在這里我們關注于瀏覽器ContentEdiable模式輸入的默認行為造成的DOM結構問題,并且通過臟DOM檢查的方式來修正這些問題,以此來保持編輯器的嚴格DOM結構。

      當前我們主要關注的是編輯器文本的輸入問題,即如何將鍵盤輸入的內容寫入到編輯器數據模型中。而接下來我們需要關注于輸入模式結構化變更的受控處理,即回車、刪除、拖拽等操作的處理,這些操作同樣也是基于輸入相關事件實現的,而且通常會涉及到文本的結構變更,屬于輸入模式的補充。

      每日一題

      參考

      posted @ 2025-10-20 11:05  WindRunnerMax  閱讀(186)  評論(0)    收藏  舉報
      ?Copyright    @Blog    @WindRunnerMax
      主站蜘蛛池模板: 免费无码中文字幕A级毛片| 国产激情艳情在线看视频| 亚洲狼人久久伊人久久伊| 元阳县| 国产精品十八禁在线观看| 四虎永久免费精品视频| 久久精品国产福利一区二区| 国产怡春院无码一区二区| 理论片午午伦夜理片影院99| 亚洲精品无码你懂的| 婷婷四虎东京热无码群交双飞视频| 无码中文字幕av免费放| 国产精品极品美女自在线观看免费| 国产一区二区午夜福利久久| 沾化县| av日韩在线一区二区三区| 国产成人免费永久在线平台| 日韩精品国产二区三区| 久久视频在线视频| 加勒比无码人妻东京热| 久久人妻无码一区二区| 国产精品中文字幕视频| 特黄做受又粗又大又硬老头| 国产真实伦在线观看视频| 精品中文字幕人妻一二| 国产黄色一区二区三区四区| 国内少妇人妻丰满av| 国产精品自在拍在线播放| 亚洲天堂av日韩精品| 乱码视频午夜在线观看| 日本污视频在线观看| 久久婷婷五月综合97色直播| 亚洲精品电影院| 国产精品一区二区三区黄| 亚洲综合一区二区国产精品| 国色天香中文字幕在线视频| 偷窥少妇久久久久久久久| 国产一区二区日韩经典| 性中国videossexo另类| 九九热免费精品在线视频| 国产性色的免费视频网站|