mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-02-13 21:05:19 +00:00
refactor(editor): remove inline editor keyboard utils and add markdown property in rich-text (#10375)
This commit is contained in:
@@ -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>
|
||||
`;
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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="${() =>
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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="${() =>
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
},
|
||||
},
|
||||
];
|
||||
@@ -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) {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user