Detect which word has been clicked on within a text

I am building a JS script which at some point is able to, on a given page, allow the user to click on any word and store this word in a variable.

I have one solution which is pretty ugly and involves class-parsing using jQuery: I first parse the entire html, split everything on each space " ", and re-append everything wrapped in a <span class="word">word</span>, and then I add an event with jQ to detect clicks on such a class, and using $(this).innerHTML I get the clicked word.

This is slow and ugly in so many ways and I was hoping that someone knows of another way to achieve this.

PS: I might consider running it as a browser extension, so if it doesn't sound possible with mere JS, and if you know a browser API that would allow that, feel free to mention it !

A possible owrkaround would be to get the user to highlight the word instead of clicking it, but I would really love to be able to achieve the same thing with only a click !

Answers


Here's a solution that will work without adding tons of spans to the document (works on Webkit and Mozilla and 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);
});​

in IE8, it has problems because of getSelection. This link ( Is there a cross-browser solution for getSelection()? ) may help with those issues. I haven't tested on Opera.

I used http://jsfiddle.net/Vap7C/1/ from a similar question as a starting point. It used the Selection.modify function:

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

Unfortunately they don't always get the whole word. As a workaround, I got the Range for the selection and added two loops to find the word boundaries. The first one keeps adding characters to the word until it reaches a space. the second loop goes to the end of the word until it reaches a space.

This will also grab any punctuation at the end of the word, so make sure you trim that out if you need to.


As far as I know, adding a span for each word is the only way to do this.

You might consider using Lettering.js, which handles the splitting for you. Though this won't really impact performance, unless your "splitting code" is inefficient.

Then, instead of binding .click() to every span, it would be more efficient to bind a single .click() to the container of the spans, and check event.target to see which span has been clicked.


The only cross-browser (IE < 8) way that I know of is wrapping in span elements. It's ugly but not really that slow.

This example is straight from the jQuery .css() function documentation, but with a huge block of text to pre-process:

http://jsfiddle.net/kMvYy/

Here's another way of doing it (given here: jquery capture the word value ) on the same block of text that doesn't require wrapping in span. http://jsfiddle.net/Vap7C/1


Here are improvements for the accepted answer:

$(".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();
});​

-EDIT- What about this? it uses getSelection() binded to 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>

I can't think of a way beside splitting, this is what I'd do, a small plugin that will split into spans and when clicked it will add its content to an array for further use:

<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>

Here is a completely different method. I am not sure about the practicality of it, but it may give you some different ideas. Here is what I am thinking if you have a container tag with position relative with just text in it. Then you could put a span around each word record its offset Height, Width, Left, and Top, then remove the span. Save those to an array then when there is a click in the area do a search to find out what word was closest to the click. This obviously would be intensive at the beginning. So this would work best in a situation where the person will be spending some time perusing the article. The benefit is you do not need to worry about possibly 100s of extra elements, but that benefit may be marginal at best.

Note I think you could remove the container element from the DOM to speed up the process and still get the offset distances, but I am not positive.


And another take on @stevendaniel's answer:

$('.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>

like this Get user selected text with jquery and its uses?


This is a followup on my comment to stevendaniels' answer (above):

In the first code section above, range.setStart(node, (range.startOffset - 1)); crashes when run on the first word in a "node," because it attempts to set range to a negative value. I tried adding logic to prevent that, but then the subsequent range.setStart(node, range.startOffset + 1); returns all but the first letter of the first word. Also, when words are separated by a newline, the last word on the previous line is returned in addition to the clicked-on word. So, this needs some work.

Here is my code to make the range expansion code in that answer work reliably:

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;
    }
}

What looks like a slightly simpler solution.

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

I'm testing


The selected solution sometimes does not work on Russian texts (shows error). I would suggest the following solution for Russian and English texts:

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);

Need Your Help

Literal Syntax For byte[] arrays using Hex notation..?

java arrays compiler-construction hex literals

The compiler seems to be ok with this (single digit hex values only):

setuptools on python3.0

python-3.x setuptools

I was trying to install "setuptool" package for python3.0. But unfortunately while