mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-02-12 04:18:54 +00:00
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:
61
blocksuite/affine/blocks/code/src/markdown.ts
Normal file
61
blocksuite/affine/blocks/code/src/markdown.ts
Normal 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);
|
||||||
|
},
|
||||||
|
});
|
||||||
@@ -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([
|
||||||
|
|||||||
@@ -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:*",
|
||||||
|
|||||||
63
blocksuite/affine/blocks/divider/src/markdown.ts
Normal file
63
blocksuite/affine/blocks/divider/src/markdown.ts
Normal 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);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
@@ -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,
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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" },
|
||||||
|
|||||||
@@ -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;
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
91
blocksuite/affine/blocks/list/src/markdown.ts
Normal file
91
blocksuite/affine/blocks/list/src/markdown.ts
Normal 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);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
@@ -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,
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
74
blocksuite/affine/blocks/paragraph/src/markdown.ts
Normal file
74
blocksuite/affine/blocks/paragraph/src/markdown.ts
Normal 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);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
@@ -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()
|
||||||
|
|||||||
@@ -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,
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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';
|
||||||
|
|||||||
@@ -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;
|
|
||||||
}
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
export { markdownInput } from './markdown-input.js';
|
|
||||||
@@ -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;
|
|
||||||
}
|
|
||||||
@@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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;
|
|
||||||
}
|
|
||||||
@@ -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;
|
|
||||||
}
|
|
||||||
@@ -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);
|
|
||||||
}
|
|
||||||
@@ -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();
|
||||||
|
}
|
||||||
|
})
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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.
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -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',
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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, ['', '']);
|
||||||
|
|
||||||
|
|||||||
@@ -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',
|
||||||
|
|||||||
@@ -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:*"
|
||||||
|
|||||||
Reference in New Issue
Block a user