文本节点交互处理合集

原文地址:https://www.zhangxinxu.com/wordpress/?p=10635

1. 最祖先的匹配元素

结果是:?

<div id="a">
    <div id="b">
        <button id="button">点击我</button>
    </div>
</div>
<p>结果是:<output id="output">?</output></p>
Element.prototype.farthest = function (selector) {
  if (typeof selector != 'string') {
    return null;
  }
  
  let eleMatch = this.closest(selector);
  let eleReturn = null;
  while (eleMatch) {
    eleReturn = eleMatch;
    eleMatch = eleMatch.parentElement.closest(selector);
  }
  
  return eleReturn;
};
button.onclick = function () {
    output.textContent = this.farthest('div').id;
}; 

2. 第一个/最后一个子节点(元素)

明克街13号

明克街13号

我喜欢坐在夜晚空无一人的大街上,听着“他们”的窃窃私语,享受着“他们”的喧嚣。

作者:纯洁滴小龙
都市连载中87.4万字
直播:我能看见过去与未来

直播:我能看见过去与未来

沈飞穿越平行世界,绑定了直播系统。能让他在直播的时候,看到连麦水友的过去与未来。并且将他们未来往好的方向引导,就能获得奖励。刚开播,沈飞便说哭了一位女主播,让她从此发愤图强,积极向上。一次,有一逃犯来到直播间与沈飞连麦。沈飞:“我拷,刑啊,大哥你早点自首吧,或许还能少判几年。”才刚讲,视频那头的逃犯就被当场抓获。结果,全国网友都震惊了。

作者:吟诗摇人
都市连载中60.54万字

第一个节点元素是:

最后一个节点元素是:

第一个元素的代码是:

最后一个元素的是:

<ui-list id="list" style="--ui-space: 1rem;">
  <a href="//m.qidian.com/book/1030870265.html" class="ui-list-item"><img src="//bookcover.yuewen.com/qdbimg/349573/1030870265/150" class="book-cover" alt="明克街13号"><div class="book-cell"><h4 class="book-title">明克街13号</h4><p class="book-desc">我喜欢坐在夜晚空无一人的大街上,听着“他们”的窃窃私语,享受着“他们”的喧嚣。</p><div class="book-meta"><div class="book-meta-l"><span class="book-author" role="option"><aria>作者:</aria>纯洁滴小龙</span></div><div class="book-meta-r"><span class="tag-small-group origin-right"><em class="tag-small gray">都市</em><em class="tag-small red">连载中</em><em class="tag-small blue">87.4万字</em></span></div></div></div></a>
  <a href="//m.qidian.com/book/1030867097.html" class="ui-list-item"><img src="//bookcover.yuewen.com/qdbimg/349573/1030867097/150" class="book-cover" alt="直播:我能看见过去与未来"><div class="book-cell"><h4 class="book-title">直播:我能看见过去与未来</h4><p class="book-desc">沈飞穿越平行世界,绑定了直播系统。能让他在直播的时候,看到连麦水友的过去与未来。并且将他们未来往好的方向引导,就能获得奖励。刚开播,沈飞便说哭了一位女主播,让她从此发愤图强,积极向上。一次,有一逃犯来到直播间与沈飞连麦。沈飞:“我拷,刑啊,大哥你早点自首吧,或许还能少判几年。”才刚讲,视频那头的逃犯就被当场抓获。结果,全国网友都震惊了。</p><div class="book-meta"><div class="book-meta-l"><span class="book-author" role="option"><aria>作者:</aria>吟诗摇人</span></div><div class="book-meta-r"><span class="tag-small-group origin-right"><em class="tag-small gray">都市</em><em class="tag-small red">连载中</em><em class="tag-small blue">60.54万字</em></span></div></div></div></a>
</ui-list>

3. 获取所有的文本节点

复用上面案例的HTML元素,直接看输出结果:

文本节点依次是:

// 获取所有的文本节点
Object.defineProperty(Element.prototype, 'childTextNodes', {
  get: function () {
    // 获取所有的文本节点
    const nodeIterator = document.createNodeIterator(this, NodeFilter.SHOW_TEXT, (node) => {
      return node.textContent.trim() ? NodeFilter.FILTER_ACCEPT : NodeFilter.FILTER_REJECT;
    });

    // 节点迭代器转为数组并返回
    const arrNodes = [];
    let node = nodeIterator.nextNode();
    while (node) {
      arrNodes.push(node);
      node = nodeIterator.nextNode();
    }

    return arrNodes;
  }
});
<h4>文本节点依次是:</h4>
<ul id="allTextNodes"></ul>
// 所有文本节点显示
allTextNodes.innerHTML = list.childTextNodes.map(node => '<li>' + node.textContent + '</li>').join('');

4. 获取选区内所有的文本节点

选区内文本节点依次是:

