@typex-platform_web_caret.js

import { multiplication } from './utils'

/** @type {Function} */
let getRect
// 提供两种计算光标坐标的方法,优先使用原生的getClientRects,因为性能较好
if (Range.prototype.getClientRects) {
  getRect = (range) => {
    const nativeRange = document.createRange()
    let rect
    if (range.container.elm.nodeType !== 3) {
      if (range.offset === 0) {
        nativeRange.setStart(range.container.elm, range.offset)
        nativeRange.setEnd(range.container.elm, range.offset + 1)
        rect = nativeRange.getClientRects()[0]
      } else {
        nativeRange.setStart(range.container.elm, range.offset - 1)
        nativeRange.setEnd(range.container.elm, range.offset)
        rect = nativeRange.getClientRects()[0]
        rect.x += rect.width
      }
    } else {
      nativeRange.setStart(range.container.elm, range.offset)
      rect = nativeRange.getClientRects()[0]
    }
    nativeRange.setStart(range.container.elm, range.offset)
    const scroll = computeScroll(range.editor.contentRef.current)
    const offset = computeOffset(range.editor.contentRef.current)
    return {
      x: rect.x + scroll.x - offset.x,
      y: rect.y + scroll.y - offset.y,
      height: rect.height,
    }
  }
} else {
  getRect = (range) => {
    const res = new Measure().measure(range.container.elm, range.offset)
    return res
  }
}

/**
 * @description 设置光标的样式
 * @param {*} dom
 * @param {*} style
 */
const setStyle = (dom, style) => {
  for (const key in style) {
    dom.style[key] = style[key]
  }
}

/**
 * @description 光标类
 * @export
 * @class Caret
 */
export default class Caret {
  /**
   * @description 光标dom
   * @memberof Caret
   * @instance
   */
  dom = null

  /**
   * @description 光标坐标
   * @memberof Caret
   * @instance
   */
  rect = null

  /**
   * @description 默认样式
   * @memberof Caret
   * @instance
   */
  defaultStyle = {}

  /**
   * @description 当前样式
   * @memberof Caret
   * @instance
   */
  style = {}
  constructor(range) {
    this.range = range
    this.dom = document.createElement('span')
    this.dom.classList.add('custom-caret')
    this.setStyle(this.dom)
  }

  /**
   * @description 设置光标样式
   * @param {*} [style={}]
   * @memberof Caret
   * @instance
   */
  setStyle(style = {}) {
    Object.assign(this.style, this.defaultStyle, style)
  }

  /**
   * @description 光标移除
   * @memberof Caret
   * @instance
   */
  remove() {
    this.dom.remove()
  }

  /**
   * @description 光标隐藏
   * @memberof Caret
   * @instance
   */
  hidden() {
    if (this.style.display === 'none') return
    this.setStyle({ display: 'none' })
    this.draw()
  }

  /**
   * @description 光标显示
   * @memberof Caret
   * @instance
   */
  show() {
    if (this.style.display === 'inline-block') return
    this.setStyle({ display: 'inline-block' })
    this.draw()
  }

  /**
   * @description 光标更新
   * @param {boolean} [drawCaret=true]
   * @memberof Caret
   * @instance
   */
  update(drawCaret = true) {
    this.rect = getRect(this.range)
    if (!drawCaret) return
    this.range.editor.contentRef.current.appendChild(this.dom)
    let elm = this.range.startContainer.elm
    if (!elm) return
    if (!(elm instanceof Element)) {
      elm = elm.parentNode
    }
    const copyStyle = getComputedStyle(elm)
    const caretStyle = {
      top: this.rect.y + 'px',
      left: this.rect.x - 1 + 'px',
      height: this.rect.height + 'px',
      fontSize: copyStyle.fontSize,
      background: copyStyle.color,
      display: this.range.collapsed ? 'inline-block' : 'none',
    }
    this.setStyle(caretStyle)
    this.draw()
  }

  /**
   * @description 绘制光标
   * @memberof Caret
   * @instance
   */
  draw() {
    setStyle(this.dom, this.style)
  }
}

/**
 * @description 光标坐标测量器
 * @class Measure
 */
class Measure {
  /**
   * @description 辅助测量dom
   * @memberof Measure
   */
  dom = null

  /**
   * @description 单例模式 实例
   * @memberof Measure
   */
  instance = null
  constructor() {
    if (!Measure.instance) {
      this.dom = document.createElement('text')
      Measure.instance = this
    } else {
      return Measure.instance
    }
  }

  /**
   * @description 测量方法
   * @param {*} container
   * @param {*} offset
   * @returns {*}
   * @memberof Measure
   */
  measure(container, offset) {
    // splitText(0)会使原dom销毁造成startContainer向上逃逸
    let temp
    if (container.nodeType === 3) {
      if (!offset) {
        container.parentNode.insertBefore(this.dom, container)
      } else {
        temp = container.splitText(offset)
        container.parentNode.insertBefore(this.dom, temp)
      }
    } else {
      if (container.childNodes[offset - 1] && container.childNodes[offset - 1].nodeName === 'BR') {
        container.insertBefore(this.dom, container.childNodes[offset - 1])
      } else if (container.childNodes[offset]) {
        container.insertBefore(this.dom, container.childNodes[offset])
      } else {
        container.appendChild(this.dom)
      }
    }
    return this._getRect(container, offset, temp)
  }

  /**
   * @description 获取坐标
   * @param {*} container
   * @param {*} offset
   * @param {*} temp
   * @private
   * @returns {*}
   * @memberof Measure
   */
  _getRect(container, offset, temp) {
    let con = container
    if (!(container instanceof Element)) {
      con = container.parentNode
    }
    const copyStyle = getComputedStyle(con)
    const h = multiplication(copyStyle.fontSize, 1.3) / 1
    const rect = {
      x: this.dom.offsetLeft,
      y: this.dom.offsetTop,
      height: h || this.dom.offsetHeight,
    }
    this.dom.remove()
    if (container.nodeType === 3 && offset) {
      if (!container.data && container.nextSibling) {
        container.nextSibling.remove()
      } else {
        container.data += temp.data
        temp.remove()
      }
    }
    return rect
  }
}

/**
 * @description 累计偏移量计算
 * @param {*} dom
 * @param {*} [res={ x: 0, y: 0 }]
 * @returns {*}
 */
function computeOffset(dom, res = { x: 0, y: 0 }) {
  res.height = res.height ?? dom.offsetHeight
  res.x += dom.offsetLeft
  res.y += dom.offsetTop
  if (dom.offsetParent && dom.offsetParent.tagName !== 'HTML') {
    return computeOffset(dom.offsetParent, res)
  }
  return res
}

/**
 * @description  累计滚动距离计算
 * @param {*} dom
 * @param {*} [res={ x: 0, y: 0 }]
 * @returns {*}
 */
function computeScroll(dom, res = { x: 0, y: 0 }) {
  res.x += dom.scrollLeft || 0
  res.y += dom.scrollTop || 0
  if (dom.parentNode) {
    return computeScroll(dom.parentNode, res)
  }
  return res
}