How to get a word under cursor using JavaScript?
Asked Answered
C

12

81

If I for example have

<p> some long text </p>

on my HTML page, how can I know that cursor of mouse is for example above the word 'text'?

Carlicarlick answered 15/3, 2010 at 0:27 Comment(2)
This a live demo for how to get a word under cursor using JavaScript based on the source code provided by Damovisa: jsfiddle.net/5gyRx.Haughty
@Carlicarlick There is a new answer to this question from a bounty. You might consider selecting it (for the sake of new users who come).Farrah
C
43

Further to the two other answers, you may be able to split your paragraphs up into spans using jQuery (or javascript generally).

That way, you wouldn't need to think about outputting your text with spans around the words. Let your javascript do it for you.

e.g.

<p>Each word will be wrapped in a span.</p>
<p>A second paragraph here.</p>
Word: <span id="word"></span>

<script type="text/javascript">
    $(function() {
        // wrap words in spans
        $('p').each(function() {
            var $this = $(this);
            $this.html($this.text().replace(/\b(\w+)\b/g, "<span>$1</span>"));
        });

        // bind to each span
        $('p span').hover(
            function() { $('#word').text($(this).css('background-color','#ffff66').text()); },
            function() { $('#word').text(''); $(this).css('background-color',''); }
        );
    });
</script>

Note that the above code, while it works, will strip out any html inside your paragraph tags.

jsFiddle example

Cape answered 15/3, 2010 at 0:38 Comment(6)
Or you could just do $(this).text().replace(/\b(\w+)\b/g, "<span>$1</span>") instead of the loop. This will handle all whitespace characters correctly.Anesthesiologist
@Chetan - thanks for that, I'm not very good with regex so I did it the easy way :) I've update it.Cape
I thought about it but it's awkward solution ( I'm newbie in JavaScript, so my way was much worse than yours ). Thanks for clarification. @Chetan - this is neat solution.Carlicarlick
How would we edit it so that it will identify h1,h2,h3, etc. tags instead of just p tags?Pollster
@Pollster You should just be able to replace the first $('p') selector with $('p,h1,h2,h3') and so on. Similarly, to get the hover to work, you'd need to change the second selector to $('p span,h1 span,h2 span,h3 span').Cape
how do I do to get two or more words?Carabin
D
44

My other answer works only in Firefox. This answer works in Chrome. (Might work in Firefox, too, I don't know.)

function getWordAtPoint(elem, x, y) {
  if(elem.nodeType == elem.TEXT_NODE) {
    var range = elem.ownerDocument.createRange();
    range.selectNodeContents(elem);
    var currentPos = 0;
    var endPos = range.endOffset;
    while(currentPos+1 < endPos) {
      range.setStart(elem, currentPos);
      range.setEnd(elem, currentPos+1);
      if(range.getBoundingClientRect().left <= x && range.getBoundingClientRect().right  >= x &&
         range.getBoundingClientRect().top  <= y && range.getBoundingClientRect().bottom >= y) {
        range.expand("word");
        var ret = range.toString();
        range.detach();
        return(ret);
      }
      currentPos += 1;
    }
  } else {
    for(var i = 0; i < elem.childNodes.length; i++) {
      var range = elem.childNodes[i].ownerDocument.createRange();
      range.selectNodeContents(elem.childNodes[i]);
      if(range.getBoundingClientRect().left <= x && range.getBoundingClientRect().right  >= x &&
         range.getBoundingClientRect().top  <= y && range.getBoundingClientRect().bottom >= y) {
        range.detach();
        return(getWordAtPoint(elem.childNodes[i], x, y));
      } else {
        range.detach();
      }
    }
  }
  return(null);
}    

In your mousemove handler, call getWordAtPoint(e.target, e.x, e.y);

Dell answered 14/9, 2010 at 15:52 Comment(8)
Code works fine on iOS (6/7), but in Android 4.0.3 getBoundingClientRect can result null. So add: range.getBoundingClientRect() != null as a condition in the first loop (before getting the left property).Busiek
The docs state that the boundary for "word" is a white space character. But the expand doesn't seem to work for urls. Any ideas?Despite
@Dell I found your code running fine in chrome and not in firefox.But when range.expand is commented it is able to give the character under cursor for firefox.Any idea to make it work in firefox?Coenosarc
This is a nice piece of code, but it will break when working with a mix of textNodes and other inline elements. There are two cases where this appears. 1. A text node with a linebreak will have a nonsense bounding box. 2. Inline elements with height greater than the textNode line can reset the vertical position of the range. I think it should be possible to overcome those by checking textNodes character by character from the start and compensating random resets of vertical position by assuming texNodes can never be higher than any of their previous siblings (but that might not always be true).Szombathely
Also the +1 in the condition in while loop is unnecessary. The last character of the textNode starts at range.endOffset (and ends at range.endOffset + 1). So, unless the condition is actually while(currentPos < endPos) the last character will never be tested.Szombathely
Looks like I might have been mistaken about the second point and the problem might have been caused by the script stopping the lookup at an earlier node (because of the broken bounding box).Szombathely
@Dell see jsfiddle.net/ohaf4ytL and #30455030 for cases where this code breaks downFarrah
@Farrah It's working fine for me. I just tested your jsfiddle and, with the console open, I see all the text appearing there. I'm using Chrome.Dell
C
43

Further to the two other answers, you may be able to split your paragraphs up into spans using jQuery (or javascript generally).

That way, you wouldn't need to think about outputting your text with spans around the words. Let your javascript do it for you.

e.g.

<p>Each word will be wrapped in a span.</p>
<p>A second paragraph here.</p>
Word: <span id="word"></span>

<script type="text/javascript">
    $(function() {
        // wrap words in spans
        $('p').each(function() {
            var $this = $(this);
            $this.html($this.text().replace(/\b(\w+)\b/g, "<span>$1</span>"));
        });

        // bind to each span
        $('p span').hover(
            function() { $('#word').text($(this).css('background-color','#ffff66').text()); },
            function() { $('#word').text(''); $(this).css('background-color',''); }
        );
    });
</script>

Note that the above code, while it works, will strip out any html inside your paragraph tags.

jsFiddle example

Cape answered 15/3, 2010 at 0:38 Comment(6)
Or you could just do $(this).text().replace(/\b(\w+)\b/g, "<span>$1</span>") instead of the loop. This will handle all whitespace characters correctly.Anesthesiologist
@Chetan - thanks for that, I'm not very good with regex so I did it the easy way :) I've update it.Cape
I thought about it but it's awkward solution ( I'm newbie in JavaScript, so my way was much worse than yours ). Thanks for clarification. @Chetan - this is neat solution.Carlicarlick
How would we edit it so that it will identify h1,h2,h3, etc. tags instead of just p tags?Pollster
@Pollster You should just be able to replace the first $('p') selector with $('p,h1,h2,h3') and so on. Similarly, to get the hover to work, you'd need to change the second selector to $('p span,h1 span,h2 span,h3 span').Cape
how do I do to get two or more words?Carabin
R
41

