Can someone help me with a javascript function that can highlight text on a web page. And the requirement is to - highlight only once, not like highlight all occurrences of the text as we do in case of search.
This question is related to
javascript
highlighting
I was wondering that too, you could try what I learned on this post.
I used:
function highlightSelection() {_x000D_
var userSelection = window.getSelection();_x000D_
for(var i = 0; i < userSelection.rangeCount; i++) {_x000D_
highlightRange(userSelection.getRangeAt(i));_x000D_
}_x000D_
_x000D_
}_x000D_
_x000D_
function highlightRange(range) {_x000D_
var newNode = document.createElement("span");_x000D_
newNode.setAttribute(_x000D_
"style",_x000D_
"background-color: yellow; display: inline;"_x000D_
);_x000D_
range.surroundContents(newNode);_x000D_
}
_x000D_
<html>_x000D_
<body contextmenu="mymenu">_x000D_
_x000D_
<menu type="context" id="mymenu">_x000D_
<menuitem label="Highlight Yellow" onclick="highlightSelection()" icon="/images/comment_icon.gif"></menuitem>_x000D_
</menu>_x000D_
<p>this is text, select and right click to high light me! if you can`t see the option, please use this<button onclick="highlightSelection()">button </button><p>
_x000D_
you could also try it here: http://henriquedonati.com/projects/Extension/extension.html
xc
The reason why it's probably a bad idea to start building your own highlighting function from scratch is because you will certainly run into issues that others have already solved. Challenges:
innerHTML
)Sounds complicated? If you want some features like ignoring some elements from highlighting, diacritics mapping, synonyms mapping, search inside iframes, separated word search, etc. this becomes more and more complicated.
When using an existing, well implemented plugin, you don't have to worry about above named things. The article 10 jQuery text highlighter plugins on Sitepoint compares popular highlighter plugins.
mark.js is such a plugin that is written in pure JavaScript, but is also available as jQuery plugin. It was developed to offer more opportunities than the other plugins with options to:
Alternatively you can see this fiddle.
Usage example:
// Highlight "keyword" in the specified context
$(".context").mark("keyword");
// Highlight the custom regular expression in the specified context
$(".context").markRegExp(/Lorem/gmi);
It's free and developed open-source on GitHub (project reference).
We if you also want it to be highlighted on page load, there is a new way.
just add #:~:text=Highlight%20These
try accessing this link in a new tab
https://stackoverflow.com/questions/38588721#:~:text=Highlight%20a%20text
Using the surroundContents() method on the Range type. Its only argument is an element which will wrap that Range.
function styleSelected() {
bg = document.createElement("span");
bg.style.backgroundColor = "yellow";
window.getSelection().getRangeAt(0).surroundContents(bg);
}
function stylizeHighlightedString() {
var text = window.getSelection();
// For diagnostics
var start = text.anchorOffset;
var end = text.focusOffset - text.anchorOffset;
range = window.getSelection().getRangeAt(0);
var selectionContents = range.extractContents();
var span = document.createElement("span");
span.appendChild(selectionContents);
span.style.backgroundColor = "yellow";
span.style.color = "black";
range.insertNode(span);
}
Since HTML5 you can use the <mark></mark>
tags to highlight text. You can use javascript to wrap some text/keyword between these tags. Here is a little example of how to mark and unmark text.
Here's my regexp pure JavaScript solution:
function highlight(text) {
document.body.innerHTML = document.body.innerHTML.replace(
new RegExp(text + '(?!([^<]+)?<)', 'gi'),
'<b style="background-color:#ff0;font-size:100%">$&</b>'
);
}
I have the same problem, a bunch of text comes in through a xmlhttp request. This text is html formatted. I need to highlight every occurrence.
str='<img src="brown fox.jpg" title="The brown fox" />'
+'<p>some text containing fox.</p>'
The problem is that I don't need to highlight text in tags. For example I need to highlight fox:
Now I can replace it with:
var word="fox";
word="(\\b"+
word.replace(/([{}()[\]\\.?*+^$|=!:~-])/g, "\\$1")
+ "\\b)";
var r = new RegExp(word,"igm");
str.replace(r,"<span class='hl'>$1</span>")
To answer your question: you can leave out the g in regexp options and only first occurrence will be replaced but this is still the one in the img src property and destroys the image tag:
<img src="brown <span class='hl'>fox</span>.jpg" title="The brown <span
class='hl'>fox</span> />
This is the way I solved it but was wondering if there is a better way, something I've missed in regular expressions:
str='<img src="brown fox.jpg" title="The brown fox" />'
+'<p>some text containing fox.</p>'
var word="fox";
word="(\\b"+
word.replace(/([{}()[\]\\.?*+^$|=!:~-])/g, "\\$1")
+ "\\b)";
var r = new RegExp(word,"igm");
str.replace(/(>[^<]+<)/igm,function(a){
return a.replace(r,"<span class='hl'>$1</span>");
});
Simply pass your word into the following function:
function highlight_words(word) {
const page = document.body.innerHTML;
document.body.innerHTML = page.replace(new RegExp(word, "gi"), (match) => `<mark>${match}</mark>`);
}
Usage:
highlight_words("hello")
This will highlight all instances of the word on the page.
The solutions offered here are quite bad.
&
for &, <
for <, >
for >, ä
for ä, ö
for ö ü
for ü ß
for ß, etc.What you need to do:
Loop through the HTML document, find all text nodes, get the textContent
, get the position of the highlight-text with indexOf
(with an optional toLowerCase
if it should be case-insensitive), append everything before indexof
as textNode
, append the matched Text with a highlight span, and repeat for the rest of the textnode (the highlight string might occur multiple times in the textContent
string).
Here is the code for this:
var InstantSearch = {
"highlight": function (container, highlightText)
{
var internalHighlighter = function (options)
{
var id = {
container: "container",
tokens: "tokens",
all: "all",
token: "token",
className: "className",
sensitiveSearch: "sensitiveSearch"
},
tokens = options[id.tokens],
allClassName = options[id.all][id.className],
allSensitiveSearch = options[id.all][id.sensitiveSearch];
function checkAndReplace(node, tokenArr, classNameAll, sensitiveSearchAll)
{
var nodeVal = node.nodeValue, parentNode = node.parentNode,
i, j, curToken, myToken, myClassName, mySensitiveSearch,
finalClassName, finalSensitiveSearch,
foundIndex, begin, matched, end,
textNode, span, isFirst;
for (i = 0, j = tokenArr.length; i < j; i++)
{
curToken = tokenArr[i];
myToken = curToken[id.token];
myClassName = curToken[id.className];
mySensitiveSearch = curToken[id.sensitiveSearch];
finalClassName = (classNameAll ? myClassName + " " + classNameAll : myClassName);
finalSensitiveSearch = (typeof sensitiveSearchAll !== "undefined" ? sensitiveSearchAll : mySensitiveSearch);
isFirst = true;
while (true)
{
if (finalSensitiveSearch)
foundIndex = nodeVal.indexOf(myToken);
else
foundIndex = nodeVal.toLowerCase().indexOf(myToken.toLowerCase());
if (foundIndex < 0)
{
if (isFirst)
break;
if (nodeVal)
{
textNode = document.createTextNode(nodeVal);
parentNode.insertBefore(textNode, node);
} // End if (nodeVal)
parentNode.removeChild(node);
break;
} // End if (foundIndex < 0)
isFirst = false;
begin = nodeVal.substring(0, foundIndex);
matched = nodeVal.substr(foundIndex, myToken.length);
if (begin)
{
textNode = document.createTextNode(begin);
parentNode.insertBefore(textNode, node);
} // End if (begin)
span = document.createElement("span");
span.className += finalClassName;
span.appendChild(document.createTextNode(matched));
parentNode.insertBefore(span, node);
nodeVal = nodeVal.substring(foundIndex + myToken.length);
} // Whend
} // Next i
}; // End Function checkAndReplace
function iterator(p)
{
if (p === null) return;
var children = Array.prototype.slice.call(p.childNodes), i, cur;
if (children.length)
{
for (i = 0; i < children.length; i++)
{
cur = children[i];
if (cur.nodeType === 3)
{
checkAndReplace(cur, tokens, allClassName, allSensitiveSearch);
}
else if (cur.nodeType === 1)
{
iterator(cur);
}
}
}
}; // End Function iterator
iterator(options[id.container]);
} // End Function highlighter
;
internalHighlighter(
{
container: container
, all:
{
className: "highlighter"
}
, tokens: [
{
token: highlightText
, className: "highlight"
, sensitiveSearch: false
}
]
}
); // End Call internalHighlighter
} // End Function highlight
};
Then you can use it like this:
function TestTextHighlighting(highlightText)
{
var container = document.getElementById("testDocument");
InstantSearch.highlight(container, highlightText);
}
Here's an example HTML document
<!DOCTYPE html>
<html>
<head>
<title>Example of Text Highlight</title>
<style type="text/css" media="screen">
.highlight{ background: #D3E18A;}
.light{ background-color: yellow;}
</style>
</head>
<body>
<div id="testDocument">
This is a test
<span> This is another test</span>
äöüÄÖÜäöüÄÖÜ
<span>Test123äöüÄÖÜ</span>
</div>
</body>
</html>
By the way, if you search in a database with LIKE
,
e.g. WHERE textField LIKE CONCAT('%', @query, '%')
[which you shouldn't do, you should use fulltext-search or Lucene], then you can escape every character with \ and add an SQL-escape-statement, that way you'll find special characters that are LIKE-expressions.
e.g.
WHERE textField LIKE CONCAT('%', @query, '%') ESCAPE '\'
and the value of @query is not '%completed%'
but '%\c\o\m\p\l\e\t\e\d%'
(tested, works with SQL-Server and PostgreSQL, and every other RDBMS system that supports ESCAPE)
A revised typescript-version:
namespace SearchTools
{
export interface IToken
{
token: string;
className: string;
sensitiveSearch: boolean;
}
export class InstantSearch
{
protected m_container: Node;
protected m_defaultClassName: string;
protected m_defaultCaseSensitivity: boolean;
protected m_highlightTokens: IToken[];
constructor(container: Node, tokens: IToken[], defaultClassName?: string, defaultCaseSensitivity?: boolean)
{
this.iterator = this.iterator.bind(this);
this.checkAndReplace = this.checkAndReplace.bind(this);
this.highlight = this.highlight.bind(this);
this.highlightNode = this.highlightNode.bind(this);
this.m_container = container;
this.m_defaultClassName = defaultClassName || "highlight";
this.m_defaultCaseSensitivity = defaultCaseSensitivity || false;
this.m_highlightTokens = tokens || [{
token: "test",
className: this.m_defaultClassName,
sensitiveSearch: this.m_defaultCaseSensitivity
}];
}
protected checkAndReplace(node: Node)
{
let nodeVal: string = node.nodeValue;
let parentNode: Node = node.parentNode;
let textNode: Text = null;
for (let i = 0, j = this.m_highlightTokens.length; i < j; i++)
{
let curToken: IToken = this.m_highlightTokens[i];
let textToHighlight: string = curToken.token;
let highlightClassName: string = curToken.className || this.m_defaultClassName;
let caseSensitive: boolean = curToken.sensitiveSearch || this.m_defaultCaseSensitivity;
let isFirst: boolean = true;
while (true)
{
let foundIndex: number = caseSensitive ?
nodeVal.indexOf(textToHighlight)
: nodeVal.toLowerCase().indexOf(textToHighlight.toLowerCase());
if (foundIndex < 0)
{
if (isFirst)
break;
if (nodeVal)
{
textNode = document.createTextNode(nodeVal);
parentNode.insertBefore(textNode, node);
} // End if (nodeVal)
parentNode.removeChild(node);
break;
} // End if (foundIndex < 0)
isFirst = false;
let begin: string = nodeVal.substring(0, foundIndex);
let matched: string = nodeVal.substr(foundIndex, textToHighlight.length);
if (begin)
{
textNode = document.createTextNode(begin);
parentNode.insertBefore(textNode, node);
} // End if (begin)
let span: HTMLSpanElement = document.createElement("span");
if (!span.classList.contains(highlightClassName))
span.classList.add(highlightClassName);
span.appendChild(document.createTextNode(matched));
parentNode.insertBefore(span, node);
nodeVal = nodeVal.substring(foundIndex + textToHighlight.length);
} // Whend
} // Next i
} // End Sub checkAndReplace
protected iterator(p: Node)
{
if (p == null)
return;
let children: Node[] = Array.prototype.slice.call(p.childNodes);
if (children.length)
{
for (let i = 0; i < children.length; i++)
{
let cur: Node = children[i];
// https://developer.mozilla.org/en-US/docs/Web/API/Node/nodeType
if (cur.nodeType === Node.TEXT_NODE)
{
this.checkAndReplace(cur);
}
else if (cur.nodeType === Node.ELEMENT_NODE)
{
this.iterator(cur);
}
} // Next i
} // End if (children.length)
} // End Sub iterator
public highlightNode(n:Node)
{
this.iterator(n);
} // End Sub highlight
public highlight()
{
this.iterator(this.m_container);
} // End Sub highlight
} // End Class InstantSearch
} // End Namespace SearchTools
Usage:
let searchText = document.getElementById("txtSearchText");
let searchContainer = document.body; // document.getElementById("someTable");
let highlighter = new SearchTools.InstantSearch(searchContainer, [
{
token: "this is the text to highlight" // searchText.value,
className: "highlight", // this is the individual highlight class
sensitiveSearch: false
}
]);
// highlighter.highlight(); // this would highlight in the entire table
// foreach tr - for each td2
highlighter.highlightNode(td2); // this highlights in the second column of table
None of the other solutions really fit my needs, and although Stefan Steiger's solution worked as I expected I found it a bit too verbose.
Following is my attempt:
/**_x000D_
* Highlight keywords inside a DOM element_x000D_
* @param {string} elem Element to search for keywords in_x000D_
* @param {string[]} keywords Keywords to highlight_x000D_
* @param {boolean} caseSensitive Differenciate between capital and lowercase letters_x000D_
* @param {string} cls Class to apply to the highlighted keyword_x000D_
*/_x000D_
function highlight(elem, keywords, caseSensitive = false, cls = 'highlight') {_x000D_
const flags = caseSensitive ? 'gi' : 'g';_x000D_
// Sort longer matches first to avoid_x000D_
// highlighting keywords within keywords._x000D_
keywords.sort((a, b) => b.length - a.length);_x000D_
Array.from(elem.childNodes).forEach(child => {_x000D_
const keywordRegex = RegExp(keywords.join('|'), flags);_x000D_
if (child.nodeType !== 3) { // not a text node_x000D_
highlight(child, keywords, caseSensitive, cls);_x000D_
} else if (keywordRegex.test(child.textContent)) {_x000D_
const frag = document.createDocumentFragment();_x000D_
let lastIdx = 0;_x000D_
child.textContent.replace(keywordRegex, (match, idx) => {_x000D_
const part = document.createTextNode(child.textContent.slice(lastIdx, idx));_x000D_
const highlighted = document.createElement('span');_x000D_
highlighted.textContent = match;_x000D_
highlighted.classList.add(cls);_x000D_
frag.appendChild(part);_x000D_
frag.appendChild(highlighted);_x000D_
lastIdx = idx + match.length;_x000D_
});_x000D_
const end = document.createTextNode(child.textContent.slice(lastIdx));_x000D_
frag.appendChild(end);_x000D_
child.parentNode.replaceChild(frag, child);_x000D_
}_x000D_
});_x000D_
}_x000D_
_x000D_
// Highlight all keywords found in the page_x000D_
highlight(document.body, ['lorem', 'amet', 'autem']);
_x000D_
.highlight {_x000D_
background: lightpink;_x000D_
}
_x000D_
<p>Hello world lorem ipsum dolor sit amet, consectetur adipisicing elit. Est vel accusantium totam, ipsum delectus et dignissimos mollitia!</p>_x000D_
<p>_x000D_
Lorem ipsum dolor sit amet, consectetur adipisicing elit. Numquam, corporis._x000D_
<small>Lorem ipsum dolor sit amet, consectetur adipisicing elit. Accusantium autem voluptas perferendis dolores ducimus velit error voluptatem, qui rerum modi?</small>_x000D_
</p>
_x000D_
I would also recommend using something like escape-string-regexp if your keywords can have special characters that would need to be escaped in regexes:
const keywordRegex = RegExp(keywords.map(escapeRegexp).join('|')), flags);
Fast forward to 2019, Web API now has natively support for highlighting texts:
const selection = document.getSelection();
selection.setBaseAndExtent(anchorNode, anchorOffset, focusNode, focusOffset);
And you are good to go! anchorNode
is the selection starting node, focusNode
is the selection ending node. And, if they are text nodes, offset
is the index of the starting and ending character in the respective nodes. Here is the documentation
They even have a live demo
I would like to share more about the usage of the scroll text fragment
syntax: #:~:text=[prefix-,]textStart[,textEnd][,-suffix]
Example | Demo link |
---|---|
#:~:text=to |
How to highlight text using javascript |
#:~:text=to,text |
How to highlight text using javascript |
#:~:text=tex-,t |
How to highlight text using javascript |
#:~:text=text-,using,-javascript |
How to highlight text using javascript |
If you want to highlight multiple text fragments in one URL (&text=
)
Example | Demo link |
---|---|
#:~:text=javascript&text=highlight&text=Ankit |
How to highlight text using javascript |
NOTE: While I agree with @Stefan in many things, I only needed a simple match highlighting:
module myApp.Search {
'use strict';
export class Utils {
private static regexFlags = 'gi';
private static wrapper = 'mark';
private static wrap(match: string): string {
return '<' + Utils.wrapper + '>' + match + '</' + Utils.wrapper + '>';
}
static highlightSearchTerm(term: string, searchResult: string): string {
let regex = new RegExp(term, Utils.regexFlags);
return searchResult.replace(regex, match => Utils.wrap(match));
}
}
}
And then constructing the actual result:
module myApp.Search {
'use strict';
export class SearchResult {
id: string;
title: string;
constructor(result, term?: string) {
this.id = result.id;
this.title = term ? Utils.highlightSearchTerm(term, result.title) : result.title;
}
}
}
Source: Stackoverflow.com