Slate Ref 和 Transform

66 阅读7分钟
  1. 为什么需要 Ref?

Slate 中的节点路径会因为应用到文档的 op 而随时发生改变。比如[2]路径,会因为执行了一个 removeNode [1] 的操作路径从[2]变为[1]Ref 作用是会根据 op 自动保持住正确的路径,从而确保拿到的路径不会错误。

// 对 [2] 使用 ref
const pathRef = Editor.PathRef(editor, [2]);
// 删除 [1]
Transform.removeNode(editor, [1]);
// ref 会根据 op 操作自动保持住正确的路径
console.log(pathRef.current); // [1]

之所以节点路径能够保持正确,是因为内部 transfrom 来就是处理这种事情。分别有 path.transfrom, range.transfrom, point.transfrom。下面我们就来看一下各种 transfrom 是如何处理的。

  1. Path.Transform 纠正 path

pathTransform 的参数主要有三个:

  • path :需要被纠正的 path
  • op :当前执行的 operation
  • options.affinityaffinity 表示的是方向,
 /** * Transform a point by an operation. */
transform(
  path: Path,
  op: Operation,
  options: { affinity?: 'forward' | 'backward' | null } = {}
): Point | null {
    return produce(path, p => {
        const { affinity = 'forward' } = options
        const { path, offset } = p
        // ... Path.transform implementation
    });
}
  1. insert_node

{ type: 'insert_node', path: [0, 1], node: Text } 这个op 为例子受到插入节点影响的有三种:

  1. 完全相同的路徑 (对应右图的红色节点)
  2. 位于 op 右边的节点 (对应右图紫色节点)
  3. 以 op 为祖先的路径 (对应右图的绿色节点)

op.path 的位置插入一个新节点,上诉的节点相当于都向右挪动,对应的 path 改变了,需要在op.path.length - 1 的地方都需要 +1

case 'insert_node': {
  const { path: op } = operation

  if (
    Path.equals(op, p) ||
    Path.endsBefore(op, p) ||
    Path.isAncestor(op, p)
  ) {
    p[op.length - 1] += 1
  }
  break
}
  1. remove_node

{ type: 'remove_node', path: [0, 1], node: Element } 为例子受到插入节点影响的有俩种:

  1. 完全相同的路徑 & 以 op 为祖先的路径 (对应左图的红色节点)

    1. 因为删除了 Element 其子节点也会跟着一起删除。在最新的视图找不到了所以直接返回 null。
  2. 位于 op 右边的节点 (对应右图紫色节点)

    1. 删除一个节点,紫色节点相当于都向左前进,需要在op.path.length - 1 的地方 -1

case 'remove_node': {
  if (Path.equals(op.path, p) || Path.isAncestor(op.path, p)) {
    return null
  } else if (Path.endsBefore(op.path, p)) {
    p[op.path.length - 1] -= 1;
  }
  break;
}
  1. split_node

