我对这个非常古老的问题的2023年答案。希望能帮助到某些人:
问题陈述
我们需要在没有任何额外节点的情况下获取范围内的所有节点
问题
- 使用
cloneContents()
或其他内置的范围函数无法解决此问题。
- 因为
commonAncestorContainer
是一个父容器,有时它在所选节点之外。例如,范围4不包括<figure>
,但其commonAncestorContainer是<figure>
- 我们需要的是DOM中的元素,而不是这些元素的副本。
- 我们需要
startContainer
和endContainer
之间的所有节点,而不仅仅是它们的血统。
- 如果我们从
startContainer
开始遍历树,可能会忽略包装该容器的标记。例如,从范围4开头的文本节点开始遍历将忽略关闭的</b>
标记和</p>
标记。
- 我们需要
startContainer
和endContainer
的元素,即使它们是文本节点也一样。
节点示例模型
<figure>
<p>Lorem ipsum dolor sit amet, <b>consectetur</b> adipiscing elit</p>
<img>
<ol>
<li>
<p>sed do eiusmod tempor incididunt</p>
</li>
<li></li>
</ol>
<p>ut labore et dolore magna aliqua. Ut <i>enim</i> ad minim veniam</p>
</figure>
模型中不同范围的示例结果
范围1
___..............lor sit amet, <b>consectetur</b> adipis........._/__
返回 <p/>
,<b/>
commonAncestorContainer 是 <p/>
(包括在内)
范围 2
___..............lor sit amet, <b>conse......_/__................_/__
返回 <p/>
,<b/>
commonAncestorContainer 是 <p/>
(包括在内)
范围 3
___............................___.....ctetur</b> adipis........._/__
返回 <p/>
,<b/>
commonAncestorContainer 是 <p/>
(包括在内)
范围 4
___...................................ectetur</b> adipiscing elit</p>
<img>
<ol>
<li>
<p>sed do eiusmod tempor incididunt</p>
</li>
<li></li>
</ol>
<p>ut labore et dolore magna aliqua. Ut <i>en.._/__................_/__
返回 <p/>
、<b/>
、<img>
、<ol/>
、<li/>
、<p/>
、<li/>
、<p/>
、<i/>
commonAncestorContainer 是 <figure/>
(故意不包含)
解决方案
function getElsList(commonAncestor, optionalArgs) {
const { startNode, endNode } = optionalArgs || {};
const domEls = [];
let beforeStart = false;
let afterEnd = false;
function getEl(nodeOrEl) {
if(nodeOrEl?.nodeType === 1) {
return nodeOrEl;
} else {
return nodeOrEl?.parentElement;
}
}
const commonAncestorEl = getEl(commonAncestor);
let endEl = commonAncestorEl;
let startEl = commonAncestorEl;
if(endNode) {
endEl = getEl(endNode);
}
if(startNode) {
startEl = getEl(startNode);
beforeStart = true;
}
let currentEl = startEl;
do {
listEls.push(currentEl);
} while(currentEl !== commonAncestorEl && (currentEl = currentEl.parentElement));
if(endEl !== commonAncestorEl && startEl !== commonAncestorEl && endEl !== startEl) {
listEls.pop();
}
listEls.reverse();
function walkTrees(branch) {
const branchNodes = branch.childNodes;
for(let i = 0; !afterEnd && i < branchNodes.length; i++) {
let currentNode = branchNodes[i];
if(currentNode === startNode) {
beforeStart = false;
}
if(!beforeStart && currentNode.nodeType === 1) {
domEls.push(currentNode);
}
if(currentNode === endNode) {
afterEnd = true;
} else {
walkTrees(currentNode);
}
}
}
walkTrees(commonAncestor);
return domEls;
}
const sel = window.getSelection();
const range = sel.getRangeAt(0);
const rangeEls = getElsList(range.commonAncestorContainer, { startNode: range.startContainer, endNode: range.endContainer })
console.log("els", rangeEls)
说明
getElsList()
之外的所有内容都是为了创建一个可工作的示例。在此示例中,我们根据所选文本获取范围。但是,选择文本并获取范围是可选的,因为该函数接受节点。
getElsList()
需要一个节点进行遍历。然后它执行以下操作:
如果提供了startNode,getElsList()将首先遍历该节点的谱系。如果“主”(共同祖先)节点不是选择的一部分,则它将从列表末尾弹出。结果被反转,以使列表顺序与DOM顺序匹配。
注意:
- do...while确保我们收集起始节点
- do...while结合设置currentEl.parentElement和检查null(谱系的末端)
getElsList()调用walkTrees(),收集起始节点和结束节点之间的所有节点。如果没有提供起始节点,则会收集起始节点。如果提供了起始节点,则在上一步中已经收集到。总是收集结束节点。
如果提供了endNode,getElsList()将停止遍历该节点。
var c=getSelection().getRangeAt(0).cloneContents(); c.querySelectorAll('*')
- caubcloneContents()
返回标签中所选部分,例如:<strong>Th|is te|xt</strong>
将返回<strong>is te</strong>
(我用 | 标记了所选内容)。 - Gevorg Hakobyan