0

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.

Unknown
  • 819
  • 9
  • 17

2 Answers2

3

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>)
};
MrHolal
  • 329
  • 3
  • 5
0

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={...} />
Unknown
  • 819
  • 9
  • 17