Preamble:

If you have multiple spans and nested HTML that separate words (or even characters in words), then all the above solutions will have trouble returning the full and correct word.

Here is an example from the bounty question: Х</span>rт0съ. How to properly return Хrт0съ? These issues were not addressed back in 2010, so I will present two solutions now (2015).


Solution 1 - Strip inner tags, wrap spans around each full word:

One solution is to strip out the span tags inside paragraphs but preserve their text. Split words and phrases are thus joined back together as regular text. Each word is found by whitespace division (not just a space), and those words are wrapped in spans which can be individually accessed.

In the demo, you can highlight the entire word and thus get the text of the whole word.


pic 0

Code:

$(function() {
  // Get the HTML in #hoverText - just a wrapper for convenience
  var $hoverText = $("#hoverText");

  // Replace all spans inside paragraphs with their text
  $("p span", $hoverText).each(function() {
    var $this = $(this);
    var text = $this.text(); // get span content
    $this.replaceWith(text); // replace all span with just content
  });

  // Wrap words in spans AND preserve the whitespace
  $("p", $hoverText).each(function() {
    var $this = $(this);
    var newText = $this.text().replace(/([\s])([^\s]+)/g, "$1<span>$2</span>");
    newText = newText.replace(/^([^\s]+)/g, "<span>$1</span>");
    $this.empty().append(newText);
  });

  // Demo - bind hover to each span
  $('#hoverText span').hover(
    function() { $(this).css('background-color', '#ffff66'); },
    function() { $(this).css('background-color', ''); }
  );
});
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.11.1/jquery.min.js"></script>
<div id="hoverText">
  <p><span class="kinovar"><span id="selection_index3337" class="selection_index"></span>По f7-мъ часЁ твори1тъ сщ7eнникъ начaло съ кади1ломъ и3 со свэщeю, цrкимъ двeремъ tвeрзєннымъ, и3 поeтъ: Х</span>rт0съ воскRсе: <span class="kinovar">со 
стіхи2. И# по стісёхъ pал0мъ: Б</span>лгcви2 душE моS гDа: <span class="kinovar">И# є3ктеніA. Тaже каfjсма nбhчнаz.</span>
  </p>
</div>

Solution 1 full-text demo


Solution 2 - Caret inspection and DOM traversal:

Here is a more sophisticated solution. It's an algorithmic solution using node traversal that accurately captures the full and correct word under a cursor in a text node.

A temporary word is found by checking the caret position (using caretPositionFromPoint or caretRangeFromPoint, credits for the idea to @chrisv). This may or may not be the full word, yet.

It is then analyzed to see if it is at either edge of its text node (beginning or end). If it is, the previous text node or the following text node is examined to see if it should be joined to make this word fragment longer.

Example:

Х</span>rт0съ must return Хrт0съ, not Х nor rт0съ.

The DOM tree is traversed to get the next non-barrier text node. If two word fragments are separated by a <p> or some other barrier tag, then they are not adjacent and thus not part of the same word.

Example:

њб.)</p><p>Во should not return њб.)Во


