feat(editor): group gfx extension (#11950)

Closes: BS-3208

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

## Summary by CodeRabbit

- **New Features**
  - Introduced a new group view extension, enhancing how group elements are handled and displayed.
  - Added new store and view modules for group-related features, improving modularity and integration.

- **Refactor**
  - Updated the group element architecture to use extension-based registration for views, toolbars, and effects.
  - Simplified and reorganized exports for group and text modules.

- **Chores**
  - Updated dependencies and project references to improve build consistency and compatibility.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
Saul-Mirone
2025-04-24 03:21:53 +00:00
parent 2d3130eac9
commit 9d90899344
22 changed files with 101 additions and 50 deletions

View File

@@ -0,0 +1,24 @@
import type { GroupElementModel } from '@blocksuite/affine-model';
import { GfxElementModelView } from '@blocksuite/std/gfx';
import { mountGroupTitleEditor } from './text/edgeless-group-title-editor';
export class GroupElementView extends GfxElementModelView<GroupElementModel> {
static override type: string = 'group';
override onCreated(): void {
super.onCreated();
this._initDblClickToEdit();
}
private _initDblClickToEdit(): void {
this.on('dblclick', () => {
const edgeless = this.std.view.getBlock(this.std.store.root!.id);
if (edgeless && !this.model.isLocked()) {
mountGroupTitleEditor(this.model, edgeless);
}
});
}
}

View File

@@ -0,0 +1,36 @@
import {
type SurfaceBlockModel,
SurfaceGroupLikeModel,
type SurfaceMiddleware,
surfaceMiddlewareExtension,
} from '@blocksuite/affine-block-surface';
const groupRelationWatcher: SurfaceMiddleware = (
surface: SurfaceBlockModel
) => {
const disposables = [
surface.elementUpdated.subscribe(({ id, props, local }) => {
if (!local) return;
const element = surface.getElementById(id)!;
// remove the group if it has no children
if (
element instanceof SurfaceGroupLikeModel &&
props['childIds'] &&
element.childIds.length === 0
) {
surface.deleteElement(id);
}
}),
];
return () => {
disposables.forEach(d => d.unsubscribe());
};
};
export const groupRelationWatcherExtension = surfaceMiddlewareExtension(
'group-relation-watcher',
groupRelationWatcher
);

View File

@@ -1,6 +1,6 @@
export * from './adapter';
export * from './command';
export * from './element-renderer';
export * from './element-view';
export * from './text/text';
export * from './toolbar/config';
export * from './view';

View File

@@ -0,0 +1,21 @@
import {
type StoreExtensionContext,
StoreExtensionProvider,
} from '@blocksuite/affine-ext-loader';
import {
groupToMarkdownAdapterMatcher,
groupToPlainTextAdapterMatcher,
} from './adapter';
import { groupRelationWatcherExtension } from './group-watcher';
export class GroupStoreExtension extends StoreExtensionProvider {
override name = 'affine-group-gfx';
override setup(context: StoreExtensionContext) {
super.setup(context);
context.register(groupToPlainTextAdapterMatcher);
context.register(groupToMarkdownAdapterMatcher);
context.register(groupRelationWatcherExtension);
}
}

View File

@@ -1,24 +1,27 @@
import type { GroupElementModel } from '@blocksuite/affine-model';
import { GfxElementModelView } from '@blocksuite/std/gfx';
import {
type ViewExtensionContext,
ViewExtensionProvider,
} from '@blocksuite/affine-ext-loader';
import { mountGroupTitleEditor } from './text/edgeless-group-title-editor';
import { effects } from './effects';
import { GroupElementRendererExtension } from './element-renderer';
import { GroupElementView } from './element-view';
import { groupToolbarExtension } from './toolbar/config';
export class GroupElementView extends GfxElementModelView<GroupElementModel> {
static override type: string = 'group';
export class GroupViewExtension extends ViewExtensionProvider {
override name = 'affine-group-gfx';
override onCreated(): void {
super.onCreated();
this._initDblClickToEdit();
override effect(): void {
super.effect();
effects();
}
private _initDblClickToEdit(): void {
this.on('dblclick', () => {
const edgeless = this.std.view.getBlock(this.std.store.root!.id);
if (edgeless && !this.model.isLocked()) {
mountGroupTitleEditor(this.model, edgeless);
}
});
override setup(context: ViewExtensionContext) {
super.setup(context);
context.register(GroupElementRendererExtension);
context.register(GroupElementView);
if (this.isEdgeless(context.scope)) {
context.register(groupToolbarExtension);
}
}
}