Day 28. slate × Transforms × Node

https://ithelp.ithome.com.tw/upload/images/20211013/20139359KnxKbPp9bM.png

最後终於来到了我们最後一个章节:『 Transforms 』。

Transform 在 slate package 里头也是占了举足轻重的地位,它提供了最 high-level 操作 Slate editor 的方法,让开发者在了解 Slate 的基本运作概念以後就能直接透过这些 apis 无痛开发。

但 High-level 的 Transform 与 Low-level 的 Operation 之间还是存在着差距的,即便名义上是说一组 Transform 是由复数个 Operations 所组成,我们仍不难猜想在各个 Transform methods 里一定有事先为我们挡掉了各种 edge-cases 甚至下了不少优化操作效能的功夫。

如果我们选择每个 Transform methods 都深入探讨里头的程序码的话 ... 另一个 30 天可能又过去了吧 ...

经过笔者缜密的思考(其实就是想省点事而已XD)後,决定让这一整个章节以偏向 reference 的形式进行,刚好官方 document 上对各个 methods 的介绍也不太完整,因此这边会尽可能补足官方 document 遗漏解释的内容,会以介绍各个 Transform methods 的功用以及传入的参数的用途为主。所以除非解释上必较,否则我们会尽可能不过度深入程序码的内容。

以下我们先从整个 NodeTrasforms 的通用 Options 开始介绍起。

NodeOptions


interface NodeOptions {
	at?: Location
	match?: NodeMatch<T>
	mode?: ('highest' | 'lowest') | ('all' | 'highest' | 'lowest')
	voids?: boolean
}
  • at : 欲将节点插入编辑器的 Location ,预设值为编辑器的 selection value

  • match : 自定义的 match function ,详细的解释请看 Day 12

  • mode :这个参数主要用於 Editor.nodes method 的 mode option ,以 insertNodes 为例:

    insertNodes(...) {
    	// ...
    
    	const [entry] = Editor.nodes(editor, {
        at: at.path,
        match,
        mode,
        voids,
      })
    
    	// ...
    }
    

    决定 Editor.nodes method 要以哪种模式遍历 Slate node tree 。它分成三种模式:

    • 'all'

      at 出发,以正常的『垂直遍历』的方式 yield 出查找到的节点

    • 'highest'

      只会查找并 yield 出最浅层的节点

    • 'lowest'

      只会查找并 yieldat 所涵盖的 branch 最底层的节点

  • voids

    决定在这个 Transform method 有呼叫到的所有操作行为中,是否要略过或避开 void nodes 的出现 。

NodeTransforms


insertNodes


  • 用途:『插入单一/复数节点进编辑器』

  • 参数:

    • editor: Editor
    • nodes: Node | Node[]
    • options: InsertNodesOptions
  • options :

    interface InsertNodesOptions extends NodeOptions {
    	hanging?: boolean
    	select?: boolean
    }
    
    • hanging

      如果传入的 at 为 Range type 的话,这个 value 会决定 Range 是否要另外修正为 unhanging type 。

      hanging 在 Slate 里头的意思代表『这段 Range 涵盖到了不存在的节点』。

      我们假设目前的 Slate Document 如下:

      [{text: 'one '}, {text: 'two', bold: true}, {text: ' three'}]
      

      这时使用者看到的显示方式应该如下:

      one two three

      假设使用者选取了 "two" ,此时的 selection 会有几种 anchorfocus points 的可能性出现

      // 1 -- no hanging
      {
        anchor: { path: [1], offset: 0 },
        focus: { path: [1], offset: 3 }
      }
      
      // 2 -- anchor hanging
      {
        anchor: { path: [0], offset: 4 },
        focus: { path: [1], offset: 3 }
      }
      
      // 3 -- focus hanging
      {
        anchor: { path: [1], offset: 0 },
        focus: { path: [2], offset: 0 }
      }
      
      // 4 -- both hanging
      {
        anchor: { path: [0], offset: 4 },
        focus: { path: [2], offset: 0 }
      }
      

      当我们传入 hanging: false 时, Slate 就会将这组 Range 传入 Editor.unhangRange method 里确保 Range 维持在第一种情形。

    • select

      决定是否更新编辑器的 selection ,如果没有传入 at 参数,以编辑器的 selection 位置去新增节点的话则会强制将 select 设为 true

liftNodes


  • 用途:将特定 Location 指向的内容,於 Document tree 里向上提升一个层级。如果有必要的话会将它的父层节点一分为二。

  • 参数

    • editor: Editor
    • options: LiftNodesOptions
  • options :

    interface LiftNodesOptions extends NodeOptions {}
    

