Сегодня на многих веб-сайтах есть разные места, где пользователям разрешено выражать свои мысли. Это могут быть комментарии к записи в блоге или статье, отзывы об услугах компании и т. Д.

Веб-сайты для создания контента и некоторые платформы обмена сообщениями позволяют пользователям добавлять к своим сообщениям базовое форматирование текста, например выделять слова жирным шрифтом, подчеркивать и т. Д. В этой статье я опишу, как добавить некоторые простые параметры форматирования текста на веб-страницу, созданную с помощью React.js.

Подготовка сцены

Начнем с создания простой страницы с текстовой областью и тремя кнопками - полужирным шрифтом, курсивом и подчеркиванием:

import React from 'react';
import './App.css';
class App extends React.Component {
  constructor() {
    super();
  }
  render() {
    return (
      <div className="App">
        <header className="App-header">
          <span className="Controls">
            <button><strong>B</strong></button>
            <button><em>I</em></button>
            <button><u>U</u></button>
          </span>
          <textarea rows="5" className="Text" />
        </header>
      </div>
    );
  }
}
export default App;

Затем мы добавляем стили:

.App {
text-align: center;
}
.App-header {
min-height: 100vh;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
font-size: calc(10px + 2vmin);
}
.Controls {
border: 1px solid grey;
width: 50vw;
text-align: left;
padding: 10px;
}
button {
font-size: calc(8px + 2vmin);
margin: 0 2px;
width: 50px;
background-color: white;
cursor: pointer;
padding: 2px;
}
.Text {
font-size: calc(5px + 2vmin);
width: 50vw;
padding: 10px;
}

Приведенный выше код дает нам следующее:

Затем мы добавляем обработчики событий к кнопкам, чтобы при каждом нажатии на одну из них цвет фона менялся на светло-серый:

/* CSS */
.Selected {
background-color: lightgrey;
color: white;
}
...
// JS
class App extends React.Component {
  constructor() {
    super();
    this.onBoldClick = this.onBoldClick.bind(this);
    this.onItalicsClick = this.onItalicsClick.bind(this);
    this.onUnderlineClick = this.onUnderlineClick.bind(this);
  }
  onBoldClick(event) {
    event.target.setAttribute("class", !this.state.bold ? "Selected" : "");
  }
  onItalicsClick(event) {
    event.target.setAttribute("class", !this.state.italized ? "Selected" : "");
  }
  onUnderlineClick(event) {
    event.target.setAttribute("class", !this.state.underlined ? "Selected" : "");
  }
  
  render() {
    ...
    <button onClick={this.onBoldClick}><strong>B</strong></button>
    <button onClick={this.onItalicsClick}><em>I</em></button>
    <button onClick={this.onUnderlineClick}><u>U</u></button>
    ...
  }
}

Приведенный выше код зависит от трех параметров состояния: this.state.bold, this.state.italizedandis.state.underlined, которые сообщают нам, какой из параметров форматирования выбран в данный момент. Нам нужно добавить их в конструктор:

constructor() {
...
this.state = {
bold: false,
italized: false,
underlined: false
};
}

При щелчке полужирным шрифтом (B) приведенное выше дает нам следующий результат:

Добавление логики

Что мы хотим сделать, так это отформатировать любой текст, который написан в текстовой области, и отобразить результат. Для этого мы создадим тег div для хранения результата, а затем присвоим каждому тегу вывода и текстовой области ref, чтобы мы могли легко получить к ним доступ позже:

constructor() {
    super();
    this.inputRef = React.createRef();
    this.outputRef = React.createRef();
    ...

}
...
<header className="App-header">
<div ref={this.outputRef}></div>
<span className="Controls">
<button onClick={this.onBoldClick}><strong>B</strong></button>
<button onClick={this.onItalicsClick}><em>I</em></button>
<button onClick={this.onUnderlineClick}><u>U</u></button>
</span>
<textarea rows="5" className="Text" ref={this.inputRef} />
</header>
...

Затем мы добавляем метод, который будет вызываться всякий раз, когда текст в текстовой области изменился:

onInputChange() {
// Do something
}
...
render() {
...
<textarea rows="5" className="Text" ref={this.inputRef} onChange={this.onInputChange} />
...
}

Затем мы обновим методы обработки событий щелчка для трех кнопок:

onBoldClick(event) {
event.target.setAttribute("class", !this.state.bold ? "Selected" : "");
if (!this.state.bold) {
this.outputRef.current.innerHTML += "<strong></strong>";
}
this.setState({
bold: !this.state.bold
});
this.inputRef.current.focus();
}

Вот что делает приведенный выше код: если нажата кнопка, выделенная жирным шрифтом, она меняет цвет фона, как и раньше, а затем проверяет, становится ли это событие щелчка полужирным или нет. Если его включить, то он добавляет <strong></strong> к выходному div. Этот сильный тег будет содержать любой текст, который пользователь вводит следующим, и сделает его жирным. Наконец, он изменяет состояние полужирного флага (на false, если он был истинным, или на true, если он был ложным). Эта логика повторяется для кнопок, выделенных курсивом и подчеркиванием.