{ type: 'split_node', path: [0], position: 1 } 为例子受到合并节点影响的有三种:

  1. 完全相同的路徑 (对应左图的黑色节点) 。这里受到會受到 options.affinity 的影响,

    1. forward 表示向前, 需要将原始路徑的 index + 1。指向新增的 element 节点。
    2. backward 我理解就是保持不动,则不需要处理。
       case 'split_node': {
           const { path: op, position } = operation
           
           if (Path.equals(op, p)) {
             if (affinity === 'forward') {
               p[p.length - 1] += 1
             } else if (affinity === 'backward') {
               // Nothing, because it still refers to the right path.
             } else {
               return null
             }
           }
           
           // xxx
           
           break
       }
       ```
    
    
  2. 位于 op 右边的节点 (对应右图紫色节点) 。因为 splitNode 的时候会在 [1] 这个地方插入新节点,对于紫色节点来说相当于后退了。所以需要在op.path.length - 1 的地方 +1

       else if (Path.endsBefore(op, p)) {
          p[op.length - 1] += 1
       }
       ```
    
    
  3. 以 op 为祖先的路径并且是在 position 位置之后 (对应右图的橘色节点以及左图的橘色框区域) 。这里需要分为2步理解

    1. 原节点会移动到后一个节点,相当于后退了。所以需要在op.path.length - 1的地方 +1
    2. 原节点位置在其前面有 position 个节点。移动之后前面相当于前面没有节点了,所以需要在 op.length 地方 -position
        else if (Path.isAncestor(op, p) && path[op.length] >= position) {
            p[op.length - 1] += 1
            p[op.length] -= position
         }
        ```
    

  1. merge_node

{ type: 'merge_node', path: [1], position: 1 } 为例子受到合并节点影响的有两种:

  1. 完全相同的路徑 (对应左图的绿色节点) 和位于 op 右边的节点 (对应右图紫色节点)

    1. 对于紫色节点来说相当于前面少一个节点,紫色节点相当于都向左前进,需要在op.path.length - 1 的地方 -1
    2. 对于绿色节点,其实在最新的model已经找不到了,但是 slate 处理方式也是在op.path.length - 1 的地方 -1
  2. 以 op 为祖先的路径 (对应右图的红色节点) 。这里需要分为2步理解

    1. 节点会合并到前一个节点,相当于前进了。所以需要在op.path.length - 1 的地方 -1
    2. 节点是在前一个节点的 position 插入。相当于前面有 position 个节点,所以需要在 op.length 地方 +position

case 'merge_node': {
    const { path: op, position } = operation
    
    if (Path.equals(op, p) || Path.endsBefore(op, p)) {
      p[op.length - 1] -= 1
    } else if (Path.isAncestor(op, p)) {
      p[op.length - 1] -= 1
      p[op.length] += position
    }
    
    break
}
  1. move_node

move_node 情况比较复杂,我花了很长时间才理解这段代码😓😓😓😓😓😓,需要分3种大情况讨论:

  • 自己移动到自己
  • 同层级移动
  • 非同层级的移动
  1. 自己移动到自己

为了避免死循环,这种情况直接 return;

case 'move_node': {
  const { path, newPath } = op;
  if (Path.equals(path, newPath)) {
    return;
  }
}
  1. 同层级移动

{ type: 'move_node', path: [1], newPath: [3] }为例子,受影响的主要是下面4部分:

  1. path 完全相同的路徑 & 以 path 为祖先的路径 (对应左图/右图的红色节点)

    • 移动的时候相当于 path 整体移动到 newPath。所以只需要在 p 中属于 path 替换成 newPath 即可。比如[1,0] 属于 path的部分就是[1], 所以只需要将 [1] 替换成 [3] 变成 [3, 1]

      • if (Path.isAncestor(path, p) || Path.equals(path, p)) {
            const copyPath = newPath.slice();
            
            // 非同层级移动逻辑
        
            // newPath + 原来其子节点的位置
            return copyPath.concat(p.slice(path.length));
        } 
        
  2. newPath 完全相同的路徑 & 以 newPath 为祖先的路径 (对应左图/右图的绿色节点)

    1. 同层节点的移动如果 pathnewPath 前面的节点,移动到 newPath 位置,那么相当于前面少了一个节点,绿色节点前进了。所以需要在 path.length - 1 地方 -1(从左到右观察绿色节点)
    2. 同层节点的移动如果 pathnewPath 后面的节点,移动到 newPath 位置,那么相当于前面多了一个节点,绿色节点后退了。所以需要在 path.length - 1 地方 +1(从右到左观察绿色节点)
        else if (Path.isAncestor(newPath, p) || Path.equals(newPath, p)) {
            if (Path.isSibling(newPath, path)) {
              if (Path.endsBefore(path, p)) {
                p[path.length - 1] -= 1
              } else {
                p[path.length - 1] += 1
              }
            } else {
              // 非同层级移动逻辑
            }
        } 
        ```
    
    
  3. 位于 newPath 右边的节点 (对应左图/右图橘色节点)

    • 同层级移动 newPath 右边的节点位置不会变化

  4. 位于 path 右边的节点 (对应左图/右图紫色节点) 。相当于前面的节点移动了,紫色节点前进,所以需要在 path.length - 1 地方 -1

       else if (Path.endsBefore(path, p)) {
           p[path.length - 1] -= 1;
       }
       ```
    

  1. 非同层级移动

{ type: 'move_node', path: [1], newPath: [3] }为例子,受影响的主要是下面4部分:

  1. path 完全相同的路徑 & 以 path 为祖先的路径 (对应左图/右图的红色节点)

    1. 因为跨层级的时候如果 pathnewPath 前面,那么从 path 移动到 newPath 相当于前面少了一个节点,所以需要在path.length - 1地方 -1

    2. 移动的时候相当于 path 整体移动到 newPath。所以只需要在 p 中属于 path 的部分替换成 newPath 即可

    3. if (Path.isAncestor(path, p) || Path.equals(path, p)) {
          const copyPath = newPath.slice();
          
          // path.length < newPath.length 表示跨层级,跨层级删除了一个节点,需要改变位置需要 -1
          if (Path.endsBefore(path, newPath) && path.length < newPath.length) {
            copyPath[path.length - 1] -= 1;
          }
      
          // newPath + 原来其子节点的位置
          return copyPath.concat(p.slice(path.length));
      } 
      
  2. newPath 完全相同的路徑 & 以 newPath 为祖先的路径 (对应左图/右图的绿色节点)

    1. 如果 path 是 newPath 前面的节点,移动到 newPath 位置,那么相当于前面少了一个节点,绿色节点前进了。所以需要在 path.length - 1 地方 -1(从左到右/从右到左观察绿色节点)

    2. path 节点移动到 newPath 位置,那么相当于前面多了一个节点,绿色节点后退了。所以需要在 newPath.length - 1 地方 +1

    3. else if (Path.isAncestor(newPath, p) || Path.equals(newPath, p)) {
          if (Path.isSibling(newPath, path)) {
            if (Path.endsBefore(path, p)) {
              p[path.length - 1] -= 1
            } else {
              p[path.length - 1] += 1
            }
          } else {
            if (Path.endsBefore(path, p)) {
              p[path.length - 1] -= 1
            }
      
            p[newPath.length - 1] += 1
          }
      } 
      
  3. 位于 newPath 右边的节点 (对应左图/右图橘色节点)

    1. 如果 pathnewPath 前面的节点,移动到 newPath 位置,那么相当于前面少了一个节点,绿色节点前进了。所以需要在 path.length - 1 地方 -1
    2. path 节点移动到 newPath 位置,那么相当于前面多了一个节点,绿色节点后退了。所以需要在 newPath.length - 1 地方 +1
    3. else if (Path.endsBefore(newPath, p)) {
          if (Path.endsBefore(path, p)) {
            p[path.length - 1] -= 1
          }
          
          p[newPath.length - 1] += 1
      } 
      
  4. 位于 path 右边的节点 (对应左图/右图紫色节点) 。相当于前面的节点移动到别的地方,紫色节点前进,所以需要在 path.length - 1 地方 -1

    1. else if (Path.endsBefore(path, p)) {
          p[path.length - 1] -= 1;
      }
      

  1. point.Transfrom 纠正 point

pointTransform 的参数主要有三个:

  • point :需要被纠正的 point
  • op :当前执行的 operation
  • options.affinity :affinity 表示的是方向,
 /** * Transform a point by an operation. */
transform(
  point: Point,
  op: Operation,
  options: { affinity?: 'forward' | 'backward' | null } = {}
): Point | null {
    return produce(point, p => {
        const { affinity = 'forward' } = options
        const { path, offset } = p
        // ... Point.transform implementation
    });
}
  1. insert_text

假设在红色区域插入0文本,只有 pathop.path 为同一路径才需要纠正:

  • 如果 op.offset 小于 offset (如下图蓝色point),表示插入的文字位于需要纠正的 point 之前,因为在其前面插入了文字的关系,因此p.offset 需要增加 op.text 的长度。
  • 如果op.offset 等于 offset(如下图红色point),则表示插入的文字位于需要纠正的 point 的位置。根据 affinity 做处理,如果是 forward 才会向前移动op.text 长度,否则不处理。
case 'insert_text': {
  if (Path.equals(op.path, path)) {
      if (
          op.offset < offset || 
          (op.offset === offset && affinity === 'forward')
      ) {
        p.offset += op.text.length
      }
  }
}

  1. remove_text

假设在红色区域删除34文本,此时的 op 为 { path: xxx, type: 'remove_text' , offset: 2, text: '34' }。只有 pathop.path 为同一路径才需要纠正:

  • 如果 op.offset 小于等于 offset (如下图蓝色和黑色point),表示删除的文字位于需要纠正的 point 之前,因为在其前面删除了文字的关系,

    • 对于蓝色 point 来说p.offset 需要扣除 text 的长度。
    • 红色 point来说,就需要扣除 offset - op.offset。其实就是回到 op.offset 的位置。
case 'remove_text': {
  if (Path.equals(op.path, path) && op.offset <= offset) {
    p.offset -= Math.min(offset - op.offset, op.text.length)
  }

  break
}

  1. split_node

{ type: 'split_node', position: 1, path: [0,0,0] }为例子:因为按照 1 分割文本节点,

  • 如果此时 offset > 1 ,光标是要跟随到下一个节点的,下一个节点的是从0开始,所以需要 p.offset -= op.position;
  • 如果位置在 offset = position,那就根据 affinity 做处理,forward 就跟随到下一个节点。
case 'split_node': {
  // 自身的 op 需要改变 offset
  if (Path.equals(op.path, path)) {
    if (
      op.position < p.offset || 
      (op.position === p.offset && affinity === 'forward')
    ) {
      p.offset -= op.position;
      p.path = Path.transform(path, op, { affinity: 'forward' })!;
    }
  } else {
    p.path = Path.transform(path, op, options)!;
  }
  break;
}

  1. insert_node

不会修改到 offset 的值,所以实际上只需要修改 path 即可。

case 'insert_node': {
  // 指向 insertNode 的尾巴
  p.path = Path.transform(path, op)!;
  break;
}
  1. remove_node

remove_node 本身也不会修改到 offset 的值,所以实际上只需要修改 path 即可。但是因为删除了 Element 其子节点也会跟着一起删除,所以如果 op.pathpath 为相同路径或者 op.path path的祖先直接返回 null。原因是在最新的视图找不到了。

case 'remove_node': {
      /**
       * 如果 point 是本身或者在其父节点,
       */
      if (Path.equals(op.path, path) || Path.isAncestor(op.path, path)) {
        return null;
      }
 
      p.path = Path.transform(path, op, options)!;
      break;
}

  1. merge_node

case 'merge_node': {
  /**
   * A,B 两个节点,B mergeTo A,
   * 此时 B 的 offset 应该变为 A.length+B.length, 
   * B 的 path 应该变为 A 的 path
   */
  if (Path.equals(op.path, path)) {
    p.offset += op.position;
  }
  p.path = Path.transform(path, op)!;
  break;
}

  1. move_node

不会修改到 offset 的值,所以实际上只需要修改 path 即可。

case 'move_node': {
  p.path = Path.transform(path, op)!;
  break;
}
  1. range.Transfrom 纠正 range

Range 本质是由两个 point 组成的,所以对 range 的transfrom 其实就是对 point 的transfrom。这里主要是针对 affinity 参数的控制,最後將 range 的 anchorfocus 丟入 Point.transform 进行转换

 /** * Transform a range by an operation. */

transform(
  range: Range,
  op: Operation,
  options: {
    affinity?: 'forward' | 'backward' | 'outward' | 'inward' | null
  } = {}
): Range | null {
    
    // 对 affinity 的处理

    const anchor = Point.transform(r.anchor, op, { affinity: affinityAnchor })
    const focus = Point.transform(r.focus, op, { affinity: affinityFocus })

    if (!anchor || !focus) {
      return null
    }

    r.anchor = anchor
    r.focus = focus
  })
},
  • forward:split_node 的时候,节点位置指向下一个节点。

  • backward:split_node 的时候,节点位置保持不变。

  • inward (内向):

    • 如果 range 是从前到后,进行 split_node 的时候,开始节点指向下一个节点, 结束节点保持不变。
    • 如果 range 是从后到前,进行 split_node 的时候,开始节点保持不变,结束节点指向下一个。
    • 如果 range 是闭合的,那就指向下一个。

split_node 是按照光标尾部 focus split,随后按照光标头部 anchor split

  • outward (外向):

    • 如果 range 是从后到前,进行 split_node 的时候,开始节点指向下一个节点, 结束节点保持不变。
    • 如果 range 是从前到后,进行 split_node 的时候,开始节点保持不变,结束节点指向下一个。

split_node 是按照光标尾部 focus split,随后按照光标头部 anchor split

const { affinity = 'inward' } = options
let affinityAnchor: 'forward' | 'backward' | null
let affinityFocus: 'forward' | 'backward' | null

if (affinity === 'inward') {
    const isCollapsed = Range.isCollapsed(r)
    if (Range.isForward(r)) { // range 从前到后
      affinityAnchor = 'forward' // 开始节点指向下一个节点
      affinityFocus = isCollapsed ? affinityAnchor : 'backward'  // 结束节点保持不变
    } else {
      affinityAnchor = 'backward' // 开始节点不变
      affinityFocus = isCollapsed ? affinityAnchor : 'forward' // 结束节点指向下一个
    }
}

else if (affinity === 'outward') {
    if (Range.isForward(r)) {
      affinityAnchor = 'backward' // 开始节点不变
      affinityFocus = 'forward' // 结束节点指向下一个
    } else {
      affinityAnchor = 'forward' // 开始节点指向下一个节点
      affinityFocus = 'backward' // 结束节点保持不变
    }
}

else {
    affinityAnchor = affinity
    affinityFocus = affinity
}
  1. 写在最后 & 参考资料

原文链接:Slate Ref 和 Transform

欢迎 star:github.com/JokerLHF/mi…