Reputation: 604
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
Upvotes: 56
Views: 113562
Reputation: 156
The same answer that Yoav Kadosh left, but mine version is adopted for typescript and there is no any warning at the console:
import React from "react";
import styled from "styled-components";
interface $Props {
children: string;
highlight: string;
}
// @ts-ignore
export const Highlighter = ({ children, highlight }: $Props): ReactElement => {
if (!highlight) return children;
const regexp = new RegExp(highlight, "g");
const matches = children.match(regexp);
if (!matches) return children;
const parts: Array<string> = children.split(new RegExp(`${highlight}`, "g"));
const results = [];
for (let i = 0; i < parts.length - 1; i++) {
results.push(
<React.Fragment key={i}>
{parts[i]}
<Fragment className="highlighted">{matches[i]}</Fragment>
</React.Fragment>
);
}
if (parts && matches && parts.length > matches.length) {
results.push(<React.Fragment>{parts[parts.length - 1]}</React.Fragment>);
}
return <div className="highlighter">{results}</div>;
};
export const Fragment = styled.span`
&.highlighted {
background-color: ${COLORS.LIGHT_YELLOW};
}
`;
Upvotes: 0
Reputation: 1
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>
</>
);
}
Upvotes: 0
Reputation: 748
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')}/>
Upvotes: 2
Reputation: 1663
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>
)
}
Upvotes: 0
Reputation: 23717
With react-mark.js
you can simply:
<Marker mark="hello">
Hello World
</Marker>
Links:
Upvotes: 3
Reputation: 8122
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>
);
}
Upvotes: 2
Reputation: 1130
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.
Upvotes: 0
Reputation: 481
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;
Upvotes: 2
Reputation: 12045
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>;
}
Upvotes: 140
Reputation: 5155
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.
Upvotes: 8
Reputation: 1094
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"/>
Upvotes: 33
Reputation: 41
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>
Upvotes: 4
Reputation: 6481
There is already a react component on NPM to do what you want:
var Highlight = require('react-highlighter');
[...]
<Highlight search={regex}>{name}</Highlight>
Upvotes: 10
Reputation: 4931
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.
Upvotes: 0
Reputation: 32787
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.
Upvotes: 0
Reputation: 1192
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>
);
}
Upvotes: 4