Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

文本的高亮标记及标记还原 #100

Open
dcharlie123 opened this issue Jan 4, 2024 · 0 comments
Open

文本的高亮标记及标记还原 #100

dcharlie123 opened this issue Jan 4, 2024 · 0 comments

Comments

@dcharlie123
Copy link
Owner

dcharlie123 commented Jan 4, 2024

选区和范围

Selection

mdn的定义:

Selection 对象表示用户选择的文本范围或插入符号的当前位置。它代表页面中的文本选区,可能横跨多个元素。文本选区由用户拖拽鼠标经过文字而产生。

const selection = window.getSelection();

image

用到的方法:

  • getRangeAt
    返回选区包含的指定区域(Range)的引用。
  • addRange
    一个区域(Range)对象将被加入选区。
  • removeAllRanges
    将所有的区域都从选区中移除。

Range

mdn的定义:

Range对象表示包含节点和部分文本节点的文档片段。通过 selection 对象获得的 range 对象才是我们操作光标的重点。获取 selection ,可以通过全局的 getSelection 方法获取 Range 对象
可以使用 Document.createRange 方法创建 Range。也可以用 Selection 对象的 getRangeAt() 方法或者 Document 对象的 caretRangeFromPoint() 方法获取 Range 对象。

const range = selection.getRangeAt(0);

image

属性

  • collapsed
    返回一个表示 Range 的起始位置和终止位置是否相同的布尔值
  • commonAncestorContainer
    返回完整包含 startContainer 和 endContainer 的、最深一级的节点
  • endContainer
    返回包含 Range 终点的节点
  • startContainer
    返回包含 Range 开始的节点
  • endOffset
    返回一个表示 Range 终点在 endContainer 中的位置的数字。
  • startOffset
    返回一个数字,表示 Range 在 startContainer 中的起始位置。

方法

高亮标记

大致思路就是根据range中的开始节点、相对开始节点的offset和终点节点、相对终点节点的offset,如果是开始标签和终点标签是同个节点或同个父节点用原生surroundContents包裹节点;否则,匹配切割首尾节点及中间节点,逐个替换成包裹节点。

// 获取选区
highlight() {
  const selection = window.getSelection()
  if (selection.rangeCount > 0) {
    const range = selection.getRangeAt(0)
    const collapsed = range.collapsed
    const root = range.commonAncestorContainer
    const snode = range.startContainer
    const enode = range.endContainer
    const { startOffset, endOffset } = range;
    // this.markData.push(this.getRangeOffset(range))
    if (snode === enode || snode.parentNode === enode.parentNode) {
      // 如果是同个父节点或者同个节点,则直接调用原生surroundContents标记
      const newnode = this.createEle("font")
      range.surroundContents(newnode)
    } else {
      // 否则调用自定义方法
      this.surroundContents(root, range)
    }
    // this.markData.push({ snode, enode, startOffset, endOffset })
    // console.log(this.markData)
  }
},
  • 自定义包裹方法:
surroundContents(root, range) {
  const selectedNodes = [];
  const $startNode = range.startContainer;
  const $endNode = range.endContainer;
  const { startOffset, endOffset } = range;
  // 方式二:先将选区内的文本节点拆分并记录,然后将拆分后的文本节点循环匹配,startOffset,使用splitText切割,创建font标签将内容复制生成新节点并替换原节点
  let withinSelectedRange = false;
  const textInfo = this.getTextInfoList()
  for (let i = 0; i < textInfo.length; i++) {
    if (textInfo[i][0] === $startNode) {
      textInfo[i][0].splitText(startOffset)
      const node = textInfo[i][0].nextSibling
      selectedNodes.push(node)
      withinSelectedRange = true
    } else if (textInfo[i][0] === $endNode) {
      const node = textInfo[i][0]
      node.splitText(endOffset)
      selectedNodes.push(node)
      break;
    } else if (withinSelectedRange) {
      selectedNodes.push(textInfo[i][0])
    }
  }

 // 方式一:同方法二,只不过没有提前拆分文本节点
// const nodeStack = [root];
// let withinSelectedRange = false;
// let curNode = null
// while ((curNode = nodeStack.shift())) {
//   if (curNode.nodeType === 1) {
//     if (curNode.contains($startNode) || curNode.contains($endNode)) {
//       nodeStack.unshift(...curNode.childNodes)
//     } else if (withinSelectedRange) {
//       nodeStack.unshift(...curNode.childNodes)
//     }
//   } else {
//     if (curNode === $startNode && curNode.nodeType === 3) {
//       curNode.splitText(startOffset)
//       const node = curNode.nextSibling
//       selectedNodes.push(node)
//       withinSelectedRange = true
//     } else if (curNode === $endNode && curNode.nodeType === 3) {
//       const node = curNode
//       node.splitText(endOffset)
//       selectedNodes.push(node)
//       break;
//     } else if (withinSelectedRange && curNode.nodeType === 3) {
//       selectedNodes.push(curNode)
//     }
//   }

// }

  selectedNodes.map(node => {
    const $wrap = this.createEle("font");
    $wrap.appendChild(node.cloneNode(false));
    node.parentNode.replaceChild($wrap, node);
  })
},
  • 方式二中的getTextInfoList
getTextInfoList() {
  const wrap_dom = document.querySelector("#container");
  const txtList = [];
  const map = function (children) {
    [...children].forEach(el => {
      if (el.nodeName === '#text') {
        txtList.push(el)
      } else {
        map(el.childNodes)
      }
    })
  }
  // 递归遍历,提取出所有 #text
  map(wrap_dom.childNodes);
  const clips = txtList.reduce((arr, item, index) => {
    const end = item.textContent.length + (arr[index - 1] ? arr[index - 1][2] : 0)
    arr.push([item, end - item.textContent.length, end])
    return arr
  }, [])
  return clips
},

标记还原(TODO)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant