refactor(editor): remove inline editor keyboard utils and add markdown property in rich-text (#10375)

This commit is contained in:
Flrande
2025-02-23 19:57:56 +08:00
committed by GitHub
parent eef2f004b8
commit 9fd1ca1c09
16 changed files with 252 additions and 725 deletions

View File

@@ -127,7 +127,7 @@ export class BlockRenderer
.attributesSchema=${this.attributesSchema}
.attributeRenderer=${this.attributeRenderer}
.embedChecker=${this.inlineManager.embedChecker}
.markdownShortcutHandler=${this.inlineManager.markdownShortcutHandler}
.markdownMatches=${this.inlineManager.markdownMatches}
class="inline-editor"
></rich-text>
`;

View File

@@ -221,7 +221,7 @@ export class RichTextCell extends BaseRichTextCell {
.attributesSchema=${this.attributesSchema}
.attributeRenderer=${this.attributeRenderer}
.embedChecker=${this.inlineManager?.embedChecker}
.markdownShortcutHandler=${this.inlineManager?.markdownShortcutHandler}
.markdownMatches=${this.inlineManager?.markdownMatches}
.readonly=${true}
class="affine-database-rich-text inline-editor"
></rich-text>`
@@ -525,7 +525,7 @@ export class RichTextCellEditing extends BaseRichTextCell {
.attributesSchema=${this.attributesSchema}
.attributeRenderer=${this.attributeRenderer}
.embedChecker=${this.inlineManager?.embedChecker}
.markdownShortcutHandler=${this.inlineManager?.markdownShortcutHandler}
.markdownMatches=${this.inlineManager?.markdownMatches}
.verticalScrollContainerGetter=${() =>
this.topContenteditableElement?.host
? getViewportElement(this.topContenteditableElement.host)

View File

@@ -187,7 +187,7 @@ export class HeaderAreaTextCell extends BaseTextCell {
.attributesSchema="${this.attributesSchema}"
.attributeRenderer="${this.attributeRenderer}"
.embedChecker="${this.inlineManager?.embedChecker}"
.markdownShortcutHandler="${this.inlineManager?.markdownShortcutHandler}"
.markdownMatches="${this.inlineManager?.markdownMatches}"
.readonly="${true}"
class="data-view-header-area-rich-text"
></rich-text>`;
@@ -391,7 +391,7 @@ export class HeaderAreaTextCellEditing extends BaseTextCell {
.attributesSchema="${this.attributesSchema}"
.attributeRenderer="${this.attributeRenderer}"
.embedChecker="${this.inlineManager?.embedChecker}"
.markdownShortcutHandler="${this.inlineManager?.markdownShortcutHandler}"
.markdownMatches="${this.inlineManager?.markdownMatches}"
.readonly="${this.readonly}"
.enableClipboard="${false}"
.verticalScrollContainerGetter="${() =>

View File

@@ -85,10 +85,6 @@ export class ListBlockComponent extends CaptionedBlockComponent<ListBlockModel>
return this.std.get(DefaultInlineManagerExtension.identifier);
}
get markdownShortcutHandler() {
return this.inlineManager.markdownShortcutHandler;
}
override get topContenteditableElement() {
if (this.std.get(DocModeProvider).getEditorMode() === 'edgeless') {
return this.closest<BlockComponent>(NOTE_SELECTOR);
@@ -193,7 +189,7 @@ export class ListBlockComponent extends CaptionedBlockComponent<ListBlockModel>
.undoManager=${this.doc.history}
.attributeRenderer=${this.attributeRenderer}
.attributesSchema=${this.attributesSchema}
.markdownShortcutHandler=${this.markdownShortcutHandler}
.markdownMatches=${this.inlineManager?.markdownMatches}
.embedChecker=${this.embedChecker}
.readonly=${this.doc.readonly}
.inlineRangeProvider=${this._inlineRangeProvider}

View File

@@ -90,10 +90,6 @@ export class ParagraphBlockComponent extends CaptionedBlockComponent<
return this.std.get(DefaultInlineManagerExtension.identifier);
}
get markdownShortcutHandler() {
return this.inlineManager.markdownShortcutHandler;
}
override get topContenteditableElement() {
if (this.std.get(DocModeProvider).getEditorMode() === 'edgeless') {
return this.closest<BlockComponent>(NOTE_SELECTOR);
@@ -294,7 +290,7 @@ export class ParagraphBlockComponent extends CaptionedBlockComponent<
.undoManager=${this.doc.history}
.attributesSchema=${this.attributesSchema}
.attributeRenderer=${this.attributeRenderer}
.markdownShortcutHandler=${this.markdownShortcutHandler}
.markdownMatches=${this.inlineManager?.markdownMatches}
.embedChecker=${this.embedChecker}
.readonly=${this.doc.readonly}
.inlineRangeProvider=${this._inlineRangeProvider}

View File

@@ -745,8 +745,7 @@ export class TableCell extends SignalWatcher(
.attributesSchema="${this.inlineManager?.getSchema()}"
.attributeRenderer="${this.inlineManager?.getRenderer()}"
.embedChecker="${this.inlineManager?.embedChecker}"
.markdownShortcutHandler="${this.inlineManager
?.markdownShortcutHandler}"
.markdownMatches="${this.inlineManager?.markdownMatches}"
.readonly="${this.readonly}"
.enableClipboard="${true}"
.verticalScrollContainerGetter="${() =>

View File

@@ -9,11 +9,8 @@ import {
baseTextAttributes,
type DeltaInsert,
getDefaultAttributeRenderer,
KEYBOARD_ALLOW_DEFAULT,
type KeyboardBindingContext,
} from '@blocksuite/inline';
import type { ExtensionType } from '@blocksuite/store';
import type * as Y from 'yjs';
import { z, type ZodObject, type ZodTypeAny } from 'zod';
import { MarkdownMatcherIdentifier } from './markdown-matcher.js';
@@ -61,27 +58,6 @@ export class InlineManager {
return schema;
};
markdownShortcutHandler = (
context: KeyboardBindingContext<AffineTextAttributes>,
undoManager: Y.UndoManager
) => {
const { inlineEditor, prefixText, inlineRange } = context;
for (const match of this.markdownMatches) {
const matchedText = prefixText.match(match.pattern);
if (matchedText) {
return match.action({
inlineEditor,
prefixText,
inlineRange,
pattern: match.pattern,
undoManager,
});
}
}
return KEYBOARD_ALLOW_DEFAULT;
};
readonly specs: Array<InlineSpecs<AffineTextAttributes>>;
constructor(

View File

@@ -4,7 +4,6 @@ import type {
DeltaInsert,
InlineEditor,
InlineRange,
KeyboardBindingHandler,
} from '@blocksuite/inline';
import type * as Y from 'yjs';
import type { ZodTypeAny } from 'zod';
@@ -28,7 +27,7 @@ export type InlineMarkdownMatchAction<
inlineRange: InlineRange;
pattern: RegExp;
undoManager: Y.UndoManager;
}) => ReturnType<KeyboardBindingHandler>;
}) => void;
export type InlineMarkdownMatch<
AffineTextAttributes extends BaseTextAttributes = BaseTextAttributes,

View File

@@ -1,8 +1,4 @@
import type { BlockComponent } from '@blocksuite/block-std';
import {
KEYBOARD_ALLOW_DEFAULT,
KEYBOARD_PREVENT_DEFAULT,
} from '@blocksuite/inline';
import type { ExtensionType } from '@blocksuite/store';
import { InlineMarkdownExtension } from '../../extension/markdown-matcher.js';
@@ -16,14 +12,13 @@ import { InlineMarkdownExtension } from '../../extension/markdown-matcher.js';
export const BoldItalicMarkdown = InlineMarkdownExtension({
name: 'bolditalic',
pattern: /(?:\*\*\*)([^\s*](?:[^*]*?[^\s*])?)(?:\*\*\*)$/g,
pattern: /.*\*{3}([^\s*][^*]*[^\s*])\*{3}$|.*\*{3}([^\s*])\*{3}$/,
action: ({ inlineEditor, prefixText, inlineRange, pattern, undoManager }) => {
const match = pattern.exec(prefixText);
if (!match) {
return KEYBOARD_ALLOW_DEFAULT;
}
const match = prefixText.match(pattern);
if (!match) return;
const annotatedText = match[0];
const targetText = match[1] ?? match[2];
const annotatedText = match[0].slice(-targetText.length - 3 * 2);
const startIndex = inlineRange.index - annotatedText.length;
inlineEditor.insertText(
@@ -68,20 +63,18 @@ export const BoldItalicMarkdown = InlineMarkdownExtension({
index: startIndex + annotatedText.length - 6,
length: 0,
});
return KEYBOARD_PREVENT_DEFAULT;
},
});
export const BoldMarkdown = InlineMarkdownExtension({
name: 'bold',
pattern: /(?:\*\*)([^\s*](?:[^*]*?[^\s*])?)(?:\*\*)$/g,
pattern: /.*\*{2}([^\s][^*]*[^\s*])\*{2}$|.*\*{2}([^\s*])\*{2}$/,
action: ({ inlineEditor, prefixText, inlineRange, pattern, undoManager }) => {
const match = pattern.exec(prefixText);
if (!match) {
return KEYBOARD_ALLOW_DEFAULT;
}
const annotatedText = match[0];
const match = prefixText.match(pattern);
if (!match) return;
const targetText = match[1] ?? match[2];
const annotatedText = match[0].slice(-targetText.length - 2 * 2);
const startIndex = inlineRange.index - annotatedText.length;
inlineEditor.insertText(
@@ -125,20 +118,18 @@ export const BoldMarkdown = InlineMarkdownExtension({
index: startIndex + annotatedText.length - 4,
length: 0,
});
return KEYBOARD_PREVENT_DEFAULT;
},
});
export const ItalicExtension = InlineMarkdownExtension({
name: 'italic',
pattern: /(?:\*)([^\s*](?:[^*]*?[^\s*])?)(?:\*)$/g,
pattern: /.*\*{1}([^\s][^*]*[^\s*])\*{1}$|.*\*{1}([^\s*])\*{1}$/,
action: ({ inlineEditor, prefixText, inlineRange, pattern, undoManager }) => {
const match = pattern.exec(prefixText);
if (!match) {
return KEYBOARD_ALLOW_DEFAULT;
}
const annotatedText = match[0];
const match = prefixText.match(pattern);
if (!match) return;
const targetText = match[1] ?? match[2];
const annotatedText = match[0].slice(-targetText.length - 1 * 2);
const startIndex = inlineRange.index - annotatedText.length;
inlineEditor.insertText(
@@ -182,20 +173,18 @@ export const ItalicExtension = InlineMarkdownExtension({
index: startIndex + annotatedText.length - 2,
length: 0,
});
return KEYBOARD_PREVENT_DEFAULT;
},
});
export const StrikethroughExtension = InlineMarkdownExtension({
name: 'strikethrough',
pattern: /(?:~~)([^\s~](?:[^~]*?[^\s~])?)(?:~~)$/g,
pattern: /.*~{2}([^\s][^~]*[^\s])~{2}$|.*~{2}([^\s~])~{2}$/,
action: ({ inlineEditor, prefixText, inlineRange, pattern, undoManager }) => {
const match = pattern.exec(prefixText);
if (!match) {
return KEYBOARD_ALLOW_DEFAULT;
}
const annotatedText = match[0];
const match = prefixText.match(pattern);
if (!match) return;
const targetText = match[1] ?? match[2];
const annotatedText = match[0].slice(-targetText.length - 2 * 2);
const startIndex = inlineRange.index - annotatedText.length;
inlineEditor.insertText(
@@ -239,20 +228,18 @@ export const StrikethroughExtension = InlineMarkdownExtension({
index: startIndex + annotatedText.length - 4,
length: 0,
});
return KEYBOARD_PREVENT_DEFAULT;
},
});
export const UnderthroughExtension = InlineMarkdownExtension({
name: 'underthrough',
pattern: /(?:~)([^\s~](?:[^~]*?[^\s~])?)(?:~)$/g,
pattern: /.*~{1}([^\s][^~]*[^\s~])~{1}$|.*~{1}([^\s~])~{1}$/,
action: ({ inlineEditor, prefixText, inlineRange, pattern, undoManager }) => {
const match = pattern.exec(prefixText);
if (!match) {
return KEYBOARD_ALLOW_DEFAULT;
}
const annotatedText = match[0];
const match = prefixText.match(pattern);
if (!match) return;
const targetText = match[1] ?? match[2];
const annotatedText = match[0].slice(-targetText.length - 1 * 2);
const startIndex = inlineRange.index - annotatedText.length;
inlineEditor.insertText(
@@ -296,25 +283,19 @@ export const UnderthroughExtension = InlineMarkdownExtension({
index: startIndex + annotatedText.length - 2,
length: 0,
});
return KEYBOARD_PREVENT_DEFAULT;
},
});
export const CodeExtension = InlineMarkdownExtension({
name: 'code',
pattern: /(?:`)([^\s`](?:[^`]*?[^\s`])?)(?:`)$/g,
pattern: /.*`([^\s][^`]*[^\s])`$|.*`([^\s`])`$/,
action: ({ inlineEditor, prefixText, inlineRange, pattern, undoManager }) => {
const match = pattern.exec(prefixText);
if (!match) {
return KEYBOARD_ALLOW_DEFAULT;
}
const annotatedText = match[0];
const startIndex = inlineRange.index - annotatedText.length;
const match = prefixText.match(pattern);
if (!match) return;
if (prefixText.match(/^([* \n]+)$/g)) {
return KEYBOARD_ALLOW_DEFAULT;
}
const targetText = match[1] ?? match[2];
const annotatedText = match[0].slice(-targetText.length - 1 * 2);
const startIndex = inlineRange.index - annotatedText.length;
inlineEditor.insertText(
{
@@ -357,23 +338,20 @@ export const CodeExtension = InlineMarkdownExtension({
index: startIndex + annotatedText.length - 2,
length: 0,
});
return KEYBOARD_PREVENT_DEFAULT;
},
});
export const LinkExtension = InlineMarkdownExtension({
name: 'link',
pattern: /(?:\[(.+?)\])(?:\((.+?)\))$/g,
pattern: /.*\[(.+?)\]\((.+?)\)$/,
action: ({ inlineEditor, prefixText, inlineRange, pattern, undoManager }) => {
const startIndex = prefixText.search(pattern);
const matchedText = prefixText.match(pattern)?.[0];
const hrefText = prefixText.match(/(?:\[(.*?)\])/g)?.[0];
const hrefLink = prefixText.match(/(?:\((.*?)\))/g)?.[0];
if (startIndex === -1 || !matchedText || !hrefText || !hrefLink) {
return KEYBOARD_ALLOW_DEFAULT;
}
const start = inlineRange.index - matchedText.length;
const match = prefixText.match(pattern);
if (!match) return;
const linkText = match[1];
const linkUrl = match[2];
const annotatedText = match[0].slice(-linkText.length - linkUrl.length - 4);
const startIndex = inlineRange.index - annotatedText.length;
inlineEditor.insertText(
{
@@ -389,35 +367,37 @@ export const LinkExtension = InlineMarkdownExtension({
undoManager.stopCapturing();
// aaa[bbb](baidu.com) + space
// delete (baidu.com) + space
inlineEditor.deleteText({
index: startIndex + 1 + linkText.length + 1,
length: 1 + linkUrl.length + 1 + 1,
});
// delete [ and ]
inlineEditor.deleteText({
index: startIndex + 1 + linkText.length,
length: 1,
});
inlineEditor.deleteText({
index: startIndex,
length: 1,
});
inlineEditor.formatText(
{
index: start,
length: hrefText.length,
index: startIndex,
length: linkText.length,
},
{
link: hrefLink.slice(1, hrefLink.length - 1),
link: linkUrl,
}
);
inlineEditor.deleteText({
index: inlineRange.index + matchedText.length,
length: 1,
});
inlineEditor.deleteText({
index: inlineRange.index - hrefLink.length - 1,
length: hrefLink.length + 1,
});
inlineEditor.deleteText({
index: start,
length: 1,
});
inlineEditor.setInlineRange({
index: start + hrefText.length - 1,
index: startIndex + linkText.length,
length: 0,
});
return KEYBOARD_PREVENT_DEFAULT;
},
});
@@ -428,9 +408,7 @@ export const LatexExtension = InlineMarkdownExtension({
/(?:\$\$)(?<content>[^$]+)(?:\$\$)$|(?<blockPrefix>\$\$\$\$)|(?<inlinePrefix>\$\$)$/g,
action: ({ inlineEditor, prefixText, inlineRange, pattern, undoManager }) => {
const match = pattern.exec(prefixText);
if (!match || !match.groups) {
return KEYBOARD_ALLOW_DEFAULT;
}
if (!match || !match.groups) return;
const content = match.groups['content'];
const inlinePrefix = match.groups['inlinePrefix'];
const blockPrefix = match.groups['blockPrefix'];
@@ -450,19 +428,19 @@ export const LatexExtension = InlineMarkdownExtension({
undoManager.stopCapturing();
if (!inlineEditor.rootElement) return KEYBOARD_ALLOW_DEFAULT;
if (!inlineEditor.rootElement) return;
const blockComponent =
inlineEditor.rootElement.closest<BlockComponent>('[data-block-id]');
if (!blockComponent) return KEYBOARD_ALLOW_DEFAULT;
if (!blockComponent) return;
const doc = blockComponent.doc;
const parentComponent = blockComponent.parentComponent;
if (!parentComponent) return KEYBOARD_ALLOW_DEFAULT;
if (!parentComponent) return;
const index = parentComponent.model.children.indexOf(
blockComponent.model
);
if (index === -1) return KEYBOARD_ALLOW_DEFAULT;
if (index === -1) return;
inlineEditor.deleteText({
index: inlineRange.index - 4,
@@ -488,7 +466,7 @@ export const LatexExtension = InlineMarkdownExtension({
})
.catch(console.error);
return KEYBOARD_PREVENT_DEFAULT;
return;
}
if (inlinePrefix === '$$') {
@@ -545,12 +523,10 @@ export const LatexExtension = InlineMarkdownExtension({
})
.catch(console.error);
return KEYBOARD_PREVENT_DEFAULT;
return;
}
if (!content || content.length === 0) {
return KEYBOARD_ALLOW_DEFAULT;
}
if (!content || content.length === 0) return;
inlineEditor.insertText(
{
@@ -592,8 +568,6 @@ export const LatexExtension = InlineMarkdownExtension({
index: startIndex + 1,
length: 0,
});
return KEYBOARD_PREVENT_DEFAULT;
},
});

View File

@@ -3,12 +3,10 @@ import { ShadowlessElement } from '@blocksuite/block-std';
import { assertExists, WithDisposable } from '@blocksuite/global/utils';
import {
type AttributeRenderer,
createInlineKeyDownHandler,
type DeltaInsert,
InlineEditor,
type InlineRange,
type InlineRangeProvider,
type KeyboardBindingContext,
type VLine,
} from '@blocksuite/inline';
import { Text } from '@blocksuite/store';
@@ -19,6 +17,7 @@ import { classMap } from 'lit/directives/class-map.js';
import * as Y from 'yjs';
import { z } from 'zod';
import type { InlineMarkdownMatch } from './extension/type.js';
import { onVBeforeinput, onVCompositionEnd } from './hooks.js';
import type { AffineInlineEditor } from './inline/index.js';
@@ -181,20 +180,40 @@ export class RichText extends WithDisposable(ShadowlessElement) {
}
const inlineEditor = this._inlineEditor;
const markdownShortcutHandler = this.markdownShortcutHandler;
if (markdownShortcutHandler) {
const keyDownHandler = createInlineKeyDownHandler(inlineEditor, {
inputRule: {
key: [' ', 'Enter'],
handler: context =>
markdownShortcutHandler(context, this.undoManager),
},
});
const markdownMatches = this.markdownMatches;
if (markdownMatches) {
inlineEditor.disposables.addFromEvent(
this.inlineEventSource ?? this.inlineEditorContainer,
'keydown',
keyDownHandler
(e: KeyboardEvent) => {
if (e.key !== ' ' && e.key !== 'Enter') return;
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
);
for (const match of markdownMatches) {
const { pattern, action } = match;
if (prefixText.match(pattern)) {
action({
inlineEditor,
prefixText,
inlineRange,
pattern,
undoManager: this.undoManager,
});
e.preventDefault();
break;
}
}
}
);
}
@@ -409,12 +428,7 @@ export class RichText extends WithDisposable(ShadowlessElement) {
accessor inlineRangeProvider: InlineRangeProvider | undefined = undefined;
@property({ attribute: false })
accessor markdownShortcutHandler:
| (<TextAttributes extends AffineTextAttributes = AffineTextAttributes>(
context: KeyboardBindingContext<TextAttributes>,
undoManager: Y.UndoManager
) => boolean)
| undefined = undefined;
accessor markdownMatches: InlineMarkdownMatch<AffineTextAttributes>[] = [];
@property({ attribute: false })
accessor readonly = false;

View File

@@ -3,7 +3,6 @@ export * from './base-attributes.js';
export * from './delta-convert.js';
export * from './embed.js';
export * from './guard.js';
export * from './keyboard.js';
export * from './point-conversion.js';
export * from './query.js';
export * from './range-conversion.js';

View File

@@ -1,135 +0,0 @@
import { IS_IOS, IS_MAC } from '@blocksuite/global/env';
import type { InlineEditor } from '../inline-editor.js';
import type { InlineRange } from '../types.js';
import type { BaseTextAttributes } from './base-attributes.js';
const SHORT_KEY_PROPERTY = IS_IOS || IS_MAC ? 'metaKey' : 'ctrlKey';
export const KEYBOARD_PREVENT_DEFAULT = false;
export const KEYBOARD_ALLOW_DEFAULT = true;
export interface KeyboardBinding {
key: number | string | string[];
handler: KeyboardBindingHandler;
prefix?: RegExp;
suffix?: RegExp;
shortKey?: boolean;
shiftKey?: boolean;
altKey?: boolean;
metaKey?: boolean;
ctrlKey?: boolean;
}
export type KeyboardBindingRecord = Record<string, KeyboardBinding>;
export interface KeyboardBindingContext<
TextAttributes extends BaseTextAttributes = BaseTextAttributes,
> {
inlineRange: InlineRange;
inlineEditor: InlineEditor<TextAttributes>;
collapsed: boolean;
prefixText: string;
suffixText: string;
raw: KeyboardEvent;
}
export type KeyboardBindingHandler = (
context: KeyboardBindingContext
) => typeof KEYBOARD_PREVENT_DEFAULT | typeof KEYBOARD_ALLOW_DEFAULT;
export function createInlineKeyDownHandler(
inlineEditor: InlineEditor,
bindings: KeyboardBindingRecord
): (evt: KeyboardEvent) => void {
const bindingStore: Record<string, KeyboardBinding[]> = {};
function normalize(binding: KeyboardBinding): KeyboardBinding {
if (binding.shortKey) {
binding[SHORT_KEY_PROPERTY] = binding.shortKey;
delete binding.shortKey;
}
return binding;
}
function keyMatch(evt: KeyboardEvent, binding: KeyboardBinding) {
if (
(['altKey', 'ctrlKey', 'metaKey', 'shiftKey'] as const).some(
key => Object.hasOwn(binding, key) && binding[key] !== evt[key]
)
) {
return false;
}
return binding.key === evt.key;
}
function addBinding(keyBinding: KeyboardBinding) {
const binding = normalize(keyBinding);
const keys = Array.isArray(binding.key) ? binding.key : [binding.key];
keys.forEach(key => {
const singleBinding = {
...binding,
key,
};
bindingStore[key] = bindingStore[key] ?? [];
bindingStore[key].push(singleBinding);
});
}
Object.values(bindings).forEach(binding => {
addBinding(binding);
});
function keyDownHandler(evt: KeyboardEvent) {
if (evt.defaultPrevented || evt.isComposing) return;
const keyBindings = bindingStore[evt.key] ?? [];
const keyMatches = keyBindings.filter(binding => keyMatch(evt, binding));
if (keyMatches.length === 0) return;
const inlineRange = inlineEditor.getInlineRange();
if (!inlineRange) return;
const startTextPoint = inlineEditor.getTextPoint(inlineRange.index);
if (!startTextPoint) return;
const [leafStart, offsetStart] = startTextPoint;
let leafEnd: Text;
let offsetEnd: number;
if (inlineRange.length === 0) {
leafEnd = leafStart;
offsetEnd = offsetStart;
} else {
const endTextPoint = inlineEditor.getTextPoint(
inlineRange.index + inlineRange.length
);
if (!endTextPoint) return;
[leafEnd, offsetEnd] = endTextPoint;
}
const prefixText = leafStart.textContent
? leafStart.textContent.slice(0, offsetStart)
: '';
const suffixText = leafEnd.textContent
? leafEnd.textContent.slice(offsetEnd)
: '';
const currContext: KeyboardBindingContext = {
inlineRange,
inlineEditor: inlineEditor,
collapsed: inlineRange.length === 0,
prefixText,
suffixText,
raw: evt,
};
const prevented = keyMatches.some(binding => {
if (binding.prefix && !binding.prefix.test(currContext.prefixText)) {
return false;
}
if (binding.suffix && !binding.suffix.test(currContext.suffixText)) {
return false;
}
return binding.handler(currContext) === KEYBOARD_PREVENT_DEFAULT;
});
if (prevented) {
evt.preventDefault();
}
}
return keyDownHandler;
}

View File

@@ -1,376 +0,0 @@
import {
type InlineEditor,
type InlineRange,
KEYBOARD_ALLOW_DEFAULT,
KEYBOARD_PREVENT_DEFAULT,
} from '@blocksuite/inline';
import type * as Y from 'yjs';
interface MarkdownMatch {
name: string;
pattern: RegExp;
action: (props: {
inlineEditor: InlineEditor;
prefixText: string;
inlineRange: InlineRange;
pattern: RegExp;
undoManager: Y.UndoManager;
}) => boolean;
}
export const markdownMatches: MarkdownMatch[] = [
{
name: 'bolditalic',
pattern: /(?:\*){3}([^* \n](.+?[^* \n])?)(?:\*){3}$/g,
action: ({
inlineEditor,
prefixText,
inlineRange,
pattern,
undoManager,
}) => {
const match = pattern.exec(prefixText);
if (!match) {
return KEYBOARD_ALLOW_DEFAULT;
}
const annotatedText = match[0];
const startIndex = inlineRange.index - annotatedText.length;
inlineEditor.insertText(
{
index: startIndex + annotatedText.length,
length: 0,
},
' '
);
undoManager.stopCapturing();
inlineEditor.formatText(
{
index: startIndex,
length: annotatedText.length,
},
{
bold: true,
italic: true,
}
);
inlineEditor.deleteText({
index: startIndex + annotatedText.length,
length: 1,
});
inlineEditor.deleteText({
index: startIndex + annotatedText.length - 3,
length: 3,
});
inlineEditor.deleteText({
index: startIndex,
length: 3,
});
inlineEditor.setInlineRange({
index: startIndex + annotatedText.length - 6,
length: 0,
});
return KEYBOARD_PREVENT_DEFAULT;
},
},
{
name: 'bold',
pattern: /(?:\*){2}([^* \n](.+?[^* \n])?)(?:\*){2}$/g,
action: ({
inlineEditor,
prefixText,
inlineRange,
pattern,
undoManager,
}) => {
const match = pattern.exec(prefixText);
if (!match) {
return KEYBOARD_ALLOW_DEFAULT;
}
const annotatedText = match[0];
const startIndex = inlineRange.index - annotatedText.length;
inlineEditor.insertText(
{
index: startIndex + annotatedText.length,
length: 0,
},
' '
);
undoManager.stopCapturing();
inlineEditor.formatText(
{
index: startIndex,
length: annotatedText.length,
},
{
bold: true,
}
);
inlineEditor.deleteText({
index: startIndex + annotatedText.length,
length: 1,
});
inlineEditor.deleteText({
index: startIndex + annotatedText.length - 2,
length: 2,
});
inlineEditor.deleteText({
index: startIndex,
length: 2,
});
inlineEditor.setInlineRange({
index: startIndex + annotatedText.length - 4,
length: 0,
});
return KEYBOARD_PREVENT_DEFAULT;
},
},
{
name: 'italic',
pattern: /(?:\*){1}([^* \n](.+?[^* \n])?)(?:\*){1}$/g,
action: ({
inlineEditor,
prefixText,
inlineRange,
pattern,
undoManager,
}) => {
const match = pattern.exec(prefixText);
if (!match) {
return KEYBOARD_ALLOW_DEFAULT;
}
const annotatedText = match[0];
const startIndex = inlineRange.index - annotatedText.length;
inlineEditor.insertText(
{
index: startIndex + annotatedText.length,
length: 0,
},
' '
);
undoManager.stopCapturing();
inlineEditor.formatText(
{
index: startIndex,
length: annotatedText.length,
},
{
italic: true,
}
);
inlineEditor.deleteText({
index: startIndex + annotatedText.length,
length: 1,
});
inlineEditor.deleteText({
index: startIndex + annotatedText.length - 1,
length: 1,
});
inlineEditor.deleteText({
index: startIndex,
length: 1,
});
inlineEditor.setInlineRange({
index: startIndex + annotatedText.length - 2,
length: 0,
});
return KEYBOARD_PREVENT_DEFAULT;
},
},
{
name: 'strikethrough',
pattern: /(?:~~)([^~ \n](.+?[^~ \n])?)(?:~~)$/g,
action: ({
inlineEditor,
prefixText,
inlineRange,
pattern,
undoManager,
}) => {
const match = pattern.exec(prefixText);
if (!match) {
return KEYBOARD_ALLOW_DEFAULT;
}
const annotatedText = match[0];
const startIndex = inlineRange.index - annotatedText.length;
inlineEditor.insertText(
{
index: startIndex + annotatedText.length,
length: 0,
},
' '
);
undoManager.stopCapturing();
inlineEditor.formatText(
{
index: startIndex,
length: annotatedText.length,
},
{
strike: true,
}
);
inlineEditor.deleteText({
index: startIndex + annotatedText.length,
length: 1,
});
inlineEditor.deleteText({
index: startIndex + annotatedText.length - 2,
length: 2,
});
inlineEditor.deleteText({
index: startIndex,
length: 2,
});
inlineEditor.setInlineRange({
index: startIndex + annotatedText.length - 4,
length: 0,
});
return KEYBOARD_PREVENT_DEFAULT;
},
},
{
name: 'underthrough',
pattern: /(?:~)([^~ \n](.+?[^~ \n])?)(?:~)$/g,
action: ({
inlineEditor,
prefixText,
inlineRange,
pattern,
undoManager,
}) => {
const match = pattern.exec(prefixText);
if (!match) {
return KEYBOARD_ALLOW_DEFAULT;
}
const annotatedText = match[0];
const startIndex = inlineRange.index - annotatedText.length;
inlineEditor.insertText(
{
index: startIndex + annotatedText.length,
length: 0,
},
' '
);
undoManager.stopCapturing();
inlineEditor.formatText(
{
index: startIndex,
length: annotatedText.length,
},
{
underline: true,
}
);
inlineEditor.deleteText({
index: startIndex + annotatedText.length,
length: 1,
});
inlineEditor.deleteText({
index: inlineRange.index - 1,
length: 1,
});
inlineEditor.deleteText({
index: startIndex,
length: 1,
});
inlineEditor.setInlineRange({
index: startIndex + annotatedText.length - 2,
length: 0,
});
return KEYBOARD_PREVENT_DEFAULT;
},
},
{
name: 'code',
pattern: /(?:`)(`{2,}?|[^`]+)(?:`)$/g,
action: ({
inlineEditor,
prefixText,
inlineRange,
pattern,
undoManager,
}) => {
const match = pattern.exec(prefixText);
if (!match) {
return KEYBOARD_ALLOW_DEFAULT;
}
const annotatedText = match[0];
const startIndex = inlineRange.index - annotatedText.length;
if (prefixText.match(/^([* \n]+)$/g)) {
return KEYBOARD_ALLOW_DEFAULT;
}
inlineEditor.insertText(
{
index: startIndex + annotatedText.length,
length: 0,
},
' '
);
undoManager.stopCapturing();
inlineEditor.formatText(
{
index: startIndex,
length: annotatedText.length,
},
{
code: true,
}
);
inlineEditor.deleteText({
index: startIndex + annotatedText.length,
length: 1,
});
inlineEditor.deleteText({
index: startIndex + annotatedText.length - 1,
length: 1,
});
inlineEditor.deleteText({
index: startIndex,
length: 1,
});
inlineEditor.setInlineRange({
index: startIndex + annotatedText.length - 2,
length: 0,
});
return KEYBOARD_PREVENT_DEFAULT;
},
},
];

View File

@@ -5,9 +5,7 @@ import {
type AttributeRenderer,
type BaseTextAttributes,
baseTextAttributes,
createInlineKeyDownHandler,
InlineEditor,
KEYBOARD_ALLOW_DEFAULT,
ZERO_WIDTH_NON_JOINER,
} from '@blocksuite/inline';
import { effects } from '@blocksuite/inline/effects';
@@ -18,8 +16,6 @@ import { styleMap } from 'lit/directives/style-map.js';
import * as Y from 'yjs';
import { z } from 'zod';
import { markdownMatches } from './markdown.js';
effects();
function inlineTextStyles(
@@ -132,30 +128,6 @@ export class TestRichText extends ShadowlessElement {
this.style.outline = 'none';
this.inlineEditor.mount(this._container, this);
const keydownHandler = createInlineKeyDownHandler(this.inlineEditor, {
inputRule: {
key: ' ',
handler: context => {
const { inlineEditor, prefixText, inlineRange } = context;
for (const match of markdownMatches) {
const matchedText = prefixText.match(match.pattern);
if (matchedText) {
return match.action({
inlineEditor,
prefixText,
inlineRange,
pattern: match.pattern,
undoManager: this.undoManager,
});
}
}
return KEYBOARD_ALLOW_DEFAULT;
},
},
});
this.addEventListener('keydown', keydownHandler);
this.inlineEditor.slots.textChange.on(() => {
const el = this.querySelector('.y-text');
if (el) {

View File

@@ -1106,31 +1106,6 @@ test('delete embed when pressing backspace after embed', async ({ page }) => {
]);
});
test('markdown shortcut using keyboard util', async ({ page }) => {
await enterInlineEditorPlayground(page);
await focusInlineRichText(page);
await page.waitForTimeout(100);
await type(page, 'aaa**bbb** ccc');
const delta = await getDeltaFromInlineRichText(page);
expect(delta).toEqual([
{
insert: 'aaa',
},
{
insert: 'bbb',
attributes: {
bold: true,
},
},
{
insert: 'ccc',
},
]);
});
test('triple click to select line', async ({ page }) => {
await enterInlineEditorPlayground(page);
await focusInlineRichText(page);

View File

@@ -4,6 +4,7 @@ import {
getCursorBlockIdAndHeight,
initEmptyParagraphState,
pressArrowLeft,
pressArrowRight,
pressBackspace,
pressEnter,
pressSpace,
@@ -228,6 +229,24 @@ test.describe('markdown inline-text', () => {
});
test('bolditalic', async ({ page }) => {
await type(page, 'aa***b*** ');
await assertRichTextInlineDeltas(page, [
{
insert: 'aa',
},
{
insert: 'b',
attributes: {
bold: true,
italic: true,
},
},
]);
await undoByKeyboard(page);
await undoByKeyboard(page);
await undoByKeyboard(page);
await assertRichTexts(page, ['']);
await type(page, 'aa***bb*** ');
await assertRichTextInlineDeltas(page, [
{
@@ -282,6 +301,23 @@ test.describe('markdown inline-text', () => {
});
test('bold', async ({ page }) => {
await type(page, 'aa**b** ');
await assertRichTextInlineDeltas(page, [
{
insert: 'aa',
},
{
insert: 'b',
attributes: {
bold: true,
},
},
]);
await undoByKeyboard(page);
await undoByKeyboard(page);
await undoByKeyboard(page);
await assertRichTexts(page, ['']);
await type(page, 'aa**bb** ');
await assertRichTextInlineDeltas(page, [
{
@@ -333,6 +369,23 @@ test.describe('markdown inline-text', () => {
});
test('italic', async ({ page }) => {
await type(page, 'aa*b* ');
await assertRichTextInlineDeltas(page, [
{
insert: 'aa',
},
{
insert: 'b',
attributes: {
italic: true,
},
},
]);
await undoByKeyboard(page);
await undoByKeyboard(page);
await undoByKeyboard(page);
await assertRichTexts(page, ['']);
await type(page, 'aa*bb* ');
await assertRichTextInlineDeltas(page, [
{
@@ -385,6 +438,23 @@ test.describe('markdown inline-text', () => {
});
test('strike', async ({ page }) => {
await type(page, 'aa~~b~~ ');
await assertRichTextInlineDeltas(page, [
{
insert: 'aa',
},
{
insert: 'b',
attributes: {
strike: true,
},
},
]);
await undoByKeyboard(page);
await undoByKeyboard(page);
await undoByKeyboard(page);
await assertRichTexts(page, ['']);
await type(page, 'aa~~bb~~ ');
await assertRichTextInlineDeltas(page, [
{
@@ -436,6 +506,23 @@ test.describe('markdown inline-text', () => {
});
test('underline', async ({ page }) => {
await type(page, 'aa~b~ ');
await assertRichTextInlineDeltas(page, [
{
insert: 'aa',
},
{
insert: 'b',
attributes: {
underline: true,
},
},
]);
await undoByKeyboard(page);
await undoByKeyboard(page);
await undoByKeyboard(page);
await assertRichTexts(page, ['']);
await type(page, 'aa~bb~ ');
await assertRichTextInlineDeltas(page, [
{
@@ -487,6 +574,23 @@ test.describe('markdown inline-text', () => {
});
test('code', async ({ page }) => {
await type(page, 'aa`b` ');
await assertRichTextInlineDeltas(page, [
{
insert: 'aa',
},
{
insert: 'b',
attributes: {
code: true,
},
},
]);
await undoByKeyboard(page);
await undoByKeyboard(page);
await undoByKeyboard(page);
await assertRichTexts(page, ['']);
await type(page, 'aa`bb` ');
await assertRichTextInlineDeltas(page, [
{
@@ -538,6 +642,40 @@ test.describe('markdown inline-text', () => {
await assertRichTexts(page, ['` test` ']);
await undoByKeyboard(page);
await assertRichTexts(page, ['']);
// https://github.com/toeverything/AFFiNE/issues/9410
await waitNextFrame(page);
await type(page, 'test**bold** ');
await assertRichTextInlineDeltas(page, [
{
insert: 'test',
},
{
insert: 'bold',
attributes: {
bold: true,
},
},
]);
await pressArrowLeft(page, 8);
await type(page, '`');
await pressArrowRight(page, 8);
await type(page, '` ');
await assertRichTextInlineDeltas(page, [
{
insert: 'test',
attributes: {
code: true,
},
},
{
insert: 'bold',
attributes: {
bold: true,
code: true,
},
},
]);
});
});