fix(editor): support markdown transform when using IME (#12778)

Fix #12284 
Close
[BS-3517](https://linear.app/affine-design/issue/BS-3517/微软新注音输入法无法使用markdown语法)

This PR refactor the markdown transform during inputting, including:
- Transfrom markdown syntax input in `inlineEditor.slots.inputting`,
where we can detect the space character inputed by IME like Microsoft
Bopomofo, but `keydown` event can't.
- Remove `markdown-input.ts` which was used in `KeymapExtension` of
paragraph, and refactor with `InlineMarkdownExtension`
- Adjust existing `InlineMarkdownExtension` since the space is included
in text.
- Add two `InlineMarkdownExtension` for paragraph and list to impl
Heading1-6, number, bullet, to-do list conversion.

Other changes:
- Improve type hint for parameter of `store.addBlock`

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

## Summary by CodeRabbit

- **New Features**
- Added markdown shortcuts for creating code blocks and dividers in the
rich text editor.
- Introduced enhanced paragraph markdown support for headings and
blockquotes with inline markdown patterns.
- Integrated new list markdown extension supporting numbered, bulleted,
and todo lists with checked states.

- **Improvements**
- Updated markdown formatting patterns to require trailing spaces for
links, LaTeX, and inline styles, improving detection accuracy.
- Markdown transformations now respond to input events instead of
keydown for smoother editing experience.
- Added focus management after markdown transformations to maintain
seamless editing flow.

- **Bug Fixes**
- Removed unnecessary prevention of default behavior on space and
shift-space key presses in list and paragraph editors.

- **Refactor**
- Enhanced event handling and typing for editor input events, improving
reliability and maintainability.
- Refined internal prefix text extraction logic for markdown processing.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
L-Sun
2025-06-11 14:12:28 +08:00
committed by GitHub
parent c846c57a12
commit 24448659a4
33 changed files with 440 additions and 593 deletions

View File

@@ -0,0 +1,61 @@
import {
type CodeBlockModel,
CodeBlockSchema,
ParagraphBlockModel,
} from '@blocksuite/affine-model';
import { focusTextModel } from '@blocksuite/affine-rich-text';
import type { AffineTextAttributes } from '@blocksuite/affine-shared/types';
import { matchModels } from '@blocksuite/affine-shared/utils';
import type { BlockComponent } from '@blocksuite/std';
import { InlineMarkdownExtension } from '@blocksuite/std/inline';
export const CodeBlockMarkdownExtension =
InlineMarkdownExtension<AffineTextAttributes>({
name: 'code-block',
pattern: /^```([a-zA-Z0-9]*)\s$/,
action: ({ inlineEditor, inlineRange, prefixText, pattern }) => {
if (inlineEditor.yTextString.slice(0, inlineRange.index).includes('\n')) {
return;
}
const match = prefixText.match(pattern);
if (!match) return;
const language = match[1];
if (!inlineEditor.rootElement) return;
const blockComponent =
inlineEditor.rootElement.closest<BlockComponent>('[data-block-id]');
if (!blockComponent) return;
const { model, std, store } = blockComponent;
if (
matchModels(model, [ParagraphBlockModel]) &&
model.props.type === 'quote'
) {
return;
}
const parent = store.getParent(model);
if (!parent) return;
const index = parent.children.indexOf(model);
store.captureSync();
const codeId = store.addBlock<CodeBlockModel>(
CodeBlockSchema.model.flavour,
{ language },
parent,
index
);
if (model.text && model.text.length > prefixText.length) {
const text = model.text.clone();
store.addBlock('affine:paragraph', { text }, parent, index + 1);
text.delete(0, prefixText.length);
}
store.deleteBlock(model, { bringChildrenTo: parent });
focusTextModel(std, codeId);
},
});

View File

@@ -21,6 +21,7 @@ import { CodeKeymapExtension } from './code-keymap.js';
import { AFFINE_CODE_TOOLBAR_WIDGET } from './code-toolbar/index.js'; import { AFFINE_CODE_TOOLBAR_WIDGET } from './code-toolbar/index.js';
import { codeSlashMenuConfig } from './configs/slash-menu.js'; import { codeSlashMenuConfig } from './configs/slash-menu.js';
import { effects } from './effects.js'; import { effects } from './effects.js';
import { CodeBlockMarkdownExtension } from './markdown.js';
const codeToolbarWidget = WidgetViewExtension( const codeToolbarWidget = WidgetViewExtension(
'affine:code', 'affine:code',
@@ -44,6 +45,7 @@ export class CodeBlockViewExtension extends ViewExtensionProvider {
BlockViewExtension('affine:code', literal`affine-code`), BlockViewExtension('affine:code', literal`affine-code`),
SlashMenuConfigExtension('affine:code', codeSlashMenuConfig), SlashMenuConfigExtension('affine:code', codeSlashMenuConfig),
CodeKeymapExtension, CodeKeymapExtension,
CodeBlockMarkdownExtension,
...getCodeClipboardExtensions(), ...getCodeClipboardExtensions(),
]); ]);
context.register([ context.register([

View File

@@ -13,6 +13,7 @@
"@blocksuite/affine-components": "workspace:*", "@blocksuite/affine-components": "workspace:*",
"@blocksuite/affine-ext-loader": "workspace:*", "@blocksuite/affine-ext-loader": "workspace:*",
"@blocksuite/affine-model": "workspace:*", "@blocksuite/affine-model": "workspace:*",
"@blocksuite/affine-rich-text": "workspace:*",
"@blocksuite/affine-shared": "workspace:*", "@blocksuite/affine-shared": "workspace:*",
"@blocksuite/global": "workspace:*", "@blocksuite/global": "workspace:*",
"@blocksuite/std": "workspace:*", "@blocksuite/std": "workspace:*",

View File

@@ -0,0 +1,63 @@
import {
type DividerBlockModel,
DividerBlockSchema,
ParagraphBlockModel,
ParagraphBlockSchema,
} from '@blocksuite/affine-model';
import { focusTextModel } from '@blocksuite/affine-rich-text';
import type { AffineTextAttributes } from '@blocksuite/affine-shared/types';
import { matchModels } from '@blocksuite/affine-shared/utils';
import type { BlockComponent } from '@blocksuite/std';
import { InlineMarkdownExtension } from '@blocksuite/std/inline';
export const DividerMarkdownExtension =
InlineMarkdownExtension<AffineTextAttributes>({
name: 'divider',
pattern: /^(-{3,}|\*{3,}|_{3,})\s$/,
action: ({ inlineEditor, inlineRange }) => {
if (inlineEditor.yTextString.slice(0, inlineRange.index).includes('\n')) {
return;
}
if (!inlineEditor.rootElement) return;
const blockComponent =
inlineEditor.rootElement.closest<BlockComponent>('[data-block-id]');
if (!blockComponent) return;
const { model, std, store } = blockComponent;
if (
matchModels(model, [ParagraphBlockModel]) &&
model.props.type !== 'quote'
) {
const parent = store.getParent(model);
if (!parent) return;
const index = parent.children.indexOf(model);
store.captureSync();
inlineEditor.deleteText({
index: 0,
length: inlineRange.index,
});
store.addBlock<DividerBlockModel>(
DividerBlockSchema.model.flavour,
{
children: model.children,
},
parent,
index
);
const nextBlock = parent.children.at(index + 1);
let id = nextBlock?.id;
if (!id) {
id = store.addBlock<ParagraphBlockModel>(
ParagraphBlockSchema.model.flavour,
{},
parent
);
}
focusTextModel(std, id);
}
},
});

View File

@@ -6,6 +6,7 @@ import { BlockViewExtension } from '@blocksuite/std';
import { literal } from 'lit/static-html.js'; import { literal } from 'lit/static-html.js';
import { effects } from './effects'; import { effects } from './effects';
import { DividerMarkdownExtension } from './markdown';
export class DividerViewExtension extends ViewExtensionProvider { export class DividerViewExtension extends ViewExtensionProvider {
override name = 'affine-divider-block'; override name = 'affine-divider-block';
@@ -19,6 +20,7 @@ export class DividerViewExtension extends ViewExtensionProvider {
super.setup(context); super.setup(context);
context.register([ context.register([
BlockViewExtension('affine:divider', literal`affine-divider`), BlockViewExtension('affine:divider', literal`affine-divider`),
DividerMarkdownExtension,
]); ]);
} }
} }

View File

@@ -10,6 +10,7 @@
{ "path": "../../components" }, { "path": "../../components" },
{ "path": "../../ext-loader" }, { "path": "../../ext-loader" },
{ "path": "../../model" }, { "path": "../../model" },
{ "path": "../../rich-text" },
{ "path": "../../shared" }, { "path": "../../shared" },
{ "path": "../../../framework/global" }, { "path": "../../../framework/global" },
{ "path": "../../../framework/std" }, { "path": "../../../framework/std" },

View File

@@ -1,6 +1,5 @@
import { textKeymap } from '@blocksuite/affine-inline-preset'; import { textKeymap } from '@blocksuite/affine-inline-preset';
import { ListBlockSchema } from '@blocksuite/affine-model'; import { ListBlockSchema } from '@blocksuite/affine-model';
import { markdownInput } from '@blocksuite/affine-rich-text';
import { getSelectedModelsCommand } from '@blocksuite/affine-shared/commands'; import { getSelectedModelsCommand } from '@blocksuite/affine-shared/commands';
import { IS_MAC } from '@blocksuite/global/env'; import { IS_MAC } from '@blocksuite/global/env';
import { KeymapExtension, TextSelection } from '@blocksuite/std'; import { KeymapExtension, TextSelection } from '@blocksuite/std';
@@ -125,20 +124,6 @@ export const ListKeymapExtension = KeymapExtension(
ctx.get('keyboardState').raw.preventDefault(); ctx.get('keyboardState').raw.preventDefault();
return true; return true;
}, },
Space: ctx => {
if (!markdownInput(std)) {
return;
}
ctx.get('keyboardState').raw.preventDefault();
return true;
},
'Shift-Space': ctx => {
if (!markdownInput(std)) {
return;
}
ctx.get('keyboardState').raw.preventDefault();
return true;
},
}; };
}, },
{ {

View File

@@ -0,0 +1,91 @@
import {
type ListBlockModel,
ListBlockSchema,
type ListType,
ParagraphBlockModel,
} from '@blocksuite/affine-model';
import { focusTextModel } from '@blocksuite/affine-rich-text';
import type { AffineTextAttributes } from '@blocksuite/affine-shared/types';
import { matchModels, toNumberedList } from '@blocksuite/affine-shared/utils';
import type { BlockComponent } from '@blocksuite/std';
import { InlineMarkdownExtension } from '@blocksuite/std/inline';
export const ListMarkdownExtension =
InlineMarkdownExtension<AffineTextAttributes>({
name: 'list',
// group 2: number
// group 3: bullet
// group 4: bullet
// group 5: todo
// group 6: todo checked
pattern: /^((\d+\.)|(-)|(\*)|(\[ ?\])|(\[x\]))\s$/,
action: ({ inlineEditor, pattern, inlineRange, prefixText }) => {
if (inlineEditor.yTextString.slice(0, inlineRange.index).includes('\n')) {
return;
}
const match = prefixText.match(pattern);
if (!match) return;
let type: ListType;
if (match[2]) {
type = 'numbered';
} else if (match[3] || match[4]) {
type = 'bulleted';
} else if (match[5] || match[6]) {
type = 'todo';
} else {
return;
}
const checked = match[6] !== undefined;
if (!inlineEditor.rootElement) return;
const blockComponent =
inlineEditor.rootElement.closest<BlockComponent>('[data-block-id]');
if (!blockComponent) return;
const { model, std, store } = blockComponent;
if (!matchModels(model, [ParagraphBlockModel])) return;
if (type !== 'numbered') {
const parent = store.getParent(model);
if (!parent) return;
const index = parent.children.indexOf(model);
store.captureSync();
inlineEditor.deleteText({
index: 0,
length: inlineRange.index,
});
const id = store.addBlock<ListBlockModel>(
ListBlockSchema.model.flavour,
{
type: type,
text: model.text?.clone(),
children: model.children,
...(type === 'todo' ? { checked } : {}),
},
parent,
index
);
store.deleteBlock(model, { deleteChildren: false });
focusTextModel(std, id);
} else {
let order = parseInt(match[2]);
if (!Number.isInteger(order)) order = 1;
store.captureSync();
inlineEditor.deleteText({
index: 0,
length: inlineRange.index,
});
const id = toNumberedList(std, model, order);
if (!id) return;
focusTextModel(std, id);
}
},
});

View File

@@ -7,6 +7,7 @@ import { literal } from 'lit/static-html.js';
import { effects } from './effects.js'; import { effects } from './effects.js';
import { ListKeymapExtension, ListTextKeymapExtension } from './list-keymap.js'; import { ListKeymapExtension, ListTextKeymapExtension } from './list-keymap.js';
import { ListMarkdownExtension } from './markdown.js';
export class ListViewExtension extends ViewExtensionProvider { export class ListViewExtension extends ViewExtensionProvider {
override name = 'affine-list-block'; override name = 'affine-list-block';
@@ -23,6 +24,7 @@ export class ListViewExtension extends ViewExtensionProvider {
BlockViewExtension('affine:list', literal`affine-list`), BlockViewExtension('affine:list', literal`affine-list`),
ListKeymapExtension, ListKeymapExtension,
ListTextKeymapExtension, ListTextKeymapExtension,
ListMarkdownExtension,
]); ]);
} }
} }

View File

@@ -0,0 +1,74 @@
import {
ListBlockModel,
ParagraphBlockModel,
ParagraphBlockSchema,
type ParagraphType,
} from '@blocksuite/affine-model';
import { focusTextModel } from '@blocksuite/affine-rich-text';
import type { AffineTextAttributes } from '@blocksuite/affine-shared/types';
import { matchModels } from '@blocksuite/affine-shared/utils';
import type { BlockComponent } from '@blocksuite/std';
import { InlineMarkdownExtension } from '@blocksuite/std/inline';
export const ParagraphMarkdownExtension =
InlineMarkdownExtension<AffineTextAttributes>({
name: 'heading',
pattern: /^((#{1,6})|(>))\s$/,
action: ({ inlineEditor, pattern, inlineRange, prefixText }) => {
if (inlineEditor.yTextString.slice(0, inlineRange.index).includes('\n')) {
return;
}
const match = prefixText.match(pattern);
if (!match) return;
const type = (
match[2] ? `h${match[2].length}` : 'quote'
) as ParagraphType;
if (!inlineEditor.rootElement) return;
const blockComponent =
inlineEditor.rootElement.closest<BlockComponent>('[data-block-id]');
if (!blockComponent) return;
const { model, std, store } = blockComponent;
if (
!matchModels(model, [ParagraphBlockModel]) &&
matchModels(model, [ListBlockModel])
) {
const parent = store.getParent(model);
if (!parent) return;
const index = parent.children.indexOf(model);
store.captureSync();
inlineEditor.deleteText({
index: 0,
length: inlineRange.index,
});
store.deleteBlock(model, { deleteChildren: false });
const id = store.addBlock<ParagraphBlockModel>(
ParagraphBlockSchema.model.flavour,
{
type: type,
text: model.text?.clone(),
children: model.children,
},
parent,
index
);
focusTextModel(std, id);
} else if (
matchModels(model, [ParagraphBlockModel]) &&
model.props.type !== type
) {
store.captureSync();
inlineEditor.deleteText({
index: 0,
length: inlineRange.index,
});
store.updateBlock(model, { type });
focusTextModel(std, model.id);
}
},
});

View File

@@ -7,7 +7,6 @@ import {
import { import {
focusTextModel, focusTextModel,
getInlineEditorByModel, getInlineEditorByModel,
markdownInput,
} from '@blocksuite/affine-rich-text'; } from '@blocksuite/affine-rich-text';
import { import {
calculateCollapsedSiblings, calculateCollapsedSiblings,
@@ -148,10 +147,6 @@ export const ParagraphKeymapExtension = KeymapExtension(
raw.preventDefault(); raw.preventDefault();
if (markdownInput(std, model.id)) {
return true;
}
if (model.props.type.startsWith('h') && model.props.collapsed) { if (model.props.type.startsWith('h') && model.props.collapsed) {
const parent = store.getParent(model); const parent = store.getParent(model);
if (!parent) return true; if (!parent) return true;
@@ -199,20 +194,6 @@ export const ParagraphKeymapExtension = KeymapExtension(
event.preventDefault(); event.preventDefault();
return true; return true;
}, },
Space: ctx => {
if (!markdownInput(std)) {
return;
}
ctx.get('keyboardState').raw.preventDefault();
return true;
},
'Shift-Space': ctx => {
if (!markdownInput(std)) {
return;
}
ctx.get('keyboardState').raw.preventDefault();
return true;
},
Tab: ctx => { Tab: ctx => {
const [success] = std.command const [success] = std.command
.chain() .chain()

View File

@@ -2,9 +2,13 @@ import {
type ViewExtensionContext, type ViewExtensionContext,
ViewExtensionProvider, ViewExtensionProvider,
} from '@blocksuite/affine-ext-loader'; } from '@blocksuite/affine-ext-loader';
import { ParagraphBlockModel } from '@blocksuite/affine-model';
import { BlockViewExtension, FlavourExtension } from '@blocksuite/std'; import { BlockViewExtension, FlavourExtension } from '@blocksuite/std';
import { literal } from 'lit/static-html.js'; import { literal } from 'lit/static-html.js';
import { z } from 'zod';
import { effects } from './effects';
import { ParagraphMarkdownExtension } from './markdown.js';
import { ParagraphBlockConfigExtension } from './paragraph-block-config.js'; import { ParagraphBlockConfigExtension } from './paragraph-block-config.js';
import { import {
ParagraphKeymapExtension, ParagraphKeymapExtension,
@@ -22,11 +26,6 @@ const placeholders = {
quote: '', quote: '',
}; };
import { ParagraphBlockModel } from '@blocksuite/affine-model';
import { z } from 'zod';
import { effects } from './effects';
const optionsSchema = z.object({ const optionsSchema = z.object({
getPlaceholder: z.optional( getPlaceholder: z.optional(
z.function().args(z.instanceof(ParagraphBlockModel)).returns(z.string()) z.function().args(z.instanceof(ParagraphBlockModel)).returns(z.string())
@@ -61,6 +60,7 @@ export class ParagraphViewExtension extends ViewExtensionProvider<
ParagraphBlockConfigExtension({ ParagraphBlockConfigExtension({
getPlaceholder, getPlaceholder,
}), }),
ParagraphMarkdownExtension,
]); ]);
} }
} }

View File

@@ -10,7 +10,7 @@ export const LatexExtension = InlineMarkdownExtension<AffineTextAttributes>({
name: 'latex', name: 'latex',
pattern: pattern:
/(?:\$\$)(?<content>[^$]+)(?:\$\$)$|(?<blockPrefix>\$\$\$\$)|(?<inlinePrefix>\$\$)$/g, /(?:\$\$)(?<content>[^$]+)(?:\$\$)\s$|(?<blockPrefix>\$\$\$\$)\s$|(?<inlinePrefix>\$\$)\s$/g,
action: ({ inlineEditor, prefixText, inlineRange, pattern, undoManager }) => { action: ({ inlineEditor, prefixText, inlineRange, pattern, undoManager }) => {
const match = pattern.exec(prefixText); const match = pattern.exec(prefixText);
if (!match || !match.groups) return; if (!match || !match.groups) return;
@@ -33,22 +33,10 @@ export const LatexExtension = InlineMarkdownExtension<AffineTextAttributes>({
const ifEdgelessText = blockComponent.closest('affine-edgeless-text'); const ifEdgelessText = blockComponent.closest('affine-edgeless-text');
if (blockPrefix === '$$$$') { if (blockPrefix === '$$$$') {
inlineEditor.insertText(
{
index: inlineRange.index,
length: 0,
},
' '
);
inlineEditor.setInlineRange({
index: inlineRange.index + 1,
length: 0,
});
undoManager.stopCapturing(); undoManager.stopCapturing();
inlineEditor.deleteText({ inlineEditor.deleteText({
index: inlineRange.index - 4, index: inlineRange.index - 5,
length: 5, length: 5,
}); });
@@ -88,34 +76,22 @@ export const LatexExtension = InlineMarkdownExtension<AffineTextAttributes>({
} }
if (inlinePrefix === '$$') { if (inlinePrefix === '$$') {
inlineEditor.insertText(
{
index: inlineRange.index,
length: 0,
},
' '
);
inlineEditor.setInlineRange({
index: inlineRange.index + 1,
length: 0,
});
undoManager.stopCapturing(); undoManager.stopCapturing();
inlineEditor.deleteText({ inlineEditor.deleteText({
index: inlineRange.index - 2, index: inlineRange.index - 3,
length: 3, length: 3,
}); });
inlineEditor.insertText( inlineEditor.insertText(
{ {
index: inlineRange.index - 2, index: inlineRange.index - 3,
length: 0, length: 0,
}, },
' ' ' '
); );
inlineEditor.formatText( inlineEditor.formatText(
{ {
index: inlineRange.index - 2, index: inlineRange.index - 3,
length: 1, length: 1,
}, },
{ {
@@ -129,7 +105,7 @@ export const LatexExtension = InlineMarkdownExtension<AffineTextAttributes>({
await inlineEditor.waitForUpdate(); await inlineEditor.waitForUpdate();
const textPoint = inlineEditor.getTextPoint( const textPoint = inlineEditor.getTextPoint(
inlineRange.index - 2 + 1 inlineRange.index - 3 + 1
); );
if (!textPoint) return; if (!textPoint) return;
@@ -159,21 +135,9 @@ export const LatexExtension = InlineMarkdownExtension<AffineTextAttributes>({
if (!content || content.length === 0) return; if (!content || content.length === 0) return;
inlineEditor.insertText(
{
index: inlineRange.index,
length: 0,
},
' '
);
inlineEditor.setInlineRange({
index: inlineRange.index + 1,
length: 0,
});
undoManager.stopCapturing(); undoManager.stopCapturing();
const startIndex = inlineRange.index - 2 - content.length - 2; const startIndex = inlineRange.index - 1 - 2 - content.length - 2;
inlineEditor.deleteText({ inlineEditor.deleteText({
index: startIndex, index: startIndex,
length: 2 + content.length + 2 + 1, length: 2 + content.length + 2 + 1,

View File

@@ -3,27 +3,18 @@ import { InlineMarkdownExtension } from '@blocksuite/std/inline';
export const LinkExtension = InlineMarkdownExtension<AffineTextAttributes>({ export const LinkExtension = InlineMarkdownExtension<AffineTextAttributes>({
name: 'link', name: 'link',
pattern: /.*\[(.+?)\]\((.+?)\)$/, pattern: /.*\[(.+?)\]\((.+?)\)\s$/,
action: ({ inlineEditor, prefixText, inlineRange, pattern, undoManager }) => { action: ({ inlineEditor, prefixText, inlineRange, pattern, undoManager }) => {
const match = prefixText.match(pattern); const match = prefixText.match(pattern);
if (!match) return; if (!match) return;
const linkText = match[1]; const linkText = match[1];
const linkUrl = match[2]; const linkUrl = match[2];
const annotatedText = match[0].slice(-linkText.length - linkUrl.length - 4); const annotatedText = match[0].slice(
const startIndex = inlineRange.index - annotatedText.length; -(linkText.length + linkUrl.length + 4 + 1),
-1
inlineEditor.insertText(
{
index: inlineRange.index,
length: 0,
},
' '
); );
inlineEditor.setInlineRange({ const startIndex = inlineRange.index - annotatedText.length - 1;
index: inlineRange.index + 1,
length: 0,
});
undoManager.stopCapturing(); undoManager.stopCapturing();

View File

@@ -59,7 +59,7 @@ export const StrikeInlineSpecExtension =
export const CodeInlineSpecExtension = export const CodeInlineSpecExtension =
InlineSpecExtension<AffineTextAttributes>({ InlineSpecExtension<AffineTextAttributes>({
name: 'code', name: 'inline-code',
schema: z.literal(true).optional().nullable().catch(undefined), schema: z.literal(true).optional().nullable().catch(undefined),
match: delta => { match: delta => {
return !!delta.attributes?.code; return !!delta.attributes?.code;

View File

@@ -13,7 +13,7 @@ import type { ExtensionType } from '@blocksuite/store';
export const BoldItalicMarkdown = InlineMarkdownExtension<AffineTextAttributes>( export const BoldItalicMarkdown = InlineMarkdownExtension<AffineTextAttributes>(
{ {
name: 'bolditalic', name: 'bolditalic',
pattern: /.*\*{3}([^\s*][^*]*[^\s*])\*{3}$|.*\*{3}([^\s*])\*{3}$/, pattern: /.*\*{3}([^\s*][^*]*[^\s*])\*{3}\s$|.*\*{3}([^\s*])\*{3}\s$/,
action: ({ action: ({
inlineEditor, inlineEditor,
prefixText, prefixText,
@@ -25,20 +25,11 @@ export const BoldItalicMarkdown = InlineMarkdownExtension<AffineTextAttributes>(
if (!match) return; if (!match) return;
const targetText = match[1] ?? match[2]; const targetText = match[1] ?? match[2];
const annotatedText = match[0].slice(-targetText.length - 3 * 2); const annotatedText = match[0].slice(
const startIndex = inlineRange.index - annotatedText.length; -(targetText.length + 3 * 2 + 1),
-1
inlineEditor.insertText(
{
index: startIndex + annotatedText.length,
length: 0,
},
' '
); );
inlineEditor.setInlineRange({ const startIndex = inlineRange.index - annotatedText.length - 1;
index: startIndex + annotatedText.length + 1,
length: 0,
});
undoManager.stopCapturing(); undoManager.stopCapturing();
@@ -54,18 +45,13 @@ export const BoldItalicMarkdown = InlineMarkdownExtension<AffineTextAttributes>(
); );
inlineEditor.deleteText({ inlineEditor.deleteText({
index: startIndex + annotatedText.length, index: inlineRange.index - 4,
length: 1, length: 4,
});
inlineEditor.deleteText({
index: startIndex + annotatedText.length - 3,
length: 3,
}); });
inlineEditor.deleteText({ inlineEditor.deleteText({
index: startIndex, index: startIndex,
length: 3, length: 3,
}); });
inlineEditor.setInlineRange({ inlineEditor.setInlineRange({
index: startIndex + annotatedText.length - 6, index: startIndex + annotatedText.length - 6,
length: 0, length: 0,
@@ -76,26 +62,14 @@ export const BoldItalicMarkdown = InlineMarkdownExtension<AffineTextAttributes>(
export const BoldMarkdown = InlineMarkdownExtension<AffineTextAttributes>({ export const BoldMarkdown = InlineMarkdownExtension<AffineTextAttributes>({
name: 'bold', name: 'bold',
pattern: /.*\*{2}([^\s][^*]*[^\s*])\*{2}$|.*\*{2}([^\s*])\*{2}$/, pattern: /.*\*{2}([^\s][^*]*[^\s*])\*{2}\s$|.*\*{2}([^\s*])\*{2}\s$/,
action: ({ inlineEditor, prefixText, inlineRange, pattern, undoManager }) => { action: ({ inlineEditor, prefixText, inlineRange, pattern, undoManager }) => {
const match = prefixText.match(pattern); const match = prefixText.match(pattern);
if (!match) return; if (!match) return;
const targetText = match[1] ?? match[2]; const targetText = match[1] ?? match[2];
const annotatedText = match[0].slice(-targetText.length - 2 * 2); const annotatedText = match[0].slice(-(targetText.length + 2 * 2 + 1), -1);
const startIndex = inlineRange.index - annotatedText.length; const startIndex = inlineRange.index - annotatedText.length - 1;
inlineEditor.insertText(
{
index: startIndex + annotatedText.length,
length: 0,
},
' '
);
inlineEditor.setInlineRange({
index: startIndex + annotatedText.length + 1,
length: 0,
});
undoManager.stopCapturing(); undoManager.stopCapturing();
@@ -110,18 +84,13 @@ export const BoldMarkdown = InlineMarkdownExtension<AffineTextAttributes>({
); );
inlineEditor.deleteText({ inlineEditor.deleteText({
index: startIndex + annotatedText.length, index: inlineRange.index - 3,
length: 1, length: 3,
});
inlineEditor.deleteText({
index: startIndex + annotatedText.length - 2,
length: 2,
}); });
inlineEditor.deleteText({ inlineEditor.deleteText({
index: startIndex, index: startIndex,
length: 2, length: 2,
}); });
inlineEditor.setInlineRange({ inlineEditor.setInlineRange({
index: startIndex + annotatedText.length - 4, index: startIndex + annotatedText.length - 4,
length: 0, length: 0,
@@ -131,26 +100,14 @@ export const BoldMarkdown = InlineMarkdownExtension<AffineTextAttributes>({
export const ItalicExtension = InlineMarkdownExtension<AffineTextAttributes>({ export const ItalicExtension = InlineMarkdownExtension<AffineTextAttributes>({
name: 'italic', name: 'italic',
pattern: /.*\*{1}([^\s][^*]*[^\s*])\*{1}$|.*\*{1}([^\s*])\*{1}$/, pattern: /.*\*{1}([^\s][^*]*[^\s*])\*{1}\s$|.*\*{1}([^\s*])\*{1}\s$/,
action: ({ inlineEditor, prefixText, inlineRange, pattern, undoManager }) => { action: ({ inlineEditor, prefixText, inlineRange, pattern, undoManager }) => {
const match = prefixText.match(pattern); const match = prefixText.match(pattern);
if (!match) return; if (!match) return;
const targetText = match[1] ?? match[2]; const targetText = match[1] ?? match[2];
const annotatedText = match[0].slice(-targetText.length - 1 * 2); const annotatedText = match[0].slice(-(targetText.length + 1 * 2 + 1), -1);
const startIndex = inlineRange.index - annotatedText.length; const startIndex = inlineRange.index - annotatedText.length - 1;
inlineEditor.insertText(
{
index: startIndex + annotatedText.length,
length: 0,
},
' '
);
inlineEditor.setInlineRange({
index: startIndex + annotatedText.length + 1,
length: 0,
});
undoManager.stopCapturing(); undoManager.stopCapturing();
@@ -165,18 +122,13 @@ export const ItalicExtension = InlineMarkdownExtension<AffineTextAttributes>({
); );
inlineEditor.deleteText({ inlineEditor.deleteText({
index: startIndex + annotatedText.length, index: inlineRange.index - 2,
length: 1, length: 2,
});
inlineEditor.deleteText({
index: startIndex + annotatedText.length - 1,
length: 1,
}); });
inlineEditor.deleteText({ inlineEditor.deleteText({
index: startIndex, index: startIndex,
length: 1, length: 1,
}); });
inlineEditor.setInlineRange({ inlineEditor.setInlineRange({
index: startIndex + annotatedText.length - 2, index: startIndex + annotatedText.length - 2,
length: 0, length: 0,
@@ -187,7 +139,7 @@ export const ItalicExtension = InlineMarkdownExtension<AffineTextAttributes>({
export const StrikethroughExtension = export const StrikethroughExtension =
InlineMarkdownExtension<AffineTextAttributes>({ InlineMarkdownExtension<AffineTextAttributes>({
name: 'strikethrough', name: 'strikethrough',
pattern: /.*~{2}([^\s][^~]*[^\s])~{2}$|.*~{2}([^\s~])~{2}$/, pattern: /.*~{2}([^\s][^~]*[^\s])~{2}\s$|.*~{2}([^\s~])~{2}\s$/,
action: ({ action: ({
inlineEditor, inlineEditor,
prefixText, prefixText,
@@ -199,20 +151,11 @@ export const StrikethroughExtension =
if (!match) return; if (!match) return;
const targetText = match[1] ?? match[2]; const targetText = match[1] ?? match[2];
const annotatedText = match[0].slice(-targetText.length - 2 * 2); const annotatedText = match[0].slice(
const startIndex = inlineRange.index - annotatedText.length; -targetText.length - (2 * 2 + 1),
-1
inlineEditor.insertText(
{
index: startIndex + annotatedText.length,
length: 0,
},
' '
); );
inlineEditor.setInlineRange({ const startIndex = inlineRange.index - annotatedText.length - 1;
index: startIndex + annotatedText.length + 1,
length: 0,
});
undoManager.stopCapturing(); undoManager.stopCapturing();
@@ -227,12 +170,8 @@ export const StrikethroughExtension =
); );
inlineEditor.deleteText({ inlineEditor.deleteText({
index: startIndex + annotatedText.length, index: inlineRange.index - 3,
length: 1, length: 3,
});
inlineEditor.deleteText({
index: startIndex + annotatedText.length - 2,
length: 2,
}); });
inlineEditor.deleteText({ inlineEditor.deleteText({
index: startIndex, index: startIndex,
@@ -249,7 +188,7 @@ export const StrikethroughExtension =
export const UnderthroughExtension = export const UnderthroughExtension =
InlineMarkdownExtension<AffineTextAttributes>({ InlineMarkdownExtension<AffineTextAttributes>({
name: 'underthrough', name: 'underthrough',
pattern: /.*~{1}([^\s][^~]*[^\s~])~{1}$|.*~{1}([^\s~])~{1}$/, pattern: /.*~{1}([^\s][^~]*[^\s~])~{1}\s$|.*~{1}([^\s~])~{1}\s$/,
action: ({ action: ({
inlineEditor, inlineEditor,
prefixText, prefixText,
@@ -261,20 +200,11 @@ export const UnderthroughExtension =
if (!match) return; if (!match) return;
const targetText = match[1] ?? match[2]; const targetText = match[1] ?? match[2];
const annotatedText = match[0].slice(-targetText.length - 1 * 2); const annotatedText = match[0].slice(
const startIndex = inlineRange.index - annotatedText.length; -(targetText.length + 1 * 2 + 1),
-1
inlineEditor.insertText(
{
index: startIndex + annotatedText.length,
length: 0,
},
' '
); );
inlineEditor.setInlineRange({ const startIndex = inlineRange.index - annotatedText.length - 1;
index: startIndex + annotatedText.length + 1,
length: 0,
});
undoManager.stopCapturing(); undoManager.stopCapturing();
@@ -289,12 +219,8 @@ export const UnderthroughExtension =
); );
inlineEditor.deleteText({ inlineEditor.deleteText({
index: startIndex + annotatedText.length, index: inlineRange.index - 2,
length: 1, length: 2,
});
inlineEditor.deleteText({
index: inlineRange.index - 1,
length: 1,
}); });
inlineEditor.deleteText({ inlineEditor.deleteText({
index: startIndex, index: startIndex,
@@ -310,26 +236,14 @@ export const UnderthroughExtension =
export const CodeExtension = InlineMarkdownExtension<AffineTextAttributes>({ export const CodeExtension = InlineMarkdownExtension<AffineTextAttributes>({
name: 'code', name: 'code',
pattern: /.*`([^\s][^`]*[^\s])`$|.*`([^\s`])`$/, pattern: /.*`([^\s][^`]*[^\s])`\s$|.*`([^\s`])`\s$/,
action: ({ inlineEditor, prefixText, inlineRange, pattern, undoManager }) => { action: ({ inlineEditor, prefixText, inlineRange, pattern, undoManager }) => {
const match = prefixText.match(pattern); const match = prefixText.match(pattern);
if (!match) return; if (!match) return;
const targetText = match[1] ?? match[2]; const targetText = match[1] ?? match[2];
const annotatedText = match[0].slice(-targetText.length - 1 * 2); const annotatedText = match[0].slice(-(targetText.length + 1 * 2 + 1), -1);
const startIndex = inlineRange.index - annotatedText.length; const startIndex = inlineRange.index - annotatedText.length - 1;
inlineEditor.insertText(
{
index: startIndex + annotatedText.length,
length: 0,
},
' '
);
inlineEditor.setInlineRange({
index: startIndex + annotatedText.length + 1,
length: 0,
});
undoManager.stopCapturing(); undoManager.stopCapturing();
@@ -344,12 +258,8 @@ export const CodeExtension = InlineMarkdownExtension<AffineTextAttributes>({
); );
inlineEditor.deleteText({ inlineEditor.deleteText({
index: startIndex + annotatedText.length, index: inlineRange.index - 2,
length: 1, length: 2,
});
inlineEditor.deleteText({
index: startIndex + annotatedText.length - 1,
length: 1,
}); });
inlineEditor.deleteText({ inlineEditor.deleteText({
index: startIndex, index: startIndex,

View File

@@ -10,6 +10,5 @@ export {
onModelTextUpdated, onModelTextUpdated,
selectTextModel, selectTextModel,
} from './dom'; } from './dom';
export { markdownInput } from './markdown';
export { RichText } from './rich-text'; export { RichText } from './rich-text';
export * from './utils'; export * from './utils';

View File

@@ -1,42 +0,0 @@
import {
DividerBlockModel,
ParagraphBlockModel,
} from '@blocksuite/affine-model';
import { matchModels } from '@blocksuite/affine-shared/utils';
import type { BlockStdScope } from '@blocksuite/std';
import type { BlockModel } from '@blocksuite/store';
import { focusTextModel } from '../dom.js';
import { beforeConvert } from './utils.js';
export function toDivider(
std: BlockStdScope,
model: BlockModel,
prefix: string
) {
const { store: doc } = std;
if (
matchModels(model, [DividerBlockModel]) ||
(matchModels(model, [ParagraphBlockModel]) && model.props.type === 'quote')
) {
return;
}
const parent = doc.getParent(model);
if (!parent) return;
const index = parent.children.indexOf(model);
beforeConvert(std, model, prefix.length);
const blockProps = {
children: model.children,
};
doc.addBlock('affine:divider', blockProps, parent, index);
const nextBlock = parent.children[index + 1];
let id = nextBlock?.id;
if (!id) {
id = doc.addBlock('affine:paragraph', {}, parent);
}
focusTextModel(std, id);
return id;
}

View File

@@ -1 +0,0 @@
export { markdownInput } from './markdown-input.js';

View File

@@ -1,54 +0,0 @@
import {
type ListProps,
type ListType,
ParagraphBlockModel,
} from '@blocksuite/affine-model';
import { matchModels, toNumberedList } from '@blocksuite/affine-shared/utils';
import type { BlockStdScope } from '@blocksuite/std';
import type { BlockModel } from '@blocksuite/store';
import { focusTextModel } from '../dom.js';
import { beforeConvert } from './utils.js';
export function toList(
std: BlockStdScope,
model: BlockModel,
listType: ListType,
prefix: string,
otherProperties?: Partial<ListProps>
) {
if (!matchModels(model, [ParagraphBlockModel])) {
return;
}
const { store: doc } = std;
const parent = doc.getParent(model);
if (!parent) return;
beforeConvert(std, model, prefix.length);
if (listType !== 'numbered') {
const index = parent.children.indexOf(model);
const blockProps = {
type: listType,
text: model.text?.clone(),
children: model.children,
...otherProperties,
};
doc.deleteBlock(model, {
deleteChildren: false,
});
const id = doc.addBlock('affine:list', blockProps, parent, index);
focusTextModel(std, id);
return id;
}
let order = parseInt(prefix.slice(0, -1));
if (!Number.isInteger(order)) order = 1;
const id = toNumberedList(std, model, order);
if (!id) return;
focusTextModel(std, id);
return id;
}

View File

@@ -1,98 +0,0 @@
import {
CalloutBlockModel,
CodeBlockModel,
ParagraphBlockModel,
} from '@blocksuite/affine-model';
import {
isHorizontalRuleMarkdown,
isMarkdownPrefix,
matchModels,
} from '@blocksuite/affine-shared/utils';
import { type BlockStdScope, TextSelection } from '@blocksuite/std';
import { getInlineEditorByModel } from '../dom.js';
import { toDivider } from './divider.js';
import { toList } from './list.js';
import { toParagraph } from './paragraph.js';
import { toCode } from './to-code.js';
import { getPrefixText } from './utils.js';
export function markdownInput(
std: BlockStdScope,
id?: string
): string | undefined {
if (!id) {
const selection = std.selection;
const text = selection.find(TextSelection);
id = text?.from.blockId;
}
if (!id) return;
const model = std.store.getBlock(id)?.model;
if (!model) return;
const inline = getInlineEditorByModel(std, model);
if (!inline) return;
const range = inline.getInlineRange();
if (!range) return;
const prefixText = getPrefixText(inline);
if (!isMarkdownPrefix(prefixText)) return;
const isParagraph = matchModels(model, [ParagraphBlockModel]);
const isHeading = isParagraph && model.props.type.startsWith('h');
const isParagraphQuoteBlock = isParagraph && model.props.type === 'quote';
const isCodeBlock = matchModels(model, [CodeBlockModel]);
if (
isHeading ||
isParagraphQuoteBlock ||
isCodeBlock ||
matchModels(model.parent, [CalloutBlockModel])
)
return;
const lineInfo = inline.getLine(range.index);
if (!lineInfo) return;
const { lineIndex, rangeIndexRelatedToLine } = lineInfo;
if (lineIndex !== 0 || rangeIndexRelatedToLine > prefixText.length) return;
// try to add code block
const codeMatch = prefixText.match(/^```([a-zA-Z0-9]*)$/g);
if (codeMatch) {
return toCode(std, model, prefixText, codeMatch[0].slice(3));
}
if (isHorizontalRuleMarkdown(prefixText.trim())) {
return toDivider(std, model, prefixText);
}
switch (prefixText.trim()) {
case '[]':
case '[ ]':
return toList(std, model, 'todo', prefixText, {
checked: false,
});
case '[x]':
return toList(std, model, 'todo', prefixText, {
checked: true,
});
case '-':
case '*':
return toList(std, model, 'bulleted', prefixText);
case '#':
return toParagraph(std, model, 'h1', prefixText);
case '##':
return toParagraph(std, model, 'h2', prefixText);
case '###':
return toParagraph(std, model, 'h3', prefixText);
case '####':
return toParagraph(std, model, 'h4', prefixText);
case '#####':
return toParagraph(std, model, 'h5', prefixText);
case '######':
return toParagraph(std, model, 'h6', prefixText);
case '>':
return toParagraph(std, model, 'quote', prefixText);
default:
return toList(std, model, 'numbered', prefixText);
}
}

View File

@@ -1,49 +0,0 @@
import {
ParagraphBlockModel,
type ParagraphType,
} from '@blocksuite/affine-model';
import { matchModels } from '@blocksuite/affine-shared/utils';
import type { BlockStdScope } from '@blocksuite/std';
import type { BlockModel } from '@blocksuite/store';
import { focusTextModel } from '../dom.js';
import { beforeConvert } from './utils.js';
export function toParagraph(
std: BlockStdScope,
model: BlockModel,
type: ParagraphType,
prefix: string
) {
const { store: doc } = std;
if (!matchModels(model, [ParagraphBlockModel])) {
const parent = doc.getParent(model);
if (!parent) return;
const index = parent.children.indexOf(model);
beforeConvert(std, model, prefix.length);
const blockProps = {
type: type,
text: model.text?.clone(),
children: model.children,
};
doc.deleteBlock(model, { deleteChildren: false });
const id = doc.addBlock('affine:paragraph', blockProps, parent, index);
focusTextModel(std, id);
return id;
}
if (matchModels(model, [ParagraphBlockModel]) && model.props.type !== type) {
beforeConvert(std, model, prefix.length);
doc.updateBlock(model, { type });
focusTextModel(std, model.id);
}
// If the model is already a paragraph with the same type, do nothing
return model.id;
}

View File

@@ -1,42 +0,0 @@
import { ParagraphBlockModel } from '@blocksuite/affine-model';
import { matchModels } from '@blocksuite/affine-shared/utils';
import type { BlockStdScope } from '@blocksuite/std';
import type { BlockModel } from '@blocksuite/store';
import { focusTextModel } from '../dom.js';
export function toCode(
std: BlockStdScope,
model: BlockModel,
prefixText: string,
language: string | null
) {
if (
matchModels(model, [ParagraphBlockModel]) &&
model.props.type === 'quote'
) {
return;
}
const doc = model.store;
const parent = doc.getParent(model);
if (!parent) {
return;
}
doc.captureSync();
const index = parent.children.indexOf(model);
const codeId = doc.addBlock('affine:code', { language }, parent, index);
if (model.text && model.text.length > prefixText.length) {
const text = model.text.clone();
doc.addBlock('affine:paragraph', { text }, parent, index + 1);
text.delete(0, prefixText.length);
}
doc.deleteBlock(model, { bringChildrenTo: parent });
focusTextModel(std, codeId);
return codeId;
}

View File

@@ -1,39 +0,0 @@
import type { BlockStdScope } from '@blocksuite/std';
import type { InlineEditor } from '@blocksuite/std/inline';
import type { BlockModel } from '@blocksuite/store';
import { focusTextModel } from '../dom.js';
export function getPrefixText(inlineEditor: InlineEditor) {
const inlineRange = inlineEditor.getInlineRange();
if (!inlineRange) return '';
const firstLineEnd = inlineEditor.yTextString.search(/\n/);
if (firstLineEnd !== -1 && inlineRange.index > firstLineEnd) {
return '';
}
const textPoint = inlineEditor.getTextPoint(inlineRange.index);
if (!textPoint) return '';
const [leafStart, offsetStart] = textPoint;
return leafStart.textContent
? leafStart.textContent.slice(0, offsetStart)
: '';
}
export function beforeConvert(
std: BlockStdScope,
model: BlockModel,
index: number
) {
const { text } = model;
if (!text) return;
// Add a space after the text, then stop capturing
// So when the user undo, the prefix will be restored with a `space`
// Ex. (| is the cursor position)
// *| <- user input
// <space> -> bullet list
// *<space>| -> undo
text.insert(' ', index);
focusTextModel(std, model.id, index + 1);
std.store.captureSync();
text.delete(0, index + 1);
}

View File

@@ -22,6 +22,7 @@ import * as Y from 'yjs';
import { z } from 'zod'; import { z } from 'zod';
import { onVBeforeinput, onVCompositionEnd } from './hooks.js'; import { onVBeforeinput, onVCompositionEnd } from './hooks.js';
import { getPrefixText } from './utils.js';
interface RichTextStackItem { interface RichTextStackItem {
meta: Map<'richtext-v-range', InlineRange | null>; meta: Map<'richtext-v-range', InlineRange | null>;
@@ -186,38 +187,60 @@ export class RichText extends WithDisposable(ShadowlessElement) {
const markdownMatches = this.markdownMatches; const markdownMatches = this.markdownMatches;
if (markdownMatches) { if (markdownMatches) {
inlineEditor.disposables.addFromEvent( const markdownTransform = (isEnter: boolean = false) => {
this.inlineEventSource ?? this.inlineEditorContainer, let inlineRange = inlineEditor.getInlineRange();
'keydown', if (!inlineRange) return false;
(e: KeyboardEvent) => {
if (e.key !== ' ' && e.key !== 'Enter') return;
const inlineRange = inlineEditor.getInlineRange(); let prefixText = getPrefixText(inlineEditor);
if (!inlineRange || inlineRange.length > 0) return; if (isEnter) prefixText = `${prefixText} `;
const nearestLineBreakIndex = inlineEditor.yTextString for (const match of markdownMatches) {
.slice(0, inlineRange.index) const { pattern, action } = match;
.lastIndexOf('\n'); if (prefixText.match(pattern)) {
const prefixText = inlineEditor.yTextString.slice( if (isEnter) {
nearestLineBreakIndex + 1, inlineEditor.insertText(
inlineRange.index {
); index: inlineRange.index,
length: 0,
for (const match of markdownMatches) { },
const { pattern, action } = match; ' '
if (prefixText.match(pattern)) { );
action({ inlineEditor.setInlineRange({
inlineEditor, index: inlineRange.index + 1,
prefixText, length: 0,
inlineRange,
pattern,
undoManager: this.undoManager,
}); });
e.preventDefault(); inlineRange = inlineEditor.getInlineRange();
break; if (!inlineRange) return false;
} }
action({
inlineEditor,
prefixText,
inlineRange,
pattern,
undoManager: this.undoManager,
});
return true;
} }
} }
return false;
};
inlineEditor.disposables.add(
inlineEditor.slots.inputting.subscribe(data => {
if (!inlineEditor.isComposing && data === ' ') {
markdownTransform();
}
})
);
inlineEditor.disposables.add(
inlineEditor.slots.keydown.subscribe(event => {
if (event.key === 'Enter' && markdownTransform(true)) {
event.stopPropagation();
event.preventDefault();
}
})
); );
} }

View File

@@ -52,3 +52,17 @@ export function clearMarksOnDiscontinuousInput(
} }
}); });
} }
export function getPrefixText(inlineEditor: InlineEditor) {
const inlineRange = inlineEditor.getInlineRange();
if (!inlineRange || inlineRange.length > 0) return '';
const nearestLineBreakIndex = inlineEditor.yTextString
.slice(0, inlineRange.index)
.lastIndexOf('\n');
const prefixText = inlineEditor.yTextString.slice(
nearestLineBreakIndex + 1,
inlineRange.index
);
return prefixText;
}

