mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-02-13 21:05:19 +00:00
384 lines
9.2 KiB
TypeScript
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);
|
|
}
|
|
}
|