这个 method 限制无法提升路径长度小於 2 的节点(路径长度为 1 的节点上层就是 Editor root 了)

if (path.length < 2) {
  throw new Error(
    `Cannot lift node at a path [${path}] because it has a depth of less than \`2\`.`
  )
}

这个 method 又可以分成四种可能性:

  1. 要提升的节点为其父层节点里的唯一子节点:向上提升并移除父层节点(因为它不再含有任何子节点了)
  2. 要提升的节点为同层节点的第一顺位:将其移动到父层节点的原本路径
  3. 要提升的节点为同层节点的最後一个顺位:将其移动到父层节点的後一个 sibling 位置
  4. 其余状况则将要提升的节点的後一个 sibling 节点作为基准点,将父层节点拆分为二,并将要提升的节点移动到原始父层节点的後一个 sibling 。
这边附上主要判断式段落的程序码,变数的命名上蛮清楚的可以大致猜想他们的用途,再搭配上述讲解的四种可能性应该就能理解程序码的撰写逻辑了。但如果读者看得很吃力的话也可以再将套件 clone 下来搭配着看会更轻松一点。
if (length === 1) {
  const toPath = Path.next(parentPath)
  Transforms.moveNodes(editor, { at: path, to: toPath, voids })
  Transforms.removeNodes(editor, { at: parentPath, voids })
} else if (index === 0) {
  Transforms.moveNodes(editor, { at: path, to: parentPath, voids })
} else if (index === length - 1) {
  const toPath = Path.next(parentPath)
  Transforms.moveNodes(editor, { at: path, to: toPath, voids })
} else {
  const splitPath = Path.next(path)
  const toPath = Path.next(parentPath)
  Transforms.splitNodes(editor, { at: splitPath, voids })
  Transforms.moveNodes(editor, { at: path, to: toPath, voids })
}

mergeNodes


  • 用途:将特定 Location 指向的内容,与它同层的前一个 sibling node 做合并。并会移除合并过後所产生的空节点。

  • 参数:

    • editor: Editor
    • options: MergeNodesOptions
  • options :

    interface MergeNodesOptions extends NodeOptions {
    	hanging?: boolean
    }
    
    • hanging

      insertNodeshanging option 一样,决定是否将 at 为 Range 时的 value 修正成为 unhanging type 。

moveNodes


  • 用途:将单个/复数个节点从旧的 Location 搬迁到新的 Path

  • 参数:

    • editor: Editor
    • options: MoveNodesOptions
  • options :

    interface MoveNodesOptions extends NodeOptions {
    	to: Path
    }
    
    • to

      欲将 at 指向的节点搬迁到的新路径( Path )

removeNodes


  • 用途:将 at Location 指向的单个/复数个节点从 Document 中移除

  • 参数:

    • editor: Editor
    • options: RemoveNodesOptions
  • options :

    interface RemoveNodesOptions extends NodeOptions {
    	hanging?: boolean
    }
    
    • hanging

      insertNodeshanging option 一样,决定是否将 at 为 Range 时的 value 修正成为 unhanging type 。

setNodes


  • 用途:为 at Location 指向的节点设置新属性

  • 参数:

    • editor: Editor

    • props: Partial<Node>

      欲设置的新属性

    • options: SetNodesOptions

  • options :

    interface SetNodesOptions extends NodeOptions {
    	hanging?: boolean
      split?: boolean
    }
    
    • hanging

      insertNodeshanging option 一样,决定是否将 at 为 Range 时的 value 修正成为 unhanging type 。

    • split

      at 为 Range type 时,决定是否将节点拆分开来。

splitNodes


  • 用途:拆分 Location 指向的节点。

  • 参数:

    • editor: Editor
    • options: SplitNodesOptions
  • options :

    interface SplitNodesOptions extends NodeOptions {
    	always?: boolean
      height?: number
    }
    
    • always

      这个布林值会决定,如:作为基准的子节点的顺位为整个子层节点的边境(第一或最後顺位)。这类实际上不需要拆分父层节点的状况是否仍要强行拆分。

    • height

      欲拆分的父层节点与 at Location 指向的节点所相差的层级高度

『拆分节点』这项功能里头针对了许多额外的情形去覆盖了 options 里头的 value ,建议读者在使用它时先阅览过一遍里头的功能,大致了解里头额外处理了哪些情境後再去使用会比较顺心一些。