Следующим шагом является добавление метода с именем formatText (), который будет принимать строку в качестве своего атрибута, проверять, какой из параметров форматирования был выбран, и соответственно применять форматирование. Если параметр форматирования не выбран, он просто добавляет строку к выводу:

formatText(text) {
switch (true) {
  case this.state.bold:
    const allBold = this.outputRef.current.getElementsByTagName("strong");
    const lastBold = allBold[allBold.length - 1];
    lastBold.innerText += text;
    break;
  case this.state.italized:
    const allItalized = this.outputRef.current.getElementsByTagName("em");
    const lastItalized = allItalized[allItalized.length - 1];
    lastItalized.innerText += text;
    break;
  case this.state.underlined:
    const allUnderlined = this.outputRef.current.getElementsByTagName("u");
    const lastUnderlined = allUnderlined[allUnderlined.length - 1];
    lastUnderlined.innerText += text;
    break;
  default:
    this.outputRef.current.innerHTML += text;
    break;
}
}

Метод formatText () работает, проверяя, какой из параметров форматирования является истинным (или выбранным). Если выбран полужирный шрифт, выполняется проверка последнего элемента strong в выходном div и добавляется текст к элементу. Это сделает текст жирным. То же самое сделано для вариантов курсива и подчеркивания.

Мы будем вызывать этот метод formatText () каждый раз, когда текст в текстовой области изменяется:

onInputChange() {
const input = this.inputRef.current.value;
const output = this.outputRef.current.innerText;
const newText = input.slice(output.length);
this.formatText(newText);
}

В приведенном выше примере, когда пользователь вводит что-то в текстовую область, текст форматируется, если выбран параметр форматирования, в противном случае он добавляется в выходной div:

Однако есть проблема - если пользователь совершает ошибку и удаляет символ, изменение не отражается в выводе, то есть вывод обновляется только тогда, когда пользователь вводит что-то новое, а не при удалении текста:

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

transferText() {
const input = this.inputRef.current.value;
const output = this.outputRef.current.innerHTML;
let inputCounter = input.length - 1, outputCounter = output.length - 1, isTag = false;
while (outputCounter > -1) {
// If the current character is '>', then we are in a HTML tag. Skip until we get to '<'.
if (output[outputCounter] === ">") {
isTag = true;
outputCounter -= 1;
continue;
}
if (isTag) {
isTag = output[outputCounter] !== "<";
outputCounter -= 1;
continue;
}
// If inputCounter <= -1, then there is no more text to add to the output, so break.
if (inputCounter <= -1) {
this.outputRef.current.innerHTML = this.outputRef.current.innerHTML.slice(outputCounter + 1);
break;
}
// Otherwise, replace the text in the output with the corresponding text in the text area.
else {
let temp = this.outputRef.current.innerHTML;
temp = temp.slice(0, outputCounter) + input[inputCounter] + temp.slice(outputCounter + 1);
this.outputRef.current.innerHTML = temp;
inputCounter -= 1;
outputCounter -= 1;
}
}
}

Вышеупомянутый метод transferText () просматривает текст во внутреннем HTML-коде выходного div, начиная справа, пропускает HTML-теги (такие как <strong>, </em> и т. Д.) И для каждого символа в выходном div, он заменяет текст в выводе соответствующим текстом в текстовой области. Если в текстовой области больше нет текста, а в выходном div все еще есть текст для цикла (это может произойти, если пользователь удалил текст), он очищает оставшийся текст в выходном div.

Затем мы добавим этот метод в обработчик события изменения для текстовой области:

onInputChange() {
const input = this.inputRef.current.value;
const output = this.outputRef.current.innerText;
if (input.length > output.length) {
const newText = input.slice(output.length);
this.formatText(newText);
}
else {
this.transferText();
}
}

Обработчик событий теперь проверяет, больше ли текста в текстовой области, чем в выводе (что означает, что пользователь только что добавил текст), и форматирует текст, если это так. Если длина текста меньше или равна длине вывода, он вызывает метод transferText () для передачи текста из текстовой области в вывод.

Теперь удаление текста работает правильно:

Давайте протестируем форматирование текста с каждым из трех выбранных вариантов:

Заключение

Мы успешно создали веб-страницу с React, которая позволяет форматировать вводимый пользователем текст. Веб-страница поддерживает форматирование полужирным шрифтом, курсивом и подчеркиванием, и пользователи без проблем удаляют или изменяют набранный текст. Одним из ограничений вышеизложенного является то, что он не поддерживает комбинирование параметров форматирования (например, использование полужирного шрифта и подчеркивания для одного и того же текста).

Спасибо, что зашли так далеко. Вы можете найти полный код этого проекта здесь. Если у вас есть вопросы или комментарии, оставьте их ниже.