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
@@ -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<SurfaceBlockProps> {
private _elementToJSON(element: Y.Map<unknown>) {
const value: Record<string, unknown> = {};
@@ -63,10 +63,6 @@ export class BrushElementModel extends GfxPrimitiveElementModel<BrushProps> {
return 'brush';
}
static override propsToY(props: BrushProps) {
return props;
}
override containsBound(bounds: Bound) {
const points = getPointsFromBoundWithRotation(this);
return points.some(point => bounds.containsPoint(point));
@@ -125,8 +125,8 @@ export class ConnectorElementModel extends GfxPrimitiveElementModel<ConnectorEle
return 'connector';
}
static override propsToY(props: ConnectorElementProps) {
if (props.text && !(props.text instanceof Y.Text)) {
static propsToY(props: ConnectorElementProps) {
if (typeof props.text === 'string') {
props.text = new Y.Text(props.text);
}
@@ -35,8 +35,8 @@ export class GroupElementModel extends GfxGroupLikeElementModel<GroupElementProp
return 'group';
}
static override propsToY(props: Record<string, unknown>) {
if ('title' in props && !(props.title instanceof Y.Text)) {
static propsToY(props: Record<string, unknown>) {
if (typeof props.title === 'string') {
props.title = new Y.Text(props.title as string);
}
@@ -180,7 +180,7 @@ export class MindmapElementModel extends GfxGroupLikeElementModel<MindmapElement
return 'mindmap';
}
static override propsToY(props: Record<string, unknown>) {
static propsToY(props: Record<string, unknown>) {
if (
props.children &&
!isNodeType(props.children as Record<string, unknown>) &&
@@ -67,8 +67,8 @@ export class ShapeElementModel extends GfxPrimitiveElementModel<ShapeProps> {
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);
}
@@ -30,9 +30,9 @@ export class TextElementModel extends GfxPrimitiveElementModel<TextElementProps>
return 'text';
}
static override propsToY(props: Record<string, unknown>) {
if (props.text && !(props.text instanceof Y.Text)) {
props.text = new Y.Text(props.text as string);
static propsToY(props: Record<string, unknown>) {
if (typeof props.text === 'string') {
props.text = new Y.Text(props.text);
}
return props;
@@ -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]
),
};
}
});
}
});
}
});
};
};
@@ -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<string, { xywh: SerializedXYWH }>
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;
}
}
});
}
@@ -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<string, string>();
let elemMap: Record<
string,
{ type: string; children?: { json: Record<string, unknown> } }
{
type: string;
xywh?: SerializedXYWH;
children?: { json: Record<string, unknown> };
}
> = {};
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<string, { xywh: SerializedXYWH }>
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);
@@ -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,
@@ -199,10 +199,6 @@ export abstract class GfxPrimitiveElementModel<
this.seed = randomSeed();
}
static propsToY(props: Record<string, unknown>) {
return props;
}
containsBound(bounds: Bound): boolean {
return getPointsFromBoundWithRotation(this).some(point =>
bounds.containsPoint(point)
@@ -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<Y.Map<Y.Map<unknown>>>;
};
@@ -390,8 +399,28 @@ export class SurfaceBlockModel extends BlockModel<SurfaceBlockProps> {
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
return (ctor.propsToY ?? GfxPrimitiveElementModel.propsToY)(props);
return ctor.propsToY ? ctor.propsToY(props) : props;
}
private _watchGroupRelationChange() {