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.
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.
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:
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.
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', ''); }
);
});
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();
}
}
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:
The caretRangeFromPoint() method of the Document interface returns a Range object for the document fragment under the specified coordinates.
From the document.caretPositionFromPointdocumentation:
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://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;
}
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>
function escapeHtml(unsafe) {
return unsafe
.replace(/&/g, "&")
.replace(/</g, "<")
.replace(/>/g, ">")
.replace(/"/g, """)
.replace(/'/g, "'");
}
// 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;
}
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);
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.
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>";
}