mirror of
https://github.com/AppFlowy-IO/AppFlowy.git
synced 2026-01-06 04:11:53 +00:00
feat: support toggle heading (#6712)
* feat: support toggle heading * fix: support others markdown
This commit is contained in:
parent
82effbf8e4
commit
f6e002edbd
@ -149,7 +149,7 @@ export const CustomEditor = {
|
||||
const blockType = block.get(YjsEditorKey.block_type) as BlockType;
|
||||
|
||||
if (blockType !== BlockType.Paragraph) {
|
||||
handleNonParagraphBlockBackspaceAndEnterWithTxn(sharedRoot, block);
|
||||
handleNonParagraphBlockBackspaceAndEnterWithTxn(editor, sharedRoot, block, point);
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@ -72,7 +72,7 @@ function insertText (ydoc: Y.Doc, editor: Editor, { path, offset, text, attribut
|
||||
|
||||
console.log('beforeAttributes', relativeOffset, beforeAttributes);
|
||||
|
||||
if (beforeAttributes && ('formula' in beforeAttributes || 'mention' in beforeAttributes)) {
|
||||
if (beforeAttributes && ('formula' in beforeAttributes || 'mention' in beforeAttributes || 'href' in beforeAttributes)) {
|
||||
const newAttributes = {
|
||||
...attributes,
|
||||
};
|
||||
@ -89,6 +89,12 @@ function insertText (ydoc: Y.Doc, editor: Editor, { path, offset, text, attribut
|
||||
});
|
||||
}
|
||||
|
||||
if ('href' in beforeAttributes) {
|
||||
Object.assign({
|
||||
href: null,
|
||||
});
|
||||
}
|
||||
|
||||
yText.insert(relativeOffset, text, newAttributes);
|
||||
} else {
|
||||
yText.insert(relativeOffset, text, attributes);
|
||||
|
||||
@ -16,9 +16,9 @@ import {
|
||||
import { nanoid } from 'nanoid';
|
||||
import Delta, { Op } from 'quill-delta';
|
||||
import {
|
||||
BasePoint,
|
||||
BaseRange,
|
||||
Descendant,
|
||||
Text,
|
||||
Editor,
|
||||
Element,
|
||||
Node,
|
||||
@ -26,8 +26,8 @@ import {
|
||||
Path,
|
||||
Point,
|
||||
Range,
|
||||
Text,
|
||||
Transforms,
|
||||
BasePoint,
|
||||
} from 'slate';
|
||||
import { ReactEditor } from 'slate-react';
|
||||
import * as Y from 'yjs';
|
||||
@ -180,13 +180,13 @@ export function handleCollapsedBreakWithTxn (editor: YjsEditor, sharedRoot: YSha
|
||||
const yText = getText(block.get(YjsEditorKey.block_external_id), sharedRoot);
|
||||
|
||||
if (yText.length === 0) {
|
||||
const point = Editor.start(editor, at);
|
||||
|
||||
if (blockType !== BlockType.Paragraph) {
|
||||
handleNonParagraphBlockBackspaceAndEnterWithTxn(sharedRoot, block);
|
||||
handleNonParagraphBlockBackspaceAndEnterWithTxn(editor, sharedRoot, block, point);
|
||||
return;
|
||||
}
|
||||
|
||||
const point = Editor.start(editor, at);
|
||||
|
||||
if (path.length > 1 && handleLiftBlockOnBackspaceAndEnterWithTxn(editor, sharedRoot, block, point)) {
|
||||
return;
|
||||
}
|
||||
@ -293,6 +293,46 @@ export function turnToBlock<T extends BlockData> (sharedRoot: YSharedRoot, sourc
|
||||
|
||||
// delete source block
|
||||
deleteBlock(sharedRoot, sourceBlock.get(YjsEditorKey.block_id));
|
||||
|
||||
// turn to toggle heading
|
||||
if (type === BlockType.ToggleListBlock && (data as unknown as ToggleListBlockData).level) {
|
||||
const nextSiblings = getNextSiblings(sharedRoot, newBlock);
|
||||
|
||||
if (!nextSiblings || nextSiblings.length === 0) return;
|
||||
// find the next sibling with the same or higher level
|
||||
const index = nextSiblings.findIndex((id) => {
|
||||
const block = getBlock(id, sharedRoot);
|
||||
const blockData = dataStringTOJson(block.get(YjsEditorKey.block_data));
|
||||
|
||||
if ('level' in blockData && (blockData as {
|
||||
level: number
|
||||
}).level <= ((data as unknown as ToggleListBlockData).level as number)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
});
|
||||
|
||||
const nodes = index > -1 ? nextSiblings.slice(0, index) : nextSiblings;
|
||||
|
||||
// if not found, return. Otherwise, indent the block
|
||||
nodes.forEach((id) => {
|
||||
const block = getBlock(id, sharedRoot);
|
||||
|
||||
indentBlock(sharedRoot, block);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function getNextSiblings (sharedRoot: YSharedRoot, block: YBlock) {
|
||||
const parent = getBlock(block.get(YjsEditorKey.block_parent), sharedRoot);
|
||||
|
||||
if (!parent) return;
|
||||
|
||||
const parentChildren = getChildrenArray(parent.get(YjsEditorKey.block_children), sharedRoot);
|
||||
const index = parentChildren.toArray().findIndex((id) => id === block.get(YjsEditorKey.block_id));
|
||||
|
||||
return parentChildren.toArray().slice(index + 1);
|
||||
}
|
||||
|
||||
function getSplitBlockOperations (sharedRoot: YSharedRoot, block: YBlock, offset: number): {
|
||||
@ -822,10 +862,26 @@ export function getBlockEntry (editor: YjsEditor, point?: Point) {
|
||||
return blockEntry as NodeEntry<Element>;
|
||||
}
|
||||
|
||||
export function handleNonParagraphBlockBackspaceAndEnterWithTxn (sharedRoot: YSharedRoot, block: YBlock) {
|
||||
export function handleNonParagraphBlockBackspaceAndEnterWithTxn (editor: YjsEditor, sharedRoot: YSharedRoot, block: YBlock, point: BasePoint) {
|
||||
const data = dataStringTOJson(block.get(YjsEditorKey.block_data));
|
||||
const blockType = block.get(YjsEditorKey.block_type);
|
||||
|
||||
if (blockType === BlockType.ToggleListBlock && (data as ToggleListBlockData).level) {
|
||||
const [, path] = getBlockEntry(editor, point);
|
||||
|
||||
Transforms.setNodes(editor, {
|
||||
data: {
|
||||
...data,
|
||||
level: null,
|
||||
},
|
||||
}, { at: path });
|
||||
return;
|
||||
}
|
||||
|
||||
const operations: (() => void)[] = [];
|
||||
|
||||
operations.push(() => {
|
||||
|
||||
turnToBlock(sharedRoot, block, BlockType.Paragraph, {});
|
||||
});
|
||||
executeOperations(sharedRoot, operations, 'turnToBlock');
|
||||
|
||||
@ -41,7 +41,7 @@ describe('Markdown editing', () => {
|
||||
cy.get('@editor').type('##');
|
||||
cy.get('@editor').realPress('Space');
|
||||
cy.wait(50);
|
||||
|
||||
|
||||
cy.get('@editor').type('Heading 2');
|
||||
expectedJson = [...expectedJson, {
|
||||
type: 'heading',
|
||||
@ -436,11 +436,152 @@ describe('Markdown editing', () => {
|
||||
},
|
||||
];
|
||||
assertJSON(expectedJson);
|
||||
|
||||
// Test 7: Toggle heading
|
||||
cy.get('@editor').realPress('Enter');
|
||||
cy.get('@editor').type('>');
|
||||
cy.get('@editor').realPress('Space');
|
||||
cy.get('@editor').type('toggle heading');
|
||||
cy.get('@editor').realPress('Enter');
|
||||
cy.get('@editor').type('toggle heading child');
|
||||
cy.get('@editor').realPress('Enter');
|
||||
cy.get('@editor').realPress(['Shift', 'Tab']);
|
||||
cy.get('@editor').type('toggle heading sibling');
|
||||
cy.get('@editor').realPress('Enter');
|
||||
cy.get('@editor').type('###');
|
||||
cy.get('@editor').realPress('Space');
|
||||
cy.get('@editor').type('heading 3');
|
||||
cy.get('@editor').selectMultipleText(['toggle heading']);
|
||||
cy.wait(500);
|
||||
cy.get('@editor').realPress(['ArrowLeft']);
|
||||
cy.get('@editor').type('#');
|
||||
cy.get('@editor').realPress('Space');
|
||||
const extraData: FromBlockJSON[] = [{
|
||||
type: 'toggle_list',
|
||||
data: {
|
||||
level: 1,
|
||||
collapsed: false,
|
||||
},
|
||||
text: [{
|
||||
insert: 'toggle heading',
|
||||
}],
|
||||
children: [{
|
||||
type: 'paragraph',
|
||||
data: {},
|
||||
text: [{
|
||||
insert: 'toggle heading child',
|
||||
}],
|
||||
children: [],
|
||||
}],
|
||||
},
|
||||
{
|
||||
type: 'paragraph',
|
||||
data: {},
|
||||
text: [{
|
||||
insert: 'toggle heading sibling',
|
||||
}],
|
||||
children: [],
|
||||
},
|
||||
{
|
||||
type: 'heading',
|
||||
data: {
|
||||
level: 3,
|
||||
},
|
||||
text: [{
|
||||
insert: 'heading 3',
|
||||
}],
|
||||
children: [],
|
||||
|
||||
}];
|
||||
|
||||
assertJSON([
|
||||
...expectedJson,
|
||||
...extraData,
|
||||
]);
|
||||
cy.get('@editor').realPress('Backspace');
|
||||
assertJSON([
|
||||
...expectedJson,
|
||||
{
|
||||
...extraData[0],
|
||||
data: {
|
||||
collapsed: false,
|
||||
level: null,
|
||||
},
|
||||
},
|
||||
extraData[1],
|
||||
extraData[2],
|
||||
] as FromBlockJSON[]);
|
||||
cy.get('@editor').realPress('Backspace');
|
||||
assertJSON([
|
||||
...expectedJson,
|
||||
{
|
||||
...extraData[0],
|
||||
type: 'paragraph',
|
||||
data: {},
|
||||
},
|
||||
extraData[1],
|
||||
extraData[2],
|
||||
] as FromBlockJSON[]);
|
||||
cy.get('@editor').type('#');
|
||||
cy.get('@editor').realPress('Space');
|
||||
cy.get('@editor').type('>');
|
||||
cy.get('@editor').realPress('Space');
|
||||
expectedJson = [
|
||||
...expectedJson,
|
||||
{
|
||||
...extraData[0],
|
||||
children: [
|
||||
extraData[0].children[0],
|
||||
extraData[1],
|
||||
extraData[2],
|
||||
],
|
||||
},
|
||||
] as FromBlockJSON[];
|
||||
|
||||
assertJSON(expectedJson);
|
||||
|
||||
cy.selectMultipleText(['heading 3']);
|
||||
cy.wait(500);
|
||||
cy.get('@editor').realPress('ArrowRight');
|
||||
cy.get('@editor').realPress('Enter');
|
||||
cy.get('@editor').realPress(['Shift', 'Tab']);
|
||||
|
||||
// Test 8: Link
|
||||
cy.get('@editor').type('Link: [Click here](https://example.com');
|
||||
cy.get('@editor').realPress(')');
|
||||
assertJSON([
|
||||
...expectedJson,
|
||||
{
|
||||
type: 'paragraph',
|
||||
data: {},
|
||||
text: [{ insert: 'Link: ' }, {
|
||||
insert: 'Click here',
|
||||
attributes: { href: 'https://example.com' },
|
||||
}],
|
||||
children: [],
|
||||
},
|
||||
]);
|
||||
cy.get('@editor').type('link anchor');
|
||||
expectedJson = [
|
||||
...expectedJson,
|
||||
{
|
||||
type: 'paragraph',
|
||||
data: {},
|
||||
text: [{ insert: 'Link: ' }, {
|
||||
insert: 'Click here',
|
||||
attributes: { href: 'https://example.com' },
|
||||
}, { insert: 'link anchor' }],
|
||||
children: [],
|
||||
},
|
||||
];
|
||||
assertJSON(expectedJson);
|
||||
cy.get('@editor').realPress('Enter');
|
||||
//
|
||||
// Last test: Divider
|
||||
cy.get('@editor').type('--');
|
||||
cy.get('@editor').realPress('-');
|
||||
expectedJson = [
|
||||
...expectedJson.slice(0, -1),
|
||||
...expectedJson,
|
||||
{
|
||||
type: 'divider',
|
||||
data: {},
|
||||
|
||||
@ -35,7 +35,7 @@ function ToggleIcon ({ block, className }: { block: ToggleListNode; className: s
|
||||
onMouseDown={(e) => {
|
||||
e.preventDefault();
|
||||
}}
|
||||
className={`${className} ${readOnly ? '' : 'cursor-pointer hover:text-fill-default'} pr-1 text-xl`}
|
||||
className={`${className} ${readOnly ? '' : 'cursor-pointer hover:text-fill-default'} pr-1 text-xl h-full`}
|
||||
>
|
||||
{collapsed ? <ExpandSvg className={'-rotate-90 transform'} /> : <ExpandSvg />}
|
||||
</span>
|
||||
|
||||
@ -1,17 +1,35 @@
|
||||
import { getHeadingCssProperty } from '@/components/editor/components/blocks/heading';
|
||||
import React, { forwardRef, memo, useMemo } from 'react';
|
||||
import { EditorElementProps, ToggleListNode } from '@/components/editor/editor.type';
|
||||
|
||||
export const ToggleList = memo(
|
||||
forwardRef<HTMLDivElement, EditorElementProps<ToggleListNode>>(({ node, children, ...attributes }, ref) => {
|
||||
const { collapsed, level } = useMemo(() => node.data || {}, [node.data]);
|
||||
const fontSizeCssProperty = getHeadingCssProperty(level || 0);
|
||||
const className = `${attributes.className ?? ''} flex w-full flex-col ${collapsed ? 'collapsed' : ''} ${fontSizeCssProperty} level-${level}`;
|
||||
const { collapsed, level = 0 } = useMemo(() => node.data || {}, [node.data]);
|
||||
const className = useMemo(() => {
|
||||
|
||||
const classList = ['flex w-full flex-col'];
|
||||
|
||||
if (attributes.className) {
|
||||
classList.push(attributes.className);
|
||||
}
|
||||
|
||||
if (collapsed) {
|
||||
classList.push('collapsed');
|
||||
}
|
||||
|
||||
if (level) {
|
||||
classList.push(`toggle-heading level-${level}`);
|
||||
}
|
||||
|
||||
return classList.join(' ');
|
||||
|
||||
}, [collapsed, level, attributes.className]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div {...attributes} ref={ref}
|
||||
className={className}
|
||||
<div
|
||||
{...attributes}
|
||||
ref={ref}
|
||||
className={className}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
|
||||
@ -344,4 +344,42 @@ span[data-slate-placeholder="true"]:not(.inline-block-content) {
|
||||
50% {
|
||||
background-color: var(--content-blue-100);
|
||||
}
|
||||
}
|
||||
|
||||
.toggle-heading {
|
||||
&.level-1 {
|
||||
> .text-element {
|
||||
@apply text-[1.75rem] max-md:text-[24px] pt-[10px] max-md:pt-[1.5vw] pb-[4px] max-md:pb-[1vw] font-bold;
|
||||
}
|
||||
}
|
||||
|
||||
&.level-2 {
|
||||
> .text-element {
|
||||
@apply text-[1.55rem] max-md:text-[22px] pt-[8px] max-md:pt-[1vw] pb-[2px] max-md:pb-[0.5vw] font-bold;
|
||||
}
|
||||
}
|
||||
|
||||
&.level-3 {
|
||||
> .text-element {
|
||||
@apply text-[1.35rem] max-md:text-[20px] pt-[4px] font-bold;
|
||||
}
|
||||
}
|
||||
|
||||
&.level-4 {
|
||||
> .text-element {
|
||||
@apply text-[1.25rem] max-md:text-[16px] pt-[4px] font-bold;
|
||||
}
|
||||
}
|
||||
|
||||
&.level-5 {
|
||||
> .text-element {
|
||||
@apply text-[1.15rem] pt-[4px] font-bold;
|
||||
}
|
||||
}
|
||||
|
||||
&.level-6 {
|
||||
> .text-element {
|
||||
@apply text-[1.05rem] pt-[4px] font-bold;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -22,7 +22,7 @@ export const withInsertText = (editor: ReactEditor) => {
|
||||
const [textNode] = textEntry as NodeEntry<Text>;
|
||||
|
||||
// If the text node is a formula or mention, split the node and insert the text
|
||||
if (textNode.formula || textNode.mention) {
|
||||
if (textNode.formula || textNode.mention || textNode.href) {
|
||||
console.log('Inserting text into formula or mention', newAt);
|
||||
Transforms.insertNodes(editor, { text }, { at: point, select: true, voids: false });
|
||||
|
||||
|
||||
@ -3,6 +3,7 @@ import { CustomEditor } from '@/application/slate-yjs/command';
|
||||
import { EditorMarkFormat } from '@/application/slate-yjs/types';
|
||||
import { getBlock, getBlockEntry, getSharedRoot, getText } from '@/application/slate-yjs/utils/yjsOperations';
|
||||
import {
|
||||
BlockData,
|
||||
BlockType,
|
||||
HeadingBlockData,
|
||||
NumberedListBlockData,
|
||||
@ -12,8 +13,13 @@ import {
|
||||
} from '@/application/types';
|
||||
import { Editor, Range, Transforms } from 'slate';
|
||||
|
||||
enum SpecialSymbol {
|
||||
EM_DASH = '—',
|
||||
RIGHTWARDS_DOUBLE_ARROW = '⇒',
|
||||
}
|
||||
|
||||
type TriggerHotKey = {
|
||||
[key in BlockType | EditorMarkFormat]?: string[];
|
||||
[key in BlockType | EditorMarkFormat | SpecialSymbol]?: string[];
|
||||
};
|
||||
|
||||
const defaultTriggerChar: TriggerHotKey = {
|
||||
@ -30,6 +36,9 @@ const defaultTriggerChar: TriggerHotKey = {
|
||||
[EditorMarkFormat.StrikeThrough]: ['~'],
|
||||
[EditorMarkFormat.Code]: ['`'],
|
||||
[EditorMarkFormat.Formula]: ['$'],
|
||||
[EditorMarkFormat.Href]: [')'],
|
||||
[SpecialSymbol.EM_DASH]: ['-'],
|
||||
[SpecialSymbol.RIGHTWARDS_DOUBLE_ARROW]: ['>'],
|
||||
};
|
||||
|
||||
// create a set of all trigger characters
|
||||
@ -37,7 +46,7 @@ export const allTriggerChars = new Set(Object.values(defaultTriggerChar).flat())
|
||||
|
||||
// Define the rules for markdown shortcuts
|
||||
type Rule = {
|
||||
type: 'block' | 'mark'
|
||||
type: 'block' | 'mark' | 'symbol';
|
||||
match: RegExp
|
||||
format: string
|
||||
transform?: (editor: YjsEditor, match: RegExpMatchArray) => void
|
||||
@ -66,16 +75,16 @@ function getNodeType (editor: YjsEditor) {
|
||||
function getBlockData (editor: YjsEditor) {
|
||||
const [node] = getBlockEntry(editor);
|
||||
|
||||
return node.data;
|
||||
return node.data as BlockData;
|
||||
}
|
||||
|
||||
function isEmptyLine (editor: YjsEditor, offset: number) {
|
||||
function getLineText (editor: YjsEditor) {
|
||||
const [node] = getBlockEntry(editor);
|
||||
const sharedRoot = getSharedRoot(editor);
|
||||
const block = getBlock(node.blockId as string, sharedRoot);
|
||||
const yText = getText(block.get(YjsEditorKey.block_external_id), sharedRoot);
|
||||
|
||||
return yText.toJSON().length === offset;
|
||||
return yText.toJSON();
|
||||
}
|
||||
|
||||
const rules: Rule[] = [
|
||||
@ -94,11 +103,42 @@ const rules: Rule[] = [
|
||||
transform: (editor, match) => {
|
||||
const level = match[1].length;
|
||||
const [node] = getBlockEntry(editor);
|
||||
const blockType = getNodeType(editor);
|
||||
|
||||
// If the current block is a toggle list block, we don't need to change the block type
|
||||
if (blockType === BlockType.ToggleListBlock) {
|
||||
CustomEditor.setBlockData(editor, node.blockId as string, { level });
|
||||
deletePrefix(editor, level);
|
||||
return;
|
||||
}
|
||||
|
||||
CustomEditor.turnToBlock<HeadingBlockData>(editor, node.blockId as string, BlockType.HeadingBlock, { level });
|
||||
deletePrefix(editor, level);
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'block',
|
||||
match: /^>\s/,
|
||||
format: BlockType.ToggleListBlock,
|
||||
filter: (editor) => {
|
||||
return getNodeType(editor) === BlockType.ToggleListBlock;
|
||||
},
|
||||
transform: (editor) => {
|
||||
const type = getNodeType(editor);
|
||||
let level: number | undefined;
|
||||
|
||||
// If the current block is a heading block, we need to get the level of the heading block
|
||||
if (type === BlockType.HeadingBlock) {
|
||||
level = (getBlockData(editor) as HeadingBlockData).level;
|
||||
}
|
||||
|
||||
CustomEditor.turnToBlock<ToggleListBlockData>(editor, getBlockEntry(editor)[0].blockId as string, BlockType.ToggleListBlock, {
|
||||
collapsed: false,
|
||||
level,
|
||||
});
|
||||
deletePrefix(editor, 1);
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'block',
|
||||
match: /^"\s/,
|
||||
@ -130,25 +170,15 @@ const rules: Rule[] = [
|
||||
deletePrefix(editor, match[0].length - 1);
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'block',
|
||||
match: /^>\s/,
|
||||
format: BlockType.ToggleListBlock,
|
||||
filter: (editor) => {
|
||||
return getNodeType(editor) === BlockType.ToggleListBlock;
|
||||
},
|
||||
transform: (editor) => {
|
||||
CustomEditor.turnToBlock<ToggleListBlockData>(editor, getBlockEntry(editor)[0].blockId as string, BlockType.ToggleListBlock, { collapsed: false });
|
||||
deletePrefix(editor, 1);
|
||||
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'block',
|
||||
match: /^(`){3,}$/,
|
||||
format: BlockType.CodeBlock,
|
||||
filter: (editor) => {
|
||||
return !isEmptyLine(editor, 2) || getNodeType(editor) === BlockType.CodeBlock;
|
||||
const text = getLineText(editor);
|
||||
|
||||
return text !== '``' || getNodeType(editor) === BlockType.CodeBlock;
|
||||
},
|
||||
transform: (editor) => {
|
||||
|
||||
@ -178,7 +208,7 @@ const rules: Rule[] = [
|
||||
const blockType = getNodeType(editor);
|
||||
const blockData = getBlockData(editor);
|
||||
|
||||
return blockType === BlockType.HeadingBlock || (blockType === BlockType.NumberedListBlock && (blockData as NumberedListBlockData).number === start);
|
||||
return ('level' in blockData && (blockData as HeadingBlockData).level > 0) || (blockType === BlockType.NumberedListBlock && (blockData as NumberedListBlockData).number === start);
|
||||
},
|
||||
transform: (editor, match) => {
|
||||
const start = parseInt(match[1]);
|
||||
@ -190,15 +220,17 @@ const rules: Rule[] = [
|
||||
|
||||
{
|
||||
type: 'block',
|
||||
match: /^([-*_]){3,}$/,
|
||||
match: /^([-*_]){3,}|(—-+)$/,
|
||||
format: BlockType.DividerBlock,
|
||||
filter: (editor) => {
|
||||
return !isEmptyLine(editor, 2) || getNodeType(editor) === BlockType.DividerBlock;
|
||||
const text = getLineText(editor);
|
||||
|
||||
return (['--', '**', '__', '—'].every(t => t !== text)) || getNodeType(editor) === BlockType.DividerBlock;
|
||||
},
|
||||
transform: (editor) => {
|
||||
transform: (editor, match) => {
|
||||
|
||||
CustomEditor.turnToBlock(editor, getBlockEntry(editor)[0].blockId as string, BlockType.DividerBlock, {});
|
||||
deletePrefix(editor, 2);
|
||||
deletePrefix(editor, match[0].length - 1);
|
||||
},
|
||||
},
|
||||
|
||||
@ -236,6 +268,34 @@ const rules: Rule[] = [
|
||||
return text.length === 0;
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'mark',
|
||||
match: /\[(.*?)\]\((.*?)\)/,
|
||||
format: EditorMarkFormat.Href,
|
||||
filter: (_editor, match) => {
|
||||
const href = match[2];
|
||||
|
||||
return href.length === 0;
|
||||
},
|
||||
transform: (editor, match) => {
|
||||
const href = match[2];
|
||||
const text = match[1];
|
||||
const { selection } = editor;
|
||||
|
||||
if (!selection) return;
|
||||
const path = selection.anchor.path;
|
||||
const start = match.index!;
|
||||
|
||||
editor.insertText(text);
|
||||
Transforms.select(editor, {
|
||||
anchor: { path, offset: start },
|
||||
focus: { path, offset: start + text.length },
|
||||
});
|
||||
|
||||
CustomEditor.addMark(editor, { key: EditorMarkFormat.Href, value: href });
|
||||
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'mark',
|
||||
match: /\$(.*?)\$/,
|
||||
@ -257,6 +317,32 @@ const rules: Rule[] = [
|
||||
CustomEditor.addMark(editor, { key: EditorMarkFormat.Formula, value: formula });
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'symbol',
|
||||
match: /--/,
|
||||
format: SpecialSymbol.EM_DASH,
|
||||
transform: (editor) => {
|
||||
|
||||
editor.delete({
|
||||
unit: 'character',
|
||||
reverse: true,
|
||||
});
|
||||
editor.insertText('—');
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'symbol',
|
||||
match: /=>/,
|
||||
format: SpecialSymbol.RIGHTWARDS_DOUBLE_ARROW,
|
||||
transform: (editor) => {
|
||||
editor.delete({
|
||||
unit: 'character',
|
||||
reverse: true,
|
||||
});
|
||||
editor.insertText('⇒');
|
||||
},
|
||||
},
|
||||
|
||||
];
|
||||
|
||||
export const applyMarkdown = (editor: YjsEditor, insertText: string): boolean => {
|
||||
@ -328,6 +414,26 @@ export const applyMarkdown = (editor: YjsEditor, insertText: string): boolean =>
|
||||
return true;
|
||||
}
|
||||
|
||||
} else if (rule.type === 'symbol') {
|
||||
const path = selection.anchor.path;
|
||||
const text = editor.string({
|
||||
anchor: {
|
||||
path,
|
||||
offset: 0,
|
||||
},
|
||||
focus: selection.focus,
|
||||
}) + insertText;
|
||||
const match = text.match(rule.match);
|
||||
|
||||
if (match) {
|
||||
if (rule.transform) {
|
||||
rule.transform(editor, match);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
console.log('symbol text', text, match);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user