Как создать собственный браузерный WYSIWYG редактор?

Предыстория

Пару лет назад меня здесь, на StackOverflow немного засмеяли за то, что поставил перед собой задачу за нереальный срок создать собственный браузерный текстовый редактор и провалил её (кто засмеял, то меня узнает). Но всё же по сей день не могу понять один странный парадокс: на дворе 2020 год, бум браузерных приложений, JavaScript - самый популярный язык в мире, а до сих пор нет устоявшихся методологий создания собственных браузерных текстовых редакторов с произвольным функционалом. При таком темпе развития JavaScript уже нативно должна предоставлять средства для быстрого крафтинга браузерных WYSIWYG-редакторов, тем не менее, ничего подобного не наблюдается, а существующие редакторы реализованы какими-то извращенскими методами.

Вопрос

Итак, стандартная для 2020 года задача: нужно создать браузерный WYSIWYG-редактор с набором функционала, соответствующим произвольному техническому заданию, при этом ничего, кроме того, что затребовано, быть не должно. В общем случае, ни один из существующих редакторов не имеет такой гибкости, чтобы подогнать его под данное техническое задание. Не только поведение, но также дизайн, иконки и прочее - всё это должно полностью соответствовать дизайну.

Что же делать? С чего начать? Какие статьи читать? Какие книги покупать? Запереться на несколько месяцев в комнате и заниматься творческим кодизвратом - единственный способ справиться этой, казалось бы, стандартной с точки зрения заказчиков задачей в 2020 году? Это мы знаем про ограничения textarea, но если встать на сторону заказчиков, то тут будет лёгкий ступор: как это так - фроентенд разработчик, а не может сделать простенький, но соответствующий техническому заданию редактор?


Ответы (1 шт):

Автор решения: Leonid

Работает как указание режима ввода, так и редактирование уже введенного текста. При выделении текста сбрасываются все режимы. Таким образом можно назначить куску текста стиль или отменить вторым нажатием.

Все вводимые символы (соответсвующие регулярке) оборачиваются в span. Только так можно обеспечить возможность перекрестных стилей. То есть ВОТТАК.

Курсор всегда перебрасывается в конец для простоты (ведь и span'ы добавляются в конец). Этот момент надо доработать.

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

let modeButtons = document.getElementsByClassName('mode');
let text = document.getElementById('text');
let modeClass = '';
let selRange = null;

[...modeButtons].forEach(b => {
  b.addEventListener('click', e => {
    e.target.classList.toggle('active');
    modeClass = ([...modeButtons].map(b => b.classList.contains('active') ? b.getAttribute('data-font') : '')).join(' ');
    if(selRange){
      if(selRange.commonAncestorContainer == text){
        let spans = [...text.getElementsByTagName('span')];
        let start = spans.indexOf(selRange.startContainer.parentElement);
        let end = spans.indexOf(selRange.endContainer.parentElement);
        for(let i = start; i <= end; i++){
          if(b.classList.contains('active')){
            spans[i].classList.add(b.getAttribute('data-font'));
          } else {
            spans[i].classList.remove(b.getAttribute('data-font'));
          }
        }
      }
    }
  })

})

text.onkeypress = e => {
  if(/[a-zA-Z0-9,.А-ЯЁа-яё]/u.test(e.key.toString()) && e.key != 'Enter'){
    e.preventDefault();

    let span = document.createElement('span');
    span.textContent = e.key;
    span.className = modeClass;
    e.target.append(span);
  
    let sel = window.getSelection();
    sel.collapse(span, 1);
  }
}

text.onmouseup = e => {
  let sel = window.getSelection();
  if(sel.type == 'Range'){
selRange = sel.getRangeAt(0);
[...modeButtons].forEach(b => {b.classList.remove('active')});
modeClass = '';
  } else {
selRange = null;
  }
}
* {
  margin: 0;
}

#text {
  width: calc(100vw - 10);
  min-height: 40px;
  outline: 1px solid lightblue;
  margin: 5px;
}

.active {
  background-color: lightblue;
}

.boldi {
  font-weight: bold;
}

.cursiv {
  font-style: italic;
}

.underli {
  text-decoration: underline;
}
<div id="text" contenteditable="true" spellcheck="false"></div>
<button data-font="boldi" class="mode">Жирно</button>
<button data-font="cursiv" class="mode">Курсив</button>
<button data-font="underli" class="mode">Подчеркн</button>

→ Ссылка