feat: enhance rich text editor with new features and plugins
All checks were successful
CI - Build and Push / Build and Push Docker Image (push) Successful in 23s
All checks were successful
CI - Build and Push / Build and Push Docker Image (push) Successful in 23s
- Added support for additional Lexical plugins: link, markdown, table, and checklist. - Implemented Markdown shortcuts and transformers for horizontal rules and tables. - Updated editor theme and styles to accommodate new elements like tables and links. - Improved the overall functionality of the rich text editor with new matchers for URLs and emails.
This commit is contained in:
1517
package-lock.json
generated
1517
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -11,10 +11,13 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@lexical/code": "^0.37.0",
|
"@lexical/code": "^0.37.0",
|
||||||
|
"@lexical/link": "^0.37.0",
|
||||||
"@lexical/list": "^0.37.0",
|
"@lexical/list": "^0.37.0",
|
||||||
|
"@lexical/markdown": "^0.37.0",
|
||||||
"@lexical/react": "^0.37.0",
|
"@lexical/react": "^0.37.0",
|
||||||
"@lexical/rich-text": "^0.37.0",
|
"@lexical/rich-text": "^0.37.0",
|
||||||
"@lexical/selection": "^0.37.0",
|
"@lexical/selection": "^0.37.0",
|
||||||
|
"@lexical/table": "^0.37.0",
|
||||||
"@lexical/utils": "^0.37.0",
|
"@lexical/utils": "^0.37.0",
|
||||||
"i18next": "^25.5.3",
|
"i18next": "^25.5.3",
|
||||||
"i18next-browser-languagedetector": "^8.2.0",
|
"i18next-browser-languagedetector": "^8.2.0",
|
||||||
@@ -35,9 +38,6 @@
|
|||||||
"globals": "^16.4.0",
|
"globals": "^16.4.0",
|
||||||
"typescript": "~5.8.3",
|
"typescript": "~5.8.3",
|
||||||
"typescript-eslint": "^8.44.0",
|
"typescript-eslint": "^8.44.0",
|
||||||
"vite": "npm:rolldown-vite@7.1.12"
|
"vite": "^7.0.0"
|
||||||
},
|
|
||||||
"overrides": {
|
|
||||||
"vite": "npm:rolldown-vite@7.1.12"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,14 +4,57 @@ import { ContentEditable } from '@lexical/react/LexicalContentEditable';
|
|||||||
import { HistoryPlugin } from '@lexical/react/LexicalHistoryPlugin';
|
import { HistoryPlugin } from '@lexical/react/LexicalHistoryPlugin';
|
||||||
import { LexicalErrorBoundary } from '@lexical/react/LexicalErrorBoundary';
|
import { LexicalErrorBoundary } from '@lexical/react/LexicalErrorBoundary';
|
||||||
import { HeadingNode, QuoteNode } from '@lexical/rich-text';
|
import { HeadingNode, QuoteNode } from '@lexical/rich-text';
|
||||||
import { CodeNode } from '@lexical/code';
|
import { CodeNode, CodeHighlightNode } from '@lexical/code';
|
||||||
import { ListItemNode, ListNode } from '@lexical/list';
|
import { ListItemNode, ListNode } from '@lexical/list';
|
||||||
import { ListPlugin } from '@lexical/react/LexicalListPlugin';
|
import { ListPlugin } from '@lexical/react/LexicalListPlugin';
|
||||||
|
import { CheckListPlugin } from '@lexical/react/LexicalCheckListPlugin';
|
||||||
|
import { HorizontalRuleNode } from '@lexical/react/LexicalHorizontalRuleNode';
|
||||||
|
import { TableCellNode, TableNode, TableRowNode } from '@lexical/table';
|
||||||
|
import { TablePlugin } from '@lexical/react/LexicalTablePlugin';
|
||||||
|
import { LinkNode, AutoLinkNode } from '@lexical/link';
|
||||||
|
import { LinkPlugin } from '@lexical/react/LexicalLinkPlugin';
|
||||||
|
import { AutoLinkPlugin } from '@lexical/react/LexicalAutoLinkPlugin';
|
||||||
|
|
||||||
import ToolbarPlugin from './plugins/ToolbarPlugin';
|
import ToolbarPlugin from './plugins/ToolbarPlugin';
|
||||||
|
import MarkdownPlugin from './plugins/MarkdownShortcutPlugin';
|
||||||
import editorTheme from './themes/EditorTheme';
|
import editorTheme from './themes/EditorTheme';
|
||||||
import './styles/editor.css';
|
import './styles/editor.css';
|
||||||
|
|
||||||
|
const URL_MATCHER =
|
||||||
|
/((https?:\/\/(www\.)?)|(www\.))[-a-zA-Z0-9@:%._+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_+.~#?&//=]*)/;
|
||||||
|
|
||||||
|
const EMAIL_MATCHER =
|
||||||
|
/(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))/;
|
||||||
|
|
||||||
|
const MATCHERS = [
|
||||||
|
(text: string) => {
|
||||||
|
const match = URL_MATCHER.exec(text);
|
||||||
|
if (match === null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const fullMatch = match[0];
|
||||||
|
return {
|
||||||
|
index: match.index,
|
||||||
|
length: fullMatch.length,
|
||||||
|
text: fullMatch,
|
||||||
|
url: fullMatch.startsWith('http') ? fullMatch : `https://${fullMatch}`,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
(text: string) => {
|
||||||
|
const match = EMAIL_MATCHER.exec(text);
|
||||||
|
if (match === null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const fullMatch = match[0];
|
||||||
|
return {
|
||||||
|
index: match.index,
|
||||||
|
length: fullMatch.length,
|
||||||
|
text: fullMatch,
|
||||||
|
url: `mailto:${fullMatch}`,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
const editorConfig = {
|
const editorConfig = {
|
||||||
namespace: 'CiallooEditor',
|
namespace: 'CiallooEditor',
|
||||||
theme: editorTheme,
|
theme: editorTheme,
|
||||||
@@ -22,8 +65,15 @@ const editorConfig = {
|
|||||||
HeadingNode,
|
HeadingNode,
|
||||||
QuoteNode,
|
QuoteNode,
|
||||||
CodeNode,
|
CodeNode,
|
||||||
|
CodeHighlightNode,
|
||||||
ListNode,
|
ListNode,
|
||||||
ListItemNode,
|
ListItemNode,
|
||||||
|
HorizontalRuleNode,
|
||||||
|
TableNode,
|
||||||
|
TableRowNode,
|
||||||
|
TableCellNode,
|
||||||
|
LinkNode,
|
||||||
|
AutoLinkNode,
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -44,6 +94,11 @@ export default function RichTextEditor() {
|
|||||||
/>
|
/>
|
||||||
<HistoryPlugin />
|
<HistoryPlugin />
|
||||||
<ListPlugin />
|
<ListPlugin />
|
||||||
|
<CheckListPlugin />
|
||||||
|
<LinkPlugin />
|
||||||
|
<AutoLinkPlugin matchers={MATCHERS} />
|
||||||
|
<TablePlugin />
|
||||||
|
<MarkdownPlugin />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</LexicalComposer>
|
</LexicalComposer>
|
||||||
|
|||||||
7
src/editor/plugins/MarkdownShortcutPlugin.tsx
Normal file
7
src/editor/plugins/MarkdownShortcutPlugin.tsx
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
import type { JSX } from 'react';
|
||||||
|
import { MarkdownShortcutPlugin } from '@lexical/react/LexicalMarkdownShortcutPlugin';
|
||||||
|
import { EDITOR_TRANSFORMERS } from './MarkdownTransformers';
|
||||||
|
|
||||||
|
export default function MarkdownPlugin(): JSX.Element {
|
||||||
|
return <MarkdownShortcutPlugin transformers={EDITOR_TRANSFORMERS} />;
|
||||||
|
}
|
||||||
229
src/editor/plugins/MarkdownTransformers.ts
Normal file
229
src/editor/plugins/MarkdownTransformers.ts
Normal file
@@ -0,0 +1,229 @@
|
|||||||
|
import type { ElementTransformer, Transformer } from '@lexical/markdown';
|
||||||
|
import {
|
||||||
|
CHECK_LIST,
|
||||||
|
ELEMENT_TRANSFORMERS,
|
||||||
|
MULTILINE_ELEMENT_TRANSFORMERS,
|
||||||
|
TEXT_FORMAT_TRANSFORMERS,
|
||||||
|
TEXT_MATCH_TRANSFORMERS,
|
||||||
|
} from '@lexical/markdown';
|
||||||
|
import {
|
||||||
|
$createHorizontalRuleNode,
|
||||||
|
$isHorizontalRuleNode,
|
||||||
|
} from '@lexical/react/LexicalHorizontalRuleNode';
|
||||||
|
import {
|
||||||
|
$createTableCellNode,
|
||||||
|
$createTableNode,
|
||||||
|
$createTableRowNode,
|
||||||
|
$isTableCellNode,
|
||||||
|
$isTableNode,
|
||||||
|
$isTableRowNode,
|
||||||
|
TableCellHeaderStates,
|
||||||
|
type TableCellNode,
|
||||||
|
type TableNode,
|
||||||
|
} from '@lexical/table';
|
||||||
|
import {
|
||||||
|
$createTextNode,
|
||||||
|
$isParagraphNode,
|
||||||
|
$isTextNode,
|
||||||
|
type LexicalNode,
|
||||||
|
} from 'lexical';
|
||||||
|
|
||||||
|
// Horizontal Rule transformer (---, ***, ___)
|
||||||
|
export const HR: ElementTransformer = {
|
||||||
|
dependencies: [],
|
||||||
|
export: (node: LexicalNode) => {
|
||||||
|
return $isHorizontalRuleNode(node) ? '***' : null;
|
||||||
|
},
|
||||||
|
regExp: /^(---|\*\*\*|___)\s?$/,
|
||||||
|
replace: (parentNode, _1, _2, isImport) => {
|
||||||
|
const line = $createHorizontalRuleNode();
|
||||||
|
|
||||||
|
if (isImport || parentNode.getNextSibling() != null) {
|
||||||
|
parentNode.replace(line);
|
||||||
|
} else {
|
||||||
|
parentNode.insertBefore(line);
|
||||||
|
}
|
||||||
|
|
||||||
|
line.selectNext();
|
||||||
|
},
|
||||||
|
type: 'element',
|
||||||
|
};
|
||||||
|
|
||||||
|
// Table transformer
|
||||||
|
const TABLE_ROW_REG_EXP = /^(?:\|)(.+)(?:\|)\s?$/;
|
||||||
|
const TABLE_ROW_DIVIDER_REG_EXP = /^(\| ?:?-*:? ?)+\|\s?$/;
|
||||||
|
|
||||||
|
const $createTableCell = (textContent: string): TableCellNode => {
|
||||||
|
const cell = $createTableCellNode(TableCellHeaderStates.NO_STATUS);
|
||||||
|
const text = $createTextNode(textContent.trim());
|
||||||
|
cell.append(text);
|
||||||
|
return cell;
|
||||||
|
};
|
||||||
|
|
||||||
|
const mapToTableCells = (textContent: string): Array<TableCellNode> | null => {
|
||||||
|
const match = textContent.match(TABLE_ROW_REG_EXP);
|
||||||
|
if (!match || !match[1]) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return match[1].split('|').map((text) => $createTableCell(text));
|
||||||
|
};
|
||||||
|
|
||||||
|
function getTableColumnsSize(table: TableNode) {
|
||||||
|
const row = table.getFirstChild();
|
||||||
|
return $isTableRowNode(row) ? row.getChildrenSize() : 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const TABLE: ElementTransformer = {
|
||||||
|
dependencies: [],
|
||||||
|
export: (node: LexicalNode) => {
|
||||||
|
if (!$isTableNode(node)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const output: string[] = [];
|
||||||
|
|
||||||
|
for (const row of node.getChildren()) {
|
||||||
|
const rowOutput = [];
|
||||||
|
if (!$isTableRowNode(row)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
let isHeaderRow = false;
|
||||||
|
for (const cell of row.getChildren()) {
|
||||||
|
if ($isTableCellNode(cell)) {
|
||||||
|
const textContent = cell.getTextContent().replace(/\n/g, '\\n').trim();
|
||||||
|
rowOutput.push(textContent);
|
||||||
|
if (cell.__headerState === TableCellHeaderStates.ROW) {
|
||||||
|
isHeaderRow = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
output.push(`| ${rowOutput.join(' | ')} |`);
|
||||||
|
if (isHeaderRow) {
|
||||||
|
output.push(`| ${rowOutput.map((_) => '---').join(' | ')} |`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return output.join('\n');
|
||||||
|
},
|
||||||
|
regExp: TABLE_ROW_REG_EXP,
|
||||||
|
replace: (parentNode, _1, match) => {
|
||||||
|
// Header row divider
|
||||||
|
if (TABLE_ROW_DIVIDER_REG_EXP.test(match[0])) {
|
||||||
|
const table = parentNode.getPreviousSibling();
|
||||||
|
if (!table || !$isTableNode(table)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const rows = table.getChildren();
|
||||||
|
const lastRow = rows[rows.length - 1];
|
||||||
|
if (!lastRow || !$isTableRowNode(lastRow)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add header state to row cells
|
||||||
|
lastRow.getChildren().forEach((cell) => {
|
||||||
|
if (!$isTableCellNode(cell)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
cell.setHeaderStyles(
|
||||||
|
TableCellHeaderStates.ROW,
|
||||||
|
TableCellHeaderStates.ROW,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Remove divider line
|
||||||
|
parentNode.remove();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const matchCells = mapToTableCells(match[0]);
|
||||||
|
|
||||||
|
if (matchCells == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const rows = [matchCells];
|
||||||
|
let sibling = parentNode.getPreviousSibling();
|
||||||
|
let maxCells = matchCells.length;
|
||||||
|
|
||||||
|
while (sibling) {
|
||||||
|
if (!$isParagraphNode(sibling)) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (sibling.getChildrenSize() !== 1) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
const firstChild = sibling.getFirstChild();
|
||||||
|
|
||||||
|
if (!$isTextNode(firstChild)) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
const cells = mapToTableCells(firstChild.getTextContent());
|
||||||
|
|
||||||
|
if (cells == null) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
maxCells = Math.max(maxCells, cells.length);
|
||||||
|
rows.unshift(cells);
|
||||||
|
const previousSibling = sibling.getPreviousSibling();
|
||||||
|
sibling.remove();
|
||||||
|
sibling = previousSibling;
|
||||||
|
}
|
||||||
|
|
||||||
|
const table = $createTableNode();
|
||||||
|
|
||||||
|
for (const cells of rows) {
|
||||||
|
const tableRow = $createTableRowNode();
|
||||||
|
table.append(tableRow);
|
||||||
|
|
||||||
|
for (let i = 0; i < maxCells; i++) {
|
||||||
|
tableRow.append(i < cells.length ? cells[i] : $createTableCell(''));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const previousSibling = parentNode.getPreviousSibling();
|
||||||
|
if (
|
||||||
|
$isTableNode(previousSibling) &&
|
||||||
|
getTableColumnsSize(previousSibling) === maxCells
|
||||||
|
) {
|
||||||
|
previousSibling.append(...table.getChildren());
|
||||||
|
parentNode.remove();
|
||||||
|
} else {
|
||||||
|
parentNode.replace(table);
|
||||||
|
}
|
||||||
|
|
||||||
|
table.selectEnd();
|
||||||
|
},
|
||||||
|
type: 'element',
|
||||||
|
};
|
||||||
|
|
||||||
|
// Export all transformers for full markdown support
|
||||||
|
// Includes support for:
|
||||||
|
// - Headings (# ## ###)
|
||||||
|
// - Bold (**text** or __text__)
|
||||||
|
// - Italic (*text* or _text_)
|
||||||
|
// - Strikethrough (~~text~~)
|
||||||
|
// - Code (`code`)
|
||||||
|
// - Links ([text](url))
|
||||||
|
// - Lists (ordered and unordered)
|
||||||
|
// - Checkboxes (- [ ] or - [x])
|
||||||
|
// - Blockquotes (>)
|
||||||
|
// - Code blocks (```)
|
||||||
|
// - Horizontal rules (---, ***, ___)
|
||||||
|
// - Tables (| col1 | col2 |)
|
||||||
|
|
||||||
|
export const EDITOR_TRANSFORMERS: Array<Transformer> = [
|
||||||
|
TABLE,
|
||||||
|
HR,
|
||||||
|
CHECK_LIST,
|
||||||
|
...ELEMENT_TRANSFORMERS,
|
||||||
|
...MULTILINE_ELEMENT_TRANSFORMERS,
|
||||||
|
...TEXT_FORMAT_TRANSFORMERS,
|
||||||
|
...TEXT_MATCH_TRANSFORMERS,
|
||||||
|
];
|
||||||
@@ -146,3 +146,103 @@
|
|||||||
.editor-nested-listitem {
|
.editor-nested-listitem {
|
||||||
list-style-type: none;
|
list-style-type: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Horizontal Rule */
|
||||||
|
.editor-hr {
|
||||||
|
border: none;
|
||||||
|
border-top: 2px solid #ccc;
|
||||||
|
margin: 15px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Link */
|
||||||
|
.editor-link {
|
||||||
|
color: #0066cc;
|
||||||
|
text-decoration: none;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.editor-link:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Table */
|
||||||
|
.editor-table {
|
||||||
|
border-collapse: collapse;
|
||||||
|
border-spacing: 0;
|
||||||
|
overflow-y: scroll;
|
||||||
|
overflow-x: scroll;
|
||||||
|
table-layout: fixed;
|
||||||
|
width: max-content;
|
||||||
|
margin: 15px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.editor-table-cell {
|
||||||
|
border: 1px solid #ccc;
|
||||||
|
min-width: 75px;
|
||||||
|
vertical-align: top;
|
||||||
|
text-align: start;
|
||||||
|
padding: 6px 8px;
|
||||||
|
position: relative;
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.editor-table-cell-header {
|
||||||
|
background-color: #f4f4f4;
|
||||||
|
font-weight: bold;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Checklist */
|
||||||
|
.editor-listitem-checked,
|
||||||
|
.editor-listitem-unchecked {
|
||||||
|
position: relative;
|
||||||
|
margin-left: 8px;
|
||||||
|
margin-right: 8px;
|
||||||
|
padding-left: 24px;
|
||||||
|
padding-right: 24px;
|
||||||
|
list-style-type: none;
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.editor-listitem-checked {
|
||||||
|
text-decoration: line-through;
|
||||||
|
}
|
||||||
|
|
||||||
|
.editor-listitem-unchecked:before,
|
||||||
|
.editor-listitem-checked:before {
|
||||||
|
content: '';
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
top: 2px;
|
||||||
|
left: 0;
|
||||||
|
cursor: pointer;
|
||||||
|
display: block;
|
||||||
|
background-size: cover;
|
||||||
|
position: absolute;
|
||||||
|
}
|
||||||
|
|
||||||
|
.editor-listitem-unchecked:before {
|
||||||
|
border: 1px solid #999;
|
||||||
|
border-radius: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.editor-listitem-checked:before {
|
||||||
|
border: 1px solid #0066cc;
|
||||||
|
border-radius: 2px;
|
||||||
|
background-color: #0066cc;
|
||||||
|
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' viewBox='0 0 16 16'%3E%3Cpath fill='white' d='M13.5 2l-7.5 7.5-3.5-3.5-1.5 1.5 5 5 9-9z'/%3E%3C/svg%3E");
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
background-position: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.editor-listitem-checked[dir='rtl']:before,
|
||||||
|
.editor-listitem-unchecked[dir='rtl']:before {
|
||||||
|
left: auto;
|
||||||
|
right: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.editor-listitem-checked[dir='rtl'],
|
||||||
|
.editor-listitem-unchecked[dir='rtl'] {
|
||||||
|
padding-left: 24px;
|
||||||
|
padding-right: 24px;
|
||||||
|
}
|
||||||
|
|||||||
@@ -18,6 +18,8 @@ const theme: EditorThemeClasses = {
|
|||||||
ol: 'editor-list-ol',
|
ol: 'editor-list-ol',
|
||||||
ul: 'editor-list-ul',
|
ul: 'editor-list-ul',
|
||||||
listitem: 'editor-listitem',
|
listitem: 'editor-listitem',
|
||||||
|
listitemChecked: 'editor-listitem-checked',
|
||||||
|
listitemUnchecked: 'editor-listitem-unchecked',
|
||||||
},
|
},
|
||||||
text: {
|
text: {
|
||||||
bold: 'editor-text-bold',
|
bold: 'editor-text-bold',
|
||||||
@@ -28,6 +30,11 @@ const theme: EditorThemeClasses = {
|
|||||||
},
|
},
|
||||||
code: 'editor-code',
|
code: 'editor-code',
|
||||||
codeHighlight: {},
|
codeHighlight: {},
|
||||||
|
link: 'editor-link',
|
||||||
|
table: 'editor-table',
|
||||||
|
tableCell: 'editor-table-cell',
|
||||||
|
tableCellHeader: 'editor-table-cell-header',
|
||||||
|
hr: 'editor-hr',
|
||||||
};
|
};
|
||||||
|
|
||||||
export default theme;
|
export default theme;
|
||||||
|
|||||||
Reference in New Issue
Block a user