unsetNodes


  • 用途:取消 at Location 指向的节点属性设置

  • 参数:

    • editor: Editor

    • props: Partial<Node>

      欲取消设置的属性

    • options: UnsetNodesOptions

  • options :

    interface UnsetNodesOptions extends NodeOptions {
      split?: boolean
    }
    
  • split

    这个 method 基本上就是多经过一层简单的处理後,呼叫 setNodes method ,因此这里的 split option 只是原封不动地作为传给 setNodes method 的参数而已。

unwrapNodes


  • 用途:将 at Location 指向的节点内容展开并提升至上一层的位置,如果传入的 at 为 Range type 则会拆分父层节点,为了确保只有展开 Range 涵盖的内容

  • 参数:

    • editor: Editor
    • options: UnwrapNodesOptions
  • options :

    interface UnwrapNodesOptions extends NodeOptions {
      split?: boolean
    }
    
  • split

    at 为 Range type 时,决定是否将节点拆分开来。

这个 method 主要的工作内容是因应各种传入的 atmatch 参数来决定要丢入进 Transforms.liftNodes method 的内容。

如果传入的 at 为 Path ,要提升的内容则为 Path 指向的节点涵盖到的所有文字作为 Range 传入到 Transforms.liftNodes

如果传入的 at 为 Range type 同时 split 参数设为 true ,才会去寻找 at Range 与欲展开的节点之间的文字交集,并丢入到 Transforms.liftNodes 由它来展开 Range 内的文字内容并拆分父层节点,否则传入的 Range 仍会以要提升的节点为单位去涵盖节点内的所有文字。

*程序码的内容有点繁琐,有兴趣的读者再麻烦自行前往查阅 ? *

wrapNodes


  • 用途:将 element 节点里的 at Location 指向的内容包装进一个新的 container 节点

  • 参数:

    • editor: Editor

    • element: Element

      涵盖了 at Location 的父层 container 节点,因应不同的 Block-type 会决定後续遍历节点所传入的 match 参数:

      if (match == null) {
        if (Path.isPath(at)) {
          match = matchPath(editor, at)
        } else if (editor.isInline(element)) {
          match = n => Editor.isInline(editor, n) || Text.isText(n)
        } else {
          match = n => Editor.isBlock(editor, n)
        }
      }
      
      // ...
      
      const matches = Array.from(
        Editor.nodes(editor, { at: a, match, mode, voids })
      )
      
      if (matches.length > 0) {
      	// wrapNodes implementation
      }
      
    • options: WrapNodesOptions

  • options :

    interface WrapNodesOptions extends NodeOptions {
    	split?: boolean
    }
    
    • split

      at 为 Range type 时,决定是否将节点拆分开来。

如果传入的 at 为 Range type 同时 split 参数设为 true ,则会先将 Range 所涵盖到的文字范围与其之外的文字边界先做节点拆分,确保只有 at 涵盖到的文字集合被包装进新的 container 节点:

if (split && Range.isRange(at)) {
  const [start, end] = Range.edges(at)
  const rangeRef = Editor.rangeRef(editor, at, {
    affinity: 'inward',
  })
  Transforms.splitNodes(editor, { at: end, match, voids })
  Transforms.splitNodes(editor, { at: start, match, voids })
  at = rangeRef.unref()!

  if (options.at == null) {
    Transforms.select(editor, at)
  }
}
剩下的就是包装新节点所需的相关操作了,一样请有兴趣的读者自行前往查阅

<<:  DAY28:VM安装套件以及GCP注意事项

>>:  Day 28 - 发表作品 - 输出与使用至不同平台

[前端暴龙机,Vue2.x 进化 Vue3 ] Day11.列表渲染

当我们有很多重复的架构,内容却不一样,以旧有无框架的开发,我们可能就需要手动一笔一笔的刻出来,更进步...

Day6|【Git】提交档案给 Git 控管 - git status 、 git add 指令

接下来让我们开始熟悉 Git 的操作流程。 使用 Git 的时候,我们会常看见以下四个指令: git...

Day15 用python写UI-聊聊Spinbox

第15天~~~完成了一半的铁人赛,之後也要继续加油! 今天要讲的内容是Spinbox,後面有几个实例...

#16. Quiz App(原生JS版)

#16. Quiz App 所谓Quiz App就是提供给用户答题的小应用,包含数个选择题,选完一个...

D24 - 如何用 Apps Script 自动化地创造与客制 Google Sheet?(ㄧ)自动化创造图表并放到报告中

今天的目标: 要怎麽针对特定资料,固定地创造图表?现在用到图表的机会越来越多,很多时候我们会需要创造...