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

JavaScript Vue.js

предисловие

Компании необходимо использовать редактор форматированного текста для создания модуля ведения заметок. Я слышал, что редактором форматированного текста является Tiankeng.Чжиху-Почему редактор форматированного текста — провал?Попробовав основные редакторы на рынке, я обнаружил, что они более или менее неудовлетворительны. В основном это следующие проблемы:

  1. CKEditor очень мощный, но слишком сложный, и есть много мест, где он не используется.
  2. Front-end фреймворк проекта — Vue, желательно редактор на базе Vue2.x
  3. Опыт онлайн-редактора с открытым исходным кодом более или менее неудовлетворителен.

К счастью, времени на разработку относительно много, поэтому я решил разработать его на основе vue-html5-editor, и, наконец, завершил онлайн-работы, призывая звезду✨ 🙋Github:my-vue-editor

Осознайте рутину

Существует два основных способа реализации редактора форматированного текста на стороне Интернета:

  1. использоватьcontenteditableпривязка атрибутаdocument.execCommandРеализация API, такая как зарубежный CKEditor, UEditor от Baidu и превосходный восходящая звезда wangEditor.
  2. Полностью собственная имитационная реализацияselection, рендеринг просмотра и многое другое. Такие как Google Doc, Youdao Cloud Notes, основанные наelectronРазработано VS Code.

Здесь мы мудро выбрали первую реализацию. Кратко представим некоторые важные понятия редактора:

Range/Selection

RangeПеревод означает диапазон и амплитуду, что аналогично понятию «интервал» в математике. предоставляется браузеромRangeОбъекты используются для описания непрерывного диапазона в дереве DOM.

startContainer,startOffsetописыватьRangeначало ,endContainer,endOffsetописыватьRangeв конце . когдаRangeКогда начало и конец совпадают,Rangeбыть вcollapsedусловие.

Selection(выбор) управляет текущимRangeа такжеRangeРисунок. когдаSelectionсерединаRangeвcollapsedКогда он находится в состоянии, это курсор, о котором говорят в повседневной жизни. Курсор на самом делеSelectionособое состояние.

document.execCommand

Браузер изначально предоставляет нам некоторыеRangeМетоды для операций с форматированным текстом на внутренних узлах, эти методы передаются черезdocument.execCommandпередача.

bool = document.execCommand(aCommandName, aShowDefaultUI, aValueArgument)

Например

// 向当前插入点插入一个p标签。
document.execCommand('insertHTML', false, '<p></p>')

// 将框选部分字体变为绿色,如果是collapsep状态则接下来输入的文字为绿色
document.execCommand('foreColor', false, '#00ff00')

Полка нашего редактора построена вокруг этих двух концепций:

  1. Когда мы нажимаем различные функциональные кнопки редактора (такие как вставка изображений, выделение жирным шрифтом, подчеркивание), область содержимого теряет фокус, поэтому нам нужен способ сохранить текущийRangeобъекты и механизмы, которые можно вызывать при необходимости.
  2. мы уже знаемdocument.execCommandОн используется для манипулирования HTML-структурой выбора, но большая часть логики нативных методов не полностью соответствует нашим потребностям или возникают проблемы с совместимостью. Итак, мы оборачиваем наш собственный конструкторCommandИспользуется для манипулирования форматированным текстом, различные нажатия кнопок создают соответствующие экземпляры.Commandи выполнять соответствующие операции.

Для первой точки необходимо определить только одно сохранение, один метод установки.

// 保存当前Range
function saveCurrentRange () {
    // 获取selection对象
    const selection = window.getSelection ? window.getSelection() : document.getSelection()
      if (!selection.rangeCount) {
        return
      }
      const content = this.$refs.content
      for (let i = 0; i < selection.rangeCount; i++) {
        // 从selection中获取第一个Range对象
        const range = selection.getRangeAt(0)
        let start = range.startContainer
        let end = range.endContainer
        // 兼容IE11 node.contains(textNode) 永远 return false的bug
        start = start.nodeType === Node.TEXT_NODE ? start.parentNode : start
        end = end.nodeType === Node.TEXT_NODE ? end.parentNode : end
        if (content.contains(start) && content.contains(end)) {
        // Range对象被保存在this.range 
          this.range = range
          break
        }
      }
}

