Files
AFFiNE-Mirror/blocksuite/blocks/src/root-block/edgeless/services/template.ts
doouding f89fcf82f8 feat: edgeless dnd (#9988)
### Changed
- Support edgelss dnd
- Simplify the drag-handle state
2025-02-12 12:37:06 +00:00

384 lines
9.2 KiB
TypeScript

import type {
SurfaceBlockModel,
SurfaceBlockTransformer,
} from '@blocksuite/affine-block-surface';
import type { ConnectorElementModel } from '@blocksuite/affine-model';
import {
assertExists,
assertType,
Bound,
getCommonBound,
Slot,
} from '@blocksuite/global/utils';
import {
type BlockModel,
type BlockSnapshot,
type DocSnapshot,
DocSnapshotSchema,
type SnapshotNode,
Transformer,
} from '@blocksuite/store';
import type * as Y from 'yjs';
/**
* Those block contains other block's id
* should defer the loading
*/
const DEFERED_BLOCK = [
'affine:surface',
'affine:surface-ref',
'affine:frame',
] as const;
/**
* Those block should not be inserted directly
* it should be merged with current existing block
*/
const MERGE_BLOCK = ['affine:surface', 'affine:page'] as const;
type MergeBlockFlavour = (typeof MERGE_BLOCK)[number];
/**
* Template type will affect the inserting behaviour
*/
const TEMPLATE_TYPES = ['template', 'sticker'] as const;
type TemplateType = (typeof TEMPLATE_TYPES)[number];
export type SlotBlockPayload = {
type: 'block';
data: {
blockJson: BlockSnapshot;
parent?: string;
index?: number;
};
};
export type SlotPayload =
| SlotBlockPayload
| {
type: 'template';
template: DocSnapshot;
bound: Bound | null;
};
export type TemplateJobConfig = {
model: SurfaceBlockModel;
type: string;
middlewares: ((job: TemplateJob) => void)[];
};
export class TemplateJob {
static middlewares: ((job: TemplateJob) => void)[] = [];
private _template: DocSnapshot | null = null;
job: Transformer;
model: SurfaceBlockModel;
slots = {
beforeInsert: new Slot<
| SlotBlockPayload
| {
type: 'template';
template: DocSnapshot;
bound: Bound | null;
}
>(),
};
type: TemplateType;
constructor({ model, type, middlewares }: TemplateJobConfig) {
this.job = new Transformer({
schema: model.doc.workspace.schema,
blobCRUD: model.doc.workspace.blobSync,
docCRUD: {
create: (id: string) => model.doc.workspace.createDoc({ id }),
get: (id: string) => model.doc.workspace.getDoc(id),
delete: (id: string) => model.doc.workspace.removeDoc(id),
},
middlewares: [],
});
this.model = model;
this.type = TEMPLATE_TYPES.includes(type as TemplateType)
? (type as TemplateType)
: 'template';
middlewares.forEach(middleware => middleware(this));
TemplateJob.middlewares.forEach(middleware => middleware(this));
}
static create(options: {
model: SurfaceBlockModel;
type: string;
middlewares: ((job: TemplateJob) => void)[];
}) {
return new TemplateJob(options);
}
private _getMergeBlockId(modelData: BlockSnapshot) {
switch (modelData.flavour as MergeBlockFlavour) {
case 'affine:page':
return this.model.doc.root!.id;
case 'affine:surface':
return this.model.id;
}
}
private _getTemplateBound() {
const bounds: Bound[] = [];
this.walk(block => {
if (block.props.xywh) {
bounds.push(Bound.deserialize(block.props['xywh'] as string));
}
if (block.flavour === 'affine:surface') {
const ignoreType = new Set(['connector', 'group']);
Object.entries(
block.props.elements as Record<string, Record<string, unknown>>
).forEach(([_, val]) => {
const type = val['type'] as string;
if (val['xywh'] && !ignoreType.has(type)) {
bounds.push(Bound.deserialize(val['xywh'] as string));
}
if (type === 'connector') {
(['target', 'source'] as const).forEach(prop => {
const propVal = val[prop];
assertType<ConnectorElementModel['source']>(propVal);
if (propVal['id'] || !propVal['position']) return;
const pos = propVal['position'];
if (pos) {
bounds.push(new Bound(pos[0], pos[1], 0, 0));
}
});
}
});
}
});
return getCommonBound(bounds);
}
private _insertToDoc(
modelDataList: {
flavour: string;
json: BlockSnapshot;
modelData: SnapshotNode<object> | null;
parent?: string;
index?: number;
}[]
) {
const doc = this.model.doc;
const mergeIdMapping = new Map<string, string>();
const deferInserting: typeof modelDataList = [];
const insert = (
data: (typeof modelDataList)[number],
defered: boolean = true
) => {
const { flavour, json, modelData, parent, index } = data;
const isMergeBlock = MERGE_BLOCK.includes(flavour as MergeBlockFlavour);
if (isMergeBlock) {
mergeIdMapping.set(json.id, this._getMergeBlockId(json));
}
if (
defered &&
DEFERED_BLOCK.includes(flavour as (typeof DEFERED_BLOCK)[number])
) {
deferInserting.push(data);
return;
} else {
if (isMergeBlock) {
this._mergeProps(
json,
this.model.doc.getBlockById(
this._getMergeBlockId(json)
) as BlockModel
);
return;
}
assertExists(modelData);
doc.addBlock(
modelData.flavour,
{
...modelData.props,
id: modelData.id,
},
parent ? (mergeIdMapping.get(parent) ?? parent) : undefined,
index
);
}
};
modelDataList.forEach(data => insert(data));
deferInserting.forEach(data => insert(data, false));
}
private async _jsonToModelData(json: BlockSnapshot) {
const job = this.job;
const defered: {
snapshot: BlockSnapshot;
parent?: string;
index?: number;
}[] = [];
const modelDataList: {
flavour: string;
json: BlockSnapshot;
modelData: SnapshotNode<object> | null;
parent?: string;
index?: number;
}[] = [];
const toModel = async (
snapshot: BlockSnapshot,
parent?: string,
index?: number,
defer: boolean = true
) => {
if (
defer &&
DEFERED_BLOCK.includes(
snapshot.flavour as (typeof DEFERED_BLOCK)[number]
)
) {
defered.push({
snapshot,
parent,
index,
});
return;
}
const slotData = {
blockJson: snapshot,
parent,
index,
};
this.slots.beforeInsert.emit({ type: 'block', data: slotData });
/**
* merge block should not be converted to model data
*/
const modelData = MERGE_BLOCK.includes(
snapshot.flavour as MergeBlockFlavour
)
? null
: ((await job.snapshotToModelData(snapshot)) ?? null);
modelDataList.push({
flavour: snapshot.flavour,
json: snapshot,
modelData,
parent,
index,
});
if (snapshot.children) {
let index = 0;
for (const child of snapshot.children) {
await toModel(child, snapshot.id, index);
++index;
}
}
};
await toModel(json);
for (const json of defered) {
await toModel(json.snapshot, json.parent, json.index, false);
}
return modelDataList;
}
private _mergeProps(from: BlockSnapshot, to: BlockModel) {
switch (from.flavour as MergeBlockFlavour) {
case 'affine:page':
break;
case 'affine:surface':
this._mergeSurfaceElements(
from.props.elements as Record<string, Record<string, unknown>>,
(to as SurfaceBlockModel).elements.getValue()!
);
break;
}
}
private _mergeSurfaceElements(
from: Record<string, Record<string, unknown>>,
to: Y.Map<Y.Map<unknown>>
) {
const schema =
this.model.doc.workspace.schema.flavourSchemaMap.get('affine:surface');
const surfaceTransformer = schema?.transformer?.(
new Map()
) as SurfaceBlockTransformer;
this.model.doc.transact(() => {
const defered: [string, Record<string, unknown>][] = [];
Object.entries(from).forEach(([id, val]) => {
if (['connector', 'group'].includes(val.type as string)) {
defered.push([id, val]);
} else {
to.set(id, surfaceTransformer.elementFromJSON(val));
}
});
defered.forEach(([key, val]) => {
to.set(key, surfaceTransformer.elementFromJSON(val));
});
});
}
async insertTemplate(template: unknown) {
DocSnapshotSchema.parse(template);
assertType<DocSnapshot>(template);
this._template = template;
const templateBound = this._getTemplateBound();
this.slots.beforeInsert.emit({
type: 'template',
template: template,
bound: templateBound,
});
const modelDataList = await this._jsonToModelData(template.blocks);
this._insertToDoc(modelDataList);
return templateBound;
}
walk(callback: (block: BlockSnapshot, template: DocSnapshot) => void) {
if (!this._template) {
throw new Error('Template not loaded, please call insertTemplate first');
}
const iterate = (block: BlockSnapshot, template: DocSnapshot) => {
callback(block, template);
if (block.children) {
block.children.forEach(child => iterate(child, template));
}
};
iterate(this._template.blocks, this._template);
}
}