In the demo, the left floating div is the word under the cursor. The right floating div, if visible, shows how a word on a boundary was formed. Other tags can safely be inline'd with the text in this solution.

pic 1

Code:

$(function() {
  // Get the HTML in #hoverText - just a wrapper for convenience
  var $hoverText = $("#hoverText");

  // Get the full word the cursor is over regardless of span breaks
  function getFullWord(event) {
     var i, begin, end, range, textNode, offset;
    
    // Internet Explorer
    if (document.body.createTextRange) {
       try {
         range = document.body.createTextRange();
         range.moveToPoint(event.clientX, event.clientY);
         range.select();
         range = getTextRangeBoundaryPosition(range, true);
      
         textNode = range.node;
         offset = range.offset;
       } catch(e) {
         return ""; // Sigh, IE
       }
    }
    
    // Firefox, Safari
    // REF: https://developer.mozilla.org/en-US/docs/Web/API/Document/caretPositionFromPoint
    else if (document.caretPositionFromPoint) {
      range = document.caretPositionFromPoint(event.clientX, event.clientY);
      textNode = range.offsetNode;
      offset = range.offset;

      // Chrome
      // REF: https://developer.mozilla.org/en-US/docs/Web/API/document/caretRangeFromPoint
    } else if (document.caretRangeFromPoint) {
      range = document.caretRangeFromPoint(event.clientX, event.clientY);
      textNode = range.startContainer;
      offset = range.startOffset;
    }

    // Only act on text nodes
    if (!textNode || textNode.nodeType !== Node.TEXT_NODE) {
      return "";
    }

    var data = textNode.textContent;

    // Sometimes the offset can be at the 'length' of the data.
    // It might be a bug with this 'experimental' feature
    // Compensate for this below
    if (offset >= data.length) {
      offset = data.length - 1;
    }

    // Ignore the cursor on spaces - these aren't words
    if (isW(data[offset])) {
      return "";
    }

    // Scan behind the current character until whitespace is found, or beginning
    i = begin = end = offset;
    while (i > 0 && !isW(data[i - 1])) {
      i--;
    }
    begin = i;

    // Scan ahead of the current character until whitespace is found, or end
    i = offset;
    while (i < data.length - 1 && !isW(data[i + 1])) {
      i++;
    }
    end = i;

    // This is our temporary word
    var word = data.substring(begin, end + 1);

    // Demo only
    showBridge(null, null, null);

    // If at a node boundary, cross over and see what 
    // the next word is and check if this should be added to our temp word
    if (end === data.length - 1 || begin === 0) {

      var nextNode = getNextNode(textNode);
      var prevNode = getPrevNode(textNode);

      // Get the next node text
      if (end == data.length - 1 && nextNode) {
        var nextText = nextNode.textContent;

        // Demo only
        showBridge(word, nextText, null);

        // Add the letters from the next text block until a whitespace, or end
        i = 0;
        while (i < nextText.length && !isW(nextText[i])) {
          word += nextText[i++];
        }

      } else if (begin === 0 && prevNode) {
        // Get the previous node text
        var prevText = prevNode.textContent;

        // Demo only
        showBridge(word, null, prevText);

        // Add the letters from the next text block until a whitespace, or end
        i = prevText.length - 1;
        while (i >= 0 && !isW(prevText[i])) {
          word = prevText[i--] + word;
        }
      }
    }
    return word;
  }

  // Return the word the cursor is over
  $hoverText.mousemove(function(e) {
    var word = getFullWord(e);
    if (word !== "") {
      $("#result").text(word);
    }
  });
});

// Helper functions

// Whitespace checker
function isW(s) {
  return /[ \f\n\r\t\v\u00A0\u2028\u2029]/.test(s);
}

// Barrier nodes are BR, DIV, P, PRE, TD, TR, ... 
function isBarrierNode(node) {
  return node ? /^(BR|DIV|P|PRE|TD|TR|TABLE)$/i.test(node.nodeName) : true;
}

// Try to find the next adjacent node
function getNextNode(node) {
  var n = null;
  // Does this node have a sibling?
  if (node.nextSibling) {
    n = node.nextSibling;

    // Doe this node's container have a sibling?
  } else if (node.parentNode && node.parentNode.nextSibling) {
    n = node.parentNode.nextSibling;
  }
  return isBarrierNode(n) ? null : n;
}

// Try to find the prev adjacent node
function getPrevNode(node) {
  var n = null;

  // Does this node have a sibling?
  if (node.previousSibling) {
    n = node.previousSibling;

    // Doe this node's container have a sibling?
  } else if (node.parentNode && node.parentNode.previousSibling) {
    n = node.parentNode.previousSibling;
  }
  return isBarrierNode(n) ? null : n;
}

// REF: https://mcmap.net/q/37673/-how-to-get-selected-textnode-in-contenteditable-div-in-ie
function getChildIndex(node) {
  var i = 0;
  while( (node = node.previousSibling) ) {
    i++;
  }
  return i;
}

