mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-07-13 16:16:46 +08:00
fix: drag connector and group element (#10385)
This commit is contained in:
@@ -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() {
|
||||
|
||||
Reference in New Issue
Block a user