Matching multiple substrings to a string - javascript

I have an application where the user types in text, then this text is sent to the server and it returns an array of words that contain this text.
But as you can see the problem starts when there are more then 1 match. Here's my current code:
state.input !== '' && vocabularyItems && (vocabularyItems.map((vocabularyItem, index) => {
const regex = new RegExp(input, 'gi');
const results = vocabularyItem.matchAll(regex);
const tmp = [];
console.log(vocabularyItem);
for (const match of results) {
console.log(match);
let currentLoop = vocabularyItem.slice(0, match.index);
currentLoop += '<strong className="tt-highlight">';
currentLoop += vocabularyItem.slice(match.index, match.index + input.length);
currentLoop += '</strong>';
currentLoop += vocabularyItem.slice(match.index + input.length, vocabularyItem.length);
tmp.push(currentLoop);
}
console.table(tmp);
return (
<div
id={index}
className={`override-strong tt-suggestion tt-selectable ${cursor === index && 'tt-cursor'}`}
onMouseDown={handleClick}
key={index}
dangerouslySetInnerHTML={{ __html: tmp }}
/>
);
}))
and here are some examples in HTML code
1.
<strong className="tt-highlight">En</strong>kelkind
2.
<strong className="tt-highlight">En</strong>gagement
Engagem<strong className="tt-highlight">en</strong>t
as you can see, it works when there is only one occurence, but duplicates the word when more then one match is present. How can I end up with just something like
<strong>en</strong>gagem<strong>en</strong>t?
engagement?
I forgot to add that I need the case preserved

First off, I would recommend to use, something, like:
const results = vocabularyItems.filter(word => word.toLowerCase().includes(input.toLowerCase()))
for case insensitive vocabulary lookup.
Next, I would highlight the match in a bit different way. I'd split suggested option into parts (matching search input and non-matching ones) , then style those respectively:
const parts = suggestion.split(new RegExp(`(?=${match})|(?<=${match})`, 'gi'))
...
parts.map((part,key) => {
const style = part.toLowerCase() == match.toLowerCase() ? 'bold' : 'normal'
return <span style={{fontWeight:style}} {...{key}}>{part}</span>
})
I think it's safe enough to assume that you build autocomplete search input, thus you might find of use the quick demo below (excluding all the styling):
//dependencies
const { render } = ReactDOM,
{ useState } = React
//vocabulary
const vocabulary = ['engagement', 'Bentley', 'English', 'seven', 'Engagement']
//suggested option component
const SuggestedOption = ({suggestion, match}) => {
const parts = suggestion.split(new RegExp(`(?=${match})|(?<=${match})`, 'gi'))
return (
<div>
{
parts.map((part,key) => {
const style = part.toLowerCase() == match.toLowerCase() ? 'bold' : 'normal'
return <span style={{fontWeight:style}} {...{key}}>{part}</span>
})
}
</div>
)
}
//autocomplete component
const SearchBar = () => {
const [suggestions, setSuggestions] = useState([]),
[inputValue, setInputValue] = useState(''),
onInput = input => {
setInputValue(input)
setSuggestions(vocabulary.filter(word => input.length && word.toLowerCase().includes(input.toLowerCase())))
}
return (
<div>
<input onKeyUp={e => onInput(e.target.value)} />
<div >
{
suggestions.map((suggestion,key) => <SuggestedOption {...{key,suggestion,match:inputValue}} />)
}
</div>
</div>
)
}
render(
<SearchBar />,
document.getElementById('root')
)
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.12.0/umd/react.production.min.js"></script><script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.11.0/umd/react-dom.production.min.js"></script><div id="root"></div>

Here's a way to do it with string.replace:
const list = [
'end',
'England',
'engagement'
]
const boldify = (search) => {
return (string) =>
string.replace(new RegExp(search, 'gi'), '<strong>$&</strong>')
}
document.body.innerHTML = list.map(boldify('en')).join('<br>')
EDIT: After some times thinking, and seeing the accepted answer, I wanted to push the vanilla JS version to have something more complete. And moreover, the React version seemed laggy to me, this one is faster!
Faster because:
it uses vanilla JavaScript
it uses correct dom manipulation (no innerHTML)
'use strict'
const list = [
'end',
'England',
'engagement',
'Ken Bogard',
'Venom',
'Engagement'
]
const boldify = (search) => {
return (string) => {
const div = document.createElement('div')
const parts = string.split(new RegExp(`(?=${search})|(?<=${search})`, 'gi'))
div.append(
...parts.map(part => {
if (part.toUpperCase() !== search.toUpperCase()) return part
const strong = document.createElement('strong')
strong.innerText = part
return strong
})
)
return div
}
}
const contains = (search) => {
search = search.toUpperCase()
return (string) => string.toUpperCase().indexOf(search) >= 0
}
const refreshSuggestions = () => {
// Fast removal of children.
while (suggestions.firstChild) suggestions.removeChild(suggestions.firstChild)
// nothing to do
if (searchInput.value.length == 0) return
const newSuggestions =
list.filter(contains(searchInput.value))
.map(boldify(searchInput.value))
suggestions.append(...newSuggestions)
}
searchInput.addEventListener('input', refreshSuggestions)
<input id="searchInput" list="suggestions">
<div id="suggestions">
</div>

