@typex-core_model_content.js

import Component from '../view/component'
import { horizontalMove, verticalMove } from '../defaultActions/caretMove'
import { del } from '../defaultActions/delete'
import { createPath } from './path'
import { uuid } from '../utils'
import { TextInsert, TextDelete } from '../transform/step'

/**
 * @description 内容管理类
 * @export
 * @class Content
 * @extends {Component}
 */
export default class Content extends Component {
  uuid = uuid()
  get renderContent () {
    return this.$editor.formater.render(this.$path)
  }
  /**
   * @description 类型
   * @readonly
   * @memberof Content
   * @instance
   */
  get displayType () {
    return 'inline'
  }
  constructor(props) {
    super(props)
  }

  /**
   * @description 内容长度
   * @readonly
   * @memberof Content
   * @instance
   */
  get contentLength () {
    return this.$path.length
  }

  /**
   * @description 对应路径
   * @readonly
   * @memberof Content
   * @instance
   */
  get $path () {
    return this.props.path
  }

  /**
   * @description 编辑器实例
   * @readonly
   * @memberof Content
   * @instance
   */
  get $editor () {
    return this.props.editor
  }
  shouldComponentUpdate () {
    return this.$path.rebuildFlag !== 1
  }
  /**
   * @description render前调用hook
   * @memberof Content
   * @memberof Content
   */
  onBeforeRender () {
    /*
     * 在diff的时候对于相同的组件类型的vdom,会被当成相同vnode,不会重新创建实例,只会切换props,
     * 但是一定会调用render,因此需要在这里切换path的组件上下文
     */
    this.$path.currentComponent = this
  }
  /**
   * @description 更新状态
   * @param {*} path
   * @param {*} range
   * @returns {*}
   * @memberof Content
   * @instance
   */
  update (path, range) {
    // 执行更新前钩子
    this.onBeforeUpdate && this.onBeforeUpdate({ path: path || this.$path, range })
    return this.setState().then(() => {
      this.$editor.selection.drawRangeBg()
      // 执行更新后钩子
      this.onAfterUpdate && this.onAfterUpdate({ range, path })
    })
  }

  /**
   * @description 内容插入处理
   * @param {*} { data, type, range }
   * @returns {*}  
   * @memberof Content
   */
  onInsert ({ data, type, range }) {
    if (type === 'text') {
      const insertTextStep = new TextInsert({ range, data })
      insertTextStep.apply()
      this.update().then(() => {
        range.collapse(false)
        range.updateCaret()
      })
      return insertTextStep
    }
  }
  onContentDelete ({ range, ts }) {
    console.log('删除', ts)
    const { endContainer, endOffset, startContainer, startOffset, collapsed } = range
    const commonPath = startContainer.queryCommonPath(endContainer)
    // 选区折叠
    if (collapsed) {
      const prevLeaf = startContainer.prevLeaf
      if (endOffset > 0) {
        // 执行删除
        const deleteTextStep = new TextDelete({ range, count: 1 })
        ts.addAndApplyStep(deleteTextStep)
        debugger
        if (startContainer.block.contentLength === 0) {
          // 块级内容被清空
          range.setStart(startContainer, 0).collapse()
          startContainer.block.$path.parent.currentComponent.update()
        } else if (startContainer.length === 0) {
          // 块不为空 容器为空
          // 删除容器
          startContainer.delete()
          if (prevLeaf.block !== startContainer.block || !prevLeaf) {
            range.setStart(startContainer.nextLeaf, 0).collapse()
          } else {
            range.setStart(prevLeaf, prevLeaf.length).collapse()
          }
        } else {
          // 容器不为空
          // 行内跨标签
          if (prevLeaf?.block === startContainer.block && endOffset === 1) {
            range.setStart(prevLeaf, prevLeaf.length).collapse()
          }
        }
      } else {
        // 块为空 光标在头部
        if (startContainer.block.contentLength === 0) {
          startContainer.block.$path.delete()
          range.setStart(prevLeaf, prevLeaf.length).collapse()
        } else if (startContainer.length === 0) {
          startContainer.delete()
        } else if (startContainer.block !== prevLeaf.block) {
          const startContainerParent = startContainer.parent
          if (prevLeaf.length === 0) {
            prevLeaf.parent.pop()
            prevLeaf.parent.push(...startContainer.parent.children)
            startContainerParent.delete()
            range.setStart(startContainer, 0)
          } else {
            prevLeaf.parent.splice(prevLeaf.index + 1, 0, ...startContainer.parent.children)
            startContainerParent.delete()
            range.setStart(startContainer, 0).collapse()
          }
        } else {
          range.setStart(prevLeaf, prevLeaf.length).collapse()
          this.onContentDelete({ range, ts })
        }
      }
    } else if (startContainer === endContainer) {
      startContainer.textDelete(endOffset, endOffset - startOffset)
      if (startContainer.length === 0) {
        startContainer.delete()
        range.setStart(startContainer.prevLeaf, startContainer.prevLeaf.length).collapse()
      }
      range.collapse(true)
    } else {
      startContainer.textDelete(startContainer.length, startContainer.length - startOffset)
      endContainer.textDelete(endOffset, endOffset)
      commonPath.deleteBetween(startContainer, endContainer)
      if (startContainer.block !== endContainer.block) {
        endContainer.parent.delete()
        startContainer.parent.splice(
          startContainer.index + 1,
          0,
          ...endContainer.parent.children.filter((i) => i.length)
        )
      }
      range.collapse(true)
    }
    commonPath.currentComponent.update(commonPath, range).then(() => {
      range.updateCaret()
    })
  }
  /**
   * @description 光标进入
   * @param {*} path
   * @param {*} range
   * @param {*} direction
   * @returns {*}
   * @memberof Content
   * @instance
   */
  enterPath (range, direction) {
    let path = null
    if (direction === 'left') {
      const formPath = range.container
      path = formPath.nextLeaf
      const isSameBlock = formPath.block === path.block
      range.set(path, isSameBlock ? 1 : 0)
    } else {
      const formPath = range.container
      path = formPath.prevLeaf
      range.set(path, path.length)
    }
    return { path, range }
  }

