Javascript: How to Detect If a Word Is Highlighted

How to check if text is highlighted (cross-browser)

You can use the Selection object :

var selection = window.getSelection();

See the documentation here : https://developer.mozilla.org/en-US/docs/Web/API/Selection

With this object, you can check the selected dom node (anchorNode). For example :

if ($(window.getSelection().anchorNode).attr('id') === 'something') { ... }

Find and highlight word in text using JS

Here is my solution. I found there are two ways to achieve this. In Firefox, you can use selection api. Unfortunately, it will not work in Chrome. A simpler solution is to just match the search text and highlight it by enclosing it in <mark> tags.

var opar = document.getElementById('paragraph').innerHTML;

function highlight() {
var paragraph = document.getElementById('paragraph');
var search = document.getElementById('typed-text').value;
search = search.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); //https://stackoverflow.com/questions/3446170/escape-string-for-use-in-javascript-regex

var re = new RegExp(search, 'g');

if (search.length > 0)
paragraph.innerHTML = opar.replace(re, `<mark>$&</mark>`);
else paragraph.innerHTML = opar;
}
<div id="highlights">
<div class="container">
<div class="row">
<div class="col-md-12" id="paragraph">
<p>
Robotics is an interdisciplinary branch of engineering and science that includes mechanical engineering, electronics engineering, information engineering, computer science, and others. Robotics deals with the design, construction, operation, and use of
robots, as well as computer systems for their control, sensory feedback, and information processing. These technologies are used to develop machines that can substitute for humans and replicate human actions. Robots can be used in many situations
and for lots of purposes, but today many are used in dangerous environments (including bomb detection and deactivation), manufacturing processes, or where humans cannot survive (e.g. in space). Robots can take on any form but some are made to
resemble humans in appearance. This is said to help in the acceptance of a robot in certain replicative behaviors usually performed by people. Such robots attempt to replicate walking, lifting, speech, cognition, and basically anything a human
can do. Many of today's robots are inspired by nature, contributing to the field of bio-inspired robotics. The concept of creating machines that can operate autonomously dates back to classical times, but research into the functionality and
potential uses of robots did not grow substantially until the 20th century.[1] Throughout history, it has been frequently assumed that robots will one day be able to mimic human behavior and manage tasks in a human-like fashion. Today, robotics
is a rapidly growing field, as technological advances continue; researching, designing, and building new robots serve various practical purposes, whether domestically, commercially, or militarily. Many robots are built to do jobs that are hazardous
to people such as defusing bombs, finding survivors in unstable ruins, and exploring mines and shipwrecks. Robotics is also used in STEM (science, technology, engineering, and mathematics) as a teaching aid. Robotics is a branch of engineering
that involves the conception, design, manufacture, and operation of robots. This field overlaps with electronics, computer science, artificial intelligence, mechatronics, nanotechnology and bioengineering.
</p>
</div>
<div class="col-md-12 input-group mt-3">
<div class="input-group-prepend">
<span class="input-group-text" id="basic-addon1">
<i class="fas fa-pencil-alt"></i>
</span>
</div>
<input id="typed-text" type="text" class="form-control" placeholder="Type text" onkeyup="highlight()">
</div>
</div>
</div>
</div>

If a word is highlighted and user clicks the connecting word, highlight both

 

UPGRADE

Ok, I am putting this at top, because it is a major update and, I believe, can even be considered as an upgrade on the previous function.

The request was to make the previous function work in reverse, i.e. when a highlighted word is clicked again, it would be removed from the total selection.

The challenge was that when a highlighted word at the edge of <p> and </p> tags or the edge of <b> and </b> tags inside the paragraphs was clicked, the startContainer or endContainer of the range had to be carried into or out of the current element they were positioned and the startOffset or endOffset had to be reset as well. I am not sure if this has been a clear expression of the problem, but, in brief, due to the way Range objects work, the words closest to HTML tags proved to be quite a challenge.