// All this code just to make this work with IE, OTL
// REF: https://mcmap.net/q/37673/-how-to-get-selected-textnode-in-contenteditable-div-in-ie
function getTextRangeBoundaryPosition(textRange, isStart) {
  var workingRange = textRange.duplicate();
  workingRange.collapse(isStart);
  var containerElement = workingRange.parentElement();
  var workingNode = document.createElement("span");
  var comparison, workingComparisonType = isStart ?
    "StartToStart" : "StartToEnd";

  var boundaryPosition, boundaryNode;

  // Move the working range through the container's children, starting at
  // the end and working backwards, until the working range reaches or goes
  // past the boundary we're interested in
  do {
    containerElement.insertBefore(workingNode, workingNode.previousSibling);
    workingRange.moveToElementText(workingNode);
  } while ( (comparison = workingRange.compareEndPoints(
    workingComparisonType, textRange)) > 0 && workingNode.previousSibling);

  // We've now reached or gone past the boundary of the text range we're
  // interested in so have identified the node we want
  boundaryNode = workingNode.nextSibling;
  if (comparison == -1 && boundaryNode) {
    // This must be a data node (text, comment, cdata) since we've overshot.
    // The working range is collapsed at the start of the node containing
    // the text range's boundary, so we move the end of the working range
    // to the boundary point and measure the length of its text to get
    // the boundary's offset within the node
    workingRange.setEndPoint(isStart ? "EndToStart" : "EndToEnd", textRange);

    boundaryPosition = {
      node: boundaryNode,
      offset: workingRange.text.length
    };
  } else {
    // We've hit the boundary exactly, so this must be an element
    boundaryPosition = {
      node: containerElement,
      offset: getChildIndex(workingNode)
    };
  }

  // Clean up
  workingNode.parentNode.removeChild(workingNode);

  return boundaryPosition;
}

// DEMO-ONLY code - this shows how the word is recombined across boundaries
function showBridge(word, nextText, prevText) {
  if (nextText) {
    $("#bridge").html("<span class=\"word\">" + word + "</span>  |  " + nextText.substring(0, 20) + "...").show();
  } else if (prevText) {
    $("#bridge").html("..." + prevText.substring(prevText.length - 20, prevText.length) + "  |  <span class=\"word\">" + word + "</span>").show();
  } else {
    $("#bridge").hide();
  }
}
.kinovar { color:red; font-size:20px;}.slavic { color: blue;}#result {top:10px;left:10px;}#bridge { top:10px; right:80px;}.floater { position: fixed; background-color:white; border:2px solid black; padding:4px;}.word { color:blue;}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.11.1/jquery.min.js"></script> <div id="bridge" class="floater"></div> <div id="result" class="floater"></div> <div id="hoverText"><p><span class="kinovar"><span id="selection_index3337" class="selection_index"></span>По f7-мъ часЁ твори1тъ сщ7eнникъ начaло съ кади1ломъ и3 со свэщeю, цrкимъ двeремъ tвeрзєннымъ, и3 поeтъ: Х</span>rт0съ воскRсе: <span class="kinovar">со стіхи2. И# по стісёхъ pал0мъ: Б</span>лгcви2 душE моS гDа: <span class="kinovar">И# є3ктеніA. Тaже каfjсма nбhчнаz.</span></p><div class="slavic"> <input value="Works around other tags!"><p><span id="selection_index3737" class="selection_index"></span>(л. рo7з њб.)</p><p><span class="kinovar"><span id="selection_index3738" class="selection_index"></span>Во вт0рникъ вeчера</span> </p><p><span class="kinovar"><span id="selection_index3739" class="selection_index"></span>tдaніе прaздника пaсхи.</span></p><p><span class="kinovar"><span id="selection_index3740" class="selection_index"></span>По f7-мъ часЁ твори1тъ сщ7eнникъ начaло съ кади1ломъ и3 со свэщeю, цrкимъ двeремъ tвeрзєннымъ, и3 поeтъ: Х</span>rт0съ воскRсе: <span class="kinovar">со стіхи2. И# по стісёхъ pал0мъ: Б</span>лгcви2 душE моS гDа: <span class="kinovar">И# є3ктеніA. Тaже каfjсма nбhчнаz.<input value="Works around inline tags too"></span></p><p><span class="kinovar"><span id="selection_index3741" class="selection_index"></span>На ГDи воззвaхъ: поeмъ стіхи6ры самоглaсны, слэпaгw, на ѕ7. Глaсъ в7:</span></p></div>

(Note: I took the liberty of applying styles to the span tags that were present in your sample HTML to illuminate where text node borders are.)

Solution 2 full-text demo

(Working in Chrome and IE so far. For IE, a method from IERange had to be used as a shim for cross-browser compatibility)