View File

@@ -118,10 +118,16 @@ Get the root block of the store.
### addBlock() ### addBlock()
> **addBlock**(`flavour`, `blockProps`, `parent?`, `parentIndex?`): `string` > **addBlock**\<`T`\>(`flavour`, `blockProps`, `parent?`, `parentIndex?`): `string`
Creates and adds a new block to the store Creates and adds a new block to the store
#### Type Parameters
##### T
`T` *extends* `BlockModel`\<`object`\> = `BlockModel`\<`object`\>
#### Parameters #### Parameters
##### flavour ##### flavour
@@ -132,7 +138,7 @@ The block's flavour (type)
##### blockProps ##### blockProps
`Partial`\<`BlockSysProps` & `Record`\<`string`, `unknown`\> & `Omit`\<`BlockProps`, `"flavour"`\>\> = `{}` `Partial`\<`BlockProps` \| `PropsOfModel`\<`T`\> & `BlockSysProps`\> = `{}`
Optional properties for the new block Optional properties for the new block

View File

@@ -165,8 +165,9 @@ export class InlineEditor<
inlineRangeSync: new Subject<Range | null>(), inlineRangeSync: new Subject<Range | null>(),
/** /**
* Corresponding to the `compositionUpdate` and `beforeInput` events, and triggered only when the `inlineRange` is not null. * Corresponding to the `compositionUpdate` and `beforeInput` events, and triggered only when the `inlineRange` is not null.
* The parameter is the `event.data`.
*/ */
inputting: new Subject<void>(), inputting: new Subject<string>(),
/** /**
* Triggered only when the `inlineRange` is not null. * Triggered only when the `inlineRange` is not null.
*/ */

