refactor(core): minimize comment editor (#12995)

#### PR Dependency Tree


* **PR #12995** 👈

This tree was auto-generated by
[Charcoal](https://github.com/danerwilliams/charcoal)

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

* **New Features**
* Introduced a new clipboard module, making clipboard-related
functionality available for external use.
* Added a comprehensive extension system for the comment editor,
supporting rich text features, widgets, and configurable options.

* **Bug Fixes**
* Improved stability by ensuring comment highlighting features and
toolbar event subscriptions handle missing dependencies gracefully,
preventing potential runtime errors.

* **Refactor**
* Simplified comment editor view manager setup for easier configuration
and maintenance.

* **Chores**
* Updated package exports to expose new clipboard modules and
configurations.
* Removed confirm modal and portal-related logic from the comment editor
component.
* Adjusted temporary store creation to omit adding an extra surface
block under the root page.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
L-Sun
2025-07-03 12:21:28 +08:00
committed by GitHub
parent 5d8ee51e8c
commit 32c40bbf09
17 changed files with 238 additions and 67 deletions

View File

@@ -285,6 +285,7 @@
"./sync": "./src/sync/index.ts",
"./extensions/store": "./src/extensions/store.ts",
"./extensions/view": "./src/extensions/view.ts",
"./foundation/clipboard": "./src/foundation/clipboard.ts",
"./foundation/store": "./src/foundation/store.ts",
"./foundation/view": "./src/foundation/view.ts"
},

View File

@@ -0,0 +1 @@
export * from '@blocksuite/affine-foundation/clipboard';

View File

@@ -94,9 +94,11 @@ export class AttachmentBlockComponent extends CaptionedBlockComponent<Attachment
}
get isCommentHighlighted() {
return this.std
.get(BlockCommentManager)
.isBlockCommentHighlighted(this.model);
return (
this.std
.getOptional(BlockCommentManager)
?.isBlockCommentHighlighted(this.model) ?? false
);
}
convertTo = () => {

View File

@@ -130,9 +130,11 @@ export class BookmarkBlockComponent extends CaptionedBlockComponent<BookmarkBloc
}
get isCommentHighlighted() {
return this.std
.get(BlockCommentManager)
.isBlockCommentHighlighted(this.model);
return (
this.std
.getOptional(BlockCommentManager)
?.isBlockCommentHighlighted(this.model) ?? false
);
}
handleClick = (event: MouseEvent) => {

View File

@@ -392,9 +392,11 @@ export class CodeBlockComponent extends CaptionedBlockComponent<CodeBlockModel>
}
get isCommentHighlighted() {
return this.std
.get(BlockCommentManager)
.isBlockCommentHighlighted(this.model);
return (
this.std
.getOptional(BlockCommentManager)
?.isBlockCommentHighlighted(this.model) ?? false
);
}
override async getUpdateComplete() {

View File

@@ -313,9 +313,11 @@ export class DatabaseBlockComponent extends CaptionedBlockComponent<DatabaseBloc
}
get isCommentHighlighted() {
return this.std
.get(BlockCommentManager)
.isBlockCommentHighlighted(this.model);
return (
this.std
.getOptional(BlockCommentManager)
?.isBlockCommentHighlighted(this.model) ?? false
);
}
override get topContenteditableElement() {

View File

@@ -63,9 +63,11 @@ export class EmbedBlockComponent<
protected embedContainerStyle: StyleInfo = {};
get isCommentHighlighted() {
return this.std
.get(BlockCommentManager)
.isBlockCommentHighlighted(this.model);
return (
this.std
.getOptional(BlockCommentManager)
?.isBlockCommentHighlighted(this.model) ?? false
);
}
renderEmbed = (content: () => TemplateResult) => {

View File

@@ -69,9 +69,11 @@ export class ImageBlockComponent extends CaptionedBlockComponent<ImageBlockModel
}
get isCommentHighlighted() {
return this.std
.get(BlockCommentManager)
.isBlockCommentHighlighted(this.model);
return (
this.std
.getOptional(BlockCommentManager)
?.isBlockCommentHighlighted(this.model) ?? false
);
}
private _handleClick(event: MouseEvent) {

View File

@@ -109,9 +109,11 @@ export class ParagraphBlockComponent extends CaptionedBlockComponent<ParagraphBl
}
get isCommentHighlighted() {
return this.std
.get(BlockCommentManager)
.isBlockCommentHighlighted(this.model);
return (
this.std
.getOptional(BlockCommentManager)
?.isBlockCommentHighlighted(this.model) ?? false
);
}
override get topContenteditableElement() {

View File

@@ -20,6 +20,10 @@ import {
isFormatSupported,
textFormatConfigs,
} from '@blocksuite/affine-inline-preset';
import {
EmbedLinkedDocBlockSchema,
EmbedSyncedDocBlockSchema,
} from '@blocksuite/affine-model';
import { textConversionConfigs } from '@blocksuite/affine-rich-text';
import {
copySelectedModelsCommand,
@@ -50,7 +54,11 @@ import {
DuplicateIcon,
LinkedPageIcon,
} from '@blocksuite/icons/lit';
import { type BlockComponent, BlockSelection } from '@blocksuite/std';
import {
type BlockComponent,
BlockSelection,
BlockViewIdentifier,
} from '@blocksuite/std';
import { toDraftModel } from '@blocksuite/store';
import { html } from 'lit';
import { repeat } from 'lit/directives/repeat.js';
@@ -214,7 +222,18 @@ const turnIntoLinkedDoc = {
id: 'f.convert-to-linked-doc',
tooltip: 'Create Linked Doc',
icon: LinkedPageIcon(),
when({ chain }) {
when({ chain, std }) {
const supportFlavours = [
EmbedLinkedDocBlockSchema,
EmbedSyncedDocBlockSchema,
].map(schema => schema.model.flavour);
if (
supportFlavours.some(
flavour => !std.getOptional(BlockViewIdentifier(flavour))
)
)
return false;
const [ok, { selectedModels }] = chain
.pipe(getSelectedModelsCommand, {
types: ['block', 'text'],

View File

@@ -143,9 +143,11 @@ export class SurfaceRefBlockComponent extends BlockComponent<SurfaceRefBlockMode
}
get isCommentHighlighted() {
return this.std
.get(BlockCommentManager)
.isBlockCommentHighlighted(this.model);
return (
this.std
.getOptional(BlockCommentManager)
?.isBlockCommentHighlighted(this.model) ?? false
);
}
private readonly _handleClick = () => {

View File

@@ -32,6 +32,7 @@
},
"exports": {
".": "./src/index.ts",
"./clipboard": "./src/clipboard.ts",
"./store": "./src/store.ts",
"./view": "./src/view.ts"
},

View File

@@ -43,7 +43,7 @@ const imageClipboardConfigs = [
});
});
const PlainTextClipboardConfig = ClipboardAdapterConfigExtension({
export const PlainTextClipboardConfig = ClipboardAdapterConfigExtension({
mimeType: 'text/plain',
adapter: MixTextAdapter,
priority: 70,

View File

@@ -579,9 +579,11 @@ export class AffineToolbarWidget extends WidgetComponent {
);
// Handles elements when resizing
const edgelessSlots = std.get(EdgelessLegacySlotIdentifier);
disposables.add(edgelessSlots.elementResizeStart.subscribe(dragStart));
disposables.add(edgelessSlots.elementResizeEnd.subscribe(dragEnd));
const edgelessSlots = std.getOptional(EdgelessLegacySlotIdentifier);
if (edgelessSlots) {
disposables.add(edgelessSlots.elementResizeStart.subscribe(dragStart));
disposables.add(edgelessSlots.elementResizeEnd.subscribe(dragEnd));
}
// Handles elements when hovering
disposables.add(

View File

@@ -1,6 +1,4 @@
import { useConfirmModal, useLitPortalFactory } from '@affine/component';
import { LitDocEditor, type PageEditor } from '@affine/core/blocksuite/editors';
import { getViewManager } from '@affine/core/blocksuite/manager/view';
import { SnapshotHelper } from '@affine/core/modules/comment/services/snapshot-helper';
import type { RichText } from '@blocksuite/affine/rich-text';
import { ViewportElementExtension } from '@blocksuite/affine/shared/services';
@@ -10,7 +8,6 @@ import { useFramework, useService } from '@toeverything/infra';
import clsx from 'clsx';
import {
forwardRef,
Fragment,
useCallback,
useEffect,
useImperativeHandle,
@@ -19,45 +16,21 @@ import {
useState,
} from 'react';
import { getCommentEditorViewManager } from './specs';
import * as styles from './style.css';
const usePatchSpecs = (readonly: boolean) => {
const [reactToLit, portals] = useLitPortalFactory();
const framework = useFramework();
const confirmModal = useConfirmModal();
// const confirmModal = useConfirmModal();
const patchedSpecs = useMemo(() => {
const manager = getViewManager()
.config.init()
.foundation(framework)
.theme(framework)
.editorConfig(framework)
.editorView({
framework,
reactToLit,
confirmModal,
})
.linkedDoc(framework)
.paragraph(false)
.codeBlockHtmlPreview(framework).value;
const manager = getCommentEditorViewManager(framework);
return manager
.get(readonly ? 'preview-page' : 'page')
.concat([ViewportElementExtension('.comment-editor-viewport')]);
}, [confirmModal, framework, reactToLit, readonly]);
}, [framework, readonly]);
return [
patchedSpecs,
useMemo(
() => (
<>
{portals.map(p => (
<Fragment key={p.id}>{p.portal}</Fragment>
))}
</>
),
[portals]
),
] as const;
return patchedSpecs;
};
interface CommentEditorProps {
@@ -115,7 +88,7 @@ export const CommentEditor = forwardRef<CommentEditorRef, CommentEditorProps>(
if (!defaultSnapshotOrDoc) {
throw new Error('Either defaultSnapshot or doc must be provided');
}
const [specs, portals] = usePatchSpecs(!!readonly);
const specs = usePatchSpecs(!!readonly);
const doc = useSnapshotDoc(defaultSnapshotOrDoc, readonly);
const snapshotHelper = useService(SnapshotHelper);
const editorRef = useRef<PageEditor>(null);
@@ -196,7 +169,6 @@ export const CommentEditor = forwardRef<CommentEditorRef, CommentEditorProps>(
className={clsx(styles.container, 'comment-editor-viewport')}
>
{doc && <LitDocEditor ref={editorRef} specs={specs} doc={doc} />}
{portals}
{!readonly && (
<div className={styles.footer}>
<button onClick={onCommit} className={styles.commitButton}>

View File

@@ -0,0 +1,162 @@
import { createLinkedWidgetConfig } from '@affine/core/blocksuite/view-extensions/editor-config/linked';
import { AffineEditorViewExtension } from '@affine/core/blocksuite/view-extensions/editor-view/editor-view';
import { AffineThemeViewExtension } from '@affine/core/blocksuite/view-extensions/theme';
import { CodeBlockViewExtension } from '@blocksuite/affine/blocks/code/view';
import { DividerViewExtension } from '@blocksuite/affine/blocks/divider/view';
import { LatexViewExtension as LatexBlockViewExtension } from '@blocksuite/affine/blocks/latex/view';
import { ListViewExtension } from '@blocksuite/affine/blocks/list/view';
import { NoteViewExtension } from '@blocksuite/affine/blocks/note/view';
import { ParagraphViewExtension } from '@blocksuite/affine/blocks/paragraph/view';
import { RootViewExtension } from '@blocksuite/affine/blocks/root/view';
import {
PeekViewExtension,
type PeekViewService,
} from '@blocksuite/affine/components/peek';
import {
type ViewExtensionContext,
ViewExtensionManager,
ViewExtensionProvider,
} from '@blocksuite/affine/ext-loader';
import { PlainTextClipboardConfig } from '@blocksuite/affine/foundation/clipboard';
import { LatexInlineSpecExtension } from '@blocksuite/affine/inlines/latex';
import { LatexViewExtension as LatexInlineViewExtension } from '@blocksuite/affine/inlines/latex/view';
import { LinkInlineSpecExtension } from '@blocksuite/affine/inlines/link';
import { LinkViewExtension } from '@blocksuite/affine/inlines/link/view';
import { MentionInlineSpecExtension } from '@blocksuite/affine/inlines/mention';
import { MentionViewExtension } from '@blocksuite/affine/inlines/mention/view';
import {
BackgroundInlineSpecExtension,
BoldInlineSpecExtension,
CodeInlineSpecExtension,
ColorInlineSpecExtension,
InlineSpecExtensions,
ItalicInlineSpecExtension,
StrikeInlineSpecExtension,
UnderlineInlineSpecExtension,
} from '@blocksuite/affine/inlines/preset';
import { ReferenceInlineSpecExtension } from '@blocksuite/affine/inlines/reference';
import { ReferenceViewExtension } from '@blocksuite/affine/inlines/reference/view';
import {
DefaultOpenDocExtension,
DocDisplayMetaService,
DocModeService,
FileSizeLimitService,
FontConfigExtension,
fontConfigSchema,
FontLoaderService,
PageViewportServiceExtension,
ThemeService,
ToolbarRegistryExtension,
} from '@blocksuite/affine/shared/services';
import type { AffineTextAttributes } from '@blocksuite/affine/shared/types';
import { InlineManagerExtension } from '@blocksuite/affine/std/inline';
import { LinkedDocViewExtension } from '@blocksuite/affine/widgets/linked-doc/view';
import { ToolbarViewExtension } from '@blocksuite/affine/widgets/toolbar/view';
import { ViewportOverlayViewExtension } from '@blocksuite/affine/widgets/viewport-overlay/view';
import type { FrameworkProvider } from '@toeverything/infra';
import { z } from 'zod';
const commentEditorViewExtensionOptionsSchema = z.object({
peekView: z.optional(z.custom<PeekViewService>()),
fontConfig: z.optional(z.array(fontConfigSchema)),
});
export type CommentEditorViewExtensionOptions = z.infer<
typeof commentEditorViewExtensionOptionsSchema
>;
class CommentEditorViewExtensionProvider extends ViewExtensionProvider<CommentEditorViewExtensionOptions> {
override name = 'comment-editor';
override schema = commentEditorViewExtensionOptionsSchema;
override setup(
context: ViewExtensionContext,
options?: CommentEditorViewExtensionOptions
) {
super.setup(context, options);
context.register([
ThemeService,
DocModeService,
DocDisplayMetaService,
DefaultOpenDocExtension,
FontLoaderService,
ToolbarRegistryExtension,
PageViewportServiceExtension,
FileSizeLimitService,
...InlineSpecExtensions,
InlineManagerExtension<AffineTextAttributes>({
id: 'DefaultInlineManager',
specs: [
BoldInlineSpecExtension.identifier,
ItalicInlineSpecExtension.identifier,
UnderlineInlineSpecExtension.identifier,
StrikeInlineSpecExtension.identifier,
CodeInlineSpecExtension.identifier,
BackgroundInlineSpecExtension.identifier,
ColorInlineSpecExtension.identifier,
LatexInlineSpecExtension.identifier,
ReferenceInlineSpecExtension.identifier,
LinkInlineSpecExtension.identifier,
MentionInlineSpecExtension.identifier,
],
}),
PlainTextClipboardConfig,
]);
if (options?.fontConfig) {
context.register(FontConfigExtension(options.fontConfig));
}
if (options?.peekView) {
context.register(PeekViewExtension(options.peekView));
}
}
}
let manager: ViewExtensionManager | null = null;
export function getCommentEditorViewManager(framework: FrameworkProvider) {
if (!manager) {
manager = new ViewExtensionManager([
CommentEditorViewExtensionProvider,
// Blocks
CodeBlockViewExtension,
DividerViewExtension,
LatexBlockViewExtension,
ListViewExtension,
NoteViewExtension,
ParagraphViewExtension,
RootViewExtension,
// Inline
LinkViewExtension,
ReferenceViewExtension,
MentionViewExtension,
LatexInlineViewExtension,
// Widget
ToolbarViewExtension,
ViewportOverlayViewExtension,
LinkedDocViewExtension,
// Affine side
AffineThemeViewExtension,
AffineEditorViewExtension,
]);
manager.configure(ParagraphViewExtension, {
getPlaceholder: () => {
return '';
},
});
manager.configure(
LinkedDocViewExtension,
createLinkedWidgetConfig(framework)
);
}
return manager;
}

View File

@@ -130,9 +130,6 @@ export class SnapshotHelper extends Service {
title: new Text(''),
});
// Add surface block
store.addBlock('affine:surface', {}, rootId);
// Add note block
const noteId = store.addBlock('affine:note', {}, rootId);