EDIT - Yevgen's answer is much nicer than this one.
A simple loop can achieve this if i understand correctly what you want:
var array = ["end","engagement","Engagement","england","enough","not this","or this"];
function filterArray(array, id) {
var returnArray = [];
for (var i = 0; i < array.length; i++) {
value = array[i];
if (value.includes(id)) {
returnArray.push(value);
}
}
return returnArray;
}
var filteredArray = filterArray(array,"en");
console.log(filteredArray);
If you wanted to ensure duplicates (where case on Engagement causes a duplicate) you could set the string to lowercase before pushing to the array, and check the array for the existence of the string before pushing again.

Related

How to find similarities between a text string and an array?

I have a string array, which I want to compare with a text string, if it finds similarities I want to be able to mark in black the similarity found within the string
example:
context.body: 'G-22-6-04136 - PatientName1'
newBody: ['G-22-6-04136 - PatientName1' , 'G-22-6-04137 - PatientName2']
When finding the similarity between the two the result would be something like this
newBody: [**'G-22-6-04136 - PatientName1'** , 'G-22-6-04137 - PatientName2']
How could I do it? Is this possible to do? In advance thank you very much for the help
const totalSize: number = this.getSizeFromAttachments(attachments);
const chunkSplit = Math.floor(isNaN(totalSize) ? 1 : totalSize / this.LIMIT_ATTACHMENTS) + 1;
const attachmentsChunk: any[][] = _.chunk(attachments, chunkSplit);
if ((totalSize > this.LIMIT_ATTACHMENTS) && attachmentsChunk?.length >= 1) {
const result = attachment.map(element => this.getCantidad.find(y => element.content === y.content))
const aux = this.namePatient
const ans = []
result.forEach(ele => {
const expected_key = ele["correlative_soli"];
if (aux[expected_key]) {
const newItem = { ...ele };
newItem["name_Patient"] = aux[expected_key]
newItem["fileName"] = `${expected_key}${aux[expected_key] ? ' - ' + aux[expected_key] : null}\n`.replace(/\n/g, '<br />')
ans.push(newItem)
}
});
let newBody: any;
const resultFilter = attachment.map(element => element.content);
const newArr = [];
ans.filter(element => {
if (resultFilter.includes(element.content)) {
newArr.push({
fileNameC: element.fileName
})
}
})
newBody = `• ${newArr.map(element => element.fileNameC)}`;
const date = this.cleanDateSolic;
const amoung= attachmentsChunk?.length;
const getCurrent = `(${index}/${attachmentsChunk?.length})`
const header = `${this.cleanDateSolic} pront ${attachmentsChunk?.length} mail. This is the (${index}/${attachmentsChunk?.length})`;
console.log('body', context.body);
// context.body is the body I receive of frontend like a string
const newContext = {
newDate: date,
amoung: amoung,
getCurrent: getCurrent,
newBody: newBody,
...context
}
return this.prepareEmail({
to: to,
subject: ` ${subject} (Correo ${index}/${attachmentsChunk?.length})`,
template: template,
context: newContext,
}, attachment);
}
I have a string array, which I want to compare with a text string, if it finds similarities I want to be able to mark in black the similarity found within the string
First of all the text will be in black by default, I think you are talking about to make that bold if matched. But again it depends - If you want to render that in HTML then instead of ** you can use <strong>.
Live Demo :
const textString = 'G-22-6-04136 - PatientName1';
let arr = ['G-22-6-04136 - PatientName1', 'G-22-6-04137 - PatientName2'];
const res = arr.map(str => str === textString ? `<strong>${str}<strong>` : str);
document.getElementById('result').innerHTML = res[0];
<div id="result">
</div>
Based on your example, it looks like the string needs to be an exact match, is that correct?
Also, "mark it in bold" is going to depend on what you're using to render this, but if you just want to wrap the string in ** or <b> or whatever, you could do something like this:
newBody.map(str => str === context.body ? `**${str}**` : str);
You can just see if string exists in the array by doing
if arr.includes(desiredStr) //make text bold

How to highlight text and display it in html [duplicate]

