获取页面上选定的文本和选定的节点?

14

在选择一段文本时(可能跨越多个DOM节点),是否可以使用Javascript提取所选的文本和节点?

想象一下这段HTML代码:


<h1>Hello World</h1><p>Hi <b>there!</b></p>

如果用户在“World...”处发起了一个mouseDown事件,然后在“there!”紧接着发起了一个mouseUp事件,我希望它会返回:

Text : { selectedText: "WorldHi there!" },
Nodes: [ 
  { node: "h1", offset: 6, length: 5 }, 
  { node: "p", offset: 0, length: 16 }, 
  { node: "p > b", offset: 0, length: 6 } 
]

我已经尝试将HTML放入文本区域,但这只会给我选定的文本。我还没有尝试过<canvas>元素,但这可能是另一个选择。

如果不使用JavaScript,是否有一种使用Firefox扩展程序实现这一点的方法?

5个回答

16
你将会面临一些困难,但这是完全可能的。主要问题在于IE和W3C对选择暴露了完全不同的接口,因此如果您想要跨浏览器功能,那么基本上必须写两遍整个内容。此外,两个接口都缺少一些基本功能。
Mozilla开发者连接有关于W3C选择的故事。Microsoft在MSDN上有文档。我建议从PPK的范围介绍开始。
以下是我认为有效的一些基本函数:
// selection objects will differ between browsers
function getSelection () {
  return ( msie ) 
    ? document.selection
    : ( window.getSelection || document.getSelection )();
}

// range objects will differ between browsers
function getRange () {
  return ( msie ) 
      ? getSelection().createRange()
      : getSelection().getRangeAt( 0 )
}

// abstract getting a parent container from a range
function parentContainer ( range ) {
  return ( msie )
      ? range.parentElement()
      : range.commonAncestorContainer;
}

你将什么作为 r 传递给 parentContainer?我没有使它工作(parentContainer 方法)。 - Khaled Al Hourani
糟糕,表述不太清楚,但那应该是一个范围,我已经修正了变量名。我认为可以像这样使用:var container = parentContainer( getRange() );这并不意味着它会百分之百地工作。该代码旨在展示完成此操作所需的工作类型,并且可能存在缺陷。您需要了解您正在处理的API(请参见链接)。 - Borgar
parentContainer() 没有帮助:两个分支不能保证返回相同的内容,因为 IE 的 TextRangeparentElement() 方法总是会返回一个元素,而 commonAncestorContainer 可能是一个文本节点。此外,没有必要进行任何浏览器嗅探(如使用 msie 所示):您可以轻松检测到所需的对象和方法。 - Tim Down
这段代码可能会导致堆栈溢出(无恶意)。请查看您的 getSelection 函数;window.getSelectiongetSelection 是同一件事,因此通过覆盖内置函数,您将无法再访问内置的 getSelection,而是会不断调用自己的 getSelection - Sapphire_Brick

9

我的Rangy库可以在IE < 9和其他主要浏览器中统一不同的API,并在Range对象上提供getNodes()函数,这将帮助您处理部分问题:

function getSelectedNodes() {
    var selectedNodes = [];
    var sel = rangy.getSelection();
    for (var i = 0; i < sel.rangeCount; ++i) {
        selectedNodes = selectedNodes.concat( sel.getRangeAt(i).getNodes() );
    }
    return selectedNodes;
}

在所有浏览器中获取所选文本都很容易。在Rangy中只需要使用以下代码:
var selectedText = rangy.getSelection().toString();

没有使用Rangy:

function getSelectedText() {
    var sel, text = "";
    if (window.getSelection) {
        text = "" + window.getSelection();
    } else if ( (sel = document.selection) && sel.type == "Text") {
        text = sel.createRange().text;
    }
    return text;
}

关于字符偏移量,您可以针对选择中的任何节点 node 做如下处理。请注意,这并不一定代表文档中可见的文本,因为它没有考虑折叠的空格、通过 CSS 隐藏的文本、通过 CSS 放置在正常文档流之外的文本、<br> 和块元素隐含的换行符以及其他细微差别。
var sel = rangy.getSelection();
var selRange = sel.getRangeAt(0);
var rangePrecedingNode = rangy.createRange();
rangePrecedingNode.setStart(selRange.startContainer, selRange.startOffset);
rangePrecedingNode.setEndBefore(node);
var startIndex = rangePrecedingNode.toString().length;
rangePrecedingNode.setEndAfter(node);
var endIndex = rangePrecedingNode.toString().length;
alert(startIndex + ", " + endIndex);

