前面的一篇文章Javascript保留格式翻译选区内容及预览(一),我们已经知道了,怎么获取选中的文字,然后交个接口返回,现在我们接着继续将这么讲翻译好的词语替换到原文的对应位置。
首先我们在选择文本并进行翻译时,我们的选区还在的,如果后续操作有存在丢掉选区的操作的话,我们需要先将选区暂存起来。

怎么保存选区

首先保存选区的前提是:选区还存在
怎么说 ?
如果我们选中的词语是abc,如果abc都被替换了,之前保存的选区信息也无法使用了,或者不准确了,就不要使用了。

const ranges = [];
for (let i = 0; i < window.getSelection().rangeCount; i++) {
      const range = window.getSelection().getRangeAt(i);
      ranges.push(range.cloneRange()); // clone后确保ranges可以用来重试
}
我们现在就把选区信息存储在`ranges`数组里面了

怎么恢复选区

既然提到保存选区,自然要知道怎么恢复选区了

// 恢复指定选区前移除所有可能存在选区
window.getSelection().removeAllRanges();
ranges.forEach(range => window.getSelection().addRange(range));

替换原文

替换原文的前提是保持原文处于选中状态,也就是保留选区,现在这一步上面已经做到了,再加上第一篇提到的获取的选区的节点的最近一层的共同父节点commonAncestorContainer。现在我们正式开始进行替换了。
我们用两个方法完成替换,一个方法(traversalParent)是查找需要可能需要替换的dom节点。另一个方法(traversalReplace)进行替换。

寻找可能需要替换的dom节点

commonAncestorContainer里面是包含我们需要替换的dom节点的,但是里面可能还有很多无关节点,所以我们必须进行排除。
这就需要window.getSelection().containsNode方法,它能帮助我们判断某个节点是不是在选区中。
从MDN上看看它的api
file
我们可以判断指定节点是否全部或者部分包含在选区内。
如果dom全部内容都在选区是最简单的,直接全量替换了;如果只有不分在选区里面就麻烦点,比如原来的dom结构是<div>你们好</div>,但是用户只选中了“好”字,这时我们就不应该把“你们”给替换了。这部分工作就需要在我们的traversalReplace方法里面实现了。
window.getSelection().anchorNodewindow.getSelection().focusNode分别能让我们知道选区的起始和结尾的节点。因为我们是人工选择文本节点,所以我们这里的起始、结尾节点极大概率是文本节点或者是包含文本的元素节点(只是我们看不见而已,这时就属于全部包含了,反而简单)。
文本节点的data属性会返回该文本的文字内容。
anchorNodefocusNode对应的window.getSelection().anchorOffsetwindow.getSelection().focusOffset可以告知我们选择的文字在该文本节点的位置。
我们替换的的原文时就可以用
原文前内容+ 翻译结果 + 原文结尾内容来替换了。
这两个方法的代码如下

traversalParent(parent, isPreviewMode = false) {
    let hit = 0;
    let isReached = false;
    const childNodes = parent.childNodes;
    if (childNodes && childNodes.length > 0) {
      for (let j = 0; j < parent.childNodes.length; j++) {
        const node = parent.childNodes[j];
        if (window.getSelection().containsNode(node)) {
          isReached = true;
          hit++;
          traversalReplace(node, false, win)
        } else if (window.getSelection().containsNode(node, true)) {
          isReached = true;
          hit++;
          traversalReplace(node, true, win)
        } else {
          if (isReached) {
            break;
          }
        }
      }
    } else if (parent.nodeType === 3) {
      traversalReplace(parent, false, window);
    }
  }

  traversalReplace(node, allowPartialContainment, win) {
    const childNodes = node.childNodes;
    if (childNodes && childNodes.length > 0) {
      let isReached = false;
      for (let i = 0; i < childNodes.length; i++) {
        const item = childNodes[i];
        // node部分包含时,再遍历它的子节点时,要继续判断子节点是否包含
        if (allowPartialContainment && !window.getSelection().containsNode(item, true)) {
          if (isReached) {
            break;
          }
        } else {
          isReached = true;
          traversalReplace(item, allowPartialContainment, win);
        }
      }
    } else if (node.nodeType === 3) {
      const data = node.data || '';
      if (data.trim().length < 1) {
        return;
      }
      let targetText = '';
      const translatedText = this.translatedWords[this.traversalIndex++] || '';
      if (node === window.getSelection().anchorNode) {
        // 存在node既是anchorNode又是focusNode的情况
        const tailText = node === window.getSelection().focusNode ? node.data.slice(window.getSelection().focusOffset) : '';
        targetText = node.data.slice(0, window.getSelection().anchorOffset) + translatedText + tailText;
      } else if (node === window.getSelection().focusNode) {
        targetText = translatedText + node.data.slice(window.getSelection().focusOffset);
      } else {
        targetText = translatedText;
      }
      // 此操作后会改变原来选中该node的Range的startOffset,endOffset等值。皮之不存毛将焉附
      node.data = targetText;
    }
  }

具体请通过npm安装translate_selection


发表评论

此站点使用Akismet来减少垃圾评论。了解我们如何处理您的评论数据