How to get a word under cursor using JavaScript?
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'?
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
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);
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.
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.
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: http://stackoverflow.com/questions/3127369/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: http://stackoverflow.com/questions/3127369/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)