I'm trying to highlight text matching the query but I can't figure out how to get the tags to display as HTML instead of text.
var Component = React.createClass({
_highlightQuery: function(name, query) {
var regex = new RegExp("(" + query + ")", "gi");
return name.replace(regex, "<strong>$1</strong>");
},
render: function() {
var name = "Javascript";
var query = "java"
return (
<div>
<input type="checkbox" /> {this._highlightQuery(name, query)}
</div>
);
}
});
Current Output: <strong>Java</strong>script
Desired Output: Javascript
Here is my simple twoliner helper method:
getHighlightedText(text, highlight) {
// Split text on highlight term, include term itself into parts, ignore case
const parts = text.split(new RegExp(`(${highlight})`, 'gi'));
return <span>{parts.map(part => part.toLowerCase() === highlight.toLowerCase() ? <b>{part}</b> : part)}</span>;
}
It returns a span, where the requested parts are highlighted with <b> </b> tags. This can be simply modified to use another tag if needed.
UPDATE: To avoid unique key missing warning, here is a solution based on spans and setting fontWeight style for matching parts:
getHighlightedText(text, highlight) {
// Split on highlight term and include term into parts, ignore case
const parts = text.split(new RegExp(`(${highlight})`, 'gi'));
return <span> { parts.map((part, i) =>
<span key={i} style={part.toLowerCase() === highlight.toLowerCase() ? { fontWeight: 'bold' } : {} }>
{ part }
</span>)
} </span>;
}
Here is an example of a react component that uses the standard <mark> tag to highlight a text:
const Highlighted = ({text = '', highlight = ''}) => {
if (!highlight.trim()) {
return <span>{text}</span>
}
const regex = new RegExp(`(${_.escapeRegExp(highlight)})`, 'gi')
const parts = text.split(regex)
return (
<span>
{parts.filter(part => part).map((part, i) => (
regex.test(part) ? <mark key={i}>{part}</mark> : <span key={i}>{part}</span>
))}
</span>
)
}
And here is how to use it
<Highlighted text="the quick brown fox jumps over the lazy dog" highlight="fox"/>
There is already a react component on NPM to do what you want:
var Highlight = require('react-highlighter');
[...]
<Highlight search={regex}>{name}</Highlight>
Here's my solution.
I tried to focus on simplicity and performance, so I avoided solutions that involved manual manipulation of the DOM outside of React, or unsafe methods like dangerouslySetInnerHTML.
Additionally, this solution takes care of combining subsequent matches into a single <span/>, thus avoiding having redundant spans.
const Highlighter = ({children, highlight}) => {
if (!highlight) return children;
const regexp = new RegExp(highlight, 'g');
const matches = children.match(regexp);
console.log(matches, parts);
var parts = children.split(new RegExp(`${highlight.replace()}`, 'g'));
for (var i = 0; i < parts.length; i++) {
if (i !== parts.length - 1) {
let match = matches[i];
// While the next part is an empty string, merge the corresponding match with the current
// match into a single <span/> to avoid consequent spans with nothing between them.
while(parts[i + 1] === '') {
match += matches[++i];
}
parts[i] = (
<React.Fragment key={i}>
{parts[i]}<span className="highlighted">{match}</span>
</React.Fragment>
);
}
}
return <div className="highlighter">{parts}</div>;
};
Usage:
<Highlighter highlight='text'>Some text to be highlighted</Highlighter>
Check out this codepen for a live example.
By default ReactJS escapes HTML to prevent XSS. If you do wish to set HTML you need to use the special attribute dangerouslySetInnerHTML.
Try the following code:
render: function() {
var name = "Javascript";
var query = "java"
return (
<div>
<input type="checkbox" /> <span dangerouslySetInnerHTML={{__html: this._highlightQuery(name, query)}}></span>
</div>
);
}
const escapeRegExp = (str = '') => (
str.replace(/([.?*+^$[\]\\(){}|-])/g, '\\$1')
);
const Highlight = ({ search = '', children = '' }) => {
const patt = new RegExp(`(${escapeRegExp(search)})`, 'i');
const parts = String(children).split(patt);
if (search) {
return parts.map((part, index) => (
patt.test(part) ? <mark key={index}>{part}</mark> : part
));
} else {
return children;
}
};
<Highlight search="la">La La Land</Highlight>
With react-mark.js you can simply:
<Marker mark="hello">
Hello World
</Marker>
Links:
NPM: https://www.npmjs.com/package/react-mark.js
Docs: https://stackblitz.com/edit/react-mark-js?file=src/App.js
Mark matches as a function
https://codesandbox.io/s/pensive-diffie-nwwxe?file=/src/App.js
import React from "react";
class App extends React.Component {
constructor(props) {
super(props);
this.state = {
res: "Lorem ipsum dolor"
};
this.markMatches = this.markMatches.bind(this);
}
markMatches(ev) {
let res = "Lorem ipsum dolor";
const req = ev.target.value;
if (req) {
const normReq = req
.toLowerCase()
.replace(/\s+/g, " ")
.trim()
.split(" ")
.sort((a, b) => b.length - a.length);
res = res.replace(
new RegExp(`(${normReq.join("|")})`, "gi"),
match => "<mark>" + match + "</mark>"
);
}
this.setState({
res: res
});
}
render() {
return (
<div className="App">
<input type="text" onChange={this.markMatches} />
<br />
<p dangerouslySetInnerHTML={{ __html: this.state.res }} />
</div>
);
}
}
export default App;
Based on #Henok T's solution, here is one without lodash.
It is implement in Typescript and uses Styled-components, but can be easily adapted to vanilla JS, by simply removing the types and adding the styles inline.
import React, { useMemo } from "react";
import styled from "styled-components";
const MarkedText = styled.mark`
background-color: #ffd580;
`;
interface IHighlighted {
text?: string;
search?: string;
}
export default function Highlighted({ text = "", search = "" }: IHighlighted): JSX.Element {
/**
* The brackets around the re variable keeps it in the array when splitting and does not affect testing
* #example 'react'.split(/(ac)/gi) => ['re', 'ac', 't']
*/
const re = useMemo(() => {
const SPECIAL_CHAR_RE = /([.?*+^$[\]\\(){}|-])/g;
const escapedSearch = search.replace(SPECIAL_CHAR_RE, "\\$1");
return new RegExp(`(${escapedSearch})`, "i");
}, [search]);
return (
<span>
{search === ""
? text
: text
.split(re)
.filter((part) => part !== "")
.map((part, i) => (re.test(part) ? <MarkedText key={part + i}>{part}</MarkedText> : part))}
</span>
);
}
const highlightMatchingText = (text, matchingText) => {
const matchRegex = RegExp(matchingText, 'ig');
// Matches array needed to maintain the correct letter casing
const matches = [...text.matchAll(matchRegex)];
return text
.split(matchRegex)
.map((nonBoldText, index, arr) => (
<React.Fragment key={index}>
{nonBoldText}
{index + 1 !== arr.length && <mark>{matches[index]}</mark>}
</React.Fragment>
));
};
How to use it:
<p>highlightMatchingText('text here', 'text')</p>
or
<YourComponent text={highlightMatchingText('text here', 'text')}/>
This should work:
var Component = React.createClass({
_highlightQuery: function(name, query) {
var regex = new RegExp("(" + query + ")", "gi");
return "<span>"+name.replace(regex, "<strong>$1</strong>")+"</span>";
},
render: function() {
var name = "Javascript";
var query = "java"
return (
<div>
<input type="checkbox" />{JSXTransformer.exec(this._highlightQuery(name, query))}
</div>
);
}
});
Basically you're generating a react component on the fly. If you want, you can put the <span> tag inside the render() function rather then the _highlightQuery() one.
I would suggest you use a different approach. Create one component, say <TextContainer />, which contains <Text /> elements.
var React = require('react');
var Text = require('Text.jsx');
var TextContainer = React.createClass({
getInitialState: function() {
return {
query: ''
};
},
render: function() {
var names = this.props.names.map(function (name) {
return <Text name={name} query={this.state.query} />
});
return (
<div>
{names}
</div>
);
}
});
module.exports = TextContainer;
As you see the text container holds as state the current query. Now, the <Text /> component could be something like this:
var React = require('react');
var Text = React.createClass({
propTypes: {
name: React.PropTypes.string.isRequired,
query: React.PropTypes.string.isRequired
},
render: function() {
var query = this.props.query;
var regex = new RegExp("(" + query + ")", "gi");
var name = this.props.name;
var parts = name.split(regex);
var result = name;
if (parts) {
if (parts.length === 2) {
result =
<span>{parts[0]}<strong>{query}</strong>{parts[1]}</span>;
} else {
if (name.search(regex) === 0) {
result = <span><strong>{query}</strong>{parts[0]}</span>
} else {
result = <span>{query}<strong>{parts[0]}</strong></span>
}
}
}
return <span>{result}</span>;
}
});
module.exports = Text;
So, the root component has as state, the current query. When its state will be changed, it will trigger the children's render() method. Each child will receive the new query as a new prop, and output the text, highlighting those parts that would match the query.
I had the requirement to search among the comments contain the HTML tags.
eg: One of my comments looks like below example
Hello World
<div>Hello<strong>World</strong></div>
So, I wanted to search among all these kinds of comments and highlight the search result.
As we all know we can highlight text using HTML tag <mark>
So. I have created one helper function which performs the task of adding <mark> tag in the text if it contains the searched text.
getHighlightedText = (text, highlight) => {
if (!highlight.trim()) {
return text;
}
const regex = new RegExp(`(${highlight})`, "gi");
const parts = text.split(regex);
const updatedParts = parts
.filter((part) => part)
.map((part, i) =>
regex.test(part) ? <mark key={i}>{part}</mark> : part
);
let newText = "";
[...updatedParts].map(
(parts) =>
(newText =
newText +
(typeof parts === "object"
? `<${parts["type"]}>${highlight}</${parts["type"]}>`
: parts))
);
return newText;
};
So, We have to pass our text and search text inside the function as arguments.
Input
getHighlightedText("<div>Hello<strong>World</strong></div>", "hello")
Output
<div><mark>Hello</mark><strong>World</strong></div>
Let me know if need more help with solutions.
I have extended the version from #Henok T from above to be able to highlight multiple text parts splitted by space but keep strings in quotes or double quotes together.
e.g. a highlight of text "some text" 'some other text' text2
would highlight the texts:
text
some text
some other text
text2 in the given text.
const Highlighted = ({text = '', highlight = ''}: { text: string; highlight: string; }) => {
if (!highlight.trim()) {
return <span>{text}</span>
}
var highlightRegex = /'([^']*)'|"([^"]*)"|(\S+)/gi; // search for all strings but keep strings with "" or '' together
var highlightArray = (highlight.match(highlightRegex) || []).map(m => m.replace(highlightRegex, '$1$2$3'));
// join the escaped parts with | to a string
const regexpPart= highlightArray.map((a) => `${_.escapeRegExp(a)}`).join('|');
// add the regular expression
const regex = new RegExp(`(${regexpPart})`, 'gi')
const parts = text.split(regex)
return (
<span>
{parts.filter(part => part).map((part, i) => (
regex.test(part) ? <mark key={i}>{part}</mark> : <span key={i}>{part}</span>
))}
</span>
)
}