Religious answered 2/6, 2015 at 2:13 Comment(5)
In this Slavonic encoding, the { signifies an accent, so I would do just to count a word as everything within a space, even genuine punctuation marks (as I will remove them myself). The answer doesn't technically meet the bounty, but if it best solves the issue, I will select.Farrah
@Farrah I've posted a second solution, a much better one, which uses DOM traversal and works in IE too. It's fast and is designed to be robust for future HTML. I like both solutions, but this one doesn't use span tag wrapping as you requested.Religious
Thanks. Works so far perfectly. I've encapsulated the functions as an object to make it work better with my application. jsfiddle.net/ohaf4ytL/1 I think this will be very useful for others as well.Farrah
why you choose such an alphabet for such a great answer... it gives me headaches to work around this.Submiss
@Submiss Thank you. The original Bounty Question was in that alphabet with a much bigger corpus to process. That was a wild and tough bounty question I have to admit.Religious
H
11

To my knowledge, you can't.

Only thing I can think of is to put each of the words in their own element, then apply mouse over events to those elements.

<p><span>Some</span> <span>long</span> <span>text</span></p>

<script>
$(document).ready(function () {
  $('p span').bind('mouseenter', function () {
    alert($(this).html() + " is what you're currently hovering over!");
  });
});
</script>
Heavyarmed answered 15/3, 2010 at 0:32 Comment(1)
Here is a demonstration of the above code on jsfiddle: jsfiddle.net/5bT4BPalais
C
10

Here's a simple solution that works in Chrome for most cases:

function getWordAtPoint(x, y) {
  var range = document.caretRangeFromPoint(x, y);

  if (range.startContainer.nodeType === Node.TEXT_NODE) {
    range.expand('word');
    return range.toString().trim();
  }

  return null;
}

I leave filtering out punctuation and properly handling hyphenated words as an exercise to the reader :).

Cribwork answered 22/4, 2018 at 2:7 Comment(3)
Just what I needed for a Chrome extension.Springy
@Springy That's exactly what motivated me to come up with this recipe :).Cribwork
The x/y coordinates need to be event.clientX and not event.pageX. If using pageX, caretRangeFromPoint() will return null when the page is scrolled and the mouse is outside of the intial viewport's coordinates.Boll
H
5

There is an API for this in the current CSSOM View draft: document.caretPositionFromPoint(x,y)

You would have to check which browser supports this, though. Firefox 7 seems not to support it at all, whereas bug reports indicate Firefox 9 will. Chrome 14 supports caretRangeFromPoint(x,y) which is essentially the same, but from an older CSSOM draft.

