I have some text:
Hello world, Attack on Titan season two!
Currently, if a user wants to highlight a word/ter
You can do that with pure JS using Range
and selectionRange
objects.
HTML:
<div id="selectable">
<p>Hello world, <b>Attack on Titan</b> season two!</p>
<p>Another paragraph with sample text.</p>
</div>
<div id="notSelectable">
<p>The selection will behave normally on this div.</p>
</div>
JS:
(function(el){
el.addEventListener('mouseup',function(evt){
if (document.createRange) { // Works on all browsers, including IE 9+
var selected = window.getSelection();
/* if(selected.toString().length){ */
var d = document,
nA = selected.anchorNode,
oA = selected.anchorOffset,
nF = selected.focusNode,
oF = selected.focusOffset,
range = d.createRange();
range.setStart(nA,oA);
range.setEnd(nF,oF);
// Check if direction of selection is right to left
if(range.startContainer !== nA || (nA === nF && oF < oA)){
range.setStart(nF,oF);
range.setEnd(nA,oA);
}
// Extend range to the next space or end of node
while(range.endOffset < range.endContainer.textContent.length && !/\s$/.test(range.toString())){
range.setEnd(range.endContainer, range.endOffset + 1);
}
// Extend range to the previous space or start of node
while(range.startOffset > 0 && !/^\s/.test(range.toString())){
range.setStart(range.startContainer, range.startOffset - 1);
}
// Remove spaces
if(/\s$/.test(range.toString()) && range.endOffset > 0)
range.setEnd(range.endContainer, range.endOffset - 1);
if(/^\s/.test(range.toString()))
range.setStart(range.startContainer, range.startOffset + 1);
// Assign range to selection
selected.addRange(range);
/* } */
} else {
// Fallback for Internet Explorer 8 and earlier
// (if you think it still is worth the effort of course)
}
// Stop Moz user select
el.style.MozUserSelect = '-moz-none';
});
/* This part is added to eliminate a FF specific dragging behavior */
el.addEventListener('mousedown',function(){
if (window.getSelection) { // Works on all browsers, including IE 9+
var selection = window.getSelection ();
selection.collapse (selection.anchorNode, selection.anchorOffset);
} else {
// Fallback for Internet Explorer 8 and earlier
// (if you think it still is worth the effort of course)
}
// Add Moz user select back
el.style.MozUserSelect = 'text';
});
})(document.getElementById('selectable'));
Please check the working example here.
UPDATES:
Updated JSFiddle here.
You cannot set events to Text, but can set events to Html Elements. Put each word inside a div element and add an event onmouseover that changes div to a new highlighted state using css.
Steps:
That's it.
So you are going to have to deal with text ranges and such. I've dealt with this, and it's extremely painful, especially if you have DOM contents like:
<p>New season of <span class="redtext">Attack on Titan!</span></p>
i.e. text nodes mixed with other DOM elements, such as a span in this case. With this in mind, I'd like to highly recommend the library rangy.js: https://github.com/timdown/rangy
It's saved me several days of headache when I was making a hashtag highlighting system.
Using node.textContent
we can find spaces and jump our selection to whole words without the need for creating new elements.
Mainly for my potential future use I've written this fairly modular, it also doesn't require a mouseup on the watched element, can deal with an element collection and also makes the selection changes if the user makes their selection using their keyboard.
var WordJumpSelection = (function() {
var watchList = [];
var WordJumpSelection = {
stopWatching: function(elem) {
var wlIdx = watchList.indexOf(elem);
if(wlIdx > -1) watchList.splice(wlIdx,1);
},
watch: function(elem) {
var elems = Array.prototype.slice.call(typeof elem.length === "number" ? elem : arguments);
if(watchList.length === 0)
{
WordJumpSelection.init();
}
elems.forEach(function(elem) {
if(watchList.indexOf(elem) === -1)
{
watchList.push(elem);
}
});
},
init: function() {
function handleSelectionChange() {
if(watchList.length === 0) return;
var selection = window.getSelection();
var selDir = getSelectionDir(selection);
var startNode,endNode,startPos,endPos;
if(selDir === 1)
{
startNode = selection.anchorNode;
endNode = selection.focusNode;
startPos = selection.anchorOffset;
endPos = selection.focusOffset;
}
else
{
startNode = selection.focusNode;
endNode = selection.anchorNode;
startPos = selection.focusOffset;
endPos = selection.anchorOffset;
}
var rangeStart = textNodeIsWatched(startNode) ? roundSelectionIndex(startNode,0,startPos) : startPos-1;
var rangeEnd = textNodeIsWatched(endNode) ? roundSelectionIndex(endNode,1,endPos) : endPos;
var r = document.createRange();
r.setStart(startNode,rangeStart+1)
r.setEnd(endNode,rangeEnd)
selection.removeAllRanges();
selection.addRange(r);
}
document.documentElement.addEventListener('mouseup', handleSelectionChange);
document.documentElement.addEventListener('keyup', function(e) {
if(e.keyCode === 16)
{
handleSelectionChange();
}
});
WordJumpSelection.init = function(){};
}
};
return WordJumpSelection;
function getSelectionDir(sel) {
var range = document.createRange();
range.setStart(sel.anchorNode,sel.anchorOffset);
range.setEnd(sel.focusNode,sel.focusOffset);
if(range.startContainer !== sel.anchorNode || (sel.anchorNode === sel.focusNode && sel.focusOffset < sel.anchorOffset)) return -1;
else return 1;
}
function roundSelectionIndex(textNode,nodeId,idx) {
var isStart = nodeId === 0;
var contents = textNode.textContent;
var nearestSpaceIdx = -1;
if(isStart)
{
nearestSpaceIdx = contents.lastIndexOf(' ',idx);
if(nearestSpaceIdx === -1) nearestSpaceIdx = -1;
}
else
{
nearestSpaceIdx = contents.indexOf(' ',idx);
if(nearestSpaceIdx === -1) nearestSpaceIdx = contents.length;
}
return nearestSpaceIdx;
}
function textNodeIsWatched(textNode) {
return watchList.indexOf(textNode.parentElement) > -1;
}
})();
An example jsFiddle
I am yet to test how this works on mobile, and haven't got it working live yet - but it might be a good start.
Update: Now selects word with a single click
This text is a text node, and text nodes simply don't fire most events. But they can fire DOM mutation events, for example DOMCharacterDataModified
, which is used to detect change to a text node's text:
var textNode = document.getElementsByClassName("drag")[0].firstChild;
textNode.addEventListener("DOMCharacterDataModified", function(e) {
console.log("Text changed from '" + e.prevValue + "' to '" + evt.newValue +"'");
}, false);
However, the text in <p class="drag">Hello world, Attack on Titan season two!</p>
is a single text node and you need every word to be a separate node.
The only solution I see is to put every word in a span
tag. You can't do this with pure text.
Here's an example how to do this with span
tags (I'm using jQuery here just to reduce the code amount, it's not necessary):
$(function() {
$('.drag').on('click', 'span', function() {
var range;
if (document.selection) {
range = document.body.createTextRange();
range.moveToElementText($(this)[0]);
range.select();
}
else if (window.getSelection) {
range = document.createRange();
range.selectNode($(this)[0]);
window.getSelection().addRange(range);
}
});
});
Here's an example on JS Bin
I edited the code snippet so the selection behaves like you asked (currently it's only works for selections from left to right, not reverse selections):
$(function(){
var range = document.createRange();
var selectionMode = false;
$(document).on('mouseup', function() {
selectionMode = false;
})
.on('mousedown', '.drag', function(e) {
selectionMode = true;
})
.on('dragstart', '.drag span', function(e) {
return false;
});
$('.drag').on('mousedown', 'span', function() {
range.setStartBefore($(this)[0]);
range.setEndAfter($(this)[0]);
window.getSelection().addRange(range);
})
.on('mousemove', 'span', function() {
if (!selectionMode) {
return;
}
range.setEndAfter($(this)[0]);
window.getSelection().addRange(range);
})
.on('mouseup', 'span', function() {
setTimeout(function(){
window.getSelection().addRange(range);
}, 1);
});
});
You can read more about HTML Range API here: https://developer.mozilla.org/en-US/docs/Web/API/Range
To select each word, there are something you must keep in mind first:
textNode
is a single sting contains all the words, you
won't be able to select each "word", since it's not a DOM node.
There is no specific event triggered in browser when you "drag and
select" a word. However, when you drag & select, there are 2 events
get fired: mouseover
is triggered when you move your mouse,
click
is triggered when you release your mouse button. (This is
true even in Mac's touchpad).
There are different implementation on "highlight" when you select a word.
Based on the concepts, you have to do the following steps sequentially to achieve your goal:
<span>
) for DOM selectionclick
event is triggered (which indicate your select has ended), highlight the word you just select.The implementation would be something likes this (with jQuery
).
And you may see the live demo here:
$(function() {
// 1. When mouseover the paragraph, wrapped each word with <span>
$('p').one('mouseover', function(event) {
$('p').html(function(index, text) {
var wordsArray = text.split(' ');
var wrappedArray = wordsArray.map(function(val, index) {
val = '<span class="chunk-' + index + '">' + val + '</span>';
return val;
});
var wrappedString = wrappedArray.join(' ');
// 2. Replace the paragraph with wrapped text
$(this).html(wrappedString);
// 3. When the word is select, highlight the word
$(this).children('span').on('click', function() {
var selector = '.' + $(this).attr('class');
SelectText(selector);
});
});
});
});
function SelectText(element) {
var doc = document,
text = doc.querySelector(element),
range, selection;
if (doc.body.createTextRange) {
range = document.body.createTextRange();
range.moveToElementText(text);
range.select();
} else if (window.getSelection) {
selection = window.getSelection();
range = document.createRange();
range.selectNodeContents(text);
selection.removeAllRanges();
selection.addRange(range);
}
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<p>Lorem ipsum dolor sit amet, consectetur adipisicing elit. Autem amet suscipit incidunt placeat dicta iure, perspiciatis libero nobis dolore, temporibus et! Quae fugiat necessitatibus ut, molestias aut. Sequi rerum earum facilis voluptates ratione architecto
officia quod aut unde voluptas? Dignissimos ducimus exercitationem perspiciatis nam numquam minima accusamus quod necessitatibus amet illo vel vero placeat voluptate eos iste ratione veniam quisquam atque non voluptatum sint hic sed, suscipit. Doloremque
officiis rerum sunt delectus unde odit eos quod earum aspernatur, tempora neque modi tempore minima maiores fuga eaque dolore quos minus veritatis aliquid, vel suscipit dolores. Voluptatem eius obcaecati, laborum ipsa a!</p>
SelectText
function should attribute to @Jason in this post on SO:
Selecting text in an element: akin to highlighting with your mouse