Typescript react -Object is possibly nil

I'm super new to react and I've been asked to help out a react typescript website. At the moment my matches const is returning an Object is possibly 'null'. I've tried making my match constant null but for the life of me I cannot figure out how to work around this not to get this error. Any help would be much appreciated.
case "text":
return (
<div>
{" "}
{note.text.map((content: string, idx: number) => {
const result = [];
const matches = content.match(
/(.*?)(<a href=")?(https?|www)((".*?>.*?<\/a>)|[^\s>]?)*(.*?)/gi
);
if (!!matches) {
matches.forEach((match) => {
let link;
if (/href="/i.test(match)) {
const url = match
.match(/<a href="(.*?)(?=">)/i)[0]
.replace('<a href="', "");
const linkText = match
.match(/(?:<a href=".*?">)(.*)(?=<\/a>)/)[0]
.replace(/(?:<a href=".*?">)/i, "");
link = <a href={url}>{linkText}</a>;
} else {
const url = match.match(/(https?|www)[^\s]*/gi).join("");
link = <a href={url}>{url}</a>;
}
const splitter = match.match(
/(<a href=")?(https?|www)[^\s]*(.*<\/a>)?/gi
)[0];
const paredPlainText = match.split(new RegExp(splitter));
result.push(paredPlainText[0]);
result.push(link);
result.push(paredPlainText[1]);
});
} else {
result.push(content);
}
return <p>{result}</p>;
})}
</div>
);
I'm not fully sure I understand your problem, the description sounds a bit confuse, but I believe you mean that line:
if (!!matches) {
right?
This converts the result of the match call (which can be an array with results or null) to a bool. Just use
if (matches) {
instead, which checks for a truthy expression (null represents a falsy result).
If this answer is not what you are after then please add more description to your question.
Thanks everyone for the response! This was my answer using non-null assertion operator ! after the .match as below
case "text":
return (
<div>
{" "}
{note.text.map((content: string, idx: number) => {
const result = [];
const matches = content.match(
/(.*?)(<a href=")?(https?|www)((".*?>.*?<\/a>)|[^\s>]?)*(.*?)/gi
);
if (!!matches) {
matches.forEach((match) => {
let link;
if (/href="/i.test(match)) {
const url = match
.match(/<a href="(.*?)(?=">)/i)![0]
.replace('<a href="', "");
const linkText = match
.match(/(?:<a href=".*?">)(.*)(?=<\/a>)/)![0]
.replace(/(?:<a href=".*?">)/i, "");
link = <a href={url}>{linkText}</a>;
} else {
const url = match.match(/(https?|www)[^\s]*/gi)!.join("");
link = <a href={url}>{url}</a>;
}
const splitter = match.match(
/(<a href=")?(https?|www)[^\s]*(.*<\/a>)?/gi
)![0];
const paredPlainText = match.split(new RegExp(splitter));
result.push(paredPlainText[0]);
result.push(link);
result.push(paredPlainText[1]);
});
} else {
result.push(content)
}
console.log(result);
return <p>{result}</p>;
})}
</div>
);

React: Highlight text between two indexes

So for every prediction the google places autocomplete API also returns matched substrings for each one.
Input: San 68
Prediction: San Francisco 68
Matched substrings: [{ offset: 0, length: 3 }, { offset: 15, length: 2 }]
Expectation: San Francisco 68
My goal is to highlight parts of the prediction using the matched substrings. Now there is a few challenges. I could use the replace function and replace every substring with <b>str</b>, but it returns a string which means unless I use dangerouslySetInnerHTML this method doesn't work.
I also don't think there is a way to replace multiple substrings. I tried to use the reduce function but after the first loop it wouldn't really work because the indexes would be wrong.
const highlight = (text, matched_substrings) => {
return matched_substrings.reduce((acc, cur) => {
return acc.replace(
acc.substring(cur.offset, cur.length),
(str) => `<b>${str}</b>`
)
}, text)
}
So is there a way to do this? I think React makes this more complicated.
probably not best solution, but definitely working one :)
Prerequisite is, that matched_substrigs array has to be sorted, by offsets
export const highlightText = (text, matched_substring, start, end) => {
const highlightTextStart = matched_substring.offset;
const highlightTextEnd = highlightTextStart + matched_substring.length;
// The part before matched text
const beforeText = text.slice(start, highlightTextStart);
// Matched text
const highlightedText = text.slice(highlightTextStart, highlightTextEnd);
// Part after matched text
// Till the end of text, or till next matched text
const afterText = text.slice(highlightTextEnd, end || text.length);
// Return in array of JSX elements
return [beforeText, <strong>{highlightedText}</strong>, afterText];
};
export const highlight = (text, matched_substrings) => {
const returnText = [];
// Just iterate through all matches
for (let i = 0; i < matched_substrings.length; i++) {
const startOfNext = matched_substrings[i + 1]?.offset;
if (i === 0) { // If its first match, we start from first character => start at index 0
returnText.push(highlightText(text, matched_substrings[i], 0, startOfNext))
} else { // If its not first match, we start from match.offset
returnText.push(highlightText(text, matched_substrings[i], matched_substrings[i].offset, startOfNext))
}
}
return returnText.map((text, i) => <React.Fragment key={i}>{text}</React.Fragment>)
};
here is a solution if you don't mind using dangerouslySetInnerHTML
const Highlight = ({ text, substrings }) => {
const html = substrings.reduce((acc, cur, idx) => {
return acc.replace(
new RegExp(
'(?<=^.{' + (cur.offset + idx * 17) + '})(.{' + cur.length + '})',
),
(str) => `<strong>${str}</strong>`,
)
}, text)
return <span dangerouslySetInnerHTML={{ __html: html }} />
}
use it like this
<Highlight text={...} substrings={...} />