// 设置Range对象
function restoreSelection () {
    // 首先获取selection对象并清除当前的Range
      const selection = window.getSelection ? window.getSelection() : document.getSelection()
      selection.removeAllRanges()
      // 从this.range中获得保存的Range设置为Selection的Range对象
      if (this.range) {
        selection.addRange(this.range)
      } else {
        // 如果之前没有保存Range则新建一个
        const content = this.$refs.content
        const row = RH.prototype.newRow({br: true})
        const range = document.createRange()
        content.appendChild(row)
        range.setStart(row, 0)
        range.setEnd(row, 0)
        selection.addRange(range)
        this.range = range
      }
    }

С помощью этих двух методов нам просто нужно зарегистрироваться в области содержимого редактора.mouseup keyup mouseoutКонтролировать реализацию события в реальном времениsaveCurrentRange, При создании экземпляра кнопки щелчкаCommandперед казньюrestoreSelection.

По второму пункту пакетexecCommandМетод прост для понимания, например, я хочу реализовать функцию "отступ отступа",document.execCommandпри условииindentЭтот параметр можно использовать напрямую.Когда диапазон находится в ul>li, он выполняется вindentЭто сделает ul вложенным ul, станет ul>ul>li, множественный отступ выполнит множественное вложение. Это соответствует нашим потребностям.

// 缩进前
<ul>
    <li>当前光标位置</li>
</ul>

// 缩进后
<ul>
    <ul>
        <li>当前光标位置</li>
    </ul>
</ul>

Но когда Range находится в обычном элементе блочного уровня, выполнитеindentсделает элементы блочного уровня вложенными снаружиblockquoteэлементы, мы хотим увеличитьmargin-leftдля обработки отступов общих элементов уровня блока.

// 缩进前
<p>当前光标位置</p>

// 缩进后
<blockquote>
    <p>当前光标位置</p>
</blockquote>

// 我们希望的情况
<p style='margin-left: 8%;'>当前光标位置</p>

Нам просто нужно инкапсулироватьexecCommandметод, когда его параметрindent, выполнить соответствующий упакованныйindentметод, суждениеRangeНезависимо от того, находится ли он в элементе списка или других элементах блочного уровня, обрабатываются отдельно. Причина принятия здесь формы конструктора вместо обычной функции заключается в том, что все родныеexecCommandПри выполнении браузер будет поддерживать стек отмен и стек повторов для редактируемой области содержимого, так что каждое изменение поведения можно будет отменить и повторить.

Наш инкапсулированный метод перезаписывает собственный метод, что нарушит непрерывность стека отмен/повторов, что приведет к ошибкам или сбоям отмен и повторов. Итак, нам нужен каждыйCommandЭкземпляр сохраняет структуру DOM (моментальный снимок) области редактора перед выполнением и структуру DOM (моментальный снимок) области редактора после выполнения и помещает экземпляр в соответствующий стек отмены/повтора. Когда мы выполняем операции отмены и повтора, нам нужно только взять сохраненный снимок из соответствующего стека и восстановить его в области содержимого. Итак, вы узнали,undoа такжеredoЭто также два надо переписатьCommand.

На данный момент вышел прототип богатого текстового редактора, нам нужно только постоянно улучшать нашCommand, а потом разбираться со стилями, которые нужно фильтровать, синхронизацией мультитерминальных структур данных, совместимостью различных браузеров и т.д. Один за другим можно сделать многофункциональный редактор. 👏👏👏😄

Смотрите здесь, давайте попробуем наш редактор, Github: my-vue-editor Я думаю, что он прост в использовании, дайте ему звезду, старое железо

Категории