你好,我正在查看你的工作。它是迄今为止最好的。但我仍然有些困难,无法将所有选定的文本作为逗号分隔的文本获取。我可以得到一些帮助吗? - Sara Kat
Tim,你能给我一些关于在重新渲染的可编辑div上恢复选择的提示吗?我在这里提出了一个问题https://stackoverflow.com/questions/61011651/cant-restore-selection-with-window-getselection-and-range-after-re-rendering。谢谢! - webprogrammer

4
这是我理解中所返回的选定节点: 当我有
<p> ... </p><p> ... </p><p> ... </p><p> ... </p><p> ... </p>...
<p> ... </p><p> ... </p><p> ... </p><p> ... </p><p> ... </p>

很多节点,我只选择了一些,然后我希望只有这些节点出现在列表中。
function getSelectedNodes() {
  // from https://developer.mozilla.org/en-US/docs/Web/API/Selection
  var selection = window.getSelection();
  if (selection.isCollapsed) {
    return [];
  };
  var node1 = selection.anchorNode;
  var node2 = selection.focusNode;
  var selectionAncestor = get_common_ancestor(node1, node2);
  if (selectionAncestor == null) {
    return [];
  }
  return getNodesBetween(selectionAncestor, node1, node2);
}

function get_common_ancestor(a, b)
{
    // from https://dev59.com/pG865IYBdhLWcg3wKLJP
    $parentsa = $(a).parents();
    $parentsb = $(b).parents();

    var found = null;

    $parentsa.each(function() {
        var thisa = this;

        $parentsb.each(function() {
            if (thisa == this)
            {
                found = this;
                return false;
            }
        });

        if (found) return false;
    });

    return found;
}

function isDescendant(parent, child) {
     // from https://dev59.com/MXE95IYBdhLWcg3wm_Mu
     var node = child;
     while (node != null) {
         if (node == parent) {
             return true;
         }
         node = node.parentNode;
     }
     return false;
}

function getNodesBetween(rootNode, node1, node2) {
  var resultNodes = [];
  var isBetweenNodes = false;
  for (var i = 0; i < rootNode.childNodes.length; i+= 1) {
    if (isDescendant(rootNode.childNodes[i], node1) || isDescendant(rootNode.childNodes[i], node2)) {
      if (resultNodes.length == 0) {
        isBetweenNodes = true;
      } else {
        isBetweenNodes = false;
      }
      resultNodes.push(rootNode.childNodes[i]);
    } else if (resultNodes.length == 0) {
    } else if (isBetweenNodes) {
      resultNodes.push(rootNode.childNodes[i]);
    } else {
      return resultNodes;
    }
  };
 if (resultNodes.length == 0) {
    return [rootNode];
  } else if (isDescendant(resultNodes[resultNodes.length - 1], node1) || isDescendant(resultNodes[resultNodes.length - 1], node2)) {
    return resultNodes;
  } else {
    // same child node for both should never happen
    return [resultNodes[0]];
  }
}

代码应该在这里获取:https://github.com/niccokunzmann/spiele-mit-kindern/blob/gh-pages/javascripts/feedback.js
我在此发布答案,因为我希望在这里找到它。

0

如果你只想要范围,那么有一个更简短的方法。

function getRange(){
    return (navigator.appName=="Microsoft Internet Explorer")
        ? document.selection.createRange().parentElement()
        : (getSelection||document.getSelection)().getRangeAt(0).commonAncestorContainer
}

2
这并不理想。首先,浏览器嗅探是无用的,因为IE 9及更高版本支持标准的“Selection”和“Range”API,您可以直接检测到所需的功能。其次,这两个分支不能保证返回相同的内容:IE的“TextRange”的“parentElement()”方法将始终返回一个元素,而“commonAncestorContainer”可能是一个文本节点。第三,命名很奇怪:函数返回的是一个节点,而不是范围。 - Tim Down
短并不一定更好。 - Stefan Falk

0

所有符合标准的代码都可以在IE11+中运行。

文本字符串

window.getSelection().getRangeAt(0).toString()

起始节点(即使文本是向后选择的):

window.getSelection().anchorNode

终节点(即使文本是向后选择的):

window.getSelection().focusNode

你想了解更多吗?选择一些文本并在控制台中运行以下 JavaScript:

console.log(window.getSelection());
console.log(window.getSelection().getRangeAt(0));

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接