refactor(editor): improve the implementation of auto complete (#11135)

This commit is contained in:
Saul-Mirone
2025-03-24 12:05:57 +00:00
parent 7e248a1379
commit f914775885
3 changed files with 75 additions and 50 deletions

View File

@@ -1,7 +1,10 @@
import { insertEdgelessTextCommand } from '@blocksuite/affine-block-edgeless-text';
import { EdgelessFrameManagerIdentifier } from '@blocksuite/affine-block-frame';
import {
CanvasElementType,
EdgelessCRUDIdentifier,
getSurfaceBlock,
getSurfaceComponent,
} from '@blocksuite/affine-block-surface';
import { FontFamilyIcon } from '@blocksuite/affine-components/icons';
import {
@@ -37,7 +40,11 @@ import {
captureEventTarget,
matchModels,
} from '@blocksuite/affine-shared/utils';
import { type BlockStdScope, stdContext } from '@blocksuite/block-std';
import {
type BlockComponent,
type BlockStdScope,
stdContext,
} from '@blocksuite/block-std';
import { GfxControllerIdentifier } from '@blocksuite/block-std/gfx';
import type { XYWH } from '@blocksuite/global/gfx';
import {
@@ -58,7 +65,6 @@ import { repeat } from 'lit/directives/repeat.js';
import { styleMap } from 'lit/directives/style-map.js';
import * as Y from 'yjs';
import type { EdgelessRootBlockComponent } from '../../edgeless-root-block.js';
import {
type AUTO_COMPLETE_TARGET_TYPE,
AutoCompleteFrameOverlay,
@@ -125,7 +131,7 @@ export class EdgelessAutoCompletePanel extends WithDisposable(LitElement) {
constructor(
position: [number, number],
edgeless: EdgelessRootBlockComponent,
edgeless: BlockComponent,
currentSource: ShapeElementModel | NoteBlockModel,
connector: ConnectorElementModel
) {
@@ -140,6 +146,10 @@ export class EdgelessAutoCompletePanel extends WithDisposable(LitElement) {
return this.std.get(EdgelessCRUDIdentifier);
}
get surface() {
return getSurfaceComponent(this.std);
}
private _addFrame() {
const bound = this._generateTarget(this.connector)?.nextBound;
if (!bound) return;
@@ -152,9 +162,10 @@ export class EdgelessAutoCompletePanel extends WithDisposable(LitElement) {
const { xywh, position } = target;
const edgeless = this.edgeless;
const { service, surfaceBlockModel } = edgeless;
const frameMgr = service.frame;
const frameIndex = service.frames.length + 1;
const surfaceBlockModel = getSurfaceBlock(this.std.store);
if (!surfaceBlockModel) return;
const frameMgr = this.std.get(EdgelessFrameManagerIdentifier);
const frameIndex = frameMgr.frames.length + 1;
const props = this.std.get(EditPropsStore).applyLastProps('affine:frame', {
title: new Y.Text(`Frame ${frameIndex}`),
xywh: serializeXYWH(...xywh),
@@ -170,7 +181,7 @@ export class EdgelessAutoCompletePanel extends WithDisposable(LitElement) {
position,
};
edgeless.service.selection.set({
this.gfx.selection.set({
elements: [frame.id],
editing: false,
});
@@ -209,7 +220,7 @@ export class EdgelessAutoCompletePanel extends WithDisposable(LitElement) {
this.crud.updateElement(this.connector.id, {
target: { id, position },
});
this.edgeless.service.selection.set({
this.gfx.selection.set({
elements: [id],
editing: false,
});
@@ -395,7 +406,7 @@ export class EdgelessAutoCompletePanel extends WithDisposable(LitElement) {
}
private _getPanelPosition() {
const { viewport } = this.edgeless.service;
const { viewport } = this.gfx;
const { boundingClientRect: viewportRect, zoom } = viewport;
const result = this._getTargetXYWH(PANEL_WIDTH / zoom, PANEL_HEIGHT / zoom);
const pos = result ? result.xywh.slice(0, 2) : this.position;
@@ -442,11 +453,14 @@ export class EdgelessAutoCompletePanel extends WithDisposable(LitElement) {
}
private _removeOverlay() {
if (this._overlay)
this.edgeless.surface.renderer.removeOverlay(this._overlay);
if (this._overlay && this.surface) {
this.surface.renderer.removeOverlay(this._overlay);
}
}
private _showFrameOverlay() {
if (!this.surface) return;
const bound = this._generateTarget(this.connector)?.nextBound;
if (!bound) return;
@@ -459,7 +473,7 @@ export class EdgelessAutoCompletePanel extends WithDisposable(LitElement) {
.get(ThemeProvider)
.getCssVariableColor('--affine-black-30');
this._overlay = new AutoCompleteFrameOverlay(this.gfx, xywh, strokeColor);
this.edgeless.surface.renderer.addOverlay(this._overlay);
this.surface.renderer.addOverlay(this._overlay);
}
private _showNoteOverlay() {
@@ -468,6 +482,7 @@ export class EdgelessAutoCompletePanel extends WithDisposable(LitElement) {
DEFAULT_NOTE_OVERLAY_HEIGHT
)?.xywh;
if (!xywh) return;
if (!this.surface) return;
const background = this.edgeless.std
.get(ThemeProvider)
@@ -478,12 +493,13 @@ export class EdgelessAutoCompletePanel extends WithDisposable(LitElement) {
true
);
this._overlay = new AutoCompleteNoteOverlay(this.gfx, xywh, background);
this.edgeless.surface.renderer.addOverlay(this._overlay);
this.surface.renderer.addOverlay(this._overlay);
}
private _showOverlay(targetType: AUTO_COMPLETE_TARGET_TYPE) {
this._removeOverlay();
if (!this._connectorExist()) return;
if (!this.surface) return;
switch (targetType) {
case 'text':
@@ -499,12 +515,13 @@ export class EdgelessAutoCompletePanel extends WithDisposable(LitElement) {
this._showShapeOverlay(targetType);
}
this.edgeless.surface.refresh();
this.surface.refresh();
}
private _showShapeOverlay(targetType: TARGET_SHAPE_TYPE) {
const bound = this._generateTarget(this.connector)?.nextBound;
if (!bound) return;
if (!this.surface) return;
const { x, y, w, h } = bound;
const xywh = [x, y, w, h] as XYWH;
@@ -537,7 +554,7 @@ export class EdgelessAutoCompletePanel extends WithDisposable(LitElement) {
shapeStyle
);
this.edgeless.surface.renderer.addOverlay(this._overlay);
this.surface.renderer.addOverlay(this._overlay);
}
private _showTextOverlay() {
@@ -546,9 +563,10 @@ export class EdgelessAutoCompletePanel extends WithDisposable(LitElement) {
DEFAULT_TEXT_HEIGHT
)?.xywh;
if (!xywh) return;
if (!this.surface) return;
this._overlay = new AutoCompleteTextOverlay(this.gfx, xywh);
this.edgeless.surface.renderer.addOverlay(this._overlay);
this.surface.renderer.addOverlay(this._overlay);
}
override connectedCallback() {
@@ -568,9 +586,7 @@ export class EdgelessAutoCompletePanel extends WithDisposable(LitElement) {
override firstUpdated() {
this.disposables.add(
this.edgeless.service.viewport.viewportUpdated.subscribe(() =>
this.requestUpdate()
)
this.gfx.viewport.viewportUpdated.subscribe(() => this.requestUpdate())
);
}
@@ -650,7 +666,7 @@ export class EdgelessAutoCompletePanel extends WithDisposable(LitElement) {
accessor currentSource: ShapeElementModel | NoteBlockModel;
@property({ attribute: false })
accessor edgeless: EdgelessRootBlockComponent;
accessor edgeless: BlockComponent;
@property({ attribute: false })
accessor position: [number, number];

View File

@@ -3,6 +3,8 @@ import {
type ConnectionOverlay,
ConnectorPathGenerator,
EdgelessCRUDIdentifier,
getSurfaceBlock,
getSurfaceComponent,
isNoteBlock,
Overlay,
OverlayIdentifier,
@@ -24,7 +26,11 @@ import {
shapeMethods,
} from '@blocksuite/affine-model';
import { handleNativeRangeAtPoint } from '@blocksuite/affine-shared/utils';
import { type BlockStdScope, stdContext } from '@blocksuite/block-std';
import {
type BlockComponent,
type BlockStdScope,
stdContext,
} from '@blocksuite/block-std';
import { GfxControllerIdentifier } from '@blocksuite/block-std/gfx';
import { DisposableGroup } from '@blocksuite/global/disposable';
import type { Bound, IVec } from '@blocksuite/global/gfx';
@@ -42,7 +48,6 @@ import { property, state } from 'lit/decorators.js';
import { classMap } from 'lit/directives/class-map.js';
import { styleMap } from 'lit/directives/style-map.js';
import type { EdgelessRootBlockComponent } from '../../edgeless-root-block.js';
import type { SelectedRect } from '../rects/edgeless-selected-rect.js';
import { EdgelessAutoCompletePanel } from './auto-complete-panel.js';
import {
@@ -158,6 +163,10 @@ export class EdgelessAutoComplete extends WithDisposable(LitElement) {
}
`;
private get _surface() {
return getSurfaceBlock(this.std.store);
}
private _autoCompleteOverlay!: AutoCompleteOverlay;
private readonly _onPointerDown = (e: PointerEvent, type: Direction) => {
@@ -167,7 +176,7 @@ export class EdgelessAutoComplete extends WithDisposable(LitElement) {
e.clientY - viewportRect.top
);
if (!this.edgeless.dispatcher) return;
if (!this.edgeless.std.event) return;
let connector: ConnectorElementModel | null;
@@ -207,7 +216,7 @@ export class EdgelessAutoComplete extends WithDisposable(LitElement) {
if (!this._isMoving) {
this._generateElementOnClick(type);
} else if (connector && !connector.target.id) {
this.edgeless.service.selection.clear();
this.gfx.selection.clear();
this._createAutoCompletePanel(e, connector);
}
@@ -343,10 +352,7 @@ export class EdgelessAutoComplete extends WithDisposable(LitElement) {
) {
if (!this.canShowAutoComplete) return;
const position = this.edgeless.service.viewport.toModelCoord(
e.clientX,
e.clientY
);
const position = this.gfx.viewport.toModelCoord(e.clientX, e.clientY);
const autoCompletePanel = new EdgelessAutoCompletePanel(
position,
this.edgeless,
@@ -358,7 +364,7 @@ export class EdgelessAutoComplete extends WithDisposable(LitElement) {
}
private _generateElementOnClick(type: Direction) {
const { doc, service } = this.edgeless;
const { doc } = this.edgeless;
const bound = this._computeNextBound(type);
const id = createEdgelessElement(this.edgeless, this.current, bound);
if (!id) return;
@@ -384,7 +390,7 @@ export class EdgelessAutoComplete extends WithDisposable(LitElement) {
if (!model) {
return;
}
const [x, y] = service.viewport.toViewCoord(
const [x, y] = this.gfx.viewport.toViewCoord(
bound.center[0],
bound.y + DEFAULT_NOTE_HEIGHT / 2
);
@@ -393,7 +399,7 @@ export class EdgelessAutoComplete extends WithDisposable(LitElement) {
});
}
this.edgeless.service.selection.set({
this.gfx.selection.set({
elements: [id],
editing: true,
});
@@ -401,9 +407,9 @@ export class EdgelessAutoComplete extends WithDisposable(LitElement) {
}
private _getConnectedElements(element: ShapeElementModel) {
const service = this.edgeless.service;
if (!this._surface) return [];
return service.getConnectors(element.id).reduce((prev, current) => {
return this._surface.getConnectors(element.id).reduce((prev, current) => {
if (current.target.id === element.id && current.source.id) {
prev.push(
this.crud.getElementById(current.source.id) as ShapeElementModel
@@ -469,7 +475,8 @@ export class EdgelessAutoComplete extends WithDisposable(LitElement) {
}
private _initOverlay() {
const { surface } = this.edgeless;
const surface = getSurfaceComponent(this.std);
if (!surface) return;
this._autoCompleteOverlay = new AutoCompleteOverlay(this.gfx);
surface.renderer.addOverlay(this._autoCompleteOverlay);
}
@@ -477,7 +484,7 @@ export class EdgelessAutoComplete extends WithDisposable(LitElement) {
private _renderArrow() {
const isShape = this.current instanceof ShapeElementModel;
const { selectedRect } = this;
const { zoom } = this.edgeless.service.viewport;
const { zoom } = this.gfx.viewport;
const width = 72;
const height = 44;
@@ -572,7 +579,7 @@ export class EdgelessAutoComplete extends WithDisposable(LitElement) {
}
const { selectedRect } = this;
const { zoom } = this.edgeless.service.viewport;
const { zoom } = this.gfx.viewport;
const size = 26;
const buttonMargin =
(mindmapButtons.mindmapNode?.children.length ?? 0) > 0
@@ -644,7 +651,8 @@ export class EdgelessAutoComplete extends WithDisposable(LitElement) {
path: IVec[],
targetType: ShapeType
) {
const { surface } = this.edgeless;
const surface = getSurfaceComponent(this.std);
if (!surface) return;
this._autoCompleteOverlay.stroke = surface.renderer.getColorValue(
current.strokeColor,
@@ -667,7 +675,7 @@ export class EdgelessAutoComplete extends WithDisposable(LitElement) {
}
override firstUpdated() {
const { _disposables, edgeless } = this;
const { _disposables, edgeless, gfx } = this;
_disposables.add(
this.gfx.selection.slots.updated.subscribe(() => {
@@ -681,8 +689,8 @@ export class EdgelessAutoComplete extends WithDisposable(LitElement) {
_disposables.add(
edgeless.host.event.add('pointerMove', ctx => {
const evt = ctx.get('pointerState');
const [x, y] = edgeless.gfx.viewport.toModelCoord(evt.x, evt.y);
const elm = edgeless.gfx.getElementByPoint(x, y);
const [x, y] = gfx.viewport.toModelCoord(evt.x, evt.y);
const elm = gfx.getElementByPoint(x, y);
if (!elm) {
this._isHover = false;
@@ -703,7 +711,9 @@ export class EdgelessAutoComplete extends WithDisposable(LitElement) {
removeOverlay() {
this._timer && clearTimeout(this._timer);
this.edgeless.surface.renderer.removeOverlay(this._autoCompleteOverlay);
const surface = getSurfaceComponent(this.std);
if (!surface) return;
surface.renderer.removeOverlay(this._autoCompleteOverlay);
}
override render() {
@@ -740,7 +750,7 @@ export class EdgelessAutoComplete extends WithDisposable(LitElement) {
accessor current!: ShapeElementModel | NoteBlockModel;
@property({ attribute: false })
accessor edgeless!: EdgelessRootBlockComponent;
accessor edgeless!: BlockComponent;
@property({ attribute: false })
accessor selectedRect!: SelectedRect;

View File

@@ -1,4 +1,5 @@
import {
EdgelessCRUDIdentifier,
type Options,
Overlay,
type RoughCanvas,
@@ -14,14 +15,13 @@ import {
type ShapeName,
type ShapeStyle,
} from '@blocksuite/affine-model';
import type { BlockComponent } from '@blocksuite/block-std';
import type { GfxController, GfxModel } from '@blocksuite/block-std/gfx';
import { BlockSuiteError, ErrorCode } from '@blocksuite/global/exceptions';
import { Bound, normalizeDegAngle, type XYWH } from '@blocksuite/global/gfx';
import { assertType } from '@blocksuite/global/utils';
import * as Y from 'yjs';
import type { EdgelessRootBlockComponent } from '../../edgeless-root-block.js';
export enum Direction {
Right,
Bottom,
@@ -270,14 +270,13 @@ export function capitalizeFirstLetter(str: string) {
}
export function createEdgelessElement(
edgeless: EdgelessRootBlockComponent,
edgeless: BlockComponent,
current: ShapeElementModel | NoteBlockModel,
bound: Bound
) {
let id;
const { service } = edgeless;
const { crud } = service;
const crud = edgeless.std.get(EdgelessCRUDIdentifier);
let id;
let element: GfxModel | null = null;
if (isShape(current)) {
@@ -331,11 +330,11 @@ export function createEdgelessElement(
}
export function createShapeElement(
edgeless: EdgelessRootBlockComponent,
edgeless: BlockComponent,
current: ShapeElementModel | NoteBlockModel,
targetType: TARGET_SHAPE_TYPE
) {
const { crud } = edgeless.service;
const crud = edgeless.std.get(EdgelessCRUDIdentifier);
const id = crud.addElement('shape', {
shapeType: getShapeType(targetType),
radius: getShapeRadius(targetType),