块状编辑器开发 - 节点样式修改

591 阅读1分钟

通常,在开发编辑器的时候,为了对每一个文字进行样式独立编辑,我们会给这个字段添加一些元信息,比如颜色,字体,大小等等配置。 上图对应的数据结构为

[['加粗',['b'],['斜体',['i'],['下划线',['s']]

在进行编辑的时候,先获得框选位置,在进行数据结构截断重组合并。

比如,我们框选了上图中的部分,增加加粗样式

实际上,此时的数据结构转换为了

[['加粗',['b'],['斜',['i'],['体',['i','b'],['下划',['b','s'],['线',['s']]

再进行合并

[['加粗',['b'],['斜',['i'],['体下划',['i','b'],['线',['s']]

伪代码实现

  1. document.getSelection,获得当前框选的位置,包含4个参数 开始 dom 节点,开始节点中的光标位置,结束 dom 节点,结束节点中的光标为止。

  2. 根据4个参数,进行数据分割,判断条件为

    • 将当前数组,切割为3大块,编辑前区域,编辑中区域,编辑后区域
    const preTitle = title.slice(0, startPoint.index)
    const lastTitle = title.slice(endPoint.index + 1)
    let editTitle = title.slice(startPoint.index, endPoint.index + 1)
    
    • 在编辑中区域进行分割,如果开始/结束的光标位置,与当前dom的text节点,长度不一致,则将原节点切割为2个
    if (startPoint.offset !== 0) {
      // 如果开始切割点不是0的话,则切割为两个
      const titleItem = editTitle.shift()
      const newTitle = [titleItem[0].substr(0, startPoint.offset), titleItem[1] || []]
      const oldTitle = [titleItem[0].substr(startPoint.offset), titleItem[1] || []]
      preTitle.push(newTitle)
      editTitle.unshift(oldTitle)
    }
    const length = get(title, `${endPoint.index}[0].length`, 0)
    if (endPoint.offset !== length) {
      // 如果结束的切割点不是末端的话,则切割为两个
      const titleItem = editTitle.pop()
      const newTitle = [titleItem[0].substr(0, endPoint.offset - startPoint.offset), titleItem[1] || []]
      const oldTitle = [titleItem[0].substr(endPoint.offset - startPoint.offset), titleItem[1] || []]
      editTitle.push(newTitle)
      lastTitle.unshift(oldTitle)
    }
    
    • 对正在编辑的区域,进行样式重置
    editTitle = editTitle.map((e, index) => {
      // 对样式进行修改
      let oldType = get(e, '1', []).map(t => ({ key: t[0], value: t[1] }))
      const newTypes = [{ key: type[0], value: type[1] }] 
      newTypes.forEach(newObj => {
        const isIncludeKey = oldType.findIndex(oldTypesObj => oldTypesObj.key === newObj.key) > -1
        if (isIncludeKey) {
          // 如果样式的 key 存在,则是移除
          const isIncludeValue = oldType.findIndex(oldTypesObj => newObj.value === oldTypesObj.value) > -1
          oldType = oldType.filter(f => newObj.key !== f.key)
          if (!isIncludeValue) {
            // 如果样式的值不同,表明是修改,则需要再push一次。
            oldType.push(newObj)
          }
        } else {
          // 如果不存在,则是添加
          oldType.push(newObj)
        }
      })
      const res = [e[0], oldType.map(ee => [ee.key, ee.value])]
      return res
    })
    
    • 样式合并,在进行切割的时候,可能新生成的节点样式,会和前/后的老节点一致,需要将样式合并(拼接 text 节点)。
     const resTitle = [...preTitle, ...editTitle, ...lastTitle].reduce(
      (preItem, nextItem, index) => {
        // 合并
        const preTitleItem = preItem.pop()
        const preType = get(preTitleItem, '1', [])
        const lastType = get(nextItem, '1', [])
        let res
        if (isEqual(preType, lastType)) {
          res = [...preItem, [`${preTitleItem[0]}${nextItem[0]}`, preType]]
        } else {
          if (preTitleItem.length && preTitleItem[0] !== '') {
            res = [...preItem, preTitleItem, nextItem]
          } else {
            res = [...preItem, nextItem]
          }
          if (nextItem.isEdit) {
            rangeMark.push({ index: res.length - 1, offset: 0, editIndex: nextItem.editIndex })
          }
        }
        return res
      },
      [['', []]],
    )
    

    ps. 暂缺节点重置前后的,光标框选留存功能点。