  /**
   * @description 光标离开
   * @param {*} path
   * @param {*} range
   * @param {*} direction 从哪个方向离开
   * @returns {*}
   * @memberof Content
   * @instance
   */
  leavePath (range, direction) {
    if (direction === 'left') {
      const path = range.container
      let toPath = path.prevLeaf
      if (!toPath) return null
      if (!path) return null
      range.set(path, path.length)
      return toPath.currentComponent.enterPath(range, 'right')
    } else {
      const path = range.endContainer
      // 从尾部离开
      let toPath = path.nextLeaf
      if (!toPath) return null
      return toPath.currentComponent.enterPath(range, 'left')
    }
  }

  /**
   * @description 光标移动处理
   * @param {*} direction
   * @param {*} range
   * @param {*} event
   * @returns {*}
   * @memberof Content
   * @instance
   */
  onCaretMove (direction, range, event) {
    const path = range.container
    const { shiftKey } = event
    let res = { path, range }
    // 重置 d
    if (range.d === 0) range.d = direction === 'left' ? -1 : 1
    if (this.isCaretShouldLeavePath(path, range, direction)) {
      // 跨path移动 先执行跨ptah动作 再执行path内移动动作
      res = this.leavePath(range, direction)
    } else {
      if (direction === 'left') {
        range.offset > 0 && (range.offset -= 1)
      } else {
        range.offset < path.length && (range.offset += 1)
      }
    }
    if (!shiftKey) {
      range.collapse(direction === 'left')
    }
    return res
  }

  /**
   * @description 换行
   * @param {Range} range
   * @param {event} [event=null]
   * @memberof Content
   */
  onLinefeed (event, range) {
    if (range.inputState.isComposing) return
    event?.preventDefault?.()
    if (!range.collapsed) {
      del({ range })
    }
    let cloneParent
    // 空行回车
    if (!range.container.length) {
      cloneParent = range.container.parent.clone(true)
      cloneParent.insertAfter(range.container.parent)
      range.set(cloneParent.children[0], 0)
    } else {
      cloneParent = range.container.parent.clone()
      let splits = []
      // 这里有三种情况 光标在path左端 在中间 在右端
      if (range.offset === 0) {
        // 在左端
        splits = [range.container.prevSibling || null, range.container]
      } else if (range.offset === range.container.length) {
        // 在右端
        splits = [range.container, range.container.nextSibling || null]
      } else {
        // 在中间
        splits = range.container.split(range.offset)
      }

      if (splits[0] === null) {
        // 向前插入空行
        const newPath = createPath({ data: '' })
        cloneParent.insertBefore(range.container.parent)
        cloneParent.push(newPath)
      } else if (splits[1] === null) {
        // 向后插入空行
        const newPath = createPath({ data: '' })
        cloneParent.insertAfter(range.container.parent)
        cloneParent.push(newPath)
        range.set(cloneParent.children[0], 0)
      } else {
        // 分割光标后的内容到新行
        cloneParent.insertAfter(range.container.parent)
        range.container.parent.children.slice(splits[1].index).forEach((path) => {
          path.moveTo(cloneParent)
        })
        range.set(cloneParent.children[0], 0)
      }
    }

    cloneParent.parent.currentComponent.update().then(() => {
      range.collapse(true)
      range.updateCaret()
    })
  }
  /**
   * @description 键盘左箭头处理
   * @param {*} range
   * @param {*} event
   * @memberof Content
   * @instance
   */
  onKeydownArrowLeft ({ event, range }) {
    horizontalMove('left', range, event)
    this.$editor.selection.updateCaret()
  }

