检测文本中单击的单词

时间:2011-09-27 01:14:42

标签: javascript html parsing

我正在构建一个JS脚本,在某个点上,它可以在给定页面上允许用户单击任何单词并将该单词存储在变量中。

我有一个非常难看的解决方案,并使用jQuery进行类解析: 我首先解析整个html,在每个空格" "上拆分所有内容,并重新附加包含在<span class="word">word</span>中的所有内容,然后添加一个带有jQ的事件来检测此类上的点击,并使用$(this).innerHTML我得到了点击的单词。

这在很多方面都是缓慢而丑陋的,我希望有人知道另一种方法来实现这一目标。

PS:我可能会考虑将其作为浏览器扩展程序运行,所以如果单纯的JS听起来不可能,并且如果您知道允许的浏览器API,请随时提及它!

一个可能的owrkaround是让用户突出显示单词而不是单击它,但我真的希望能够通过一次点击实现相同的功能!

14 个答案:

答案 0 :(得分:49)

这是一个解决方案,可以在不向文档添加大量跨度的情况下工作(适用于Webkit和Mozilla和IE9 +):

http://jsfiddle.net/Vap7C/15/

<p class="clickable">some words</p>

$(".clickable").click(function(e) {
    s = window.getSelection();
    var range = s.getRangeAt(0);
    var node = s.anchorNode;
    while (range.toString().indexOf(' ') != 0) {
        range.setStart(node, (range.startOffset - 1));
    }
    range.setStart(node, range.startOffset + 1);
    do {
        range.setEnd(node, range.endOffset + 1);

    } while (range.toString().indexOf(' ') == -1 && range.toString().trim() != '' && range.endOffset < node.length);
    var str = range.toString().trim();
    alert(str);
});​
在IE8中,由于getSelection,它有问题。此链接(Is there a cross-browser solution for getSelection()?)可能有助于解决这些问题。我没有在Opera上测试过。

我使用类似问题的http://jsfiddle.net/Vap7C/1/作为起点。它使用了Selection.modify函数:

s.modify('extend','forward','word');
s.modify('extend','backward','word');

不幸的是,他们并不总能得到全部的信息。作为一种解决方法,我得到了Range用于选择并添加了两个循环来查找单词边界。第一个不断添加字符直到它到达空格。第二个循环到达单词的末尾,直到到达空格。

这也会抓住单词末尾的任何标点符号,因此如果需要,请务必将其修剪掉。

答案 1 :(得分:13)

据我所知,为每个单词添加span是唯一的方法。

您可以考虑使用Lettering.js来处理the splitting。虽然这不会真正影响性能,但除非你的“分裂代码”效率低下。

然后,不是将.click()绑定到每个span,而是将单个.click()绑定到span的容器更有效,并检查{ {3}}查看已点击的span

答案 2 :(得分:5)

我所知道的唯一的跨浏览器(IE&lt; 8)方式是包含在span元素中。这很难看,但并不是那么慢。

这个例子直接来自jQuery .css()函数文档,但有一大块文本需要预处理:

http://jsfiddle.net/kMvYy/

这是另一种方法(在此处给出:jquery capture the word value)在同一文本块上,不需要包裹spanhttp://jsfiddle.net/Vap7C/1

答案 3 :(得分:3)

<强> - 编辑 - 那这个呢?它使用getSelection()绑定到mouseup

<script type="text/javascript" src="jquery-1.6.3.min.js"></script>
<script>
$(document).ready(function(){
    words = [];
    $("#myId").bind("mouseup",function(){
        word = window.getSelection().toString();
        if(word != ''){
            if( confirm("Add *"+word+"* to array?") ){words.push(word);}
        }
    });
    //just to see what we've got
    $('button').click(function(){alert(words);});
});
</script>

<div id='myId'>
    Some random text in here with many words huh
</div>
<button>See content</button>

我想不出除分裂之外的方法,这就是我要做的,一个小插件将分成spans,点击后它会将其内容添加到array进一步使用:

<script type="text/javascript" src="jquery-1.6.3.min.js"></script>
<script>
//plugin, take it to another file
(function( $ ){
$.fn.splitWords = function(ary) {
    this.html('<span>'+this.html().split(' ').join('</span> <span>')+'</span>');
    this.children('span').click(function(){
        $(this).css("background-color","#C0DEED");
        ary.push($(this).html());
    });
};
})( jQuery );
//plugin, take it to another file

$(document).ready(function(){
    var clicked_words = [];
    $('#myId').splitWords(clicked_words);
    //just to see what we've stored
    $('button').click(function(){alert(clicked_words);});
});
</script>

<div id='myId'>
    Some random text in here with many words huh
</div>
<button>See content</button>

答案 4 :(得分:3)

以下是对已接受答案的改进:

$(".clickable").click(function (e) {
    var selection = window.getSelection();
    if (!selection || selection.rangeCount < 1) return true;
    var range = selection.getRangeAt(0);
    var node = selection.anchorNode;
    var word_regexp = /^\w*$/;

    // Extend the range backward until it matches word beginning
    while ((range.startOffset > 0) && range.toString().match(word_regexp)) {
      range.setStart(node, (range.startOffset - 1));
    }
    // Restore the valid word match after overshooting
    if (!range.toString().match(word_regexp)) {
      range.setStart(node, range.startOffset + 1);
    }

    // Extend the range forward until it matches word ending
    while ((range.endOffset < node.length) && range.toString().match(word_regexp)) {
      range.setEnd(node, range.endOffset + 1);
    }
    // Restore the valid word match after overshooting
    if (!range.toString().match(word_regexp)) {
      range.setEnd(node, range.endOffset - 1);
    }

    var word = range.toString();
});​

答案 5 :(得分:2)

这是一种完全不同的方法。我不确定它的实用性,但它可能会给你一些不同的想法。 如果你有一个容器标签,其位置相对只有文本,我正在考虑这里。然后你可以在每个单词记录周围放置一个跨度,它的偏移高度,宽度,左边和顶部,然后删除跨度。将它们保存到数组中然后当区域中有单击时,执行搜索以找出最接近单击的单词。这显然在开始时是密集的。因此,在这个人花费一些时间阅读文章的情况下,这将最有效。好处是你不需要担心可能有100多个额外的元素,但这种好处最多可能是微不足道的。

注意我认为您可以从DOM中删除容器元素以加快进程并仍然获得偏移距离,但我不是正面的。

答案 6 :(得分:1)

答案 7 :(得分:1)

然后对@stevendaniel的答案采取另一种做法:

$('.clickable').click(function(){
   var sel=window.getSelection();
   var str=sel.anchorNode.nodeValue,len=str.length, a=b=sel.anchorOffset;
   while(str[a]!=' '&&a--){}; if (str[a]==' ') a++; // start of word
   while(str[b]!=' '&&b++<len){};                   // end of word+1
   console.log(str.substring(a,b));
});
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>

<p class="clickable">The objective can also be achieved by simply analysing the
string you get from <code>sel=window.getSelection()</code>. Two simple searches for
the next blank before and after the word, pointed to by the current position
(<code>sel.anchorOffset</code>) and the work is done:</p>

<p>This second paragraph is <em>not</em> clickable. I tested this on Chrome and Internet explorer (IE11)</p>

答案 8 :(得分:1)

以下是适用于西里尔文的已接受答案的替代方案。我不明白为什么需要检查单词边界,但默认情况下,出于某种原因,选择是折叠的。

let selection = window.getSelection();
if (!selection || selection.rangeCount < 1) return
let node = selection.anchorNode
let range = selection.getRangeAt(0)

let text = selection.anchorNode.textContent

let startIndex, endIndex
startIndex = endIndex = selection.anchorOffset
const expected = /[A-ZА-Я]*/i

function testSlice() {
  let slice = text.slice(startIndex, endIndex)
  return slice == slice.match(expected)[0]
}

while(startIndex > 0 && testSlice()) {
  startIndex -= 1
}
startIndex += 1

while(endIndex < text.length && testSlice()){
  endIndex += 1
}
endIndex -= 1

range.setStart(node, startIndex)
range.setEnd(node, endIndex)

