Оберните несколько строк в HTML способом React

Я создаю маркер сущности, чтобы загрузить текстовый файл, просмотреть содержимое на экране, а затем выделить слова, которые находятся в массиве. Это массив заполняется пользователем, когда они вручную выделяют выделение, например..

const entities = ['John Smith', 'Apple', 'some other word'];

This is my text document that is displayed on the screen. It contains a lot of text, and some of this text needs to be visually highlighted to the user once they manually highlight some text, like the name John Smith, Apple and some other word

теперь я хочу визуально выделить все экземпляры сущности в тексте, обернув его в некоторую разметку и сделав что-то вроде этого прекрасно:

getFormattedText() {
    const paragraphs = this.props.text.split(/n/);
    const { entities } = this.props;

    return paragraphs.map((p) => {
        let entityWrapped = p;

        entities.forEach((text) => {
        const re = new RegExp(`${text}`, 'g');
        entityWrapped =
            entityWrapped.replace(re, `<em>${text}</em>`);
        });

        return `<p>${entityWrapped}</p>`;
    }).toString().replace(/</p>,/g, '</p>');
}

...однако(!), это просто дает мне большую строку, поэтому я должен опасно установить внутренний HTML, и поэтому я не могу прикрепить событие onClick "The React way" к любому из этих выделенных объектов, что мне нужно сделать.

способ React сделать это - вернуть массив, который выглядит примерно так:

['This is my text document that is displayed on the screen. It contains a lot of text, and some of this text needs to be visually highlighted to the user, like the name', {}, {}, {}] здесь {} являются объектами React, содержащими материал JSX.

у меня был удар при этом с несколькими вложенными циклами, но это багги, как ад, трудно читать, и поскольку я постепенно добавляю больше объектов, производительность принимает огромный удар.

Итак, мой вопрос... как лучше всего решить эту проблему? Обеспечение кода просто и читаемо, и что мы не получаем огромные проблемы с производительностью, так как я потенциально имею дело с документами, которые очень длинные. Это время, когда я отпустил свою мораль и dangerouslySetInnerHTML, а также события, связанные непосредственно с дом?

обновление

@andricicezar's answer ниже делает идеальную работу по форматированию массива строк и объектов, готовых реагировать на рендеринг, однако он не очень эффективен, как только массив сущностей большой (>100), а текст также большой (>100kb). Мы смотрим на 10x дольше, чтобы отобразить это как строку массива V.

кто-нибудь знает лучший способ сделать это, что дает скорость рендеринга большую строку, но гибкость о возможности прикреплять события React к элементам? Или dangerouslySetInnerHTML-лучшее решение в этом сценарии?

3 ответов


вот решение, которое использует регулярное выражение для разделения строки по каждому ключевому слову. Вы можете сделать это проще, если вам не нужно, чтобы он был нечувствительным к регистру или выделял ключевые слова, которые являются несколькими словами.

import React from 'react';

const input = 'This is a test. And this is another test.';
const keywords = ['this', 'another test'];

export default class Highlighter extends React.PureComponent {
    highlight(input, regexes) {
        if (!regexes.length) {
            return input;
        }
        let split = input.split(regexes[0]);
        // Only needed if matches are case insensitive and we need to preserve the
        // case of the original match
        let replacements = input.match(regexes[0]);
        let result = [];
        for (let i = 0; i < split.length - 1; i++) {
            result.push(this.highlight(split[i], regexes.slice(1)));
            result.push(<em>{replacements[i]}</em>);
        }
        result.push(this.highlight(split[split.length - 1], regexes.slice(1)));
        return result;
    }
    render() {
        let regexes = keywords.map(word => new RegExp(`\b${word}\b`, 'ig'));
        return (
            <div>
                { this.highlight(input, regexes) }
            </div>);
    }
}

вы пробовали что-то подобное?

сложность-это количество абзацев * количество ключевых слов. Для абзаца из 22 273 слов (121 104 символов) и 3 ключевых слов требуется 44 МС на моем ПК для генерации массива.

!!! ОБНОВЛЕНИЕ: Я думаю, что это лучшее и efficientest способ, чтобы выделить ключевые слова. Я использовал ответ Джеймса Брайерли, чтобы оптимизировать его.

Я тестировал на 320kb данных с 500 ключевыми словами, и он загружается довольно медленно. Еще одна идея будет придавать пунктам прогрессивный характер. Визуализируйте первые 10 абзацев, а после этого, при прокрутке или через некоторое время, визуализируйте остальные.

и JS возиться с вашим примером:https://jsfiddle.net/69z2wepo/79047/

