feat: edgeless dnd (#9988)

### Changed
- Support edgelss dnd
- Simplify the drag-handle state
This commit is contained in:
doouding
2025-02-12 12:37:06 +00:00
parent 8129434a2e
commit f89fcf82f8
22 changed files with 1041 additions and 350 deletions

View File

@@ -13,25 +13,22 @@ import {
import { Peekable } from '@blocksuite/affine-components/peek';
import {
FrameBlockModel,
GroupElementModel,
RootBlockModel,
type SurfaceRefBlockModel,
} from '@blocksuite/affine-model';
import {
DocModeProvider,
EditorSettingExtension,
EditorSettingProvider,
EditPropsStore,
GeneralSettingSchema,
ThemeProvider,
} from '@blocksuite/affine-shared/services';
import {
matchModels,
requestConnectedFrame,
SpecProvider,
} from '@blocksuite/affine-shared/utils';
import {
BlockComponent,
BlockSelection,
BlockServiceWatcher,
BlockStdScope,
type EditorHost,
LifeCycleWatcher,
@@ -40,8 +37,8 @@ import {
import {
GfxBlockElementModel,
GfxControllerIdentifier,
GfxExtension,
type GfxModel,
GfxPrimitiveElementModel,
} from '@blocksuite/block-std/gfx';
import { BlockSuiteError, ErrorCode } from '@blocksuite/global/exceptions';
import {
@@ -52,12 +49,10 @@ import {
type SerializedXYWH,
} from '@blocksuite/global/utils';
import type { BaseSelection, Store } from '@blocksuite/store';
import { signal } from '@preact/signals-core';
import { css, html, nothing, type TemplateResult } from 'lit';
import { query, state } from 'lit/decorators.js';
import { styleMap } from 'lit/directives/style-map.js';
import type { EdgelessPreviewer } from './types.js';
import { noContentPlaceholder } from './utils.js';
const REF_LABEL_ICON = {
@@ -427,48 +422,26 @@ export class SurfaceRefBlockComponent extends BlockComponent<SurfaceRefBlockMode
private _initSpec() {
const refreshViewport = this._refreshViewport.bind(this);
// oxlint-disable-next-line @typescript-eslint/no-this-alias
const self = this;
const editorSetting =
this.std.getOptional(EditorSettingProvider) ??
signal(GeneralSettingSchema.parse({}));
class PageViewWatcher extends BlockServiceWatcher {
static override readonly flavour = 'affine:page';
class SurfaceRefViewportInitializer extends LifeCycleWatcher {
static override readonly key = 'surfaceRefViewportInitializer';
override mounted() {
this.blockService.disposables.add(
this.blockService.specSlots.viewConnected.once(({ component }) => {
const edgelessBlock = component as BlockComponent &
EdgelessPreviewer;
edgelessBlock.editorViewportSelector = 'ref-viewport';
refreshViewport();
const gfx = edgelessBlock.std.get(GfxControllerIdentifier);
gfx.viewport.sizeUpdated.once(() => {
const disposable = this.std.view.viewUpdated.on(payload => {
if (
payload.type === 'add' &&
matchModels(payload.view.model, [RootBlockModel])
) {
disposable.dispose();
queueMicrotask(() => refreshViewport());
const gfx = this.std.get(GfxControllerIdentifier);
gfx.viewport.sizeUpdated.on(() => {
refreshViewport();
});
})
);
}
});
}
}
class ViewportInitializer extends GfxExtension {
static override readonly key = 'surface-ref-viewport-initializer';
override mounted() {
this.gfx.viewport.setViewportByBound(
Bound.deserialize(self._referenceXYWH!)
);
refreshViewport();
}
}
this._previewSpec.extend([
ViewportInitializer,
PageViewWatcher,
EditorSettingExtension(editorSetting),
]);
this._previewSpec.extend([SurfaceRefViewportInitializer]);
const referenceId = this.model.reference;
const setReferenceXYWH = (xywh: typeof this._referenceXYWH) => {
@@ -501,7 +474,7 @@ export class SurfaceRefBlockComponent extends BlockComponent<SurfaceRefBlockMode
refreshViewport();
})
);
} else if (referenceElement instanceof GroupElementModel) {
} else if (referenceElement instanceof GfxPrimitiveElementModel) {
_disposable.add(
surfaceModel.elementUpdated.on(({ id, oldValues }) => {
if (
@@ -513,8 +486,6 @@ export class SurfaceRefBlockComponent extends BlockComponent<SurfaceRefBlockMode
}
})
);
} else {
console.warn('Unsupported reference element type');
}
}

View File

@@ -31,7 +31,8 @@ export const SurfaceBlockSchema = defineBlockSchema({
'affine:edgeless-text',
],
},
transformer: () => new SurfaceBlockTransformer(),
transformer: transformerConfigs =>
new SurfaceBlockTransformer(transformerConfigs),
toModel: () => new SurfaceBlockModel(),
});

View File

@@ -92,9 +92,18 @@ export class SurfaceBlockTransformer extends BaseBlockTransformer<SurfaceBlockPr
const snapshot = super.toSnapshot(payload);
const elementsValue = payload.model.elements.getValue();
const value: Record<string, unknown> = {};
/**
* When the selectedElements is defined, only the selected elements will be serialized.
*/
const selectedElements = this.transformerConfigs.get(
'selectedElements'
) as Set<string>;
if (elementsValue) {
elementsValue.forEach((element, key) => {
value[key] = this._elementToJSON(element as Y.Map<unknown>);
if (selectedElements?.has(key) || !selectedElements) {
value[key] = this._elementToJSON(element as Y.Map<unknown>);
}
});
}
snapshot.props = {

View File

@@ -81,7 +81,8 @@ export const AttachmentBlockSchema = defineBlockSchema({
'affine:list',
],
},
transformer: () => new AttachmentBlockTransformer(),
transformer: transformerConfigs =>
new AttachmentBlockTransformer(transformerConfigs),
toModel: () => new AttachmentBlockModel(),
});

View File

@@ -35,7 +35,8 @@ export const ImageBlockSchema = defineBlockSchema({
version: 1,
role: 'content',
},
transformer: () => new ImageBlockTransformer(),
transformer: transformerConfigs =>
new ImageBlockTransformer(transformerConfigs),
toModel: () => new ImageBlockModel(),
});

View File

@@ -4,10 +4,9 @@ import { DocModeProvider } from '@blocksuite/affine-shared/services';
import {
isInsideEdgelessEditor,
isInsidePageEditor,
isTopLevelBlock,
} from '@blocksuite/affine-shared/utils';
import { type BlockComponent, WidgetComponent } from '@blocksuite/block-std';
import type { GfxBlockElementModel } from '@blocksuite/block-std/gfx';
import type { GfxModel } from '@blocksuite/block-std/gfx';
import {
DisposableGroup,
type IVec,
@@ -15,8 +14,9 @@ import {
type Rect,
} from '@blocksuite/global/utils';
import { computed, type ReadonlySignal, signal } from '@preact/signals-core';
import { html } from 'lit';
import { html, nothing } from 'lit';
import { query, state } from 'lit/decorators.js';
import { classMap } from 'lit/directives/class-map.js';
import { styleMap } from 'lit/directives/style-map.js';
import type { AFFINE_DRAG_HANDLE_WIDGET } from './consts.js';
@@ -53,12 +53,13 @@ export class AffineDragHandleWidget extends WidgetComponent<RootBlockModel> {
this.dragHoverRect = null;
this.anchorBlockId.value = null;
this.isDragHandleHovered = false;
this.isHoverDragHandleVisible = false;
this.isTopLevelDragHandleVisible = false;
this.pointerEventWatcher.reset();
};
@state()
accessor activeDragHandle: 'block' | 'gfx' | null = null;
anchorBlockId = signal<string | null>(null);
anchorBlockComponent = computed<BlockComponent | null>(() => {
@@ -67,16 +68,14 @@ export class AffineDragHandleWidget extends WidgetComponent<RootBlockModel> {
return this.std.view.getBlock(this.anchorBlockId.value);
});
anchorEdgelessElement: ReadonlySignal<GfxBlockElementModel | null> = computed(
() => {
if (!this.anchorBlockId.value) return null;
if (this.mode === 'page') return null;
anchorEdgelessElement: ReadonlySignal<GfxModel | null> = computed(() => {
if (!this.anchorBlockId.value) return null;
if (this.mode === 'page') return null;
const crud = this.std.get(EdgelessCRUDIdentifier);
const edgelessElement = crud.getElementById(this.anchorBlockId.value);
return isTopLevelBlock(edgelessElement) ? edgelessElement : null;
}
);
const crud = this.std.get(EdgelessCRUDIdentifier);
const edgelessElement = crud.getElementById(this.anchorBlockId.value);
return edgelessElement;
});
// Single block: drag handle should show on the vertical middle of the first line of element
center: IVec = [0, 0];
@@ -115,15 +114,18 @@ export class AffineDragHandleWidget extends WidgetComponent<RootBlockModel> {
if (this.dragging && !force) return;
updateDragHandleClassName();
this.isHoverDragHandleVisible = false;
this.isTopLevelDragHandleVisible = false;
this.isDragHandleHovered = false;
this.anchorBlockId.value = null;
this.activeDragHandle = null;
if (this.dragHandleContainer) {
this.dragHandleContainer.removeAttribute('style');
this.dragHandleContainer.style.display = 'none';
}
if (this.dragHandleGrabber) {
this.dragHandleGrabber.removeAttribute('style');
}
if (force) {
this._reset();
@@ -132,9 +134,13 @@ export class AffineDragHandleWidget extends WidgetComponent<RootBlockModel> {
isDragHandleHovered = false;
isHoverDragHandleVisible = false;
get isBlockDragHandleVisible() {
return this.activeDragHandle === 'block';
}
isTopLevelDragHandleVisible = false;
get isGfxDragHandleVisible() {
return this.activeDragHandle === 'gfx';
}
noteScale = signal(1);
@@ -190,7 +196,7 @@ export class AffineDragHandleWidget extends WidgetComponent<RootBlockModel> {
override render() {
const hoverRectStyle = styleMap(
this.dragHoverRect
this.dragHoverRect && this.activeDragHandle
? {
width: `${this.dragHoverRect.width}px`,
height: `${this.dragHoverRect.height}px`,
@@ -201,11 +207,27 @@ export class AffineDragHandleWidget extends WidgetComponent<RootBlockModel> {
display: 'none',
}
);
const isGfx = this.activeDragHandle === 'gfx';
const classes = {
'affine-drag-handle-grabber': true,
dots: isGfx ? true : false,
};
return html`
<div class="affine-drag-handle-widget">
<div class="affine-drag-handle-container" draggable="true">
<div class="affine-drag-handle-grabber"></div>
<div class="affine-drag-handle-container">
<div class=${classMap(classes)}>
${isGfx
? html`
<div class="dot"></div>
<div class="dot"></div>
<div class="dot"></div>
<div class="dot"></div>
<div class="dot"></div>
<div class="dot"></div>
`
: nothing}
</div>
</div>
<div class="affine-drag-hover-rect" style=${hoverRectStyle}></div>
</div>

View File

@@ -15,7 +15,7 @@ import {
export class RectHelper {
private readonly _getHoveredBlocks = (): BlockComponent[] => {
if (!this.widget.isHoverDragHandleVisible || !this.widget.anchorBlockId)
if (!this.widget.isBlockDragHandleVisible || !this.widget.anchorBlockId)
return [];
const hoverBlock = this.widget.anchorBlockComponent.peek();

View File

@@ -0,0 +1,49 @@
import type { SurfaceBlockModel } from '@blocksuite/affine-block-surface';
import type { BlockStdScope } from '@blocksuite/block-std';
import { isGfxGroupCompatibleModel } from '@blocksuite/block-std/gfx';
import type { TransformerMiddleware } from '@blocksuite/store';
/**
* Used to filter out gfx elements that are not selected
* @param ids
* @param std
* @returns
*/
export const gfxBlocksFilter = (
ids: string[],
std: BlockStdScope
): TransformerMiddleware => {
const selectedIds = new Set<string>();
const store = std.store;
const surface = store.getBlocksByFlavour('affine:surface')[0]
.model as SurfaceBlockModel;
const idsToCheck = ids.slice();
for (const id of idsToCheck) {
const blockOrElem = store.getBlock(id)?.model ?? surface.getElementById(id);
if (!blockOrElem) continue;
if (isGfxGroupCompatibleModel(blockOrElem)) {
idsToCheck.push(...blockOrElem.childIds);
}
selectedIds.add(id);
}
return ({ slots, transformerConfigs }) => {
slots.beforeExport.on(payload => {
if (payload.type !== 'block') {
return;
}
if (payload.model.flavour === 'affine:surface') {
transformerConfigs.set('selectedElements', selectedIds);
payload.model.children = payload.model.children.filter(model =>
selectedIds.has(model.id)
);
return;
}
});
};
};

View File

@@ -1,3 +1,4 @@
import { unsafeCSSVarV2 } from '@blocksuite/affine-shared/theme';
import { css } from 'lit';
import { DRAG_HANDLE_CONTAINER_WIDTH } from './config.js';
@@ -35,6 +36,32 @@ export const styles = css`
transition: width 0.25s ease;
}
.affine-drag-handle-grabber.dots {
width: 14px;
height: 26px;
box-sizing: border-box;
padding: 5px 2px;
border-radius: 4px;
gap: 2px;
display: flex;
flex-wrap: wrap;
background-color: transparent;
transform: translateX(-100%);
transition: unset;
}
.affine-drag-handle-grabber.dots:hover {
background-color: ${unsafeCSSVarV2('layer/background/hoverOverlay')};
}
.affine-drag-handle-grabber.dots > .dot {
width: 4px;
height: 4px;
border-radius: 50%;
flex: 0 0 4px;
background-color: ${unsafeCSSVarV2('icon/secondary')};
}
@media print {
.affine-drag-handle-widget {
display: none;

View File

@@ -151,7 +151,7 @@ export const isOutOfNoteBlock = (
};
export const getParentNoteBlock = (blockComponent: BlockComponent) => {
return blockComponent.closest('affine-note') ?? null;
return blockComponent.closest('affine-note, affine-edgeless-note') ?? null;
};
export const getClosestNoteBlock = (

View File

@@ -2,10 +2,7 @@ import {
EdgelessLegacySlotIdentifier,
type SurfaceBlockComponent,
} from '@blocksuite/affine-block-surface';
import {
getSelectedRect,
isTopLevelBlock,
} from '@blocksuite/affine-shared/utils';
import { getSelectedRect } from '@blocksuite/affine-shared/utils';
import {
GfxControllerIdentifier,
type GfxToolsFullOptionValue,
@@ -16,19 +13,23 @@ import { effect } from '@preact/signals-core';
import {
DRAG_HANDLE_CONTAINER_OFFSET_LEFT_TOP_LEVEL,
DRAG_HANDLE_CONTAINER_WIDTH_TOP_LEVEL,
DRAG_HANDLE_GRABBER_BORDER_RADIUS,
DRAG_HANDLE_GRABBER_WIDTH_HOVERED,
HOVER_AREA_RECT_PADDING_TOP_LEVEL,
} from '../config.js';
import type { AffineDragHandleWidget } from '../drag-handle.js';
/**
* Used to control the drag handle visibility in edgeless mode
*
* 1. Show drag handle on every block and gfx element
* 2. Multiple selection is not supported
*/
export class EdgelessWatcher {
private readonly _handleEdgelessToolUpdated = (
newTool: GfxToolsFullOptionValue
) => {
// @ts-expect-error FIXME: resolve after gfx tool refactor
if (newTool.type === 'default') {
this.checkTopLevelBlockSelection();
this.updateAnchorElement();
} else {
this.widget.hide();
}
@@ -52,17 +53,15 @@ export class EdgelessWatcher {
this.widget.center = [...center];
}
if (this.widget.isTopLevelDragHandleVisible) {
this._showDragHandleOnTopLevelBlocks().catch(console.error);
if (this.widget.isGfxDragHandleVisible) {
this._showDragHandle().catch(console.error);
this._updateDragHoverRectTopLevelBlock();
} else {
} else if (this.widget.activeDragHandle) {
this.widget.hide();
}
};
private readonly _showDragHandleOnTopLevelBlocks = async () => {
if (this.widget.mode === 'page') return;
private readonly _showDragHandle = async () => {
const surfaceModel = this.widget.doc.getBlockByFlavour('affine:surface');
const surface = this.widget.std.view.getBlock(
surfaceModel[0]!.id
@@ -75,84 +74,67 @@ export class EdgelessWatcher {
const grabber = this.widget.dragHandleGrabber;
if (!container || !grabber) return;
const area = this.hoverAreaTopLevelBlock;
const area = this.hoveredElemArea;
if (!area) return;
const height = area.height;
const posLeft = area.left;
const posTop = (area.top += area.padding);
container.style.transition = 'none';
container.style.paddingTop = `0px`;
container.style.paddingBottom = `0px`;
container.style.width = `${area.containerWidth}px`;
container.style.left = `${posLeft}px`;
container.style.top = `${posTop}px`;
container.style.left = `${area.left}px`;
container.style.top = `${area.top}px`;
container.style.display = 'flex';
container.style.height = `${height}px`;
grabber.style.width = `${DRAG_HANDLE_GRABBER_WIDTH_HOVERED * this.widget.scale.peek()}px`;
grabber.style.borderRadius = `${
DRAG_HANDLE_GRABBER_BORDER_RADIUS * this.widget.scale.peek()
}px`;
this.widget.handleAnchorModelDisposables();
this.widget.isTopLevelDragHandleVisible = true;
this.widget.activeDragHandle = 'gfx';
};
private readonly _updateDragHoverRectTopLevelBlock = () => {
if (!this.widget.dragHoverRect) return;
this.widget.dragHoverRect = this.hoverAreaRectTopLevelBlock;
this.widget.dragHoverRect = this.hoveredElemAreaRect;
};
checkTopLevelBlockSelection = () => {
if (!this.widget.isConnected) return;
get gfx() {
return this.widget.std.get(GfxControllerIdentifier);
}
updateAnchorElement = () => {
if (!this.widget.isConnected) return;
if (this.widget.doc.readonly || this.widget.mode === 'page') {
this.widget.hide();
return;
}
const { std } = this.widget;
const gfx = std.get(GfxControllerIdentifier);
const { selection } = gfx;
const { selection } = this.gfx;
const editing = selection.editing;
const selectedElements = selection.selectedElements;
if (editing || selectedElements.length !== 1) {
if (editing || selectedElements.length !== 1 || this.widget.doc.readonly) {
this.widget.hide();
return;
}
const selectedElement = selectedElements[0];
if (!isTopLevelBlock(selectedElement)) {
this.widget.hide();
return;
}
this.widget.anchorBlockId.value = selectedElement.id;
this._showDragHandleOnTopLevelBlocks().catch(console.error);
this._showDragHandle().catch(console.error);
};
get hoverAreaRectTopLevelBlock() {
const area = this.hoverAreaTopLevelBlock;
get hoveredElemAreaRect() {
const area = this.hoveredElemArea;
if (!area) return null;
return new Rect(area.left, area.top, area.right, area.bottom);
}
get hoverAreaTopLevelBlock() {
get hoveredElemArea() {
const edgelessElement = this.widget.anchorEdgelessElement.peek();
if (!edgelessElement) return null;
const { std } = this.widget;
const gfx = std.get(GfxControllerIdentifier);
const { viewport } = gfx;
const { viewport } = this.gfx;
const rect = getSelectedRect([edgelessElement]);
let [left, top] = viewport.toViewCoord(rect.left, rect.top);
const scale = this.widget.scale.peek();
@@ -186,6 +168,10 @@ export class EdgelessWatcher {
constructor(readonly widget: AffineDragHandleWidget) {}
watch() {
if (this.widget.mode === 'page') {
return;
}
const { disposables, std } = this.widget;
const gfx = std.get(GfxControllerIdentifier);
const { viewport, selection, tool } = gfx;
@@ -197,7 +183,19 @@ export class EdgelessWatcher {
disposables.add(
selection.slots.updated.on(() => {
this.checkTopLevelBlockSelection();
this.updateAnchorElement();
})
);
disposables.add(
edgelessSlots.readonlyUpdated.on(() => {
this.updateAnchorElement();
})
);
disposables.add(
edgelessSlots.elementResizeEnd.on(() => {
this.updateAnchorElement();
})
);
@@ -209,22 +207,10 @@ export class EdgelessWatcher {
})
);
disposables.add(
edgelessSlots.readonlyUpdated.on(() => {
this.checkTopLevelBlockSelection();
})
);
disposables.add(
edgelessSlots.elementResizeStart.on(() => {
this.widget.hide();
})
);
disposables.add(
edgelessSlots.elementResizeEnd.on(() => {
this.checkTopLevelBlockSelection();
})
);
}
}

View File

@@ -7,7 +7,7 @@ import type { AffineDragHandleWidget } from '../drag-handle.js';
export class HandleEventWatcher {
private readonly _onDragHandlePointerDown = () => {
if (!this.widget.isHoverDragHandleVisible || !this.widget.anchorBlockId)
if (!this.widget.isBlockDragHandleVisible || !this.widget.anchorBlockId)
return;
this.widget.dragHoverRect = this.widget.draggingAreaRect.value;
@@ -18,7 +18,7 @@ export class HandleEventWatcher {
const grabber = this.widget.dragHandleGrabber;
if (!container || !grabber) return;
if (this.widget.isHoverDragHandleVisible && this.widget.anchorBlockId) {
if (this.widget.isBlockDragHandleVisible && this.widget.anchorBlockId) {
const block = this.widget.anchorBlockComponent;
if (!block) return;
@@ -35,9 +35,9 @@ export class HandleEventWatcher {
}px`;
this.widget.isDragHandleHovered = true;
} else if (this.widget.isTopLevelDragHandleVisible) {
} else if (this.widget.isGfxDragHandleVisible) {
this.widget.dragHoverRect =
this.widget.edgelessWatcher.hoverAreaRectTopLevelBlock;
this.widget.edgelessWatcher.hoveredElemAreaRect;
this.widget.isDragHandleHovered = true;
}
};
@@ -46,7 +46,7 @@ export class HandleEventWatcher {
this.widget.isDragHandleHovered = false;
this.widget.dragHoverRect = null;
if (this.widget.isTopLevelDragHandleVisible) return;
if (this.widget.isGfxDragHandleVisible) return;
if (this.widget.dragging) return;
@@ -54,7 +54,7 @@ export class HandleEventWatcher {
};
private readonly _onDragHandlePointerUp = () => {
if (!this.widget.isHoverDragHandleVisible) return;
if (!this.widget.isBlockDragHandleVisible) return;
this.widget.dragHoverRect = null;
};

View File

@@ -29,6 +29,9 @@ import {
updateDragHandleClassName,
} from '../utils.js';
/**
* Used to control the drag handle visibility in page mode
*/
export class PointerEventWatcher {
private get _gfx() {
return this.widget.std.get(GfxControllerIdentifier);
@@ -51,7 +54,7 @@ export class PointerEventWatcher {
* Should clear selection if current block is the first selected block
*/
private readonly _clickHandler: UIEventHandler = ctx => {
if (!this.widget.isHoverDragHandleVisible) return;
if (!this.widget.isBlockDragHandleVisible) return;
const state = ctx.get('pointerState');
const { target } = state.raw;
@@ -152,7 +155,7 @@ export class PointerEventWatcher {
* And update hover block id and path
*/
private readonly _pointerMoveOnBlock = (state: PointerEventState) => {
if (this.widget.isTopLevelDragHandleVisible) return;
if (this.widget.isGfxDragHandleVisible) return;
const point = new Point(state.raw.x, state.raw.y);
const closestBlock = getClosestBlockByPoint(
@@ -182,7 +185,7 @@ export class PointerEventWatcher {
this.widget.anchorBlockId.peek(),
this._lastHoveredBlockId
) ||
!this.widget.isHoverDragHandleVisible) &&
!this.widget.isBlockDragHandleVisible) &&
!this.widget.isDragHandleHovered
) {
this.showDragHandleOnHoverBlock();
@@ -224,7 +227,7 @@ export class PointerEventWatcher {
this.widget.hide();
return;
}
if (this.widget.isTopLevelDragHandleVisible) return;
if (this.widget.isGfxDragHandleVisible) return;
const state = ctx.get('pointerState');
const { target } = state.raw;
@@ -263,7 +266,9 @@ export class PointerEventWatcher {
return true;
}
this.widget.hide();
if (this.widget.activeDragHandle) {
this.widget.hide();
}
return false;
},
1000 / 60
@@ -278,7 +283,7 @@ export class PointerEventWatcher {
const grabber = this.widget.dragHandleGrabber;
if (!container || !grabber) return;
this.widget.isHoverDragHandleVisible = true;
this.widget.activeDragHandle = 'block';
const draggingAreaRect = this.widget.draggingAreaRect.peek();
if (!draggingAreaRect) return;

View File

@@ -322,8 +322,9 @@ export class TemplateJob {
) {
const schema =
this.model.doc.workspace.schema.flavourSchemaMap.get('affine:surface');
const surfaceTransformer =
schema?.transformer?.() as SurfaceBlockTransformer;
const surfaceTransformer = schema?.transformer?.(
new Map()
) as SurfaceBlockTransformer;
this.model.doc.transact(() => {
const defered: [string, Record<string, unknown>][] = [];

View File

@@ -433,6 +433,10 @@ export class SurfaceBlockModel extends BlockModel<SurfaceBlockProps> {
this._watchGroupRelationChange();
}
getConstructor(type: string) {
return this._elementCtorMap[type];
}
addElement<T extends object = Record<string, unknown>>(
props: Partial<T> & { type: string }
) {

View File

@@ -50,7 +50,7 @@ function createTestOptions() {
return { id: 'test-collection', idGenerator, schema };
}
const transformer = new BaseBlockTransformer();
const transformer = new BaseBlockTransformer(new Map());
const blobCRUD = new MemoryBlobCRUD();
const assets = new AssetsManager({ blob: blobCRUD });

View File

@@ -40,7 +40,7 @@ export const BlockSchema = z.object({
}),
transformer: z
.function()
.args()
.args(z.custom<Map<string, unknown>>())
.returns(z.custom<BaseBlockTransformer>())
.optional(),
});
@@ -69,14 +69,14 @@ export function defineBlockSchema<
metadata: Metadata;
props?: (internalPrimitives: InternalPrimitives) => Props;
toModel?: () => Model;
transformer?: () => Transformer;
transformer?: (transformerConfig: Map<string, unknown>) => Transformer;
}): {
version: number;
model: {
props: PropsGetter<Props>;
flavour: Flavour;
} & Metadata;
transformer?: () => Transformer;
transformer?: (transformerConfig: Map<string, unknown>) => Transformer;
};
export function defineBlockSchema({
@@ -96,7 +96,9 @@ export function defineBlockSchema({
};
props?: (internalPrimitives: InternalPrimitives) => Record<string, unknown>;
toModel?: () => BlockModel;
transformer?: () => BaseBlockTransformer;
transformer?: (
transformerConfig: Map<string, unknown>
) => BaseBlockTransformer;
}): BlockSchemaType {
const schema = {
version: metadata.version,

View File

@@ -51,6 +51,8 @@ export class BaseBlockTransformer<Props extends object = object> {
);
}
constructor(public readonly transformerConfigs: Map<string, unknown>) {}
fromSnapshot({
json,
}: FromSnapshotPayload): Promise<SnapshotNode<Props>> | SnapshotNode<Props> {

View File

@@ -82,7 +82,8 @@ type TransformerMiddlewareOptions = {
assetsManager: AssetsManager;
slots: TransformerSlots;
docCRUD: DocCRUD;
adapterConfigs: Map<string, string>;
adapterConfigs: Map<string, unknown>;
transformerConfigs: Map<string, unknown>;
};
export type TransformerMiddleware = (

View File

@@ -56,6 +56,8 @@ const BATCH_SIZE = 100;
export class Transformer {
private readonly _adapterConfigs = new Map<string, string>();
private readonly _transformerConfigs = new Map<string, unknown>();
private readonly _assetsManager: AssetsManager;
private readonly _schema: Schema;
@@ -72,6 +74,11 @@ export class Transformer {
blockToSnapshot = (model: DraftModel): BlockSnapshot | undefined => {
try {
const snapshot = this._blockToSnapshot(model);
if (!snapshot) {
return;
}
BlockSnapshotSchema.parse(snapshot);
return snapshot;
@@ -354,24 +361,28 @@ export class Transformer {
docCRUD: this._docCRUD,
assetsManager: this._assetsManager,
adapterConfigs: this._adapterConfigs,
transformerConfigs: this._transformerConfigs,
});
});
}
private _blockToSnapshot(model: DraftModel): BlockSnapshot {
private _blockToSnapshot(model: DraftModel): BlockSnapshot | null {
this._slots.beforeExport.emit({
type: 'block',
model,
});
const schema = this._getSchema(model.flavour);
const transformer = this._getTransformer(schema);
const snapshotLeaf = transformer.toSnapshot({
model,
assets: this._assetsManager,
});
const children = model.children.map(child => {
return this._blockToSnapshot(child);
});
const children = model.children
.map(child => {
return this._blockToSnapshot(child);
})
.filter(Boolean) as BlockSnapshot[];
const snapshot: BlockSnapshot = {
type: 'block',
...snapshotLeaf,
@@ -489,7 +500,10 @@ export class Transformer {
}
private _getTransformer(schema: BlockSchemaType) {
return schema.transformer?.() ?? new BaseBlockTransformer();
return (
schema.transformer?.(this._transformerConfigs) ??
new BaseBlockTransformer(this._transformerConfigs)
);
}
private async _insertBlockTree(

View File

@@ -204,13 +204,13 @@ test.describe('edgeless text block', () => {
)!;
return container.getBoundingClientRect();
});
const model = await page.evaluate(() => {
const modelXYWH = await page.evaluate(() => {
const block = window.host.view.getBlock(
'4'
) as EdgelessTextBlockComponent;
return block.model;
return block.model.xywh;
});
const bound = Bound.deserialize(model.xywh);
const bound = Bound.deserialize(modelXYWH);
expect(rect.width).toBeCloseTo(bound.w);
expect(rect.height).toBeCloseTo(bound.h);
});