let word = range.toString()
return word

答案 9 :(得分:0)

这是我对stevendaniels' answer的评论的跟进(上图):

  

在上面的第一个代码部分中,range.setStart(node,   (range.startOffset - 1));在a中的第一个单词上运行时崩溃   “node”,因为它试图将范围设置为负值。我试过了   添加逻辑以防止,但随后   range.setStart(node,range.startOffset + 1);返回除第一个之外的所有   第一个字的字母。此外,当单词被换行分隔时,   除了之外,还返回上一行的最后一个单词   点击了一下。所以,这需要一些工作。

这是我的代码,可以使答案中的范围扩展代码可靠地运行:

while (range.startOffset !== 0) {                   // start of node
    range.setStart(node, range.startOffset - 1)     // back up 1 char
    if (range.toString().search(/\s/) === 0) {      // space character
        range.setStart(node, range.startOffset + 1);// move forward 1 char
        break;
    }
}

while (range.endOffset < node.length) {         // end of node
    range.setEnd(node, range.endOffset + 1)     // forward 1 char
    if (range.toString().search(/\s/) !== -1) { // space character
        range.setEnd(node, range.endOffset - 1);// back 1 char
        break;
    }
}

答案 10 :(得分:0)

看起来更简单的解决方案。

document.addEventListener('selectionchange', () => {
  const selection = window.getSelection();
  const matchingRE = new RegExp(`^.{0,${selection.focusOffset}}\\s+(\\w+)`);
  const clickedWord = (matchingRE.exec(selectiaon.focusNode.textContent) || ['']).pop();
});

我正在测试

答案 11 :(得分:0)

所选解决方案有时对俄语文本不起作用(显示错误)。我建议对俄语和英语文本采用以下解决方案:

function returnClickedWord(){
    let selection = window.getSelection(),
        text = selection.anchorNode.data,
        index = selection.anchorOffset,
        symbol = "a";
    while(/[a-zA-z0-9а-яА-Я]/.test(symbol)&&symbol!==undefined){
        symbol = text[index--];
    }
    index += 2;
    let word = "";
    symbol = "a";
    while(/[a-zA-z0-9а-яА-Я]/.test(symbol) && index<text.length){
        symbol = text[index++];
    word += symbol;
    }
    alert(word);
}
document.addEventListener("click", returnClickedWord);

答案 12 :(得分:0)

为使其余答案更完整,我将对使用的主要方法进行解释:

  • window.getSelection():这是主要方法。它用于获取有关您在文本中所做选择的信息(通过按下鼠标按钮,拖动然后释放,而不是简单地单击)。 它返回一个Selection对象,其主要属性是anchorOffset和focusOffset,它们分别是所选的第一个和最后一个字符的位置。如果这没有什么意义,这是我先前链接的MDN网站的定位和重点说明:

    锚点是用户开始选择的地方,焦点是用户结束选择的地方

    • toString() :此方法返回所选文本。

    • anchorOffset :在您做出选择的节点的文本中开始的选择索引。
      如果您有这个html:

      <div>aaaa<span>bbbb cccc dddd</span>eeee/div>
      

      ,然后选择“ cccc”,然后选择anchorOffset == 5,因为在节点内,选择从html元素的第5个字符开始。

    • focusOffset :您做出选择的节点文本中的最终选择索引。
      在前面的示例之后,focusOffset == 9。

    • getRangeAt() :返回 Range 对象。它接收一个索引作为参数,因为(我怀疑,我实际上需要对此进行确认)在某些浏览器(例如Firefox)中,您可以select multiple independent texts at once

      • startOffset :此Range的属性类似于anchorOffset。
      • endOffset :正如预期的那样,这一点类似于focusOffset。
      • toString :类似于Selection对象的toString()方法。

除了其他解决方案之外,还有另一种似乎没人注意到的方法:Document.caretRangeFromPoint()

Document接口的caretRangeFromPoint()方法在指定坐标下返回文档片段的Range对象。

如果遵循此link,您将看到实际上该文档如何提供一个与OP所要求的非常相似的示例。此示例未获得用户单击的特定单词,而是在用户单击的字符之后添加了<br>