Horsecar answered 22/10, 2011 at 10:55 Comment(2)
It looks like your answer suits my bounty on the project. It just needs a bit of work to actually find the word expanded from the caret point. The native range expand method doesn't work well enough. I can research this myself, but if you can provide code to work with my demo jsfiddle.net/ohaf4ytL, that would be great.Farrah
@Farrah I have implemented this solution (https://mcmap.net/q/37610/-how-to-get-a-word-under-cursor-using-javascript). Does it fit your needs?Flycatcher
F
5

Here is the solution for the bounty.

As suggested by chrisv you can use document.caretRangeFromPoint (chrome) or document.caretPositionFromPoint (Firefox). I think this solution better answer your question as it doesn't alter your text or the DOM.

This function return the word under the mouse cursor without altering the DOM:

From the document.caretRangeFromPoint documentation:

The caretRangeFromPoint() method of the Document interface returns a Range object for the document fragment under the specified coordinates.

From the document.caretPositionFromPoint documentation:

This method is used to retrieve the caret position in a document based on two coordinates. A CaretPosition is returned, containing the found DOM node and the character offset in that node.

The two function are slightly different but they both return the node containing the text and the offset of the cursor in this text. So it is easy to get the word under the mouse.

See the full example:

$(function () {
    function getWordUnderCursor(event) {
        var range, textNode, offset;

        if (document.body.createTextRange) {           // Internet Explorer
            try {
                range = document.body.createTextRange();
                range.moveToPoint(event.clientX, event.clientY);
                range.select();
                range = getTextRangeBoundaryPosition(range, true);
  
                textNode = range.node;
                offset = range.offset;
            } catch(e) {
                return "";
            }
        }
        else if (document.caretPositionFromPoint) {    // Firefox
            range = document.caretPositionFromPoint(event.clientX, event.clientY);
            textNode = range.offsetNode;
            offset = range.offset;
        } else if (document.caretRangeFromPoint) {     // Chrome
            range = document.caretRangeFromPoint(event.clientX, event.clientY);
            textNode = range.startContainer;
            offset = range.startOffset;
        }

        //data contains a full sentence
        //offset represent the cursor position in this sentence
        var data = textNode.data,
            i = offset,
            begin,
            end;

        //Find the begin of the word (space)
        while (i > 0 && data[i] !== " ") { --i; };
        begin = i;

        //Find the end of the word
        i = offset;
        while (i < data.length && data[i] !== " ") { ++i; };
        end = i;

        //Return the word under the mouse cursor
        return data.substring(begin, end);
    }

    //Get the HTML in a div #hoverText and detect mouse move on it
    var $hoverText = $("#hoverText");
    $hoverText.mousemove(function (e) {
        var word = getWordUnderCursor(e);
        
        //Show the word in a div so we can test the result
        if (word !== "") 
            $("#testResult").text(word);
    });
});

// This code make it works with IE
// REF: https://mcmap.net/q/37673/-how-to-get-selected-textnode-in-contenteditable-div-in-ie
function getTextRangeBoundaryPosition(textRange, isStart) {
  var workingRange = textRange.duplicate();
  workingRange.collapse(isStart);
  var containerElement = workingRange.parentElement();
  var workingNode = document.createElement("span");
  var comparison, workingComparisonType = isStart ?
    "StartToStart" : "StartToEnd";

  var boundaryPosition, boundaryNode;

  // Move the working range through the container's children, starting at
  // the end and working backwards, until the working range reaches or goes
  // past the boundary we're interested in
  do {
    containerElement.insertBefore(workingNode, workingNode.previousSibling);
    workingRange.moveToElementText(workingNode);
  } while ( (comparison = workingRange.compareEndPoints(
    workingComparisonType, textRange)) > 0 && workingNode.previousSibling);

  // We've now reached or gone past the boundary of the text range we're
  // interested in so have identified the node we want
  boundaryNode = workingNode.nextSibling;
  if (comparison == -1 && boundaryNode) {
    // This must be a data node (text, comment, cdata) since we've overshot.
    // The working range is collapsed at the start of the node containing
    // the text range's boundary, so we move the end of the working range
    // to the boundary point and measure the length of its text to get
    // the boundary's offset within the node
    workingRange.setEndPoint(isStart ? "EndToStart" : "EndToEnd", textRange);

    boundaryPosition = {
      node: boundaryNode,
      offset: workingRange.text.length
    };
  } else {
    // We've hit the boundary exactly, so this must be an element
    boundaryPosition = {
      node: containerElement,
      offset: getChildIndex(workingNode)
    };
  }

  // Clean up
  workingNode.parentNode.removeChild(workingNode);

  return boundaryPosition;
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script> 
<b><div id="testResult"></div></b>
<div id="hoverText">   <p><span class="kinovar"><span id="selection_index3337" class="selection_index"></span>По f7-мъ часЁ твори1тъ сщ7eнникъ начaло съ кади1ломъ и3 со свэщeю, цrкимъ двeремъ tвeрзєннымъ, и3 поeтъ: Х</span>rт0съ воскRсе: <span class="kinovar">со стіхи2. И# по стісёхъ pал0мъ: Б</span>лгcви2 душE моS гDа: <span class="kinovar">И# є3ктеніA. Тaже каfjсма nбhчнаz.</span> </p> <div class="slavic"><p><span id="selection_index3737" class="selection_index"></span>(л. рo7з њб.)</p> <p><span class="kinovar"><span id="selection_index3738" class="selection_index"></span>Во вт0рникъ вeчера</span></p> <p><span class="kinovar"><span id="selection_index3739" class="selection_index"></span>tдaніе прaздника пaсхи.</span></p><p><span class="kinovar"><span id="selection_index3740" class="selection_index"></span>По f7-мъ часЁ твори1тъ сщ7eнникъ начaло съ кади1ломъ и3 со свэщeю, цrкимъ двeремъ tвeрзєннымъ, и3 поeтъ: Х</span>rт0съ воскRсе: <span class="kinovar">состіхи2. И# по стісёхъ pал0мъ: Б</span>лгcви2 душE моS гDа: <span class="kinovar">И# є3ктеніA. Тaже каfjсма nбhчнаz.</span> </p><p><span class="kinovar"><span id="selection_index3741" class="selection_index"></span>На ГDи воззвaхъ: поeмъ стіхи6ры самоглaсны, слэпaгw, на ѕ7. Глaсъ в7:</span> </p><p><span class="kinovar"><span id="selection_index3742" class="selection_index"></span>С</span>лэпhй роди1выйсz, въ своeмъ п0мыслэ глаг0лаше: є3дA ѓзъ грBхъ рaди роди1тельныхъ роди1хсz без8 џчію; (л. рo7и) є3дA ѓзъ за невёріе kзhкwвъ роди1хсz во њбличeніе; не домышлsюсz вопрошaти: когдA н0щь, когдA дeнь; не терпи1та ми2 н0зэ кaменнагw претыкaніz, не ви1дэхъ сlнца сіsюща, нижE во џбразэ менE создaвшагw. но молю1 ти сz хrтE б9е, при1зри на мS, и3 поми1луй мS.</p></div></div>
Flycatcher answered 2/6, 2015 at 21:1 Comment(5)
"Get the HTML in #hoverText - just a wrapper for convenience" - looks familiarReligious
@Religious I read all the answer here and found your idea to wrap the text in a div good, so i kept the same name. But the code after that is totally different :)Flycatcher
@Religious By the way i found your solution very good. Good luck for the bounty ;)Flycatcher
"textNode is undefined" for some reason in IE.Podagra
Thanks Ludovic. Just what I needed and it's working wonderfully. I'm using it only for Safari and noticed that caretRangeFromPoint will return the closest range even if you click on any blank space on the page. Seems like it's a long standing bug: bugs.webkit.org/show_bug.cgi?id=29249Miscount
D
3

Aw yiss! Here is ho!

Simple as it is and whitout Jquery or any other framework Fiddle: https://jsfiddle.net/703c96dr/