Highlight text using ReactJS

I'm trying to highlight text matching the query but I can't figure out how to get the tags to display as HTML instead of text.
var Component = React.createClass({
_highlightQuery: function(name, query) {
var regex = new RegExp("(" + query + ")", "gi");
return name.replace(regex, "<strong>$1</strong>");
},
render: function() {
var name = "Javascript";
var query = "java"
return (
<div>
<input type="checkbox" /> {this._highlightQuery(name, query)}
</div>
);
}
});
Current Output: <strong>Java</strong>script
Desired Output: Javascript
Here is my simple twoliner helper method:
getHighlightedText(text, highlight) {
// Split text on highlight term, include term itself into parts, ignore case
const parts = text.split(new RegExp(`(${highlight})`, 'gi'));
return <span>{parts.map(part => part.toLowerCase() === highlight.toLowerCase() ? <b>{part}</b> : part)}</span>;
}
It returns a span, where the requested parts are highlighted with <b> </b> tags. This can be simply modified to use another tag if needed.
UPDATE: To avoid unique key missing warning, here is a solution based on spans and setting fontWeight style for matching parts:
getHighlightedText(text, highlight) {
// Split on highlight term and include term into parts, ignore case
const parts = text.split(new RegExp(`(${highlight})`, 'gi'));
return <span> { parts.map((part, i) =>
<span key={i} style={part.toLowerCase() === highlight.toLowerCase() ? { fontWeight: 'bold' } : {} }>
{ part }
</span>)
} </span>;
}
Here is an example of a react component that uses the standard <mark> tag to highlight a text:
const Highlighted = ({text = '', highlight = ''}) => {
if (!highlight.trim()) {
return <span>{text}</span>
}
const regex = new RegExp(`(${_.escapeRegExp(highlight)})`, 'gi')
const parts = text.split(regex)
return (
<span>
{parts.filter(part => part).map((part, i) => (
regex.test(part) ? <mark key={i}>{part}</mark> : <span key={i}>{part}</span>
))}
</span>
)
}
And here is how to use it
<Highlighted text="the quick brown fox jumps over the lazy dog" highlight="fox"/>
There is already a react component on NPM to do what you want:
var Highlight = require('react-highlighter');
[...]
<Highlight search={regex}>{name}</Highlight>
Here's my solution.
I tried to focus on simplicity and performance, so I avoided solutions that involved manual manipulation of the DOM outside of React, or unsafe methods like dangerouslySetInnerHTML.
Additionally, this solution takes care of combining subsequent matches into a single <span/>, thus avoiding having redundant spans.
const Highlighter = ({children, highlight}) => {
if (!highlight) return children;
const regexp = new RegExp(highlight, 'g');
const matches = children.match(regexp);
console.log(matches, parts);
var parts = children.split(new RegExp(`${highlight.replace()}`, 'g'));
for (var i = 0; i < parts.length; i++) {
if (i !== parts.length - 1) {
let match = matches[i];
// While the next part is an empty string, merge the corresponding match with the current
// match into a single <span/> to avoid consequent spans with nothing between them.
while(parts[i + 1] === '') {
match += matches[++i];
}
parts[i] = (
<React.Fragment key={i}>
{parts[i]}<span className="highlighted">{match}</span>
</React.Fragment>
);
}
}
return <div className="highlighter">{parts}</div>;
};
Usage:
<Highlighter highlight='text'>Some text to be highlighted</Highlighter>
Check out this codepen for a live example.
By default ReactJS escapes HTML to prevent XSS. If you do wish to set HTML you need to use the special attribute dangerouslySetInnerHTML.
Try the following code:
render: function() {
var name = "Javascript";
var query = "java"
return (
<div>
<input type="checkbox" /> <span dangerouslySetInnerHTML={{__html: this._highlightQuery(name, query)}}></span>
</div>
);
}
const escapeRegExp = (str = '') => (
str.replace(/([.?*+^$[\]\\(){}|-])/g, '\\$1')
);
const Highlight = ({ search = '', children = '' }) => {
const patt = new RegExp(`(${escapeRegExp(search)})`, 'i');
const parts = String(children).split(patt);
if (search) {
return parts.map((part, index) => (
patt.test(part) ? <mark key={index}>{part}</mark> : part
));
} else {
return children;
}
};
<Highlight search="la">La La Land</Highlight>
With react-mark.js you can simply:
<Marker mark="hello">
Hello World
</Marker>
Links:
NPM: https://www.npmjs.com/package/react-mark.js
Docs: https://stackblitz.com/edit/react-mark-js?file=src/App.js
Mark matches as a function
https://codesandbox.io/s/pensive-diffie-nwwxe?file=/src/App.js
import React from "react";
class App extends React.Component {
constructor(props) {
super(props);
this.state = {
res: "Lorem ipsum dolor"
};
this.markMatches = this.markMatches.bind(this);
}
markMatches(ev) {
let res = "Lorem ipsum dolor";
const req = ev.target.value;
if (req) {
const normReq = req
.toLowerCase()
.replace(/\s+/g, " ")
.trim()
.split(" ")
.sort((a, b) => b.length - a.length);
res = res.replace(
new RegExp(`(${normReq.join("|")})`, "gi"),
match => "<mark>" + match + "</mark>"
);
}
this.setState({
res: res
});
}
render() {
return (
<div className="App">
<input type="text" onChange={this.markMatches} />
<br />
<p dangerouslySetInnerHTML={{ __html: this.state.res }} />
</div>
);
}
}
export default App;
Based on #Henok T's solution, here is one without lodash.
It is implement in Typescript and uses Styled-components, but can be easily adapted to vanilla JS, by simply removing the types and adding the styles inline.
import React, { useMemo } from "react";
import styled from "styled-components";
const MarkedText = styled.mark`
background-color: #ffd580;
`;
interface IHighlighted {
text?: string;
search?: string;
}
export default function Highlighted({ text = "", search = "" }: IHighlighted): JSX.Element {
/**
* The brackets around the re variable keeps it in the array when splitting and does not affect testing
* #example 'react'.split(/(ac)/gi) => ['re', 'ac', 't']
*/
const re = useMemo(() => {
const SPECIAL_CHAR_RE = /([.?*+^$[\]\\(){}|-])/g;
const escapedSearch = search.replace(SPECIAL_CHAR_RE, "\\$1");
return new RegExp(`(${escapedSearch})`, "i");
}, [search]);
return (
<span>
{search === ""
? text
: text
.split(re)
.filter((part) => part !== "")
.map((part, i) => (re.test(part) ? <MarkedText key={part + i}>{part}</MarkedText> : part))}
</span>
);
}
const highlightMatchingText = (text, matchingText) => {
const matchRegex = RegExp(matchingText, 'ig');
// Matches array needed to maintain the correct letter casing
const matches = [...text.matchAll(matchRegex)];
return text
.split(matchRegex)
.map((nonBoldText, index, arr) => (
<React.Fragment key={index}>
{nonBoldText}
{index + 1 !== arr.length && <mark>{matches[index]}</mark>}
</React.Fragment>
));
};
How to use it:
<p>highlightMatchingText('text here', 'text')</p>
or
<YourComponent text={highlightMatchingText('text here', 'text')}/>
This should work:
var Component = React.createClass({
_highlightQuery: function(name, query) {
var regex = new RegExp("(" + query + ")", "gi");
return "<span>"+name.replace(regex, "<strong>$1</strong>")+"</span>";
},
render: function() {
var name = "Javascript";
var query = "java"
return (
<div>
<input type="checkbox" />{JSXTransformer.exec(this._highlightQuery(name, query))}
</div>
);
}
});
Basically you're generating a react component on the fly. If you want, you can put the <span> tag inside the render() function rather then the _highlightQuery() one.
I would suggest you use a different approach. Create one component, say <TextContainer />, which contains <Text /> elements.
var React = require('react');
var Text = require('Text.jsx');
var TextContainer = React.createClass({
getInitialState: function() {
return {
query: ''
};
},
render: function() {
var names = this.props.names.map(function (name) {
return <Text name={name} query={this.state.query} />
});
return (
<div>
{names}
</div>
);
}
});
module.exports = TextContainer;
As you see the text container holds as state the current query. Now, the <Text /> component could be something like this:
var React = require('react');
var Text = React.createClass({
propTypes: {
name: React.PropTypes.string.isRequired,
query: React.PropTypes.string.isRequired
},
render: function() {
var query = this.props.query;
var regex = new RegExp("(" + query + ")", "gi");
var name = this.props.name;
var parts = name.split(regex);
var result = name;
if (parts) {
if (parts.length === 2) {
result =
<span>{parts[0]}<strong>{query}</strong>{parts[1]}</span>;
} else {
if (name.search(regex) === 0) {
result = <span><strong>{query}</strong>{parts[0]}</span>
} else {
result = <span>{query}<strong>{parts[0]}</strong></span>
}
}
}
return <span>{result}</span>;
}
});
module.exports = Text;
So, the root component has as state, the current query. When its state will be changed, it will trigger the children's render() method. Each child will receive the new query as a new prop, and output the text, highlighting those parts that would match the query.
I had the requirement to search among the comments contain the HTML tags.
eg: One of my comments looks like below example
Hello World
<div>Hello<strong>World</strong></div>
So, I wanted to search among all these kinds of comments and highlight the search result.
As we all know we can highlight text using HTML tag <mark>
So. I have created one helper function which performs the task of adding <mark> tag in the text if it contains the searched text.
getHighlightedText = (text, highlight) => {
if (!highlight.trim()) {
return text;
}
const regex = new RegExp(`(${highlight})`, "gi");
const parts = text.split(regex);
const updatedParts = parts
.filter((part) => part)
.map((part, i) =>
regex.test(part) ? <mark key={i}>{part}</mark> : part
);
let newText = "";
[...updatedParts].map(
(parts) =>
(newText =
newText +
(typeof parts === "object"
? `<${parts["type"]}>${highlight}</${parts["type"]}>`
: parts))
);
return newText;
};
So, We have to pass our text and search text inside the function as arguments.
Input
getHighlightedText("<div>Hello<strong>World</strong></div>", "hello")
Output
<div><mark>Hello</mark><strong>World</strong></div>
Let me know if need more help with solutions.
I have extended the version from #Henok T from above to be able to highlight multiple text parts splitted by space but keep strings in quotes or double quotes together.
e.g. a highlight of text "some text" 'some other text' text2
would highlight the texts:
text
some text
some other text
text2 in the given text.
const Highlighted = ({text = '', highlight = ''}: { text: string; highlight: string; }) => {
if (!highlight.trim()) {
return <span>{text}</span>
}
var highlightRegex = /'([^']*)'|"([^"]*)"|(\S+)/gi; // search for all strings but keep strings with "" or '' together
var highlightArray = (highlight.match(highlightRegex) || []).map(m => m.replace(highlightRegex, '$1$2$3'));
// join the escaped parts with | to a string
const regexpPart= highlightArray.map((a) => `${_.escapeRegExp(a)}`).join('|');
// add the regular expression
const regex = new RegExp(`(${regexpPart})`, 'gi')
const parts = text.split(regex)
return (
<span>
{parts.filter(part => part).map((part, i) => (
regex.test(part) ? <mark key={i}>{part}</mark> : <span key={i}>{part}</span>
))}
</span>
)
}
import React from 'react';
//cm_chandan_
export default function App() {
const getSelection = () => {
if (window.getSelection().focusNode) {
var sel = window.getSelection();
var range = sel.getRangeAt(0);
if (
sel.rangeCount &&
range.startContainer.isSameNode(range.endContainer)
) {
var selectionAnchorOffset = sel.anchorOffset;
var selectionFocusffset = sel.focusOffset;
if (selectionAnchorOffset > selectionFocusffset) {
selectionAnchorOffset = sel.focusOffset;
selectionFocusffset = sel.anchorOffset;
}
let parentNodeSelection = sel.anchorNode.parentNode;
let childNodesArr = parentNodeSelection.childNodes;
for (let i = 0; i < childNodesArr.length; i++) {
if (childNodesArr[i].nodeType === 3) {
if (childNodesArr[i].isEqualNode(sel.anchorNode)) {
let contentNodeText = childNodesArr[i].textContent;
let startTextNode = document.createTextNode(
contentNodeText.slice(0, selectionAnchorOffset)
);
let endTextNode = document.createTextNode(
contentNodeText.slice(
selectionFocusffset,
contentNodeText.length
)
);
let highlightSpan = document.createElement('span');
highlightSpan.innerHTML = sel.toString();
highlightSpan.style.background = 'red';
childNodesArr[i].after(endTextNode);
childNodesArr[i].after(highlightSpan);
childNodesArr[i].after(startTextNode);
childNodesArr[i].remove();
}
}
}
} else {
alert('Wrong Text Select!');
}
} else {
alert('Please Text Select!');
}
};
return (
<>
<p id="textHilight">
<span id="heeader">
<b>
Organization’s vision An organization’s vision for a privacy program
needs to include data protection as well as data usage functions.
</b>
</span>
<br />
<span id="desc">
To be effective, the privacy program vision may also need to include
IT governance if this is lacking. Executive sponsorship is the formal
or informal approval to commit resources to a business problem or
challenge Privacy is no exception: without executive sponsorship,
privacy would be little more than an idea.As vision gives way to
strategy, the organization’s privacy leader must ensure that the
information privacy program fits in with the rest of the organization.
idea
</span>
</p>
<button
onClick={() => {
getSelection();
}}
>
click
</button>
</>
);
}

Categories

Resources