View File

@@ -119,7 +119,7 @@ export class EventService<TextAttributes extends BaseTextAttributes> {
this.editor as never this.editor as never
); );
this.editor.slots.inputting.next(); this.editor.slots.inputting.next(event.data ?? '');
}; };
private readonly _onClick = (event: MouseEvent) => { private readonly _onClick = (event: MouseEvent) => {
@@ -181,10 +181,10 @@ export class EventService<TextAttributes extends BaseTextAttributes> {
}); });
} }
this.editor.slots.inputting.next(); this.editor.slots.inputting.next(event.data ?? '');
}; };
private readonly _onCompositionStart = () => { private readonly _onCompositionStart = (event: CompositionEvent) => {
this._isComposing = true; this._isComposing = true;
if (!this.editor.rootElement) return; if (!this.editor.rootElement) return;
// embeds is not editable and it will break IME // embeds is not editable and it will break IME
@@ -201,9 +201,11 @@ export class EventService<TextAttributes extends BaseTextAttributes> {
} else { } else {
this._compositionInlineRange = null; this._compositionInlineRange = null;
} }
this.editor.slots.inputting.next(event.data ?? '');
}; };
private readonly _onCompositionUpdate = () => { private readonly _onCompositionUpdate = (event: CompositionEvent) => {
if (!this.editor.rootElement || !this.editor.rootElement.isConnected) { if (!this.editor.rootElement || !this.editor.rootElement.isConnected) {
return; return;
} }
@@ -216,7 +218,7 @@ export class EventService<TextAttributes extends BaseTextAttributes> {
) )
return; return;
this.editor.slots.inputting.next(); this.editor.slots.inputting.next(event.data ?? '');
}; };
private readonly _onKeyDown = (event: KeyboardEvent) => { private readonly _onKeyDown = (event: KeyboardEvent) => {
@@ -359,13 +361,9 @@ export class EventService<TextAttributes extends BaseTextAttributes> {
'compositionupdate', 'compositionupdate',
this._onCompositionUpdate this._onCompositionUpdate
); );
this.editor.disposables.addFromEvent( this.editor.disposables.addFromEvent(eventSource, 'compositionend', e => {
eventSource, this._onCompositionEnd(e).catch(console.error);
'compositionend', });
(event: CompositionEvent) => {
this._onCompositionEnd(event).catch(console.error);
}
);
this.editor.disposables.addFromEvent( this.editor.disposables.addFromEvent(
eventSource, eventSource,
'keydown', 'keydown',

View File

@@ -740,9 +740,9 @@ export class Store {
* *
* @category Block CRUD * @category Block CRUD
*/ */
addBlock( addBlock<T extends BlockModel = BlockModel>(
flavour: string, flavour: string,
blockProps: Partial<BlockProps & Omit<BlockProps, 'flavour'>> = {}, blockProps: Partial<(PropsOfModel<T> & BlockSysProps) | BlockProps> = {},
parent?: BlockModel | string | null, parent?: BlockModel | string | null,
parentIndex?: number parentIndex?: number
): string { ): string {

View File

@@ -758,11 +758,12 @@ test('Delete the blank line between two dividers', async ({ page }) => {
await initEmptyParagraphState(page); await initEmptyParagraphState(page);
await focusRichText(page); await focusRichText(page);
await type(page, '--- '); await type(page, '--- ');
await waitNextFrame(page);
await assertDivider(page, 1); await assertDivider(page, 1);
await waitNextFrame(page);
await pressEnter(page); await pressEnter(page);
await type(page, '--- '); await type(page, '--- ');
await waitNextFrame(page);
await assertDivider(page, 2); await assertDivider(page, 2);
await assertRichTexts(page, ['', '']); await assertRichTexts(page, ['', '']);

View File

@@ -184,6 +184,7 @@ export const PackageList = [
'blocksuite/affine/components', 'blocksuite/affine/components',
'blocksuite/affine/ext-loader', 'blocksuite/affine/ext-loader',
'blocksuite/affine/model', 'blocksuite/affine/model',
'blocksuite/affine/rich-text',
'blocksuite/affine/shared', 'blocksuite/affine/shared',
'blocksuite/framework/global', 'blocksuite/framework/global',
'blocksuite/framework/std', 'blocksuite/framework/std',

View File

@@ -2593,6 +2593,7 @@ __metadata:
"@blocksuite/affine-components": "workspace:*" "@blocksuite/affine-components": "workspace:*"
"@blocksuite/affine-ext-loader": "workspace:*" "@blocksuite/affine-ext-loader": "workspace:*"
"@blocksuite/affine-model": "workspace:*" "@blocksuite/affine-model": "workspace:*"
"@blocksuite/affine-rich-text": "workspace:*"
"@blocksuite/affine-shared": "workspace:*" "@blocksuite/affine-shared": "workspace:*"
"@blocksuite/global": "workspace:*" "@blocksuite/global": "workspace:*"
"@blocksuite/std": "workspace:*" "@blocksuite/std": "workspace:*"