const Term = ({ children }) => (
  <em style={{backgroundColor: "red"}} onClick={() => alert(children)}>
    {children}
  </em>
);

const Paragraph = ({ paragraph, keywords }) => {
  let keyCount = 0;
  console.time("Measure paragraph");

  let myregex = keywords.join('\b|\b');
  let splits = paragraph.split(new RegExp(`\b${myregex}\b`, 'ig'));
  let matches = paragraph.match(new RegExp(`\b${myregex}\b`, 'ig'));
  let result = [];

  for (let i = 0; i < splits.length; ++i) {
    result.push(splits[i]);
    if (i < splits.length - 1)
      result.push(<Term key={++keyCount}>{matches[i]}</Term>);
  }

  console.timeEnd("Measure paragraph");

  return (
    <p>{result}</p>
  );
};


const FormattedText = ({ paragraphs, keywords }) => {
    console.time("Measure");

    const result = paragraphs.map((paragraph, index) =>
      <Paragraph key={index} paragraph={paragraph} keywords={keywords} /> );

    console.timeEnd("Measure");
    return (
      <div>
        {result}
      </div>
    );
};

const paragraphs = ["Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nulla ornare tellus scelerisque nunc feugiat, sed posuere enim congue. Vestibulum efficitur, erat sit amet aliquam lacinia, urna lorem vehicula lectus, sit amet ullamcorper ex metus vitae mi. Sed ullamcorper varius congue. Morbi sollicitudin est magna. Pellentesque sodales interdum convallis. Vivamus urna lectus, porta eget elit in, laoreet feugiat augue. Quisque dignissim sed sapien quis sollicitudin. Curabitur vehicula, ex eu tincidunt condimentum, sapien elit consequat enim, at suscipit massa velit quis nibh. Suspendisse et ipsum in sem fermentum gravida. Nulla facilisi. Vestibulum nisl augue, efficitur sit amet dapibus nec, convallis nec velit. Nunc accumsan odio eu elit pretium, quis consectetur lacus varius"];
const keywords = ["Lorem Ipsum"];

class App extends React.Component {
  constructor(props) {
    super(props);

    this.state = {
      limitParagraphs: 10
    };
  }

  componentDidMount() {
    setTimeout(
      () =>
        this.setState({
          limitParagraphs: 200
        }),
      1000
    );
  }

  render() {
    return (
      <FormattedText paragraphs={paragraphs.slice(0, this.state.limitParagraphs)} keywords={keywords} />
    );
  }
}

ReactDOM.render(
  <App />, 
  document.getElementById("root"));
<script src="https://cdn.jsdelivr.net/lodash/4.17.4/lodash.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react-dom.min.js"></script>

<div id="root">
</div>

первое, что я сделал, это разделил абзац на массив слов.

const words = paragraph.split( ' ' );

затем я сопоставил массив слов с кучей <span> теги. Это позволяет мне прикрепить onDoubleClick мероприятия к каждому слову.

return (
  <div>
    {
      words.map( ( word ) => {
        return (
          <span key={ uuid() }
                onDoubleClick={ () => this.highlightSelected() }>
                {
                  this.checkHighlighted( word ) ?
                  <em>{ word } </em>
                  :
                  <span>{ word } </span>
                }
          </span>
        )
      })
    }
  </div>
);

поэтому, если слово дважды щелкнуло, я увольняю this.highlightSelected() функция, а затем, как я условно отображаю слово, основанное на том, выделено оно или нет.

highlightSelected() {

    const selected = window.getSelection();
    const { data } = selected.baseNode;

    const formattedWord = this.formatWord( word );
    let { entities } = this.state;

    if( entities.indexOf( formattedWord ) !== -1 ) {
      entities = entities.filter( ( entity ) => {
        return entity !== formattedWord;
      });
    } else {
      entities.push( formattedWord );
    }  

    this.setState({ entities: entities });
}

все, что я делаю здесь, это либо удаление, либо нажатие слово к массиву в состоянии моего компонента. checkHighlighted() просто проверит, существует ли слово, отображаемое в этом массиве.

checkHighlighted( word ) {

    const formattedWord = this.formatWord( word );

    if( this.state.entities.indexOf( formattedWord ) !== -1 ) {
      return true;
    }
    return false;
  }

и наконец,formatWord() функция просто удаляет любые точки или запятые и делает все в нижнем регистре.

formatWord( word ) {
    return word.replace(/([a-z]+)[.,]/ig, '').toLowerCase();
}

надеюсь, что это помогает!