function insertBreakAtPoint(e) {
  let range;
  let textNode;
  let offset;

  if (document.caretPositionFromPoint) {
    range = document.caretPositionFromPoint(e.clientX, e.clientY);
    textNode = range.offsetNode;
    offset = range.offset;    
  } else if (document.caretRangeFromPoint) {
    range = document.caretRangeFromPoint(e.clientX, e.clientY);
    textNode = range.startContainer;
    offset = range.startOffset;
  }
  // Only split TEXT_NODEs
  if (textNode && textNode.nodeType == 3) {
    let replacement = textNode.splitText(offset);
    let br = document.createElement('br');
    textNode.parentNode.insertBefore(br, replacement);
  }
}

let paragraphs = document.getElementsByTagName("p");
for (let i = 0; i < paragraphs.length; i++) {
  paragraphs[i].addEventListener('click', insertBreakAtPoint, false);
}
<p>Lorem ipsum dolor sit amet, consetetur sadipscing elitr,
sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat,
sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum.
Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet.</p>

通过在上一个空白字符和下一个空白字符之前获取所有文本来获取单词是一个问题。

答案 13 :(得分:0)

accepted answer 一样,此解决方案使用 window.getSelection 来推断文本中的光标位置。它使用正则表达式来可靠地找到单词边界,并且不限制 starting nodeending node 为同一个节点。

此代码相对于 accepted answer 有以下改进:

  • 在文本开头起作用。
  • 允许跨多个节点进行选择。
  • 不修改选择范围。
  • 允许用户使用自定义选择覆盖范围。
  • 即使被非空格(例如 "\t\n")包围也能检测到单词
  • 仅使用普通 JavaScript。
  • 没有警报!

getBoundaryPoints = (range) => ({ start: range.startOffset, end: range.endOffset })

function expandTextRange(range) {
    // expand to include a whole word

    matchesStart = (r) => r.toString().match(/^\s/) // Alternative: /^\W/
    matchesEnd = (r) => r.toString().match(/\s$/)   // Alternative: /\W$/

    // Find start of word 
    while (!matchesStart(range) && range.startOffset > 0) {
        range.setStart(range.startContainer, range.startOffset - 1)
    }
    if (matchesStart(range)) range.setStart(range.startContainer, range.startOffset + 1)

    // Find end of word
    var length = range.endContainer.length || range.endContainer.childNodes.length
    while (!matchesEnd(range) && range.endOffset < length) {
        range.setEnd(range.endContainer, range.endOffset + 1)
    }
    if (matchesEnd(range) && range.endOffset > 0) range.setEnd(range.endContainer, range.endOffset - 1)

    //console.log(JSON.stringify(getBoundaryPoints(range)))
    //console.log('"' + range.toString() + '"')
    var str = range.toString()
}

function getTextSelectedOrUnderCursor() {
    var sel = window.getSelection()
    var range = sel.getRangeAt(0).cloneRange()

    if (range.startOffset == range.endOffset) expandTextRange(range)
    return range.toString()
}

function onClick() {
    console.info('"' + getTextSelectedOrUnderCursor() + '"')
}

var content = document.body
content.addEventListener("click", onClick)
<div id="text">
<p>Vel consequatur incidunt voluptatem. Sapiente quod qui rem libero ut sunt ratione. Id qui id sit id alias rerum officia non. A rerum sunt repudiandae. Aliquam ut enim libero praesentium quia eum.</p>

<p>Occaecati aut consequuntur voluptatem quae reiciendis et esse. Quis ut sunt quod consequatur quis recusandae voluptas. Quas ut in provident. Provident aut vel ea qui ipsum et nesciunt eum.</p>
</div>

因为它使用了arrow functions,所以这段代码在 IE 中不起作用;但这很容易调整。此外,因为它允许用户选择跨越节点,所以它可能返回用户通常不可见的文本,例如存在于用户选择中的脚本标签的内容。 (三击最后一段来演示这个缺陷。)

您应该决定用户应该看到哪些类型的节点,并过滤掉不需要的节点,我认为这超出了问题的范围。