  /**
   * @description 键盘右箭头处理
   * @param {*} range
   * @param {*} event
   * @memberof Content
   * @instance
   */
  onKeydownArrowRight ({ event, range }) {
    horizontalMove('right', range, event)
    this.$editor.selection.updateCaret()
  }

  /**
   * @description 键盘上箭头处理
   * @param {*} range
   * @param {*} event
   * @memberof Content
   * @instance
   */
  onKeydownArrowUp ({ event, range }) {
    verticalMove('up', range, event)
    this.$editor.selection.updateCaret()
  }

  /**
   * @description 键盘下箭头处理
   * @param {*} range
   * @param {*} event
   * @memberof Content
   * @instance
   */
  onKeydownArrowDown ({ event, range }) {
    verticalMove('down', range, event)
    this.$editor.selection.updateCaret()
  }

  /**
   * @description 键盘空格处理
   * @param {*} range
   * @memberof Content
   * @instance
   */
  onKeydownBackspace ({ event, range, ts }) {
    del({ event, range, ts })
  }

  /**
   * @description 键盘回车处理
   * @param {*} range
   * @param {*} event
   * @memberof Content
   * @instance
   */
  onKeydownEnter ({ event, range }) {
    this.onLinefeed(event, range)
  }
  /**
   * @description 检测光标是否要离开当前path
   * @param {Path} path - 路径
   * @param {Range} range - 选取范围
   * @param {'left'|'right'} direction - 方向
   * @returns {Boolean}
   * @memberof Content
   * @instance
   */
  isCaretShouldLeavePath (path, range, direction) {
    if (direction === 'left' && range.offset <= 1) {
      let toPath = path.prevLeaf
      if (!toPath) return false
      // 细节处理:同块不同文本光标左移,两个path的交界处 取前一个path的右端点
      if (range.offset === 0) {
        return true
      } else {
        return path.block === toPath.block
      }
    }
    return direction === 'right' && range.offset === path.length
  }

  /**
   * @description range端点更新
   * @param {*} container
   * @param {*} position
   * @param {*} distance
   * @param {*} newContainer
   * @memberof Content
   * @instance
   */
  _updatePoints (container, position, distance, newContainer) {
    this.$editor.selection.updatePoints(container, position, distance, newContainer)
  }

  /**
   * @description 格式设置
   * @param {Range} range
   * @param {function} callback 格式处理回调
   * @memberof Content
   * @instance
   */
  setFormat (range, callback) {
    const commonPath = range.startContainer.queryCommonPath(range.endContainer)
    const selectedPath = this.$editor.selection.getLeafPaths()
    for (const path of selectedPath) {
      callback(path)
    }
    commonPath.currentComponent.update().then(() => {
      range.updateCaret()
      this.$editor.selection.drawRangeBg()
    })
  }

  /**
   * @description 组件设置
   *  @param {Range} range
   * @param {function} callback 格式处理回调
   * @memberof Content
   * @instance
   */
  setComponent (range, callback) {
    let commonPath, blockPaths
    if (range.collapsed) {
      commonPath = range.startContainer.block.$path.parent
      blockPaths = [range.startContainer.block.$path]
    } else {
      commonPath = range.startContainer.queryCommonPath(range.endContainer)
      if (range.startContainer.block === range.endContainer.block) {
        commonPath = range.startContainer.block.$path.parent
      }
      const selectedPath = this.$editor.selection.getLeafPaths(false)
      blockPaths = [...(new Set([...selectedPath].map(path => path.block.$path)))]
    }
    for (const path of blockPaths) {
      callback(path)
    }
    console.log(commonPath.currentComponent);
    commonPath.currentComponent.update().then(() => {
      range.updateCaret()
      this.$editor.selection.drawRangeBg()
    })
  }
}