The Solution was to introduce a few new regex tests, several if checks, and a local function for finding the next/previous sibling. During the process, I have also fixed a few things which had escaped my attention before. The new function is below and the updated fiddle is here.

 

(function(el){
// variable declaration for previous range info
// and function for finding the sibling
var prevRangeInfo = {},
findSibling = function(thisNode, direction){
// get the child node list of the parent node
var childNodeList = thisNode.parentNode.childNodes,
children = [];

// convert the child node list to an array
for(var i=0, l=childNodeList.length; i<l; i++) children.push(childNodeList[i]);

return children[children.indexOf(thisNode) + direction];
};

el.addEventListener('mouseup',function(evt){
if (document.createRange) { // Works on all browsers, including IE 9+

var selected = window.getSelection();
// Removing the following line from comments will make the function drag-only
/* if(selected.toString().length){ */
var d = document,
nA = selected.anchorNode,
oA = selected.anchorOffset,
nF = selected.focusNode,
oF = selected.focusOffset,
range = d.createRange(),
rangeLength = 0;

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

// Store the length of the range
rangeLength = range.toString().length;

// Check if another range was previously selected
if(prevRangeInfo.startContainer && nA === nF && oA === oF){
var rangeTryContain = d.createRange(),
rangeTryLeft = d.createRange(),
rangeTryRight = d.createRange(),
nAp = prevRangeInfo.startContainer;
oAp = prevRangeInfo.startOffset;
nFp = prevRangeInfo.endContainer;
oFp = prevRangeInfo.endOffset;

rangeTryContain.setStart(nAp, oAp);
rangeTryContain.setEnd(nFp, oFp);
rangeTryLeft.setStart(nFp, oFp-1);
rangeTryLeft.setEnd(range.endContainer, range.endOffset);
rangeTryRight.setStart(range.startContainer, range.startOffset);
rangeTryRight.setEnd(nAp, oAp+1);

// Store range boundary comparisons
// & inner nodes close to the range boundary --> stores null if none
var compareStartPoints = range.compareBoundaryPoints(0, rangeTryContain) === 0,
compareEndPoints = range.compareBoundaryPoints(2, rangeTryContain) === 0,
leftInnerNode = range.endContainer.previousSibling,
rightInnerNode = range.startContainer.nextSibling;

// Do nothing if clicked on the right end of a word
if(range.toString().length < 1){
range.setStart(nAp,oAp);
range.setEnd(nFp,oFp);
}

// Collapse the range if clicked on last highlighted word
else if(compareStartPoints && compareEndPoints)
range.collapse();

// Remove a highlighted word from left side if clicked on
// This part is quite tricky!
else if(compareStartPoints){
range.setEnd(nFp,oFp);

if(range.startOffset + rangeLength + 1 >= range.startContainer.length){
if(rightInnerNode)
// there is a right inner node, set its start point as range start
range.setStart(rightInnerNode.firstChild, 0);

else {
// there is no right inner node
// there must be a text node on the right side of the clicked word

// set start of the next text node as start point of the range
var rightTextNode = findSibling(range.startContainer.parentNode, 1),
rightTextContent = rightTextNode.textContent,
level=1;

// if beginning of paragraph, find the first child of the paragraph
if(/^(?:\r\n|[\r\n])|\s{2,}$/.test(rightTextContent)){
rightTextNode = findSibling(rightTextNode, 1).firstChild;
level--;
}

range.setStart(rightTextNode, level);

}
}
else
range.setStart(range.startContainer, range.startOffset + rangeLength + 1);
}

// Remove a hightlighted word from right side if clicked on
// This part is also tricky!
else if (compareEndPoints){
range.setStart(nAp,oAp);

if(range.endOffset - rangeLength - 1 <= 0){
if(leftInnerNode)
// there is a right inner node, set its start point as range start
range.setEnd(leftInnerNode.lastChild, leftInnerNode.lastChild.textContent.length);

else {
// there is no left inner node
// there must be a text node on the left side of the clicked word

// set start of the previous text node as start point of the range
var leftTextNode = findSibling(range.endContainer.parentNode, -1),
leftTextContent = leftTextNode.textContent,
level = 1;

// if end of paragraph, find the last child of the paragraph
if(/^(?:\r\n|[\r\n])|\s{2,}$/.test(leftTextContent)){
leftTextNode = findSibling(leftTextNode, -1).lastChild;
level--;
}

range.setEnd(leftTextNode, leftTextNode.length - level);
}
}
else
range.setEnd(range.endContainer, range.endOffset - rangeLength - 1);
}

// Add previously selected range if adjacent
// Upgraded to include previous/next word even in a different paragraph
else if(/^[^\s]*((?:\r\n|[\r\n])|\s{1,})[^\s]*$/.test(rangeTryLeft.toString()))
range.setStart(nAp,oAp);
else if(/^[^\s]*((?:\r\n|[\r\n])|\s{1,})[^\s]*$/.test(rangeTryRight.toString()))
range.setEnd(nFp,oFp);

// Detach the range objects we are done with, clear memory
rangeTryContain.detach();
rangeTryRight.detach();
rangeTryLeft.detach();
}

// Save the current range --> not the whole Range object but what is neccessary
prevRangeInfo = {
startContainer: range.startContainer,
startOffset: range.startOffset,
endContainer: range.endContainer,
endOffset: range.endOffset
};

// Clear the saved range info if clicked on last highlighted word
if(compareStartPoints && compareEndPoints)
prevRangeInfo = {};

// Remove all ranges from selection --> necessary due to potential removals
selected.removeAllRanges();

// Assign the current range as selection
selected.addRange(range);

// Detach the range object we are done with, clear memory
range.detach();

el.style.MozUserSelect = '-moz-none';

// Removing the following line from comments will make the function drag-only
/* } */

} else {
// Fallback for Internet Explorer 8 and earlier
// (if you think it still is worth the effort of course)
}
});

/* This part is necessary to eliminate a FF specific dragging behavior */
el.addEventListener('mousedown',function(e){
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)
}
el.style.MozUserSelect = 'text';
});
})(document.getElementById('selectable'));

 


 

