Andrew Hunt
Andrew Hunt

Reputation: 604

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

Upvotes: 56

Views: 113562

Answers (16)

alexis_dia
alexis_dia

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

Chandan Kashyap
Chandan Kashyap

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

Peter Rhodes
Peter Rhodes

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

BHoft
BHoft

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

Aakash
Aakash

Reputation: 23717

With react-mark.js you can simply:

<Marker mark="hello">
  Hello World
</Marker>

Links:

Upvotes: 3

lbragile
lbragile

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

Krunal Rajkotiya
Krunal Rajkotiya

Reputation: 1130

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.

Upvotes: 0

user3713526
user3713526

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

peter.bartos
peter.bartos

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

Yoav Kadosh
Yoav Kadosh

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

Henok T
Henok T

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

Yaroslav Shugailo
Yaroslav Shugailo

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

aij
aij

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

gcedo
gcedo

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

Cristik
Cristik

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

Yanik Ceulemans
Yanik Ceulemans

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

Related Questions