Files
AFFiNE-Mirror/blocksuite/affine/blocks/root/src/edgeless/utils/clone-utils.ts
Saul-Mirone 1f45cc5dec refactor(editor): unify directories naming (#11516)
**Directory Structure Changes**

- Renamed multiple block-related directories by removing the "block-" prefix:
  - `block-attachment` → `attachment`
  - `block-bookmark` → `bookmark`
  - `block-callout` → `callout`
  - `block-code` → `code`
  - `block-data-view` → `data-view`
  - `block-database` → `database`
  - `block-divider` → `divider`
  - `block-edgeless-text` → `edgeless-text`
  - `block-embed` → `embed`
2025-04-07 12:34:40 +00:00

238 lines
6.4 KiB
TypeScript

import type {
FrameBlockProps,
NodeDetail,
SerializedConnectorElement,
SerializedGroupElement,
SerializedMindmapElement,
} from '@blocksuite/affine-model';
import {
ConnectorElementModel,
GroupElementModel,
MindmapElementModel,
} from '@blocksuite/affine-model';
import type { BlockStdScope } from '@blocksuite/std';
import {
getTopElements,
GfxBlockElementModel,
type GfxModel,
type GfxPrimitiveElementModel,
isGfxGroupCompatibleModel,
type SerializedElement,
} from '@blocksuite/std/gfx';
import type { BlockSnapshot, Transformer } from '@blocksuite/store';
/**
* return all elements in the tree of the elements
*/
export function getSortedCloneElements(elements: GfxModel[]) {
const set = new Set<GfxModel>();
elements.forEach(element => {
// this element subtree has been added
if (set.has(element)) return;
set.add(element);
if (isGfxGroupCompatibleModel(element)) {
element.descendantElements.forEach(descendant => set.add(descendant));
}
});
return sortEdgelessElements([...set]);
}
export function prepareCloneData(elements: GfxModel[], std: BlockStdScope) {
elements = sortEdgelessElements(elements);
const job = std.store.getTransformer();
const res = elements.map(element => {
const data = serializeElement(element, elements, job);
return data;
});
return res.filter((d): d is SerializedElement | BlockSnapshot => !!d);
}
export function serializeElement(
element: GfxModel,
elements: GfxModel[],
job: Transformer
) {
if (element instanceof GfxBlockElementModel) {
const snapshot = job.blockToSnapshot(element);
if (!snapshot) {
return;
}
return { ...snapshot };
} else if (element instanceof ConnectorElementModel) {
return serializeConnector(element, elements);
} else {
return element.serialize();
}
}
export function serializeConnector(
connector: ConnectorElementModel,
elements: GfxModel[]
) {
const sourceId = connector.source?.id;
const targetId = connector.target?.id;
const serialized = connector.serialize();
// if the source or target element not to be cloned
// transfer connector position to absolute path
if (sourceId && elements.every(s => s.id !== sourceId)) {
serialized.source = { position: connector.absolutePath[0] };
}
if (targetId && elements.every(s => s.id !== targetId)) {
serialized.target = {
position: connector.absolutePath[connector.absolutePath.length - 1],
};
}
return serialized;
}
/**
* There are interdependencies between elements,
* so they must be added in a certain order
* @param elements edgeless model list
* @returns sorted edgeless model list
*/
export function sortEdgelessElements(elements: GfxModel[]) {
// Since each element has a parent-child relationship, and from-to connector relationship
// the child element must be added before the parent element
// and the connected elements must be added before the connector element
// To achieve this, we do a post-order traversal of the tree
if (elements.length === 0) return [];
const result: GfxModel[] = [];
const topElements = getTopElements(elements);
// the connector element must be added after the connected elements
const moveConnectorToEnd = (elements: GfxModel[]) => {
const connectors = elements.filter(
element => element instanceof ConnectorElementModel
);
const rest = elements.filter(
element => !(element instanceof ConnectorElementModel)
);
return [...rest, ...connectors];
};
const traverse = (element: GfxModel) => {
if (isGfxGroupCompatibleModel(element)) {
moveConnectorToEnd(element.childElements).forEach(child =>
traverse(child)
);
}
result.push(element);
};
moveConnectorToEnd(topElements).forEach(element => traverse(element));
return result;
}
/**
* map connector source & target ids
* @param props serialized element props
* @param ids old element id to new element id map
* @returns updated element props
*/
export function mapConnectorIds(
props: SerializedConnectorElement,
ids: Map<string, string>
) {
if (props.source.id) {
props.source.id = ids.get(props.source.id);
}
if (props.target.id) {
props.target.id = ids.get(props.target.id);
}
return props;
}
/**
* map group children ids
* @param props serialized element props
* @param ids old element id to new element id map
* @returns updated element props
*/
export function mapGroupIds(
props: SerializedGroupElement,
ids: Map<string, string>
) {
if (props.children) {
const newMap: Record<string, boolean> = {};
for (const [key, value] of Object.entries(props.children)) {
const newKey = ids.get(key);
if (newKey) {
newMap[newKey] = value;
}
}
props.children = newMap;
}
return props;
}
/**
* map frame children ids
* @param props frame block props
* @param ids old element id to new element id map
* @returns updated frame block props
*/
export function mapFrameIds(props: FrameBlockProps, ids: Map<string, string>) {
const oldChildIds = props.childElementIds
? Object.keys(props.childElementIds)
: [];
const newChildIds: Record<string, boolean> = {};
oldChildIds.forEach(oldId => {
const newIds = ids.get(oldId);
if (newIds) newChildIds[newIds] = true;
});
props.childElementIds = newChildIds;
return props;
}
/**
* map mindmap children & parent ids
* @param props serialized element props
* @param ids old element id to new element id map
* @returns updated element props
*/
export function mapMindmapIds(
props: SerializedMindmapElement,
ids: Map<string, string>
) {
if (props.children) {
const newMap: Record<string, NodeDetail> = {};
for (const [key, value] of Object.entries(props.children)) {
const newKey = ids.get(key);
if (value.parent) {
const newParent = ids.get(value.parent);
value.parent = newParent;
}
if (newKey) {
newMap[newKey] = value;
}
}
props.children = newMap;
}
return props;
}
export function getElementProps(
element: GfxPrimitiveElementModel,
ids: Map<string, string>
) {
if (element instanceof ConnectorElementModel) {
const props = element.serialize();
return mapConnectorIds(props, ids);
}
if (element instanceof GroupElementModel) {
const props = element.serialize();
return mapGroupIds(props, ids);
}
if (element instanceof MindmapElementModel) {
const props = element.serialize();
return mapMindmapIds(props, ids);
}
return element.serialize();
}