BEFORE UPGRADE

Storing the last range in an object and checking if the previously selected range is adjacent to the new range every time a new selection is made, does the job:

(function(el){
var prevRangeInfo = {};
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);

// Check if another range was previously selected
if(prevRangeInfo.startContainer){
var rangeTryLeft = d.createRange(),
rangeTryRight = d.createRange(),
nAp = prevRangeInfo.startContainer;
oAp = prevRangeInfo.startOffset;
nFp = prevRangeInfo.endContainer;
oFp = prevRangeInfo.endOffset;
rangeTryLeft.setStart(nFp,oFp-1);
rangeTryLeft.setEnd(range.endContainer,range.endOffset);
rangeTryRight.setStart(range.startContainer,range.startOffset);
rangeTryRight.setEnd(nAp,oAp+1);

// Add previously selected range if adjacent
if(/^[^\s]*\s{1}[^\s]*$/.test(rangeTryLeft.toString())) range.setStart(nAp,oAp);
else if(/^[^\s]*\s{1}[^\s]*$/.test(rangeTryRight.toString())) range.setEnd(nFp,oFp);
}

// Save the current range
prevRangeInfo = {
startContainer: range.startContainer,
startOffset: range.startOffset,
endContainer: range.endContainer,
endOffset: range.endOffset
};

// Assign range to selection
selected.addRange(range);

el.style.MozUserSelect = '-moz-none';
/* } */
} else {
// Fallback for Internet Explorer 8 and earlier
// (if you think it still is worth the effort of course)
}
});

/* This part is necessary to eliminate a FF specific dragging behavior */
el.addEventListener('mousedown',function(e){
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)
}
el.style.MozUserSelect = 'text';
});
})(document.getElementById('selectable'));

JS Fiddle here.

Update (was done before upgrade):

If you want to this feature to be effective when clicking but not dragging, all you have to do is to change the if(prevRangeInfo.startContainer) condition as follows:

