diff --git a/blocksuite/affine/block-database/src/detail-panel/block-renderer.ts b/blocksuite/affine/block-database/src/detail-panel/block-renderer.ts index f6dc118f6a..2a6742ad89 100644 --- a/blocksuite/affine/block-database/src/detail-panel/block-renderer.ts +++ b/blocksuite/affine/block-database/src/detail-panel/block-renderer.ts @@ -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" > `; diff --git a/blocksuite/affine/block-database/src/properties/rich-text/cell-renderer.ts b/blocksuite/affine/block-database/src/properties/rich-text/cell-renderer.ts index fe3b7b3b1d..c4f4aea256 100644 --- a/blocksuite/affine/block-database/src/properties/rich-text/cell-renderer.ts +++ b/blocksuite/affine/block-database/src/properties/rich-text/cell-renderer.ts @@ -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" >` @@ -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) diff --git a/blocksuite/affine/block-database/src/properties/title/text.ts b/blocksuite/affine/block-database/src/properties/title/text.ts index b780562609..d3bd49af45 100644 --- a/blocksuite/affine/block-database/src/properties/title/text.ts +++ b/blocksuite/affine/block-database/src/properties/title/text.ts @@ -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" >`; @@ -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="${() => diff --git a/blocksuite/affine/block-list/src/list-block.ts b/blocksuite/affine/block-list/src/list-block.ts index b6b5b36723..24955ddb65 100644 --- a/blocksuite/affine/block-list/src/list-block.ts +++ b/blocksuite/affine/block-list/src/list-block.ts @@ -85,10 +85,6 @@ export class ListBlockComponent 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(NOTE_SELECTOR); @@ -193,7 +189,7 @@ export class ListBlockComponent extends CaptionedBlockComponent .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} diff --git a/blocksuite/affine/block-paragraph/src/paragraph-block.ts b/blocksuite/affine/block-paragraph/src/paragraph-block.ts index c00f0f2990..48fb4d4ccb 100644 --- a/blocksuite/affine/block-paragraph/src/paragraph-block.ts +++ b/blocksuite/affine/block-paragraph/src/paragraph-block.ts @@ -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(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} diff --git a/blocksuite/affine/block-table/src/table-cell.ts b/blocksuite/affine/block-table/src/table-cell.ts index e5c4cff504..d2c87647c7 100644 --- a/blocksuite/affine/block-table/src/table-cell.ts +++ b/blocksuite/affine/block-table/src/table-cell.ts @@ -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="${() => diff --git a/blocksuite/affine/components/src/rich-text/extension/inline-manager.ts b/blocksuite/affine/components/src/rich-text/extension/inline-manager.ts index f97cd3be46..b5b6da37de 100644 --- a/blocksuite/affine/components/src/rich-text/extension/inline-manager.ts +++ b/blocksuite/affine/components/src/rich-text/extension/inline-manager.ts @@ -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, - 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>; constructor( diff --git a/blocksuite/affine/components/src/rich-text/extension/type.ts b/blocksuite/affine/components/src/rich-text/extension/type.ts index 79c265a145..0842f33eb2 100644 --- a/blocksuite/affine/components/src/rich-text/extension/type.ts +++ b/blocksuite/affine/components/src/rich-text/extension/type.ts @@ -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; +}) => void; export type InlineMarkdownMatch< AffineTextAttributes extends BaseTextAttributes = BaseTextAttributes, diff --git a/blocksuite/affine/components/src/rich-text/inline/presets/markdown.ts b/blocksuite/affine/components/src/rich-text/inline/presets/markdown.ts index 5527d6c22d..4c1b2d41ce 100644 --- a/blocksuite/affine/components/src/rich-text/inline/presets/markdown.ts +++ b/blocksuite/affine/components/src/rich-text/inline/presets/markdown.ts @@ -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({ /(?:\$\$)(?[^$]+)(?:\$\$)$|(?\$\$\$\$)|(?\$\$)$/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('[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; }, }); diff --git a/blocksuite/affine/components/src/rich-text/rich-text.ts b/blocksuite/affine/components/src/rich-text/rich-text.ts index fedee0aa75..acc98d86d8 100644 --- a/blocksuite/affine/components/src/rich-text/rich-text.ts +++ b/blocksuite/affine/components/src/rich-text/rich-text.ts @@ -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: - | (( - context: KeyboardBindingContext, - undoManager: Y.UndoManager - ) => boolean) - | undefined = undefined; + accessor markdownMatches: InlineMarkdownMatch[] = []; @property({ attribute: false }) accessor readonly = false; diff --git a/blocksuite/framework/inline/src/utils/index.ts b/blocksuite/framework/inline/src/utils/index.ts index beec1d2927..cac07e09d6 100644 --- a/blocksuite/framework/inline/src/utils/index.ts +++ b/blocksuite/framework/inline/src/utils/index.ts @@ -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'; diff --git a/blocksuite/framework/inline/src/utils/keyboard.ts b/blocksuite/framework/inline/src/utils/keyboard.ts deleted file mode 100644 index f7efc04052..0000000000 --- a/blocksuite/framework/inline/src/utils/keyboard.ts +++ /dev/null @@ -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; - -export interface KeyboardBindingContext< - TextAttributes extends BaseTextAttributes = BaseTextAttributes, -> { - inlineRange: InlineRange; - inlineEditor: InlineEditor; - 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 = {}; - - 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; -} diff --git a/blocksuite/playground/examples/inline/markdown.ts b/blocksuite/playground/examples/inline/markdown.ts deleted file mode 100644 index 68fd4acb0a..0000000000 --- a/blocksuite/playground/examples/inline/markdown.ts +++ /dev/null @@ -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; - }, - }, -]; diff --git a/blocksuite/playground/examples/inline/test-page.ts b/blocksuite/playground/examples/inline/test-page.ts index 1ee7366d69..f1cefc121a 100644 --- a/blocksuite/playground/examples/inline/test-page.ts +++ b/blocksuite/playground/examples/inline/test-page.ts @@ -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) { diff --git a/blocksuite/tests-legacy/e2e/inline/inline-editor.spec.ts b/blocksuite/tests-legacy/e2e/inline/inline-editor.spec.ts index 1196a2a6b3..2639a410e7 100644 --- a/blocksuite/tests-legacy/e2e/inline/inline-editor.spec.ts +++ b/blocksuite/tests-legacy/e2e/inline/inline-editor.spec.ts @@ -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); diff --git a/blocksuite/tests-legacy/e2e/markdown.spec.ts b/blocksuite/tests-legacy/e2e/markdown.spec.ts index 7163b5f17f..7d5f42d915 100644 --- a/blocksuite/tests-legacy/e2e/markdown.spec.ts +++ b/blocksuite/tests-legacy/e2e/markdown.spec.ts @@ -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, + }, + }, + ]); }); });