Profer
Profer

Reputation: 643

How to highlight text in para using array of values?

I have array of values

const tags = ['Hello', 'moment', 'somewhere'];

and the paragraphs to show in my react component

const paras = [{
  text: 'Hello, tina. Thank you for all waiting. Hello?'
}, {
  text: 'When can I go? Okay. One moment. Let me just'
}, {
  text: 'if I can help you somewhere, if you are interested'
}]

enter image description here

I want to highlight the para words with the tags element. And also only need few words before and after highlighted words like in the first sentence I only need to show like

 *One **moment**. Let *

How can I do that?

What I have tried:

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() ? <span className="highlight">{part}</span> : part)}</span>;
  }

{
  paras.map((para) => {
    return (
      <Row key={para._id}>
        <Col md={11}>
          <div className="record">
            <p className="mb-0">
              {this.getHighlightedText(para.text, "hello")}
            </p>
          </div>
        </Col>
        <Col md={1}>
          <Button className="buttonLink" color="link">
            <b>{"View >"}</b>
          </Button>
        </Col>
      </Row>
    );
  });
}

Upvotes: 2

Views: 1249

Answers (2)

deckele
deckele

Reputation: 4883

Here is a super simple to use reusable component for text highlighting by tags:

function Highlight({ children: text = "", tags = [] }) {
  if (!tags?.length) return text;
  const matches = [...text.matchAll(new RegExp(tags.join("|"), "ig"))];
  const startText = text.slice(0, matches[0]?.index);
  return (
    <span>
      {startText}
      {matches.map((match, i) => {
        const startIndex = match.index;
        const currentText = match[0];
        const endIndex = startIndex + currentText.length;
        const nextIndex = matches[i + 1]?.index;
        const untilNextText = text.slice(endIndex, nextIndex);
        return (
          <span key={i}>
            <mark>{currentText}</mark>
            {untilNextText}
          </span>
        );
      })}
    </span>
  );
}

export default function App() {
  return (
    <div className="App">
      <h1>
        <Highlight tags={["Hel", "co", "nd"]}>Hello CodeSandbox</Highlight>
      </h1>
      <h2>
        <Highlight tags={["ditin", "e"]}>
          Start editing to see some magic happen!
        </Highlight>
      </h2>
    </div>
  );
}

Demo in Codesndbox

example code output

Using it with your example:

const tags = ["Hello", "moment", "somewhere"];
const paras = [
  {
    text: "Hello, tina. Thank you for all waiting. Hello?"
  },
  {
    text: "When can I go? Okay. One moment. Let me just"
  },
  {
    text: "if I can help you somewhere, if you are interested"
  }
];
function ParasList() {
  return (
    <ul>
      {paras.map((para, i) => (
        <li key={i}>
          <Highlight tags={tags}>{para.text}</Highlight>
        </li>
      ))}
    </ul>
  );
}

example code #2 output

Explanation:

The Highlight component is easy to use, you just wrap around some text, and put on the "tags" attribute: <Highlight tags={["some"]}>some text</Highlight>

Inside the component, I'm using the new and shiny string.prototype.matchAll function, which gives us information about the match and the index where it was found.

Also, I'm creating the regex using join("|") on the tags, so they have an "OR" relationship between them. The flag i makes the highlight case insensitive, but you could delete it if you would want to keep case sensitivity. The g flag is the global flag, so duplicate matches are found and highlighted. It is required in a matchAll search.

Lastly, I'm using the <mark> html tags for highlighting, but you could easily change this, or even pass a custom tag through props for more versatility. You could also just enclose the "marked" bits with a regular span, and mark them with a "highlighted" className, and customize the appearance that way, using CSS.

Upvotes: 6

Anees Hikmat Abu Hmiad
Anees Hikmat Abu Hmiad

Reputation: 3560

You can resolve this issue by covert tags to string inside your regexp, so that you will simplify to check the list of words via it, then you need to check parts of your para that is contain any string and put and replace the value direct.

For example:

const reg = new RegExp(highlight.join("|").toLowerCase());

and the result of code:

  const getHighlightedText = useCallback((text, highlight) => {
    // Split text on highlight term, include term itself into parts, ignore case
    const parts = text.split(" ");
    console.log(parts);

    const reg = new RegExp(highlight.join("|").toLowerCase());
    const result = {};

    for (let i = 0; i < parts.length; i++) {
      result[i] = parts[i] + " ";
    }

    for (let i = 0; i < parts.length; i++) {
      if (reg.test(parts[i].replace(/[^a-zA-Z ]/g, "").toLowerCase())) {
        result[i] = <HighlightText part={parts[i]} />;

        if (result.hasOwnProperty(i - 1)) {
          result[i - 1] = <HighlightText part={parts[i - 1]} />;
        }

        if (result.hasOwnProperty(i + 1)) {
          result[i + 1] = <HighlightText part={parts[i + 1]} />;
        }
      }
    }

    return <span>{Object.values(result)}</span>;
  }, []);

And this is the DEMO URL

ScreenShot:

enter image description here

NOTE: You can enhance the example code, but its just to explain how to use your code in simple way.

Update 1:(You can also check only first detect like this):

  const getHighlightedTextOnlyOne = useCallback((text, highlight) => {
    // Split text on highlight term, include term itself into parts, ignore case
    const parts = text.split(" ");

    const reg = new RegExp(highlight.join("|").toLowerCase());
    const result = {};
    const alreadyDetect = [];

    for (let i = 0; i < parts.length; i++) {
      result[i] = parts[i] + " ";
    }

    for (let i = 0; i < parts.length; i++) {
      const checkString = parts[i].replace(/[^a-zA-Z ]/g, "").toLowerCase();
      if (reg.test(checkString) && !alreadyDetect.includes(checkString)) {
        result[i] = <HighlightText part={parts[i]} />;

        if (result.hasOwnProperty(i - 1)) {
          result[i - 1] = <HighlightText part={parts[i - 1]} />;
        }

        if (result.hasOwnProperty(i + 1)) {
          result[i + 1] = <HighlightText part={parts[i + 1]} />;
        }

        alreadyDetect.push(checkString);
      }
    }

    return <span>{Object.values(result)}</span>;
  }, []);

enter image description here

Update 2:(without regExp search and object loop and convert)

  const getHighlightedViaStringReplace = useCallback((text, highlight) => {
    // Split text on highlight term, include term itself into parts, ignore case
    const parts = text.split(" ");
    const partsForCheck = text.toLowerCase().replace(/[^a-zA-Z ]/g, "").split(" ");

    for (let i = 0; i < highlight.length; i++) {
      const index = partsForCheck.indexOf(highlight[i].toLowerCase());

      if(index !== -1){
        partsForCheck[index] = <HighlightText part={parts[index]} />;

        if(index - 1 > 0){
          partsForCheck[index - 1] = <HighlightText part={parts[index - 1]} />;
        }

        if(index + 1 < parts.length){
          partsForCheck[index + 1] = <HighlightText part={parts[index + 1]} />;
        }
      }
    }

    for (let i = 0; i < partsForCheck.length; i++) {
      if (typeof partsForCheck[i] === 'string' || partsForCheck[i] instanceof String){
        partsForCheck[i] = partsForCheck[i] + " ";
      }
    }

    return <span>{partsForCheck}</span>;
  }, []);

Upvotes: 2

Related Questions