if(prevRangeInfo.startContainer && nA === nF && oA === oF){
// rest of the code is the same...

The updated JS Fiddle is here.

Detect if text is highlighted on mouse up jQuery

Try this:

$(document).mouseup(function(){
var highlightedText = "";
if (window.getSelection) {
highlightedText = window.getSelection().toString();
}
else if (document.selection && document.selection.type != "Control") {
highlightedText = document.selection.createRange().text;
}
if(highlightedText != "")
console.log("text highlighted.");
});

How to highlight search text from string of html content without breaking

One thing you can use is getClientRects method of the Range object: https://developer.mozilla.org/en-US/docs/Web/API/range/getClientRects

This allows you to add divs with the coordinates of your search, allowing you to highlight text without having to manipulate the DOM.

Finding the nodes isn't that straighforward (especially if you have a complicated structure), but you can iterate through all text nodes to match the index of the search in the textContent of the element in which you want to search.

So first you match the search result to the DOM. In the example I use a recursive generator, but any recursive loop will do. Basically what you need to do is go through every text node to match the index of a search. So you go through every descendant node and count the texts length so you can match your search to a node.

Once this is done, you can create a Range from these results, and then you add elements with the coordinates of the rectangles you get with getClientRects. By giving then a z-index negative and an absolute position, they will appear under the text at the right place. So you'll have a highlight effect, but without touching the HTML you are searching.
Like this:

document.querySelector('#a').onclick = (e) => {
let topParent = document.querySelector('#b'); let s, range; let strToSearch = document.querySelector('#search').value let re = RegExp(strToSearch, 'g')
removeHighlight() s = window.getSelection(); s.removeAllRanges() // to handle multiple result you need to go through all matches while (match = re.exec(topParent.textContent)) {
let it = iterateNode(topParent); let currentIndex = 0; // the result is the text node, so you can iterate and compare the index you are searching to all text nodes length let result = it.next();
while (!result.done) { if (match.index >= currentIndex && match.index < currentIndex + result.value.length) { // when we have the correct node and index we add a range range = new Range(); range.setStart(result.value, match.index - currentIndex)
} if (match.index + strToSearch.length >= currentIndex && match.index + strToSearch.length < currentIndex + result.value.length) { // when we find the end node, we can set the range end range.setEnd(result.value, match.index + strToSearch.length - currentIndex) s.addRange(range)
// this is where we add the divs based on the client rects of the range addHighlightDiv(range.getClientRects())

} currentIndex += result.value.length; result = it.next(); } } s.removeAllRanges()
}

function* iterateNode(topNode) { // this iterate through all descendants of the topnode let childNodes = topNode.childNodes; for (let i = 0; i < childNodes.length; i++) { let node = childNodes[i] if (node.nodeType === 3) { yield node; } else { yield* iterateNode(node); } }
}
function addHighlightDiv(rects) { for (let i = 0; i < rects.length; i++) {
let rect = rects[i]; let highlightRect = document.createElement('DIV') document.body.appendChild(highlightRect) highlightRect.classList.add('hl') highlightRect.style.top = rect.y + window.scrollY + 'px' highlightRect.style.left = rect.x + 'px' highlightRect.style.height = rect.height + 'px' highlightRect.style.width = rect.width + 'px'
}
}
function removeHighlight() { let highlights = document.querySelectorAll('.hl'); for (let i = 0; i < highlights.length; i++) { highlights[i].remove(); }}
.hl {  background-color: red;  position: absolute;  z-index: -1;}
<input type="text" id="search" /><button id="a">search</button><div id="b">  <h1>Lorem ipsum dolor sit amet</h1>, consectetur  <h2>adipiscing elit, sed do</h2> eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud <strong>exercitation <span>ullamco laboris</span> nisi ut aliquip ex ea commodo</strong> consequat. Duis aute irure dolor  in reprehenderit in voluptate velit <em>esse cillum dolore eu fugiat nulla pariatur.</em> Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.</div>


Related Topics



Leave a reply



Submit