предисловие
Компании необходимо использовать редактор форматированного текста для создания модуля ведения заметок. Я слышал, что редактором форматированного текста является Tiankeng.Чжиху-Почему редактор форматированного текста — провал?Попробовав основные редакторы на рынке, я обнаружил, что они более или менее неудовлетворительны. В основном это следующие проблемы:
- CKEditor очень мощный, но слишком сложный, и есть много мест, где он не используется.
- Front-end фреймворк проекта — Vue, желательно редактор на базе Vue2.x
- Опыт онлайн-редактора с открытым исходным кодом более или менее неудовлетворителен.
К счастью, времени на разработку относительно много, поэтому я решил разработать его на основе vue-html5-editor, и, наконец, завершил онлайн-работы, призывая звезду✨ 🙋Github:my-vue-editor
Осознайте рутину
Существует два основных способа реализации редактора форматированного текста на стороне Интернета:
- использовать
contenteditableпривязка атрибутаdocument.execCommandРеализация API, такая как зарубежный CKEditor, UEditor от Baidu и превосходный восходящая звезда wangEditor. - Полностью собственная имитационная реализация
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')
Полка нашего редактора построена вокруг этих двух концепций:
- Когда мы нажимаем различные функциональные кнопки редактора (такие как вставка изображений, выделение жирным шрифтом, подчеркивание), область содержимого теряет фокус, поэтому нам нужен способ сохранить текущий
Rangeобъекты и механизмы, которые можно вызывать при необходимости. - мы уже знаем
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, а потом разбираться со стилями, которые нужно фильтровать, синхронизацией мультитерминальных структур данных, совместимостью различных браузеров и т.д. Один за другим можно сделать многофункциональный редактор. 👏👏👏😄