It will put spans on each word and add a onmouseover and onomouseout function. I could create a simple class to make it more usable but the code is so simple that anyone can edit and use.

<p>This is my text example of word highlighting or, if you want, word hovering</p>
<p>This is another text example of word highlighting or, if you want, word hovering</p>

Simple code

function onmouseoverspan(){
    this.style.backgroundColor = "red";
}
function onmouseoutspan(){
    this.style.backgroundColor = "transparent";
}
var spans,p = document.getElementsByTagName("p");
for(var i=0;i<p.length;i++) {
    if(p[i]==undefined) continue;
    p[i].innerHTML = p[i].innerHTML.replace(/\b(\w+)\b/g, "<span>$1</span>");
    spans = p[i].getElementsByTagName("span")
    for(var a=0;a<spans.length;a++) {
        spans[a].onmouseover = onmouseoverspan;
        spans[a].onmouseout = onmouseoutspan;
    }
}
Dawnedawson answered 3/6, 2015 at 19:5 Comment(0)
W
2

You would probably have to break up the paragraph so that each word was contained inside of its own separate <span> element and then add onmouseover event attributes to each of them.

..And I think you mean "<p>some long text</p>"; backslashes are not part of HTML.

Wrongdoing answered 15/3, 2010 at 0:31 Comment(0)
D
2

In Firefox you can hook the mousemove event. The callback has one argument, e. In the callback, do this:

var range = HTTparent.ownerDocument.createRange();
range.selectNode(e.rangeParent);
var str = range.toString();
range.detach();

Now str has the entire text that the mouse was over. e.rangeOffset is the location of the mousepointer within that string. In your case, str would be "some long text" and e.rangeOffset would be 11 if you were over the "e" in "text".

This code will get a little confused if you are in the margins, for instance when the mouse pointer is on the same line as the text but after the end of it. To fix this, you need to check that you are actually on top of text. Here's the test:

if(e && e.rangeParent && e.rangeParent.nodeType == e.rangeParent.TEXT_NODE
   && e.rangeParent.parentNode == e.target)

This technique works in Firefox. Doesn't work in Chrome.

Dell answered 14/9, 2010 at 14:10 Comment(1)
Thanks man it helped me a lot, once you get range on both Chrome and Firefox you can use the range.offsetNode || range.startContainer as the rangeParentNode and it will work for Chromium based engines too.Pear
K
1

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>
<body onclick="myfunc(event)">
    <span>hello </span><span> world </span>


    <script>
       function myfunc(event){

           console.log(event.target.innerHTML);
       }


    </script>
</body>
</html>
Karakul answered 18/2, 2021 at 13:50 Comment(1)
span tag makes your job easy :)Karakul
T
0

