fix: drag connector and group element (#10385)

This commit is contained in:
doouding
2025-02-24 06:13:04 +00:00
parent b9e3fc54fd
commit a0a97d0751
13 changed files with 255 additions and 51 deletions

View File

@@ -1,4 +1,8 @@
import type { SurfaceBlockProps } from '@blocksuite/block-std/gfx'; import type { SurfaceBlockProps } from '@blocksuite/block-std/gfx';
import {
SURFACE_TEXT_UNIQ_IDENTIFIER,
SURFACE_YMAP_UNIQ_IDENTIFIER,
} from '@blocksuite/block-std/gfx';
import type { import type {
FromSnapshotPayload, FromSnapshotPayload,
SnapshotNode, SnapshotNode,
@@ -7,10 +11,6 @@ import type {
import { BaseBlockTransformer } from '@blocksuite/store'; import { BaseBlockTransformer } from '@blocksuite/store';
import * as Y from 'yjs'; 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<SurfaceBlockProps> { export class SurfaceBlockTransformer extends BaseBlockTransformer<SurfaceBlockProps> {
private _elementToJSON(element: Y.Map<unknown>) { private _elementToJSON(element: Y.Map<unknown>) {
const value: Record<string, unknown> = {}; const value: Record<string, unknown> = {};

View File

@@ -63,10 +63,6 @@ export class BrushElementModel extends GfxPrimitiveElementModel<BrushProps> {
return 'brush'; return 'brush';
} }
static override propsToY(props: BrushProps) {
return props;
}
override containsBound(bounds: Bound) { override containsBound(bounds: Bound) {
const points = getPointsFromBoundWithRotation(this); const points = getPointsFromBoundWithRotation(this);
return points.some(point => bounds.containsPoint(point)); return points.some(point => bounds.containsPoint(point));

View File

@@ -125,8 +125,8 @@ export class ConnectorElementModel extends GfxPrimitiveElementModel<ConnectorEle
return 'connector'; return 'connector';
} }
static override propsToY(props: ConnectorElementProps) { static propsToY(props: ConnectorElementProps) {
if (props.text && !(props.text instanceof Y.Text)) { if (typeof props.text === 'string') {
props.text = new Y.Text(props.text); props.text = new Y.Text(props.text);
} }

View File

@@ -35,8 +35,8 @@ export class GroupElementModel extends GfxGroupLikeElementModel<GroupElementProp
return 'group'; return 'group';
} }
static override propsToY(props: Record<string, unknown>) { static propsToY(props: Record<string, unknown>) {
if ('title' in props && !(props.title instanceof Y.Text)) { if (typeof props.title === 'string') {
props.title = new Y.Text(props.title as string); props.title = new Y.Text(props.title as string);
} }

View File

@@ -180,7 +180,7 @@ export class MindmapElementModel extends GfxGroupLikeElementModel<MindmapElement
return 'mindmap'; return 'mindmap';
} }
static override propsToY(props: Record<string, unknown>) { static propsToY(props: Record<string, unknown>) {
if ( if (
props.children && props.children &&
!isNodeType(props.children as Record<string, unknown>) && !isNodeType(props.children as Record<string, unknown>) &&

View File

@@ -67,8 +67,8 @@ export class ShapeElementModel extends GfxPrimitiveElementModel<ShapeProps> {
return 'shape'; return 'shape';
} }
static override propsToY(props: ShapeProps) { static propsToY(props: ShapeProps) {
if (props.text && !(props.text instanceof Y.Text)) { if (typeof props.text === 'string') {
props.text = new Y.Text(props.text); props.text = new Y.Text(props.text);
} }

View File

@@ -30,9 +30,9 @@ export class TextElementModel extends GfxPrimitiveElementModel<TextElementProps>
return 'text'; return 'text';
} }
static override propsToY(props: Record<string, unknown>) { static propsToY(props: Record<string, unknown>) {
if (props.text && !(props.text instanceof Y.Text)) { if (typeof props.text === 'string') {
props.text = new Y.Text(props.text as string); props.text = new Y.Text(props.text);
} }
return props; return props;

View File

@@ -1,6 +1,16 @@
import type { SurfaceBlockModel } from '@blocksuite/affine-block-surface'; import type { SurfaceBlockModel } from '@blocksuite/affine-block-surface';
import type { ConnectorElementModel } from '@blocksuite/affine-model';
import type { BlockStdScope } from '@blocksuite/block-std'; 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'; import type { TransformerMiddleware } from '@blocksuite/store';
/** /**
@@ -18,6 +28,7 @@ export const gfxBlocksFilter = (
const surface = store.getBlocksByFlavour('affine:surface')[0] const surface = store.getBlocksByFlavour('affine:surface')[0]
.model as SurfaceBlockModel; .model as SurfaceBlockModel;
const idsToCheck = ids.slice(); const idsToCheck = ids.slice();
const gfx = std.get(GfxController);
for (const id of idsToCheck) { for (const id of idsToCheck) {
const blockOrElem = store.getBlock(id)?.model ?? surface.getElementById(id); const blockOrElem = store.getBlock(id)?.model ?? surface.getElementById(id);
@@ -45,5 +56,62 @@ export const gfxBlocksFilter = (
return; 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]
),
};
}
});
}
});
}
});
}; };
}; };

View File

@@ -290,13 +290,28 @@ export function getSnapshotRect(snapshot: SliceSnapshot): Bound | null {
if (block.flavour === 'affine:surface') { if (block.flavour === 'affine:surface') {
if (block.props.elements) { if (block.props.elements) {
Object.values( Object.values(
block.props.elements as Record<string, { xywh: SerializedXYWH }> block.props.elements as Record<
string,
{ type: string; xywh: SerializedXYWH }
>
).forEach(elem => { ).forEach(elem => {
if (elem.xywh) { if (elem.xywh) {
bound = bound bound = bound
? bound.unite(Bound.deserialize(elem.xywh)) ? bound.unite(Bound.deserialize(elem.xywh))
: 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;
}
}
}); });
} }

View File

@@ -1,5 +1,4 @@
import { ParagraphBlockComponent } from '@blocksuite/affine-block-paragraph'; import { ParagraphBlockComponent } from '@blocksuite/affine-block-paragraph';
import { SurfaceBlockModel } from '@blocksuite/affine-block-surface';
import { DropIndicator } from '@blocksuite/affine-components/drop-indicator'; import { DropIndicator } from '@blocksuite/affine-components/drop-indicator';
import { import {
AttachmentBlockModel, AttachmentBlockModel,
@@ -45,11 +44,14 @@ import {
type GfxModel, type GfxModel,
GfxPrimitiveElementModel, GfxPrimitiveElementModel,
isGfxGroupCompatibleModel, isGfxGroupCompatibleModel,
SURFACE_YMAP_UNIQ_IDENTIFIER,
SurfaceBlockModel,
} from '@blocksuite/block-std/gfx'; } from '@blocksuite/block-std/gfx';
import { import {
assertType, assertType,
Bound, Bound,
groupBy, groupBy,
type IVec,
last, last,
Point, Point,
Rect, Rect,
@@ -752,7 +754,11 @@ export class DragEventWatcher {
const idRemap = new Map<string, string>(); const idRemap = new Map<string, string>();
let elemMap: Record< let elemMap: Record<
string, string,
{ type: string; children?: { json: Record<string, unknown> } } {
type: string;
xywh?: SerializedXYWH;
children?: { json: Record<string, unknown> };
}
> = {}; > = {};
const blockMap: Record< const blockMap: Record<
string, string,
@@ -766,7 +772,7 @@ export class DragEventWatcher {
const constructor = surface.getConstructor(elem.type); const constructor = surface.getConstructor(elem.type);
const isGroup = Object.isPrototypeOf.call( const isGroup = Object.isPrototypeOf.call(
GfxGroupLikeElementModel.prototype, GfxGroupLikeElementModel.prototype,
constructor constructor.prototype
); );
return isGroup; return isGroup;
@@ -782,24 +788,40 @@ export class DragEventWatcher {
if (block.flavour === 'affine:surface') { if (block.flavour === 'affine:surface') {
elemMap = (block.props.elements as typeof elemMap) ?? {}; elemMap = (block.props.elements as typeof elemMap) ?? {};
Object.entries(elemMap).forEach(([elemId, elem]) => { Object.entries(elemMap).forEach(([elemId, elem]) => {
if (isGroupLikeElem(elem)) { if (
// only add the group to the root if it's not a child of any other element Object.values(containerTree).every(
if ( childSet => !childSet.has(elemId)
Object.values(containerTree).every( )
childSet => !childSet.has(elemId) ) {
) containerTree['root'].add(elemId);
) { }
containerTree['root'].add(elem.type);
}
if (isGroupLikeElem(elem)) {
Object.keys(elem.children?.json ?? {}).forEach(childId => { Object.keys(elem.children?.json ?? {}).forEach(childId => {
containerTree[elemId] = containerTree[elemId] ?? new Set(); containerTree[elemId] = containerTree[elemId] ?? new Set();
containerTree[elemId].add(childId); containerTree[elemId].add(childId);
// if the child was already added to the root, remove it // if the child was already added to the root, remove it
containerTree['root'].delete(childId); containerTree['root'].delete(childId);
}); });
} else { return;
containerTree['root'].add(elemId); } 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); idRemap.set(id, slices.content[0].id);
} }
} else if (elemMap[id]) { } else if (elemMap[id]) {
if (elemMap[id].children) { const elem = elemMap[id];
const childJson = elemMap[id].children.json;
Object.keys(childJson).forEach(childId => { Object.entries(elem).forEach(([_, val]) => {
if (idRemap.has(childId)) { if (
const remappedId = idRemap.get(childId)!; val instanceof Object &&
childJson[remappedId] = childJson[childId]; Reflect.has(val, SURFACE_YMAP_UNIQ_IDENTIFIER)
delete childJson[childId]; ) {
} else { const childJson = Reflect.get(val, 'json') as Record<
delete childJson[childId]; 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); idRemap.set(id, newId);
} }
}; };
@@ -918,8 +979,45 @@ export class DragEventWatcher {
if (block.flavour === 'affine:surface') { if (block.flavour === 'affine:surface') {
if (block.props.elements) { if (block.props.elements) {
Object.values( Object.values(
block.props.elements as Record<string, { xywh: SerializedXYWH }> block.props.elements as Record<
string,
{ type: string; xywh?: SerializedXYWH }
>
).forEach(elem => { ).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) { if (elem.xywh) {
const elemBound = Bound.deserialize(elem.xywh); const elemBound = Bound.deserialize(elem.xywh);

View File

@@ -58,6 +58,8 @@ export {
prop, prop,
} from './model/surface/local-element-model.js'; } from './model/surface/local-element-model.js';
export { export {
SURFACE_TEXT_UNIQ_IDENTIFIER,
SURFACE_YMAP_UNIQ_IDENTIFIER,
SurfaceBlockModel, SurfaceBlockModel,
type SurfaceBlockProps, type SurfaceBlockProps,
type SurfaceMiddleware, type SurfaceMiddleware,

View File

@@ -199,10 +199,6 @@ export abstract class GfxPrimitiveElementModel<
this.seed = randomSeed(); this.seed = randomSeed();
} }
static propsToY(props: Record<string, unknown>) {
return props;
}
containsBound(bounds: Bound): boolean { containsBound(bounds: Bound): boolean {
return getPointsFromBoundWithRotation(this).some(point => return getPointsFromBoundWithRotation(this).some(point =>
bounds.containsPoint(point) bounds.containsPoint(point)

View File

@@ -12,11 +12,20 @@ import { createDecoratorState } from './decorators/common.js';
import { initializeObservers, initializeWatchers } from './decorators/index.js'; import { initializeObservers, initializeWatchers } from './decorators/index.js';
import { import {
GfxGroupLikeElementModel, GfxGroupLikeElementModel,
GfxPrimitiveElementModel, type GfxPrimitiveElementModel,
syncElementFromY, syncElementFromY,
} from './element-model.js'; } from './element-model.js';
import type { GfxLocalElementModel } from './local-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 = { export type SurfaceBlockProps = {
elements: Boxed<Y.Map<Y.Map<unknown>>>; elements: Boxed<Y.Map<Y.Map<unknown>>>;
}; };
@@ -390,8 +399,28 @@ export class SurfaceBlockModel extends BlockModel<SurfaceBlockProps> {
throw new Error(`Invalid element type: ${type}`); 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<string, unknown>;
const childrenYMap = new Y.Map<unknown>();
Object.keys(childJson).forEach(childId => {
childrenYMap.set(childId, childJson[childId]);
});
Reflect.set(props, key, childrenYMap);
}
}
});
// @ts-expect-error ignore // @ts-expect-error ignore
return (ctor.propsToY ?? GfxPrimitiveElementModel.propsToY)(props); return ctor.propsToY ? ctor.propsToY(props) : props;
} }
private _watchGroupRelationChange() { private _watchGroupRelationChange() {