refactor(editor): improve edgeless editors (#11019)

This commit is contained in:
Saul-Mirone
2025-03-20 01:49:56 +00:00
parent 258c70cf07
commit cdd405bbe5
7 changed files with 138 additions and 89 deletions
@@ -1,5 +1,6 @@
import {
EdgelessCRUDIdentifier,
getSurfaceBlock,
TextUtils,
} from '@blocksuite/affine-block-surface';
import type { ConnectorElementModel } from '@blocksuite/affine-model';
@@ -7,9 +8,11 @@ import type { RichText } from '@blocksuite/affine-rich-text';
import { ThemeProvider } from '@blocksuite/affine-shared/services';
import { almostEqual } from '@blocksuite/affine-shared/utils';
import {
type BlockComponent,
RANGE_SYNC_EXCLUDE_ATTR,
ShadowlessElement,
} from '@blocksuite/block-std';
import { GfxControllerIdentifier } from '@blocksuite/block-std/gfx';
import { Bound, Vec } from '@blocksuite/global/gfx';
import { WithDisposable } from '@blocksuite/global/lit';
import { css, html, nothing } from 'lit';
@@ -17,8 +20,6 @@ import { property, query } from 'lit/decorators.js';
import { styleMap } from 'lit/directives/style-map.js';
import * as Y from 'yjs';
import type { EdgelessRootBlockComponent } from '../../edgeless-root-block.js';
const HORIZONTAL_PADDING = 2;
const VERTICAL_PADDING = 2;
const BORDER_WIDTH = 1;
@@ -63,6 +64,14 @@ export class EdgelessConnectorLabelEditor extends WithDisposable(
return this.edgeless.std.get(EdgelessCRUDIdentifier);
}
get gfx() {
return this.edgeless.std.get(GfxControllerIdentifier);
}
get selection() {
return this.gfx.selection;
}
private _isComposition = false;
private _keeping = false;
@@ -113,8 +122,9 @@ export class EdgelessConnectorLabelEditor extends WithDisposable(
}
override firstUpdated() {
const { edgeless, connector } = this;
const { dispatcher } = edgeless;
const { edgeless, connector, selection } = this;
const dispatcher = edgeless.std.event;
const store = edgeless.std.store;
this._resizeObserver = new ResizeObserver(() => {
this._updateLabelRect();
@@ -142,7 +152,7 @@ export class EdgelessConnectorLabelEditor extends WithDisposable(
if (!isComposing && (isModEnter || isEscape)) {
this.inlineEditorContainer?.blur();
edgeless.service.selection.set({
selection.set({
elements: [connector.id],
editing: false,
});
@@ -152,14 +162,18 @@ export class EdgelessConnectorLabelEditor extends WithDisposable(
})
);
this.disposables.add(
edgeless.service.surface.elementUpdated.subscribe(({ id }) => {
if (id === connector.id) this.requestUpdate();
})
);
const surface = getSurfaceBlock(store);
if (surface) {
this.disposables.add(
surface.elementUpdated.subscribe(({ id }) => {
if (id === connector.id) this.requestUpdate();
})
);
}
this.disposables.add(
edgeless.service.viewport.viewportUpdated.subscribe(() => {
this.gfx.viewport.viewportUpdated.subscribe(() => {
this.requestUpdate();
})
);
@@ -189,7 +203,7 @@ export class EdgelessConnectorLabelEditor extends WithDisposable(
connector.lableEditing = false;
edgeless.service.selection.set({
selection.set({
elements: [],
editing: false,
});
@@ -254,7 +268,7 @@ export class EdgelessConnectorLabelEditor extends WithDisposable(
fontSize,
fontWeight
);
const { translateX, translateY, zoom } = this.edgeless.service.viewport;
const { translateX, translateY, zoom } = this.gfx.viewport;
const [x, y] = Vec.mul(connector.getPointByOffsetDistance(distance), zoom);
const transformOperation = [
'translate(-50%, -50%)',
@@ -316,7 +330,7 @@ export class EdgelessConnectorLabelEditor extends WithDisposable(
accessor connector!: ConnectorElementModel;
@property({ attribute: false })
accessor edgeless!: EdgelessRootBlockComponent;
accessor edgeless!: BlockComponent;
@query('rich-text')
accessor richText!: RichText;
@@ -6,9 +6,11 @@ import {
frameTitleStyleVars,
} from '@blocksuite/affine-widget-frame-title';
import {
type BlockComponent,
RANGE_SYNC_EXCLUDE_ATTR,
ShadowlessElement,
} from '@blocksuite/block-std';
import { GfxControllerIdentifier } from '@blocksuite/block-std/gfx';
import { Bound } from '@blocksuite/global/gfx';
import { WithDisposable } from '@blocksuite/global/lit';
import { cssVarV2 } from '@toeverything/theme/v2';
@@ -16,8 +18,6 @@ import { css, html, nothing } from 'lit';
import { property, query } from 'lit/decorators.js';
import { styleMap } from 'lit/directives/style-map.js';
import type { EdgelessRootBlockComponent } from '../../edgeless-root-block.js';
export class EdgelessFrameTitleEditor extends WithDisposable(
ShadowlessElement
) {
@@ -46,10 +46,18 @@ export class EdgelessFrameTitleEditor extends WithDisposable(
return this.richText?.inlineEditor;
}
get gfx() {
return this.edgeless.std.get(GfxControllerIdentifier);
}
get selection() {
return this.gfx.selection;
}
private _unmount() {
// dispose in advance to avoid execute `this.remove()` twice
this.disposables.dispose();
this.edgeless.service.selection.set({
this.selection.set({
elements: [],
editing: false,
});
@@ -62,7 +70,7 @@ export class EdgelessFrameTitleEditor extends WithDisposable(
}
override firstUpdated(): void {
const dispatcher = this.edgeless.dispatcher;
const dispatcher = this.edgeless.std.event;
this.updateComplete
.then(() => {
if (!this.inlineEditor) return;
@@ -87,7 +95,7 @@ export class EdgelessFrameTitleEditor extends WithDisposable(
})
);
this.disposables.add(
this.edgeless.service.viewport.viewportUpdated.subscribe(() => {
this.gfx.viewport.viewportUpdated.subscribe(() => {
this.requestUpdate();
})
);
@@ -117,10 +125,10 @@ export class EdgelessFrameTitleEditor extends WithDisposable(
const rootBlockId = this.editorHost.doc.root?.id;
if (!rootBlockId) return nothing;
const viewport = this.edgeless.service.viewport;
const viewport = this.gfx.viewport;
const bound = Bound.deserialize(this.frameModel.xywh);
const [x, y] = viewport.toViewCoord(bound.x, bound.y);
const isInner = this.edgeless.service.gfx.grid.has(
const isInner = this.gfx.grid.has(
this.frameModel.elementBound,
true,
true,
@@ -167,7 +175,7 @@ export class EdgelessFrameTitleEditor extends WithDisposable(
}
@property({ attribute: false })
accessor edgeless!: EdgelessRootBlockComponent;
accessor edgeless!: BlockComponent;
@property({ attribute: false })
accessor frameModel!: FrameBlockModel;
@@ -6,17 +6,17 @@ import {
import type { GroupElementModel } from '@blocksuite/affine-model';
import type { RichText } from '@blocksuite/affine-rich-text';
import {
type BlockComponent,
RANGE_SYNC_EXCLUDE_ATTR,
ShadowlessElement,
} from '@blocksuite/block-std';
import { GfxControllerIdentifier } from '@blocksuite/block-std/gfx';
import { Bound } from '@blocksuite/global/gfx';
import { WithDisposable } from '@blocksuite/global/lit';
import { html, nothing } from 'lit';
import { property, query } from 'lit/decorators.js';
import { styleMap } from 'lit/directives/style-map.js';
import type { EdgelessRootBlockComponent } from '../../edgeless-root-block.js';
export class EdgelessGroupTitleEditor extends WithDisposable(
ShadowlessElement
) {
@@ -28,11 +28,19 @@ export class EdgelessGroupTitleEditor extends WithDisposable(
return this.inlineEditor?.rootElement;
}
get gfx() {
return this.edgeless.std.get(GfxControllerIdentifier);
}
get selection() {
return this.gfx.selection;
}
private _unmount() {
// dispose in advance to avoid execute `this.remove()` twice
this.disposables.dispose();
this.group.showTitle = true;
this.edgeless.service.selection.set({
this.selection.set({
elements: [this.group.id],
editing: false,
});
@@ -45,7 +53,7 @@ export class EdgelessGroupTitleEditor extends WithDisposable(
}
override firstUpdated(): void {
const dispatcher = this.edgeless.dispatcher;
const dispatcher = this.edgeless.std.event;
this.updateComplete
.then(() => {
@@ -72,7 +80,7 @@ export class EdgelessGroupTitleEditor extends WithDisposable(
})
);
this.disposables.add(
this.edgeless.service.viewport.viewportUpdated.subscribe(() => {
this.gfx.viewport.viewportUpdated.subscribe(() => {
this.requestUpdate();
})
);
@@ -103,7 +111,7 @@ export class EdgelessGroupTitleEditor extends WithDisposable(
console.error('group.externalXYWH is not set');
return nothing;
}
const viewport = this.edgeless.service.viewport;
const viewport = this.gfx.viewport;
const bound = Bound.deserialize(this.group.externalXYWH);
const [x, y] = viewport.toViewCoord(bound.x, bound.y);
@@ -137,7 +145,7 @@ export class EdgelessGroupTitleEditor extends WithDisposable(
}
@property({ attribute: false })
accessor edgeless!: EdgelessRootBlockComponent;
accessor edgeless!: BlockComponent;
@property({ attribute: false })
accessor group!: GroupElementModel;
@@ -8,9 +8,11 @@ import type { RichText } from '@blocksuite/affine-rich-text';
import { ThemeProvider } from '@blocksuite/affine-shared/services';
import { getSelectedRect } from '@blocksuite/affine-shared/utils';
import {
type BlockComponent,
RANGE_SYNC_EXCLUDE_ATTR,
ShadowlessElement,
} from '@blocksuite/block-std';
import { GfxControllerIdentifier } from '@blocksuite/block-std/gfx';
import { Bound, toRadian, Vec } from '@blocksuite/global/gfx';
import { WithDisposable } from '@blocksuite/global/lit';
import { html, nothing } from 'lit';
@@ -18,13 +20,7 @@ import { property, query } from 'lit/decorators.js';
import { styleMap } from 'lit/directives/style-map.js';
import * as Y from 'yjs';
import type { EdgelessRootBlockComponent } from '../../edgeless-root-block.js';
export class EdgelessShapeTextEditor extends WithDisposable(ShadowlessElement) {
get crud() {
return this.edgeless.std.get(EdgelessCRUDIdentifier);
}
private _keeping = false;
private _lastXYWH = '';
@@ -35,6 +31,18 @@ export class EdgelessShapeTextEditor extends WithDisposable(ShadowlessElement) {
return this.richText.inlineEditor;
}
get crud() {
return this.edgeless.std.get(EdgelessCRUDIdentifier);
}
get gfx() {
return this.edgeless.std.get(GfxControllerIdentifier);
}
get selection() {
return this.gfx.selection;
}
get inlineEditorContainer() {
return this.inlineEditor?.rootElement;
}
@@ -47,8 +55,7 @@ export class EdgelessShapeTextEditor extends WithDisposable(ShadowlessElement) {
if (!this.isMindMapNode) {
return;
}
const service = this.edgeless.service;
const selection = this.selection;
this._disposables.addFromEvent(this, 'keydown', evt => {
switch (evt.key) {
@@ -57,7 +64,7 @@ export class EdgelessShapeTextEditor extends WithDisposable(ShadowlessElement) {
if (evt.shiftKey || evt.isComposing) return;
(this.ownerDocument.activeElement as HTMLElement).blur();
service.selection.set({
selection.set({
elements: [this.element.id],
editing: false,
});
@@ -67,7 +74,7 @@ export class EdgelessShapeTextEditor extends WithDisposable(ShadowlessElement) {
case 'Tab': {
evt.stopPropagation();
(this.ownerDocument.activeElement as HTMLElement).blur();
service.selection.set({
selection.set({
elements: [this.element.id],
editing: false,
});
@@ -109,7 +116,7 @@ export class EdgelessShapeTextEditor extends WithDisposable(ShadowlessElement) {
this.element.textDisplay = true;
this.remove();
this.edgeless.service.selection.set({
this.selection.set({
elements: [],
editing: false,
});
@@ -134,8 +141,10 @@ export class EdgelessShapeTextEditor extends WithDisposable(ShadowlessElement) {
toRadian(-this.element.rotate)
);
const [modelLeftTopX, modelLeftTopY] =
this.edgeless.service.viewport.toModelCoord(leftTopX, leftTopY);
const [modelLeftTopX, modelLeftTopY] = this.gfx.viewport.toModelCoord(
leftTopX,
leftTopY
);
this.crud.updateElement(this.element.id, {
xywh: new Bound(
@@ -161,7 +170,7 @@ export class EdgelessShapeTextEditor extends WithDisposable(ShadowlessElement) {
this.richText.style.minHeight = `${containerHeight}px`;
}
this.edgeless.service.selection.set({
this.selection.set({
elements: [this.element.id],
editing: true,
});
@@ -173,12 +182,12 @@ export class EdgelessShapeTextEditor extends WithDisposable(ShadowlessElement) {
}
override firstUpdated(): void {
const dispatcher = this.edgeless.dispatcher;
const dispatcher = this.edgeless.std.event;
this.element.textDisplay = false;
this.disposables.add(
this.edgeless.service.viewport.viewportUpdated.subscribe(() => {
this.gfx.viewport.viewportUpdated.subscribe(() => {
this.requestUpdate();
this.updateComplete
.then(() => {
@@ -228,7 +237,7 @@ export class EdgelessShapeTextEditor extends WithDisposable(ShadowlessElement) {
this.disposables.addFromEvent(this, 'keydown', evt => {
if (evt.key === 'Escape') {
requestAnimationFrame(() => {
this.edgeless.service.selection.set({
this.selection.set({
elements: [this.element.id],
editing: false,
});
@@ -256,7 +265,7 @@ export class EdgelessShapeTextEditor extends WithDisposable(ShadowlessElement) {
const [verticalPadding, horiPadding] = this.element.padding;
const textResizing = this.element.textResizing;
const viewport = this.edgeless.service.viewport;
const viewport = this.gfx.viewport;
const zoom = viewport.zoom;
const rect = getSelectedRect([this.element]);
const rotate = this.element.rotate;
@@ -265,10 +274,7 @@ export class EdgelessShapeTextEditor extends WithDisposable(ShadowlessElement) {
[rect.left + rect.width / 2, rect.top + rect.height / 2],
toRadian(rotate)
);
const [x, y] = this.edgeless.service.viewport.toViewCoord(
leftTopX,
leftTopY
);
const [x, y] = this.gfx.viewport.toViewCoord(leftTopX, leftTopY);
const autoWidth = textResizing === TextResizing.AUTO_WIDTH_AND_HEIGHT;
const color = this.edgeless.std
.get(ThemeProvider)
@@ -344,17 +350,14 @@ export class EdgelessShapeTextEditor extends WithDisposable(ShadowlessElement) {
}
@property({ attribute: false })
accessor edgeless!: EdgelessRootBlockComponent;
accessor edgeless!: BlockComponent;
@property({ attribute: false })
accessor element!: ShapeElementModel;
@property({ attribute: false })
accessor mountEditor:
| ((
element: ShapeElementModel,
edgeless: EdgelessRootBlockComponent
) => void)
| ((element: ShapeElementModel, edgeless: BlockComponent) => void)
| undefined = undefined;
@query('rich-text')
@@ -29,7 +29,7 @@ import {
handleNativeRangeAtPoint,
resetNativeSelection,
} from '@blocksuite/affine-shared/utils';
import type { PointerEventState } from '@blocksuite/block-std';
import type { BlockComponent, PointerEventState } from '@blocksuite/block-std';
import {
BaseTool,
getTopElements,
@@ -41,12 +41,11 @@ import {
import { DisposableGroup } from '@blocksuite/global/disposable';
import type { IVec } from '@blocksuite/global/gfx';
import { Bound, getCommonBoundWithRotation, Vec } from '@blocksuite/global/gfx';
import { noop } from '@blocksuite/global/utils';
import { effect } from '@preact/signals-core';
import clamp from 'lodash-es/clamp';
import last from 'lodash-es/last';
import type { EdgelessRootBlockComponent } from '../edgeless-root-block.js';
import type { EdgelessRootBlockComponent } from '../index.js';
import { prepareCloneData } from '../utils/clone-utils.js';
import { calPanDelta } from '../utils/panning-utils.js';
import { isCanvasElement, isEdgelessTextBlock } from '../utils/query.js';
@@ -191,10 +190,8 @@ export class DefaultTool extends BaseTool {
enableHover = true;
private get _edgeless(): EdgelessRootBlockComponent | null {
const block = this.std.view.getBlock(this.doc.root!.id);
return (block as EdgelessRootBlockComponent) ?? null;
private get _edgeless(): BlockComponent | null {
return this.std.view.getBlock(this.doc.root!.id);
}
/**
@@ -243,7 +240,11 @@ export class DefaultTool extends BaseTool {
private async _cloneContent() {
if (!this._edgeless) return;
const clipboardController = this._edgeless?.clipboardController;
// FIXME: edgeless clipboard should be an extension
const clipboardController = (
this._edgeless as EdgelessRootBlockComponent | null
)?.clipboardController;
if (!clipboardController) return;
const snapshot = prepareCloneData(this._toBeMoved, this.std);
const bound = getCommonBoundWithRotation(this._toBeMoved);
@@ -551,7 +552,6 @@ export class DefaultTool extends BaseTool {
this._stopAutoPanning();
this._clearDisposable();
this._accumulateDelta = [0, 0];
noop();
}
override doubleClick(e: PointerEventState) {
@@ -1,5 +1,6 @@
import {
EdgelessFrameManager,
EdgelessFrameManagerIdentifier,
isFrameBlock,
} from '@blocksuite/affine-block-frame';
import { isNoteBlock } from '@blocksuite/affine-block-surface';
@@ -13,6 +14,7 @@ import type {
ShapeElementModel,
} from '@blocksuite/affine-model';
import { getElementsWithoutGroup } from '@blocksuite/affine-shared/utils';
import type { BlockComponent } from '@blocksuite/block-std';
import {
generateKeyBetweenV2,
type GfxModel,
@@ -99,7 +101,7 @@ type FrameSnapshot = BlockSnapshot & {
export function createNewPresentationIndexes(
raw: (SerializedElement | BlockSnapshot)[],
edgeless: EdgelessRootBlockComponent
edgeless: BlockComponent
) {
const frames = raw
.filter((block): block is FrameSnapshot => {
@@ -108,7 +110,7 @@ export function createNewPresentationIndexes(
})
.sort((a, b) => EdgelessFrameManager.framePresentationComparator(a, b));
const frameMgr = edgeless.service.frame;
const frameMgr = edgeless.std.get(EdgelessFrameManagerIdentifier);
let before = frameMgr.generatePresentationIndex();
const result = new Map<string, string>();
frames.forEach(frame => {
@@ -5,6 +5,8 @@ import type {
GroupElementModel,
} from '@blocksuite/affine-model';
import { ShapeElementModel } from '@blocksuite/affine-model';
import type { BlockComponent } from '@blocksuite/block-std';
import { GfxControllerIdentifier } from '@blocksuite/block-std/gfx';
import { BlockSuiteError, ErrorCode } from '@blocksuite/global/exceptions';
import type { IVec } from '@blocksuite/global/gfx';
import { Bound } from '@blocksuite/global/gfx';
@@ -14,28 +16,31 @@ import { EdgelessConnectorLabelEditor } from '../components/text/edgeless-connec
import { EdgelessFrameTitleEditor } from '../components/text/edgeless-frame-title-editor.js';
import { EdgelessGroupTitleEditor } from '../components/text/edgeless-group-title-editor.js';
import { EdgelessShapeTextEditor } from '../components/text/edgeless-shape-text-editor.js';
import type { EdgelessRootBlockComponent } from '../edgeless-root-block.js';
export function mountShapeTextEditor(
shapeElement: ShapeElementModel,
edgeless: EdgelessRootBlockComponent
edgeless: BlockComponent
) {
if (!edgeless.mountElm) {
const mountElm = edgeless.querySelector('.edgeless-mount-point');
if (!mountElm) {
throw new BlockSuiteError(
ErrorCode.ValueNotExists,
"edgeless block's mount point does not exist"
);
}
const updatedElement = edgeless.service.crud.getElementById(shapeElement.id);
const gfx = edgeless.std.get(GfxControllerIdentifier);
const crud = edgeless.std.get(EdgelessCRUDIdentifier);
const updatedElement = crud.getElementById(shapeElement.id);
if (!(updatedElement instanceof ShapeElementModel)) {
console.error('Cannot mount text editor on a non-shape element');
return;
}
edgeless.gfx.tool.setTool('default');
edgeless.gfx.selection.set({
gfx.tool.setTool('default');
gfx.selection.set({
elements: [shapeElement.id],
editing: true,
});
@@ -52,22 +57,25 @@ export function mountShapeTextEditor(
shapeEditor.edgeless = edgeless;
shapeEditor.mountEditor = mountShapeTextEditor;
edgeless.mountElm.append(shapeEditor);
mountElm.append(shapeEditor);
}
export function mountFrameTitleEditor(
frame: FrameBlockModel,
edgeless: EdgelessRootBlockComponent
edgeless: BlockComponent
) {
if (!edgeless.mountElm) {
const mountElm = edgeless.querySelector('.edgeless-mount-point');
if (!mountElm) {
throw new BlockSuiteError(
ErrorCode.ValueNotExists,
"edgeless block's mount point does not exist"
);
}
edgeless.gfx.tool.setTool('default');
edgeless.gfx.selection.set({
const gfx = edgeless.std.get(GfxControllerIdentifier);
gfx.tool.setTool('default');
gfx.selection.set({
elements: [frame.id],
editing: true,
});
@@ -76,22 +84,25 @@ export function mountFrameTitleEditor(
frameEditor.frameModel = frame;
frameEditor.edgeless = edgeless;
edgeless.mountElm.append(frameEditor);
mountElm.append(frameEditor);
}
export function mountGroupTitleEditor(
group: GroupElementModel,
edgeless: EdgelessRootBlockComponent
edgeless: BlockComponent
) {
if (!edgeless.mountElm) {
const mountElm = edgeless.querySelector('.edgeless-mount-point');
if (!mountElm) {
throw new BlockSuiteError(
ErrorCode.ValueNotExists,
"edgeless block's mount point does not exist"
);
}
edgeless.gfx.tool.setTool('default');
edgeless.gfx.selection.set({
const gfx = edgeless.std.get(GfxControllerIdentifier);
gfx.tool.setTool('default');
gfx.selection.set({
elements: [group.id],
editing: true,
});
@@ -100,23 +111,26 @@ export function mountGroupTitleEditor(
groupEditor.group = group;
groupEditor.edgeless = edgeless;
edgeless.mountElm.append(groupEditor);
mountElm.append(groupEditor);
}
export function mountConnectorLabelEditor(
connector: ConnectorElementModel,
edgeless: EdgelessRootBlockComponent,
edgeless: BlockComponent,
point?: IVec
) {
if (!edgeless.mountElm) {
const mountElm = edgeless.querySelector('.edgeless-mount-point');
if (!mountElm) {
throw new BlockSuiteError(
ErrorCode.ValueNotExists,
"edgeless block's mount point does not exist"
);
}
edgeless.gfx.tool.setTool('default');
edgeless.gfx.selection.set({
const gfx = edgeless.std.get(GfxControllerIdentifier);
gfx.tool.setTool('default');
gfx.selection.set({
elements: [connector.id],
editing: true,
});
@@ -146,7 +160,7 @@ export function mountConnectorLabelEditor(
editor.connector = connector;
editor.edgeless = edgeless;
edgeless.mountElm.append(editor);
mountElm.append(editor);
editor.updateComplete
.then(() => {
editor.inlineEditor?.focusEnd();