function escapeHtml(unsafe) {
  return unsafe
    .replace(/&/g, "&amp;")
    .replace(/</g, "&lt;")
    .replace(/>/g, "&gt;")
    .replace(/"/g, "&quot;")
    .replace(/'/g, "&#039;");
}

// REF: https://mcmap.net/q/37673/-how-to-get-selected-textnode-in-contenteditable-div-in-ie
function getChildIndex(node) {
  var i = 0;
  while( (node = node.previousSibling) ) {
    i++;
  }
  return i;
}

// All this code just to make this work with IE, OTL
// REF: https://mcmap.net/q/37673/-how-to-get-selected-textnode-in-contenteditable-div-in-ie
function getTextRangeBoundaryPosition(textRange, isStart) {
  var workingRange = textRange.duplicate();
  workingRange.collapse(isStart);
  var containerElement = workingRange.parentElement();
  var workingNode = document.createElement("span");
  var comparison, workingComparisonType = isStart ?
    "StartToStart" : "StartToEnd";

  var boundaryPosition, boundaryNode;

  // Move the working range through the container's children, starting at
  // the end and working backwards, until the working range reaches or goes
  // past the boundary we're interested in
  do {
    containerElement.insertBefore(workingNode, workingNode.previousSibling);
    workingRange.moveToElementText(workingNode);
  } while ( (comparison = workingRange.compareEndPoints(
    workingComparisonType, textRange)) > 0 && workingNode.previousSibling);

  // We've now reached or gone past the boundary of the text range we're
  // interested in so have identified the node we want
  boundaryNode = workingNode.nextSibling;
  if (comparison == -1 && boundaryNode) {
    // This must be a data node (text, comment, cdata) since we've overshot.
    // The working range is collapsed at the start of the node containing
    // the text range's boundary, so we move the end of the working range
    // to the boundary point and measure the length of its text to get
    // the boundary's offset within the node
    workingRange.setEndPoint(isStart ? "EndToStart" : "EndToEnd", textRange);

    boundaryPosition = {
      node: boundaryNode,
      offset: workingRange.text.length
    };
  } else {
    // We've hit the boundary exactly, so this must be an element
    boundaryPosition = {
      node: containerElement,
      offset: getChildIndex(workingNode)
    };
  }

  // Clean up
  workingNode.parentNode.removeChild(workingNode);

  return boundaryPosition;
}

function onClick(event) {
  var elt = document.getElementById('info');
  elt.innerHTML = "";
  var textNode;
  var offset;
  // Internet Explorer
  if (document.body.createTextRange) {
		  elt.innerHTML = elt.innerHTML+("*************** IE **************<br/>");
      range = document.body.createTextRange();
      range.moveToPoint(event.clientX, event.clientY);
      range.select();
      range = getTextRangeBoundaryPosition(range, true);

      textNode = range.node;
      offset = range.offset;
      elt.innerHTML = elt.innerHTML + "IE ok, result: [" + escapeHtml(textNode.nodeName) + "]/[" + escapeHtml(textNode.textContent) + "] @" + offset + "</br>";

  }
  
  // Internet Explorer method 2
  if (document.body.createTextRange) {
		  elt.innerHTML = elt.innerHTML+("*************** IE, Method 2 **************<br/>");
      range = document.body.createTextRange();
      range.moveToPoint(event.clientX, event.clientY);
      range.select();
			var sel = document.getSelection();
      textNode = sel.anchorNode;
      offset = sel.anchorOffset;
      elt.innerHTML = elt.innerHTML + "IE M2 ok, result: [" + escapeHtml(textNode.nodeName) + "]/[" + escapeHtml(textNode.textContent) + "] @" + offset + "</br>";
  }  

  // Firefox, Safari
  // REF: https://developer.mozilla.org/en-US/docs/Web/API/Document/caretPositionFromPoint
  if (document.caretPositionFromPoint) {
		  elt.innerHTML = elt.innerHTML+("*************** Firefox, Safari **************<br/>");  
    range = document.caretPositionFromPoint(event.clientX, event.clientY);
    textNode = range.offsetNode;
    offset = range.offset;
    elt.innerHTML = elt.innerHTML + "caretPositionFromPoint ok, result: [" + escapeHtml(textNode.nodeName) + "]/[" + escapeHtml(textNode.textContent) + "] @" + offset + "</br>";
    // Chrome
    // REF: https://developer.mozilla.org/en-US/docs/Web/API/document/caretRangeFromPoint
  }
  if (document.caretRangeFromPoint) {
		  elt.innerHTML = elt.innerHTML+("*************** Chrome **************<br/>");  
    range = document.caretRangeFromPoint(event.clientX, event.clientY);
    textNode = range.startContainer;
    offset = range.startOffset;
    elt.innerHTML = elt.innerHTML + "caretRangeFromPoint ok, result: [" + escapeHtml(textNode.nodeName) + "]/[" + escapeHtml(textNode.textContent) + "] @" + offset + "</br>";
  }
}

document.addEventListener('click', onClick);
#info {
  position: absolute;
  bottom: 0;
  background-color: cyan;
}
<div class="parent">
  <div class="child">SPACE&nbsp;SPACE Bacon ipsum dolor amet <span>SPAN SPANTT SPOOR</span> meatball bresaola t-bone tri-tip brisket. Jowl pig picanha cupim SPAXE landjaeger, frankfurter spare ribs chicken. Porchetta jowl pancetta drumstick shankle cow spare ribs jerky
    tail kevin biltong capicola brisket venison bresaola. Flank sirloin jowl andouille meatball venison salami ground round rump boudin turkey capicola t-bone. Sirloin filet mignon tenderloin beef, biltong doner bresaola brisket shoulder pork loin shankle
    turducken shank cow. Bacon ball tip sirloin ham.
  </div>
  <div id="info">Click somewhere in the paragraph above</div>
</div>

My answer is derived from Drakes' "Solution 2 - Caret inspection and DOM traversal". Thanks a lot to Drakes for pointing to this solution!

However, there's two problems with Drakes' solution 2 when working on IE. (1) the offset as calculated is incorrect, and (2) too complex, lots of code.

See my demonstration on JSFiddle at here.

For problem 1, if you click somewhere at about the last line of the text, for example somewhere in "shoulder pork loin shankle turducken shank cow. Bacon ball tip sirloin ham.", you can notice the offset calculation is different with IE (original solution) and IE method 2 (my solution). Also, the results from IE method 2 (my solution) and from Chrome, Firefox are the same.

My solution is also much simpler. The trick is, after use TextRange to make selection at the absolute X/Y position, get a type of IHTMLSelection by calling document.getSelection(). This does not work for IE<9 but if that's OK for you, this method is much simpler. Another caveat is, with IE the method's side effect (same as the original method) is change of selection (i.e. losing user's original selection).

  // Internet Explorer method 2
  if (document.body.createTextRange) {
          elt.innerHTML = elt.innerHTML+("*************** IE, Method 2 **************<br/>");
      range = document.body.createTextRange();
      range.moveToPoint(event.clientX, event.clientY);
      range.select();
      var sel = document.getSelection();
      textNode = sel.anchorNode;
      offset = sel.anchorOffset;
      elt.innerHTML = elt.innerHTML + "IE M2 ok, result: [" + escapeHtml(textNode.nodeName) + "]/[" + escapeHtml(textNode.textContent) + "] @" + offset + "</br>";
  }  
Tartuffe answered 8/12, 2016 at 0:18 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.