Руководство по использованию плагина Lexical Markdown с React.
Отказ от ответственности. Подумайте дважды, прежде чем использовать плагин Markdown, если вы собираетесь использовать собственные узлы; Позвольте мне вам сказать, что преобразование пользовательского узла в уценку и наоборот потребует создания собственных Трансформеров, а это непростая задача.
Предварительные условия
Эта статья является продолжением предыдущей:
Трансформеры
Прежде чем мы добавим MarkdownShortcutPlugin
в наше приложение, нам нужно понять, что такое ТРАНСФОРМЕРЫ:
import { MarkdownShortcutPlugin } from '@lexical/react/LexicalMarkdownShortcutPlugin'; import { TRANSFORMERS } from '@lexical/markdown'; ... <MarkdownShortcutPlugin transformers={TRANSFORMERS} />
ТРАНСФОРМАТОРЫ — это массив объектов…
const TRANSFORMERS: Array<Transformer> = [ ...ELEMENT_TRANSFORMERS, ...TEXT_FORMAT_TRANSFORMERS, ...TEXT_MATCH_TRANSFORMERS, ];
… которые имеют свойства, которые объясняют Markdown, как Node должен быть представлен в нотации Markdown и какой Node следует использовать при преобразовании из Markdown:
Пример:
export const UNORDERED_LIST: ElementTransformer = { dependencies: [ListNode, ListItemNode], export: (node, exportChildren) => { return $isListNode(node) ? listExport(node, exportChildren, 0) : null; }, regExp: /^(\s*)[-*+]\s/, replace: listReplace('bullet'), type: 'element', };
Объяснение:
MarkdownPlugin будет анализировать текст Markdown и использовать regExp
для поиска конкретного совпадения. Например, несортированный список будет представлен как:
- Hello - Hello
^(\s*)[-*+]\s
найдет два совпадения;replace
будет запущена функция, преобразующая текст в ListNode;- Функция
export
используется, когда мы конвертируем Node в Markdown;
Теперь давайте добавим MarkdownShortcutPlugin
в наше приложение.
MarkdownShortcutПлагин
Добавьте компонент MarkdownShortcutPlugin
плагина LexicalComposer
со свойством transformers
. Нам нужно будет добавить зависимости узлов, необходимые для Markdown:
Index: src/App.tsx @@ -4,6 +4,8 @@ import { ContentEditable } from '@lexical/react/LexicalContentEditable'; import LexicalErrorBoundary from '@lexical/react/LexicalErrorBoundary'; import { HistoryPlugin } from '@lexical/react/LexicalHistoryPlugin'; +import { MarkdownShortcutPlugin } from '@lexical/react/LexicalMarkdownShortcutPlugin'; +import { TRANSFORMERS } from '@lexical/markdown'; import { OnChangePlugin, CustomTextActions, @@ -14,11 +16,14 @@ CustomBannerPlugin, CustomBannerActions, } from './components'; -import { HeadingNode } from '@lexical/rich-text'; import initialStae from './initialState.json'; +import { HeadingNode, QuoteNode } from '@lexical/rich-text'; +import { CodeNode } from '@lexical/code'; +import { ListItemNode, ListNode } from '@lexical/list'; +import { LinkNode } from '@lexical/link'; import { BannerNode, ImageNode } from './nodes'; import { CustomDraggableBlockPlugin, DraggableWrapper } from './plugins'; import './App.css'; export const App: React.FC = () => { @@ -51,7 +56,16 @@ const lexicalConfig: InitialConfigType = { namespace: 'My Rich Text Editor', - nodes: [BannerNode, HeadingNode, ImageNode], + nodes: [ + BannerNode, + HeadingNode, + ImageNode, + QuoteNode, + CodeNode, + ListNode, + ListItemNode, + LinkNode, + ], editable: true, theme: { root: 'root', @@ -98,6 +112,7 @@ /> <HistoryPlugin /> <OnChangePlugin /> + <MarkdownShortcutPlugin transformers={TRANSFORMERS} /> <CustomHeadingPlugin /> <CustomBannerPlugin /> <CustomDraggableBlockPlugin />
Затем добавьте действия Markdown:
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext'; import { $createTextNode, $getRoot } from 'lexical'; import { $createCodeNode, $isCodeNode } from '@lexical/code'; import { $convertFromMarkdownString, $convertToMarkdownString } from '@lexical/markdown'; import { TRANSFORMERS } from '@lexical/markdown'; export const MarkdownActions = () => { const [editor] = useLexicalComposerContext(); const handleOnClick = () => { editor.update(() => { const root = $getRoot(); const firstChild = root.getFirstChild(); if ($isCodeNode(firstChild) && firstChild.getLanguage() === 'markdown') { // Markdown -> Node $convertFromMarkdownString(firstChild.getTextContent(), TRANSFORMERS); } else { // Node -> Markdown const markdown = $convertToMarkdownString(TRANSFORMERS); root.clear().append($createCodeNode('markdown').append($createTextNode(markdown))); } }); }; return ( <div style={{ marginTop: '10px' }}> <span style={{ fontWeight: 'bold' }}>Markdown</span> <div> <button onClick={handleOnClick}>MARKDOWN</button> </div> </div> ); };
Добавьте новые простые стили CSS для Markdown:
Index: src/App.css @@ -54,6 +54,13 @@ outline: none; } +.markdown-code { + font-size: 12px; + line-height: 1.5; + background-color: #dedede; + display: block; +} + p { margin: 0; }
Добавьте компонент MarkdownActions
в App.tsx:
Index: src/App.tsx @@ -15,6 +15,7 @@ CustomHeadingPlugin, CustomBannerPlugin, CustomBannerActions, + MarkdownActions, } from './components'; import { HeadingNode, QuoteNode } from '@lexical/rich-text'; import { CodeNode } from '@lexical/code'; @@ -80,6 +81,7 @@ superscript: 'text-superscript', }, banner: 'banner', + code: 'markdown-code', }, onError: (e) => { console.log('ERROR:', e); @@ -104,6 +106,7 @@ <CustomHeadingActions /> <CustomTextActions /> <CustomAlignActions /> + <MarkdownActions /> </div> <RichTextPlugin contentEditable={CustomContent}
Давайте посмотрим, что мы получили на данный момент:
Как вы можете видеть, наш перетаскиваемый элемент из предыдущей статьи станет видимым после преобразования узлов в Markdown. Давайте это исправим.
Исправить перетаскиваемый элемент
Во-первых, нам нужно будет обновить draggableStore
новыми свойствами, которые будут определять режимы «Вкл./Выкл.» Markdown:
Index: src/plugins/CustomDraggableBlockPlugin/store/draggableStore.ts @@ -27,6 +27,8 @@ line?: Line; setLine: (value: Line) => void; resetState: () => void; + isMarkdown: boolean; + setIsMarkdown: (value: boolean) => void; } export const draggableStore = create<MiniLibraryStoreProps, [['zustand/devtools', never]]>( @@ -73,6 +75,16 @@ { type: 'setLine', value: newLine }, ); }, + isMarkdown: false, + setIsMarkdown: (value: boolean) => { + setState( + { + isMarkdown: value, + }, + false, + { type: 'setIsMarkdown', value }, + ); + }, }), { name: 'draggableStore' }, ), @@ -80,9 +92,10 @@ export const useDraggableStore = () => draggableStore( - ({ draggable, resetState }) => ({ + ({ draggable, resetState, isMarkdown }) => ({ draggable, resetState, + isMarkdown, }), shallow, );
После этого мы обновим действия уценки, которые обновят состояние isMarkdown
:
Index: src/components/MarkdownActions/MarkdownActions.tsx @@ -3,7 +3,7 @@ import { $createCodeNode, $isCodeNode } from '@lexical/code'; import { $convertFromMarkdownString, $convertToMarkdownString } from '@lexical/markdown'; import { TRANSFORMERS } from '@lexical/markdown'; +import { draggableStore } from '../../plugins'; export const MarkdownActions = () => { const [editor] = useLexicalComposerContext(); @@ -15,11 +15,13 @@ $convertFromMarkdownString(firstChild.getTextContent(), TRANSFORMERS); + draggableStore.getState().setIsMarkdown(false); } else { const markdown = $convertToMarkdownString(TRANSFORMERS); root.clear().append($createCodeNode('markdown').append($createTextNode(markdown))); + draggableStore.getState().setIsMarkdown(true); } }); };
Обновить компонент CustomDraggableBlockPlugin
:
Index: src/plugins/CustomDraggableBlockPlugin/CustomDraggableBlockPlugin.tsx @@ -3,10 +3,13 @@ import { DRAGGABLE_WRAPPER_ID, DraggableElement, OnDragLine } from './components'; import { useOnDrop, useDragListeners } from './hooks'; import { createPortal } from 'react-dom'; +import { useDraggableStore } from './store'; export const CustomDraggableBlockPlugin: React.FC = () => { const [editor] = useLexicalComposerContext(); + const { isMarkdown } = useDraggableStore(); + useDragListeners(); useOnDrop(); @@ -14,7 +17,7 @@ const wrapperHtmlElement = document.getElementById(DRAGGABLE_WRAPPER_ID); - if (!isEditable || !wrapperHtmlElement) { + if (!isEditable || !wrapperHtmlElement || isMarkdown) { return null; }
Результат:
Репозиторий GitHub:
Статьи по теме:
На простом английском языке
Спасибо, что вы являетесь частью нашего сообщества! Прежде чем уйти:
- Обязательно аплодируйте и следуйте за автором! 👏
- Еще больше контента вы можете найти на PlainEnglish.io 🚀
- Подпишитесь на нашу бесплатную еженедельную рассылку. 🗞️
- Следуйте за нами в Twitter(X), LinkedIn, YouTube и Discord.