From a0a97d075181a6bd711ef3d69e239b48a4179284 Mon Sep 17 00:00:00 2001 From: doouding Date: Mon, 24 Feb 2025 06:13:04 +0000 Subject: [PATCH] fix: drag connector and group element (#10385) --- .../block-surface/src/surface-transformer.ts | 8 +- .../affine/model/src/elements/brush/brush.ts | 4 - .../model/src/elements/connector/connector.ts | 4 +- .../affine/model/src/elements/group/group.ts | 4 +- .../model/src/elements/mindmap/mindmap.ts | 2 +- .../affine/model/src/elements/shape/shape.ts | 4 +- .../affine/model/src/elements/text/text.ts | 6 +- .../src/middleware/blocks-filter.ts | 70 ++++++++- .../affine/widget-drag-handle/src/utils.ts | 17 +- .../src/watchers/drag-event-watcher.ts | 148 +++++++++++++++--- .../framework/block-std/src/gfx/index.ts | 2 + .../src/gfx/model/surface/element-model.ts | 4 - .../src/gfx/model/surface/surface-model.ts | 33 +++- 13 files changed, 255 insertions(+), 51 deletions(-) diff --git a/blocksuite/affine/block-surface/src/surface-transformer.ts b/blocksuite/affine/block-surface/src/surface-transformer.ts index 325680cf13..0ee97513a0 100644 --- a/blocksuite/affine/block-surface/src/surface-transformer.ts +++ b/blocksuite/affine/block-surface/src/surface-transformer.ts @@ -1,4 +1,8 @@ import type { SurfaceBlockProps } from '@blocksuite/block-std/gfx'; +import { + SURFACE_TEXT_UNIQ_IDENTIFIER, + SURFACE_YMAP_UNIQ_IDENTIFIER, +} from '@blocksuite/block-std/gfx'; import type { FromSnapshotPayload, SnapshotNode, @@ -7,10 +11,6 @@ import type { import { BaseBlockTransformer } from '@blocksuite/store'; import * as Y from 'yjs'; -const SURFACE_TEXT_UNIQ_IDENTIFIER = 'affine:surface:text'; -// Used for group children field -const SURFACE_YMAP_UNIQ_IDENTIFIER = 'affine:surface:ymap'; - export class SurfaceBlockTransformer extends BaseBlockTransformer { private _elementToJSON(element: Y.Map) { const value: Record = {}; diff --git a/blocksuite/affine/model/src/elements/brush/brush.ts b/blocksuite/affine/model/src/elements/brush/brush.ts index 5d8c8a832f..0768927b2b 100644 --- a/blocksuite/affine/model/src/elements/brush/brush.ts +++ b/blocksuite/affine/model/src/elements/brush/brush.ts @@ -63,10 +63,6 @@ export class BrushElementModel extends GfxPrimitiveElementModel { return 'brush'; } - static override propsToY(props: BrushProps) { - return props; - } - override containsBound(bounds: Bound) { const points = getPointsFromBoundWithRotation(this); return points.some(point => bounds.containsPoint(point)); diff --git a/blocksuite/affine/model/src/elements/connector/connector.ts b/blocksuite/affine/model/src/elements/connector/connector.ts index 57d83419f8..230fceed53 100644 --- a/blocksuite/affine/model/src/elements/connector/connector.ts +++ b/blocksuite/affine/model/src/elements/connector/connector.ts @@ -125,8 +125,8 @@ export class ConnectorElementModel extends GfxPrimitiveElementModel) { - if ('title' in props && !(props.title instanceof Y.Text)) { + static propsToY(props: Record) { + if (typeof props.title === 'string') { props.title = new Y.Text(props.title as string); } diff --git a/blocksuite/affine/model/src/elements/mindmap/mindmap.ts b/blocksuite/affine/model/src/elements/mindmap/mindmap.ts index d09a21d480..01d57ec1bc 100644 --- a/blocksuite/affine/model/src/elements/mindmap/mindmap.ts +++ b/blocksuite/affine/model/src/elements/mindmap/mindmap.ts @@ -180,7 +180,7 @@ export class MindmapElementModel extends GfxGroupLikeElementModel) { + static propsToY(props: Record) { if ( props.children && !isNodeType(props.children as Record) && diff --git a/blocksuite/affine/model/src/elements/shape/shape.ts b/blocksuite/affine/model/src/elements/shape/shape.ts index 27dfbaa862..59b56790af 100644 --- a/blocksuite/affine/model/src/elements/shape/shape.ts +++ b/blocksuite/affine/model/src/elements/shape/shape.ts @@ -67,8 +67,8 @@ export class ShapeElementModel extends GfxPrimitiveElementModel { return 'shape'; } - static override propsToY(props: ShapeProps) { - if (props.text && !(props.text instanceof Y.Text)) { + static propsToY(props: ShapeProps) { + if (typeof props.text === 'string') { props.text = new Y.Text(props.text); } diff --git a/blocksuite/affine/model/src/elements/text/text.ts b/blocksuite/affine/model/src/elements/text/text.ts index 1a33de8604..5422b9b66b 100644 --- a/blocksuite/affine/model/src/elements/text/text.ts +++ b/blocksuite/affine/model/src/elements/text/text.ts @@ -30,9 +30,9 @@ export class TextElementModel extends GfxPrimitiveElementModel return 'text'; } - static override propsToY(props: Record) { - if (props.text && !(props.text instanceof Y.Text)) { - props.text = new Y.Text(props.text as string); + static propsToY(props: Record) { + if (typeof props.text === 'string') { + props.text = new Y.Text(props.text); } return props; diff --git a/blocksuite/affine/widget-drag-handle/src/middleware/blocks-filter.ts b/blocksuite/affine/widget-drag-handle/src/middleware/blocks-filter.ts index 963cb457b6..10dfc3e534 100644 --- a/blocksuite/affine/widget-drag-handle/src/middleware/blocks-filter.ts +++ b/blocksuite/affine/widget-drag-handle/src/middleware/blocks-filter.ts @@ -1,6 +1,16 @@ import type { SurfaceBlockModel } from '@blocksuite/affine-block-surface'; +import type { ConnectorElementModel } from '@blocksuite/affine-model'; import type { BlockStdScope } from '@blocksuite/block-std'; -import { isGfxGroupCompatibleModel } from '@blocksuite/block-std/gfx'; +import { + GfxController, + type GfxModel, + isGfxGroupCompatibleModel, +} from '@blocksuite/block-std/gfx'; +import { + assertType, + type IVec, + type SerializedXYWH, +} from '@blocksuite/global/utils'; import type { TransformerMiddleware } from '@blocksuite/store'; /** @@ -18,6 +28,7 @@ export const gfxBlocksFilter = ( const surface = store.getBlocksByFlavour('affine:surface')[0] .model as SurfaceBlockModel; const idsToCheck = ids.slice(); + const gfx = std.get(GfxController); for (const id of idsToCheck) { const blockOrElem = store.getBlock(id)?.model ?? surface.getElementById(id); @@ -45,5 +56,62 @@ export const gfxBlocksFilter = ( return; } }); + + slots.afterExport.on(payload => { + if (payload.type !== 'block') { + return; + } + + if (payload.model.flavour === 'affine:surface') { + const { snapshot } = payload; + const elementsMap = snapshot.props.elements as Record< + string, + { type: string } + >; + + Object.entries(elementsMap).forEach(([elementId, val]) => { + if (val.type === 'connector') { + assertType<{ + type: 'connector'; + source: { position: IVec; id?: string }; + target: { position: IVec; id?: string }; + xywh: SerializedXYWH; + }>(val); + + const connectorElem = gfx.getElementById( + elementId + ) as ConnectorElementModel; + + if (!connectorElem) { + delete elementsMap[elementId]; + return; + } + + // should be deleted during the import process + val.xywh = connectorElem.xywh; + + ['source', 'target'].forEach(key => { + const endpoint = val[key as 'source' | 'target']; + if (endpoint.id && !selectedIds.has(endpoint.id)) { + const endElem = gfx.getElementById(endpoint.id); + + if (!endElem) { + delete elementsMap[elementId]; + return; + } + + const endElemBound = (endElem as GfxModel).elementBound; + + val[key as 'source' | 'target'] = { + position: endElemBound.getRelativePoint( + endpoint.position ?? [0.5, 0.5] + ), + }; + } + }); + } + }); + } + }); }; }; diff --git a/blocksuite/affine/widget-drag-handle/src/utils.ts b/blocksuite/affine/widget-drag-handle/src/utils.ts index 92fac620fb..7a2a99f19e 100644 --- a/blocksuite/affine/widget-drag-handle/src/utils.ts +++ b/blocksuite/affine/widget-drag-handle/src/utils.ts @@ -290,13 +290,28 @@ export function getSnapshotRect(snapshot: SliceSnapshot): Bound | null { if (block.flavour === 'affine:surface') { if (block.props.elements) { Object.values( - block.props.elements as Record + block.props.elements as Record< + string, + { type: string; xywh: SerializedXYWH } + > ).forEach(elem => { if (elem.xywh) { bound = bound ? bound.unite(Bound.deserialize(elem.xywh)) : Bound.deserialize(elem.xywh); } + + if (elem.type === 'connector') { + let connectorBound: Bound | undefined; + + if (elem.xywh) { + connectorBound = Bound.deserialize(elem.xywh); + } + + if (connectorBound) { + bound = bound ? bound.unite(connectorBound) : connectorBound; + } + } }); } diff --git a/blocksuite/affine/widget-drag-handle/src/watchers/drag-event-watcher.ts b/blocksuite/affine/widget-drag-handle/src/watchers/drag-event-watcher.ts index c1078daee8..acb3dc4d65 100644 --- a/blocksuite/affine/widget-drag-handle/src/watchers/drag-event-watcher.ts +++ b/blocksuite/affine/widget-drag-handle/src/watchers/drag-event-watcher.ts @@ -1,5 +1,4 @@ import { ParagraphBlockComponent } from '@blocksuite/affine-block-paragraph'; -import { SurfaceBlockModel } from '@blocksuite/affine-block-surface'; import { DropIndicator } from '@blocksuite/affine-components/drop-indicator'; import { AttachmentBlockModel, @@ -45,11 +44,14 @@ import { type GfxModel, GfxPrimitiveElementModel, isGfxGroupCompatibleModel, + SURFACE_YMAP_UNIQ_IDENTIFIER, + SurfaceBlockModel, } from '@blocksuite/block-std/gfx'; import { assertType, Bound, groupBy, + type IVec, last, Point, Rect, @@ -752,7 +754,11 @@ export class DragEventWatcher { const idRemap = new Map(); let elemMap: Record< string, - { type: string; children?: { json: Record } } + { + type: string; + xywh?: SerializedXYWH; + children?: { json: Record }; + } > = {}; const blockMap: Record< string, @@ -766,7 +772,7 @@ export class DragEventWatcher { const constructor = surface.getConstructor(elem.type); const isGroup = Object.isPrototypeOf.call( GfxGroupLikeElementModel.prototype, - constructor + constructor.prototype ); return isGroup; @@ -782,24 +788,40 @@ export class DragEventWatcher { if (block.flavour === 'affine:surface') { elemMap = (block.props.elements as typeof elemMap) ?? {}; Object.entries(elemMap).forEach(([elemId, elem]) => { - if (isGroupLikeElem(elem)) { - // only add the group to the root if it's not a child of any other element - if ( - Object.values(containerTree).every( - childSet => !childSet.has(elemId) - ) - ) { - containerTree['root'].add(elem.type); - } + if ( + Object.values(containerTree).every( + childSet => !childSet.has(elemId) + ) + ) { + containerTree['root'].add(elemId); + } + if (isGroupLikeElem(elem)) { Object.keys(elem.children?.json ?? {}).forEach(childId => { containerTree[elemId] = containerTree[elemId] ?? new Set(); containerTree[elemId].add(childId); // if the child was already added to the root, remove it containerTree['root'].delete(childId); }); - } else { - containerTree['root'].add(elemId); + return; + } else if (elem.type === 'connector') { + assertType<{ + type: 'connector'; + source: { position: IVec; id?: string }; + target: { position: IVec; id?: string }; + }>(elem); + + if (elem.source.id) { + containerTree[elemId] = containerTree[elemId] ?? new Set(); + containerTree[elemId].add(elem.source.id); + containerTree['root'].delete(elem.source.id); + } + + if (elem.target.id) { + containerTree[elemId] = containerTree[elemId] ?? new Set(); + containerTree[elemId].add(elem.target.id); + containerTree['root'].delete(elem.target.id); + } } }); @@ -876,19 +898,58 @@ export class DragEventWatcher { idRemap.set(id, slices.content[0].id); } } else if (elemMap[id]) { - if (elemMap[id].children) { - const childJson = elemMap[id].children.json; - Object.keys(childJson).forEach(childId => { - if (idRemap.has(childId)) { - const remappedId = idRemap.get(childId)!; - childJson[remappedId] = childJson[childId]; - delete childJson[childId]; - } else { - delete childJson[childId]; + const elem = elemMap[id]; + + Object.entries(elem).forEach(([_, val]) => { + if ( + val instanceof Object && + Reflect.has(val, SURFACE_YMAP_UNIQ_IDENTIFIER) + ) { + const childJson = Reflect.get(val, 'json') as Record< + string, + unknown + >; + + Object.keys(childJson).forEach(oldChildId => { + if (idRemap.has(oldChildId)) { + const remappedId = idRemap.get(oldChildId)!; + const val = structuredClone(childJson[oldChildId]); + + if (elem.type === 'mindmap') { + assertType<{ parent?: string }>(val); + if (val.parent) { + val.parent = idRemap.get(val.parent); + } + } + childJson[remappedId] = val; + delete childJson[oldChildId]; + } else { + delete childJson[oldChildId]; + } + }); + } + }); + + if (elem.type === 'connector') { + assertType<{ + type: 'connector'; + source: { position: IVec; id?: string }; + target: { position: IVec; id?: string }; + }>(elem); + + (['source', 'target'] as const).forEach(key => { + const endpoint = elem[key]; + if (endpoint.id) { + if (idRemap.get(endpoint.id)) { + endpoint.id = idRemap.get(endpoint.id); + } else { + delete endpoint.id; + } } }); } - const newId = surface.addElement(elemMap[id]); + + const newId = surface.addElement(elem); idRemap.set(id, newId); } }; @@ -918,8 +979,45 @@ export class DragEventWatcher { if (block.flavour === 'affine:surface') { if (block.props.elements) { Object.values( - block.props.elements as Record + block.props.elements as Record< + string, + { type: string; xywh?: SerializedXYWH } + > ).forEach(elem => { + if (elem.type === 'connector') { + assertType<{ + type: 'connector'; + xywh?: SerializedXYWH; + source: { position: IVec; id?: string }; + target: { position: IVec; id?: string }; + }>(elem); + + const connectorBound = elem.xywh + ? Bound.deserialize(elem.xywh) + : new Bound(0, 0, 0, 0); + + delete elem.xywh; + + (['source', 'target'] as const).forEach(key => { + const endpoint = elem[key]; + if (!endpoint.id) { + const originalPos = endpoint.position; + + elem[key] = { + position: ignoreOriginalPos + ? [ + originalPos[0] - connectorBound.x + modelX, + originalPos[1] - connectorBound.y + modelY, + ] + : [ + originalPos[0] - rect.x + modelX, + originalPos[1] - rect.y + modelY, + ], + }; + } + }); + } + if (elem.xywh) { const elemBound = Bound.deserialize(elem.xywh); diff --git a/blocksuite/framework/block-std/src/gfx/index.ts b/blocksuite/framework/block-std/src/gfx/index.ts index c5264aa8b0..8022689907 100644 --- a/blocksuite/framework/block-std/src/gfx/index.ts +++ b/blocksuite/framework/block-std/src/gfx/index.ts @@ -58,6 +58,8 @@ export { prop, } from './model/surface/local-element-model.js'; export { + SURFACE_TEXT_UNIQ_IDENTIFIER, + SURFACE_YMAP_UNIQ_IDENTIFIER, SurfaceBlockModel, type SurfaceBlockProps, type SurfaceMiddleware, diff --git a/blocksuite/framework/block-std/src/gfx/model/surface/element-model.ts b/blocksuite/framework/block-std/src/gfx/model/surface/element-model.ts index f3f1404afe..f4a32ced40 100644 --- a/blocksuite/framework/block-std/src/gfx/model/surface/element-model.ts +++ b/blocksuite/framework/block-std/src/gfx/model/surface/element-model.ts @@ -199,10 +199,6 @@ export abstract class GfxPrimitiveElementModel< this.seed = randomSeed(); } - static propsToY(props: Record) { - return props; - } - containsBound(bounds: Bound): boolean { return getPointsFromBoundWithRotation(this).some(point => bounds.containsPoint(point) diff --git a/blocksuite/framework/block-std/src/gfx/model/surface/surface-model.ts b/blocksuite/framework/block-std/src/gfx/model/surface/surface-model.ts index 6518a0448a..9ef7207954 100644 --- a/blocksuite/framework/block-std/src/gfx/model/surface/surface-model.ts +++ b/blocksuite/framework/block-std/src/gfx/model/surface/surface-model.ts @@ -12,11 +12,20 @@ import { createDecoratorState } from './decorators/common.js'; import { initializeObservers, initializeWatchers } from './decorators/index.js'; import { GfxGroupLikeElementModel, - GfxPrimitiveElementModel, + type GfxPrimitiveElementModel, syncElementFromY, } from './element-model.js'; import type { GfxLocalElementModel } from './local-element-model.js'; +/** + * Used for text field + */ +export const SURFACE_TEXT_UNIQ_IDENTIFIER = 'affine:surface:text'; +/** + * Used for field that use Y.Map. E.g. group children field + */ +export const SURFACE_YMAP_UNIQ_IDENTIFIER = 'affine:surface:ymap'; + export type SurfaceBlockProps = { elements: Boxed>>; }; @@ -390,8 +399,28 @@ export class SurfaceBlockModel extends BlockModel { throw new Error(`Invalid element type: ${type}`); } + Object.entries(props).forEach(([key, val]) => { + if (val instanceof Object) { + if (Reflect.has(val, SURFACE_TEXT_UNIQ_IDENTIFIER)) { + const yText = new Y.Text(); + yText.applyDelta(Reflect.get(val, 'delta')); + Reflect.set(props, key, yText); + } + + if (Reflect.has(val, SURFACE_YMAP_UNIQ_IDENTIFIER)) { + const childJson = Reflect.get(val, 'json') as Record; + const childrenYMap = new Y.Map(); + + Object.keys(childJson).forEach(childId => { + childrenYMap.set(childId, childJson[childId]); + }); + Reflect.set(props, key, childrenYMap); + } + } + }); + // @ts-expect-error ignore - return (ctor.propsToY ?? GfxPrimitiveElementModel.propsToY)(props); + return ctor.propsToY ? ctor.propsToY(props) : props; } private _watchGroupRelationChange() {