const getNodesInRange = function (range) {
  var start = range.startContainer;
  var end = range.endContainer;
  var commonAncestor = range.commonAncestorContainer;
  var nodes = [];
  var node;
  // 使用公用的祖先进行节点遍历
  for (node = start.parentNode; node; node = node.parentNode) {
    nodes.push(node);
    if (node == commonAncestor) {
      break;
    }
  }
  nodes.reverse();

  const getNextNode = function (node) {
    if (node.firstChild) {
      return node.firstChild;
    }
    while (node) {
      if (node.nextSibling) {
        return node.nextSibling;
      }
      node = node.parentNode;
    }
  }

  // 遍历子元素和兄弟元素
  for (node = start; node; node = getNextNode(node)) {
    nodes.push(node);
    if (node == end) {
      break;
    }
  }
  return nodes;
}
// 获取选区内的所有文本节点
const getTextNodesInRange = function (range) {
  return getNodesInRange(range).filter(node => node.nodeType == 3 && node.textContent.trim());
};
<h4>选区内文本节点依次是:</h4>
<ul id="allRangeNodes"></ul>
// 选区内文本节点显示
document.addEventListener('mouseup', function () {
  const selection = document.getSelection();
  if (selection.rangeCount && !selection.isCollapsed) {
    allRangeNodes.innerHTML = getTextNodesInRange(selection.getRangeAt(0)).map(node => '<li>' + node.textContent + '</li>').join('');
  } else {
    allRangeNodes.innerHTML = '<li class="gray">并无框选内容</li>';
  }
});

5. 改变元素的标签

原始 HTML 为:

<b>钓鱼可否?<a href="https://item.jd.com/13356308.html">《CSS新世界》</a></b>

预览效果:

钓鱼可否?《CSS新世界》


现在的 HTML 为:

// 改变元素的标签方法
const propsTagName= Object.getOwnPropertyDescriptor(Element.prototype, 'tagName');
Object.defineProperty(Element.prototype, 'tagName', {
  ...propsTagName,
  set: function (name) {
    if (typeof name != 'string' || name.toUpperCase() == this.tagName) {
      return;
    }

    const eleNew = document.createElement(name.toLowerCase());
    eleNew.append.apply(eleNew, [...this.childNodes]);

    this.replaceWith(eleNew);

    return name;
  }
});
<h4>预览效果:</h4>
<b>钓鱼可否?<a href="https://item.jd.com/13356308.html">《CSS新世界》</a></b>
<hr>
<button id="btnConvert">点击我转换</button>

<h4>现在的 HTML 为:</h4>
<div id="currentHtml"></div>
// 标签类型转换
btnConvert.onclick = function () {
  const ele = document.querySelector('b');
  const eleParent = ele.parentElement;
  // 改变标签
  ele.tagName = 'strong';
  // 显示此时的 HTML 元素内容
  currentHtml.innerHTML = eleParent.querySelector('strong').outerHTML.replaceAll('<', '&lt;');
};

6. 部分文字变成 HTML 元素

原始 HTML 为:

<p id="wrap">保佑今天钓鱼爆护!</p>

现在的 HTML 为:

保佑今天钓鱼爆护!

// 部分文字使用 HTML 包装
// selector 表示希望包裹的HTML元素选择器
// 仅支持标签和类名选择器
// text 表示希望包裹的字符,不设置表示整个文本节点
Text.prototype.surround = function (selector, text) {
  const textContent = this.nodeValue;
  if (!textContent || !selector) {
    return null;
  }

  text = text || textContent;

  // 包装的元素标签和类名
  const arrClass = selector.split('.');
  const tagName = arrClass[0] || 'span';
  const className = arrClass.slice(1).join(' ');

  // 索引范围
  const startIndex = textContent.indexOf(text);

  if (startIndex == -1) {
    return null;
  }

  const range = document.createRange();
  range.setStart(this, startIndex);
  range.setEnd(this, startIndex + text.length);

  // 元素创建
  const eleSurround = document.createElement(tagName);
  if (className) {
    eleSurround.className = className;
  }

  // 执行最后一击
  range.surroundContents(eleSurround);

  return eleSurround;
};
<p id="wrap">保佑今天钓鱼爆护!</p>
wrap.firstChild.surround('strong', '钓鱼');

7. 合并类似元素

原始 HTML 为:

<p id="mergeP">我是<strong>加粗1</strong><strong>加粗2</strong>,我是<span class="blue">蓝色</span><span class="blue">蓝色</span><span class="red">红色</span> <span class="red">红色</span>,只会合并蓝色。</p>

现在的 HTML 为:

我是加粗1加粗2,我是蓝色蓝色红色 红色,只会合并蓝色。

// 合并等同元素
Element.prototype.merge = function (selector) {
  if (!selector) {
    return;
  }

  [...this.querySelectorAll(selector)].some(ele => {
    // 如果和前面的节点类型一致,合并
    let nodePrev = ele.previousSibling;
    let elePrev = ele.previousElementSibling;

    if (!nodePrev || !elePrev) {
      return false;
    }

    // 非内联元素换行符忽略
    const display = getComputedStyle(ele).display;
    if (nodePrev.nodeType == 3 && (nodePrev.nodeValue === '' || (!/inline/.test(display) && !nodePrev.nodeValue.trim()))) {
      nodePrev.remove();
      // 递归处理
      this.merge(selector);

      return true;
    }

    // 如果前面的节点也是元素,同时类名一致
    if (nodePrev == elePrev && ele.cloneNode().isEqualNode(nodePrev.cloneNode())) {
      elePrev.append.apply(elePrev, [...ele.childNodes]);
      ele.remove();
      // 递归处理
      this.merge(selector);

      return true;
    }
  });
};
// 测试代码
mergeP.merge('strong, span[class]');
  1. 列表
  2. 列表
  3. 列表
  1. 列表
  2. 列表
  3. 列表

// 测试代码
document.body.merge('ol');