Что касается редактора форматированного текста, многие студенты никогда не использовали его и не слышали о нем. Это яма, на которую никто не хочет наступать. Насколько это жалко?
Я поднял часть большого брата на Жиху.отвечать, если интересно, можете посмотреть. Судя по основным примерам, с которыми я столкнулся до сих пор, чтобы довести редактор до коммерческого качества, требуется слишком много времени для самостоятельной разработки:
-
Редактор перьев,
QuillПрошло четыре года с тех пор, как в 2012 году был получен первый выпуск, а в 2016 году была выпущена версия 1.0. -
Редактор полуошибок,
ProsemirrorАвтор занимался разработкой в течение полугода до официального сбора средств и обслуживания открытого исходного кода в 2015 году, и прошло почти три года, когда вышла версия 1.0. - SlateОт опенсорса почти два года, до сих пор есть куча необъяснимых багов, которые используются по углам и углам.
Для вышеуказанных редакторских проектов под руководством одного человека для достижения стабильного качества время исчисляется годами. При нынешнем ритме Интернета «выход в онлайн на следующей неделе» тратить несколько лет нерентабельно. Таким образом, в условиях нехватки рабочей силы и рационального использования времени использование среды с открытым исходным кодом является лучшим выбором.
Хотите модульный редактор с широкими возможностями настройки, который определяет, что это не стандартное приложение, иQuillОн объединяет множество стилей и логики взаимодействия, и это уже приложение, иногда некоторые специфические требования не могут быть полностью удовлетворены.Slateосновывается наReactДля слоя просмотра наш стек технологий:Vue, рассматривать не буду, будет возможность изучить в будущем, поэтому выбрал окончательноprosemirror, но два других по-прежнему являются очень мощными редакторами, которые стоит изучить.
из-заprosemirrorВ настоящее время почти нет информации на китайском языке, которую можно найти с помощью поисковых систем, и в случае возникновения проблем вы можете перейти только на论坛,issueПоищите внутри или задайте вопрос автору. Следующий контент упрощен с официального сайта и моего собственного понимания во время использования. Я надеюсь, что после прочтения вы сможетеprosemirrorИнтересуйтесь, учитесь у автора дизайнерских идей и делитесь ими вместе.
Введение в ProseMirror
A toolkit for building rich-text editors on the web
prosemirrorавторMarijnдаcodemirrorредактор иacornАвтор интерпретатора, бывший уже вChromeа такжеFirefoxОн используется во встроенных средствах отладки, а последнийbabelзависимость.
prosemirrorЭто не большой и всеобъемлющий фреймворк, он состоит из бесчисленного множества маленьких модулей, это составной редактор, такой как Lego.
Его основные библиотеки:
-
prosemirror-model: определяет модель документа редактора, структуру данных, используемую для описания содержимого редактора. -
prosemirror-state: Предоставляет структуру данных, описывающую все состояние редактора, включаяselection(выбор), и от одного состояния к другомуtransaction(бизнес) -
prosemirror-view: реализует компонент пользовательского интерфейса, который отображает заданное состояние редактора как редактируемый элемент в браузере и обрабатывает взаимодействие с пользователем. -
prosemirror-transform: включает возможность изменять документ в записанном и воспроизводимом виде, чтоstateмодульtransaction(транзакций), а также позволяет отменить и совместное редактирование.
также,prosemirrorТакже предусмотрено множество модулей, таких какprosemirror-commandsосновные команды редактирования,prosemirror-keymapпривязки ключей,prosemirror-historyзапись истории,prosemirror-inputrulesвведите макрос,prosemirror-collabсовместное редактирование,prosemirror-schema-basicПростой режим документа и т. д.
Теперь у вас должно быть общее представление о том, что они делают, и они являются основой всего редактора.
Реализовать демонстрацию редактора
import { schema } from "prosemirror-schema-basic"
import { EditorState } from "prosemirror-state"
import { EditorView } from "prosemirror-view"
let state = EditorState.create({ schema })
let view = new EditorView(document.body, { state })
Давайте посмотрим, что делает приведенный выше код, начиная с первой строки.prosemirrorТребуется указать схему, которой соответствует документ. Итак, изprosemirror-schema-basicпредставил базовыйschema. тогда этоschemaЧто тогда?
потому чтоprosemirrorОпределяет собственную структуру данных для представления содержимого документа. существуетprosemirror结构а такжеHTML的Dom结构Между ними требуется синтаксический анализ и преобразование, и они преобразуются друг в друга.мост, нашеschema, так что давайте сначала разберемсяprosemirrorструктура документа.
структура документа prosmirror
prosemirrorДокумент представляет собойNode, который содержит ноль или болееchild NodesизFragment(片段).
немного похоже на браузерDOMрекурсивные и древовидные структуры. Но он отличается тем, как он хранит встроенный контент.
<p>This is <strong>strong text with <em>emphasis</em></strong></p>
существуетHTML, это древовидная структура, подобная этой:
p //"this is "
strong //"strong text with "
em //"emphasis"
существуетprosemirror, встроенный контент моделируется как плоская последовательность,strong、em(Mark)так какparagraph(Node)Дополнительные данные для:
"paragraph(Node)"
// "this is " | "strong text with" | "emphasis"
"strong(Mark)" "strong(Mark)", "em(Mark)"
prosemirrorОбъектная структура документа выглядит следующим образом
Node:
type: NodeType //包含了Node的名字与属性等
content: Fragment //包含多个Node
attrs: Object //自定义属性,image可以用来存储src等。
marks: [Mark, Mark...] // 包含一组Mark实例的数组,例如em和strong
Mark:
type: MarkType //包含Mark的名字与属性等
attrs: Object //自定义属性
prosemirrorПредусмотрено два типа индексов
- типы деревьев, это и
dom结构аналогично, вы можете использоватьchildилиchildCountи другие методы для прямого доступа к дочерним узлам - Плоская последовательность токенов, которая принимает индекс в последовательности токенов как позицию документа, они являются соглашением о подсчете.
- в начале всего документа, в позиции индекса 0
- Вход или выход из узла, который не является конечным узлом, записывается как маркер.
- Каждый узел в текстовом узле считается токеном
- Листовые узлы без содержимого (например,
image) также считается маркером
Например, естьHTMLФрагмент
<p>One</p>
<blockquote><p>Two<img src="..."></p></blockquote>
то счет помечен как
0 1 2 3 4 5
<p> O n e </p>
5 6 7 8 9 10 11 12 13
<blockquote> <p> T w o <img> </p> </blockquote>
Каждый узел имеетnodeSizeСвойство представляет размер всего узла. Ручной анализ этих местоположений включает довольно много подсчетов,prosemirrorпредоставляет намNode.resolveметод для анализа этих позиций и получения дополнительной информации об этой позиции, например, что такое родительский узел, смещение от родительского узла, каковы предки родительского узла и некоторую другую информацию.
понялprosemirrorструктура данных, знатьschemaЭто режим преобразования между двумя документами.Возвращаясь к предыдущему месту, мы начинаем сprosemirror-schema-basicпредставил базовыйschema, то это основноеschemaНа что это похоже? просмотревисходный кодпоследняя строка
export const schema = new Schema({nodes, marks})
schemaдаSchemaчерез входящийnodes, marksсгенерированный экземпляр.
И код перед определением экземпляраnodesа такжеmarks, сложите код и найдитеnodesда
{
doc: {...} // 顶级文档
blockquote: {...} //<blockquote>
code_block: {...} //<pre>
hard_break: {...} //<br>
heading: {...} //<h1>..<h6>
horizontal_rule: {...} //<hr>
image: {...} //<img>
paragraph: {...} //<p>
text: {...} //文本
}
marksда
{
em: {...} //<em>
link: {...} //<a>
strong: {...} //<strong>
code: {...} //<code>
}
Они представляют типы узлов, которые могут отображаться в редакторе, и способы их вложения. Каждый из них содержит набор правил, описывающихprosemirror文档а такжеDom文档отношения между тем, какDomпревратиться вNodeилиNodeпревратиться вDom. Каждый узел в документе имеет соответствующий тип.
начать сверхуdocНачать просмотр:
doc: {
content: "block+"
}
каждыйschemaДолжен быть определен узел верхнего уровня, т.е.doc.contentОпределяет, какие последовательности дочерних узлов допустимы для этого типа узла.
Например"paragraph"представляет абзац,"paragraph+"означает один или несколько абзацев,"paragraph*"Чтобы представить ноль или более абзацев, вы можете использовать диапазон, подобный регулярному выражению, после имени. Также вы можете использовать составные выражения, такие как"heading paragraph+","{paragraph | blockquote}+". здесь"block+"выражать"(paragraph | blockquote)+".
Тогда см.em:
em: {
parseDOM: [
{ tag: "i" },
{ tag: "em" },
{ style: "font-style=italic" }
],
toDOM: function() {
return ["em"]
}
}
parseDOMа такжеtoDOMУказывает на взаимное преобразование между документами.Приведенный выше код имеет три правила синтаксического анализа:
-
<i>Этикетка -
<em>Этикетка -
font-style=italicстиль
Когда правило соответствует, оно отображается какHTMLиз<em>структура.
Точно так же мы можем реализовать подчеркиваниеmark:
underline: {
parseDOM: [
{ tag: 'u' },
{ style: 'text-decoration:underline' }
],
toDOM: function() {
return ['span', { style: 'text-decoration:underline' }]
}
}
Nodeа такжеMarkможет быть использованattrsдля хранения пользовательских свойств, таких какimage, допустимыйattrsсредний накопительsrc,alt,title.
вернуться к только что
import { schema } from "prosemirror-schema-basic"
import { EditorState } from "prosemirror-state"
import { EditorView } from "prosemirror-view"
let state = EditorState.create({ schema })
let view = new EditorView(document.body, { state })
Мы используемEditorState.createпо основным правиламschemaСостояние, в котором был создан редакторstate. Далее, для государстваstateПредставление редактора создано и присоединено кdocument.body. Это изменит наше состояниеstateвизуализировать как редактируемыйdom节点, который генерируется, когда пользователь вводит transaction.
Transaction
Генерируется, когда пользователь вводит или иным образом взаимодействует с представлениемtransaction. пара описанияstateизменяется и может быть использован для создания новыхstate, затем обновите представление.
На картинке нижеprosemirrorПростой циклический поток данныхdata flow: в редакторе отображается заданныйstate, когда что-то происходитevent, это создаетtransactionа такжеbroadcastЭто. Тогда этоtransactionОбычно используется для создания новыхstate,ДолженstateиспользоватьupdateStateметод, предоставляемый представлению.
DOM event
↗ ↘
EditorView Transaction
↖ ↙
new EditorState
по умолчанию,stateВсе обновления происходят под капотом, однако вы можете писать плагиныpluginИли настроить представление для достижения. Например, мы изменим приведенный выше код, чтобы создать представление:
// (Imports omitted)
let state = EditorState.create({schema})
let view = new EditorView(document.body, {
state,
dispatchTransaction(transaction) {
console.log("create new transaction")
let newState = view.state.apply(transaction)
view.updateState(newState)
}
})
дляEditorViewдобавилdispatchTransactionизprop, каждый разtransaction, функция будет вызвана.
Таким образом, каждыйstateОбновления должны вызываться вручнуюupdateState.
Immutable
prosemirrorСтруктура данныхimmutableДа, неизменяемый, вы не можете назначить его напрямую, вы можете только передать соответствующийAPIдля создания новой ссылки. Но между разными ссылками одни и те же части являются общими. Это как иметьimmutableГлубоко вложенное дерево документов, даже если вы измените где-то только конечный узел, будет сгенерировано новое дерево, но это новое дерево, за исключением только что измененного конечного узла, будет таким же, как исходное дерево. имеютimmutable, когда каждый раз, когда вы печатаете, редактор будет генерировать новыйstate, вы в каждом разныеstateПереключайтесь между ними, вы можете добиться отмены повторных операций. При этом обновитьstateПерерисовка документов также стала более эффективной.
State
что составляетprosemirrorизstateШерстяная ткань?stateЕсть три основных компонента: ваш документdoc, текущий выборselectionи хранится в настоящее времяmarkзадаватьstoredMarks.
инициализацияstate, вы можете пройтиdocдает ему исходный документ для использования. Здесь мы можем использоватьidдляcontentвнизdom结构Как исходный документ редактора.Dom解析器БудуDom结构Через наш режим парсингаschemaПреобразовать его вprosemirror结构.
import { DOMParser } from "prosemirror-model"
import { EditorState } from "prosemirror-state"
import { schema } from "prosemirror-schema-basic"
let state = EditorState.create({
doc: DOMParser.fromSchema(schema).parse(document.querySelector("#content"))
})
prosemirrorПоддерживает несколько типовselection(И разрешить стороннему коду определять новые типы выбора, обратите внимание: любой новый тип должен наследоваться отSelection).selectionс документацией и прочее сstateСвязанные значения такие же, какimmutable, изменятьselection, чтобы создать новыйselectionи держать его новымstate.selectionиметь по крайней мереfromа такжеtoУказывает на расположение текущего документа, чтобы указать область выбора. Самый распространенный вид отбораTextSelection, для курсоров или выделенного текста.prosemirrorтакже поддерживаетNodeSelection, например, когда вы нажимаетеctrl / cmdнажмите наNodeВремя. Выбор варьируется от позиции перед узлом до позиции после него.storedMarksпредставляет собой набор элементов, которые необходимо применить к следующему вводуMark.
Plugins
pluginРасширяйте редактор и редактор различными способамиstate. при создании новогоstate, вы можете предоставить ему сериюplugin, они будут сохранены здесьstateи поэтомуstateлюбой производныйstateсередина. и может повлиятьtransactionаппликационный метод и на его основеstateкак ведет себя редактор.
СоздайтеpluginКогда его передано, цель, указывающая ее поведение.
let myPlugin = new Plugin({
props: {
handleKeyDown(view, event) {
//当收到keydown事件时调用
console.log("A key was pressed!")
return false // We did not handle this
}
}
})
let state = EditorState.create({schema, plugins: [myPlugin]})
Когда плагину нужен собственныйplugin state, вы можете пройтиstateсвойства для определения.
let transactionCounter = new Plugin({
state: {
init() { return 0 },
apply(tr, value) { return value + 1 }
}
})
function getTransactionCount(state) {
return transactionCounter.getState(state)
}
Приведенный выше плагин определяет простойplugin state, который был применен кstateизtransactionсчитать.
Ниже приведена вспомогательная функция, которая вызываетpluginизgetStateметод, из полного редактораstateполучено вpluginизstate.
потому что редакторstateдаimmutable, а такжеplugin stateэтоstateчасть, поэтомуplugin stateСлишкомimmutable, то есть ихapplyМетод должен возвращать новое значение, а не изменять старое значение.pluginобычно даютtransactionдобавить дополнительную информациюmetadata. Например, при отмене операции истории сгенерированныйtransaction,когдаpluginКогда он увидит это, он не будетtransactionобращаться с ним так же, он обрабатывает его особым образом: удалить из вершины стека отмен,transactionПоместить в стек повторов.
Возвращаясь к исходному примеру, мы можемcommandпривязан к вводу с клавиатурыkeymap plugin, а такжеhistory plugin, что наблюдаютtransactionотменить и повторить.
// (Omitted repeated imports)
import { undo, redo, history } from "prosemirror-history"
import { keymap } from "prosemirror-keymap"
let state = EditorState.create({
schema,
plugins: [
history(),
keymap({"Mod-z": undo, "Mod-y": redo})
]
})
let view = new EditorView(document.body, {state})
Создайтеstateзарегистрируетplugin,сквозь этоstateсозданный вид вы сможете нажатьCtrl-Z(илиOS XВверхCmd-Z), чтобы отменить последнее изменение.
Commands
надundo, redoэтоcommand, большинство операций редактирования обрабатываются какcommand. Его можно привязать к меню или клавише или каким-либо другим образом предоставить пользователю доступ к нему. существуетprosemirrorсередина,commandЭто функция реализации операций редактирования, и большинство из них используют редакторы.stateа такжеdispatchфункция(EditorView.dispatchили какой-то другой принятыйtransactionфункция) завершена. Вот простой пример:
function deleteSelection(state, dispatch) {
if (state.selection.empty) return false
if (dispatch) dispatch(state.tr.deleteSelection())
return true
}
когдаcommandдолжен вернуться, когда не применимоfalseИли ничего не делать. Требуется, если применимоdispatchОдинtransactionзатем вернутьсяtrue, чтобы можно было запроситьcommandприменимо ли оно к данномуstateфактически не выполняя его,dispatchПараметр является необязательным, если он не передаетсяdispatchчас,commandдолжен просто вернутьсяtrue, ничего не делать, это можно использовать, чтобы сделать строку меню серой, чтобы указать текущийcommandНе исполняемый.
Немногоcommandможет потребоваться сdomИнтерактивный, вы можете передать ему третий параметрview, вид всего редактора.prosemirror-commandsпредоставляет множество редакторовcommand, от простого к сложному. Также поставляется с основнымkeymap, привязки клавиш, которые можно дать редактору, чтобы позволить редактору выполнять такие операции, как ввод и удаление, которые сочетают в себе множествоschemaнесвязанныйcommandПривязать к обычно используемым для них клавишам. Он также экспортирует многиеcommandконструктор, напримерtoggleMark, который проходит вmarkТипы и пользовательские свойстваattrs, который возвращаетcommandфункция переключения токаselectionнаmarkТипы.
Чтобы настроить редактор или разрешить пользователям взаимодействовать сNodeдля взаимодействия, вы можете написать свой собственныйcommand.
Например, простая щетка формата стиля чисткиcommand:
function clear(state, dispatch) {
if (state.selection.empty) return false;
const { $from, $to } = state.selection;
if (dispatch) dispatch(state.tr.removeMark($from.pos, $to.pos, null));
return true
}
Суммировать
Приведенное выше введение можно использовать какprosemirrorПростое понимание того, как это работает, позволяет не видеть так много библиотек и не знать, с чего начать, когда вы впервые соприкасаетесь с ним.prosemirrorКроме понятий, введенных выше, существуютDecorations,NodeViewsи т. д., они позволяют вам управлять тем, как представление рисует документ. Если вы хотите продолжить узнавать большеprosemirror, вы можете перейти к егоОфициальный сайта такжеФорум, надеюсь, вы сможете стать его участником.
Добро пожаловать в публичный аккаунт «Front-end Micro Food». Я буду время от времени делиться и обобщать интерфейс и прилегающие области, рабочие мысли и опыт, чтобы помочь вам продвинуться к интерфейсу.