mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-09-07 09:21:24 +08:00
feat: edgeless dnd (#9988)
### Changed - Support edgelss dnd - Simplify the drag-handle state
This commit is contained in:
@@ -13,25 +13,22 @@ import {
|
|||||||
import { Peekable } from '@blocksuite/affine-components/peek';
|
import { Peekable } from '@blocksuite/affine-components/peek';
|
||||||
import {
|
import {
|
||||||
FrameBlockModel,
|
FrameBlockModel,
|
||||||
GroupElementModel,
|
RootBlockModel,
|
||||||
type SurfaceRefBlockModel,
|
type SurfaceRefBlockModel,
|
||||||
} from '@blocksuite/affine-model';
|
} from '@blocksuite/affine-model';
|
||||||
import {
|
import {
|
||||||
DocModeProvider,
|
DocModeProvider,
|
||||||
EditorSettingExtension,
|
|
||||||
EditorSettingProvider,
|
|
||||||
EditPropsStore,
|
EditPropsStore,
|
||||||
GeneralSettingSchema,
|
|
||||||
ThemeProvider,
|
ThemeProvider,
|
||||||
} from '@blocksuite/affine-shared/services';
|
} from '@blocksuite/affine-shared/services';
|
||||||
import {
|
import {
|
||||||
|
matchModels,
|
||||||
requestConnectedFrame,
|
requestConnectedFrame,
|
||||||
SpecProvider,
|
SpecProvider,
|
||||||
} from '@blocksuite/affine-shared/utils';
|
} from '@blocksuite/affine-shared/utils';
|
||||||
import {
|
import {
|
||||||
BlockComponent,
|
BlockComponent,
|
||||||
BlockSelection,
|
BlockSelection,
|
||||||
BlockServiceWatcher,
|
|
||||||
BlockStdScope,
|
BlockStdScope,
|
||||||
type EditorHost,
|
type EditorHost,
|
||||||
LifeCycleWatcher,
|
LifeCycleWatcher,
|
||||||
@@ -40,8 +37,8 @@ import {
|
|||||||
import {
|
import {
|
||||||
GfxBlockElementModel,
|
GfxBlockElementModel,
|
||||||
GfxControllerIdentifier,
|
GfxControllerIdentifier,
|
||||||
GfxExtension,
|
|
||||||
type GfxModel,
|
type GfxModel,
|
||||||
|
GfxPrimitiveElementModel,
|
||||||
} from '@blocksuite/block-std/gfx';
|
} from '@blocksuite/block-std/gfx';
|
||||||
import { BlockSuiteError, ErrorCode } from '@blocksuite/global/exceptions';
|
import { BlockSuiteError, ErrorCode } from '@blocksuite/global/exceptions';
|
||||||
import {
|
import {
|
||||||
@@ -52,12 +49,10 @@ import {
|
|||||||
type SerializedXYWH,
|
type SerializedXYWH,
|
||||||
} from '@blocksuite/global/utils';
|
} from '@blocksuite/global/utils';
|
||||||
import type { BaseSelection, Store } from '@blocksuite/store';
|
import type { BaseSelection, Store } from '@blocksuite/store';
|
||||||
import { signal } from '@preact/signals-core';
|
|
||||||
import { css, html, nothing, type TemplateResult } from 'lit';
|
import { css, html, nothing, type TemplateResult } from 'lit';
|
||||||
import { query, state } from 'lit/decorators.js';
|
import { query, state } from 'lit/decorators.js';
|
||||||
import { styleMap } from 'lit/directives/style-map.js';
|
import { styleMap } from 'lit/directives/style-map.js';
|
||||||
|
|
||||||
import type { EdgelessPreviewer } from './types.js';
|
|
||||||
import { noContentPlaceholder } from './utils.js';
|
import { noContentPlaceholder } from './utils.js';
|
||||||
|
|
||||||
const REF_LABEL_ICON = {
|
const REF_LABEL_ICON = {
|
||||||
@@ -427,48 +422,26 @@ export class SurfaceRefBlockComponent extends BlockComponent<SurfaceRefBlockMode
|
|||||||
|
|
||||||
private _initSpec() {
|
private _initSpec() {
|
||||||
const refreshViewport = this._refreshViewport.bind(this);
|
const refreshViewport = this._refreshViewport.bind(this);
|
||||||
// oxlint-disable-next-line @typescript-eslint/no-this-alias
|
class SurfaceRefViewportInitializer extends LifeCycleWatcher {
|
||||||
const self = this;
|
static override readonly key = 'surfaceRefViewportInitializer';
|
||||||
const editorSetting =
|
|
||||||
this.std.getOptional(EditorSettingProvider) ??
|
|
||||||
signal(GeneralSettingSchema.parse({}));
|
|
||||||
|
|
||||||
class PageViewWatcher extends BlockServiceWatcher {
|
|
||||||
static override readonly flavour = 'affine:page';
|
|
||||||
|
|
||||||
override mounted() {
|
override mounted() {
|
||||||
this.blockService.disposables.add(
|
const disposable = this.std.view.viewUpdated.on(payload => {
|
||||||
this.blockService.specSlots.viewConnected.once(({ component }) => {
|
if (
|
||||||
const edgelessBlock = component as BlockComponent &
|
payload.type === 'add' &&
|
||||||
EdgelessPreviewer;
|
matchModels(payload.view.model, [RootBlockModel])
|
||||||
|
) {
|
||||||
edgelessBlock.editorViewportSelector = 'ref-viewport';
|
disposable.dispose();
|
||||||
refreshViewport();
|
queueMicrotask(() => refreshViewport());
|
||||||
const gfx = edgelessBlock.std.get(GfxControllerIdentifier);
|
const gfx = this.std.get(GfxControllerIdentifier);
|
||||||
gfx.viewport.sizeUpdated.once(() => {
|
gfx.viewport.sizeUpdated.on(() => {
|
||||||
refreshViewport();
|
refreshViewport();
|
||||||
});
|
});
|
||||||
})
|
}
|
||||||
);
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
this._previewSpec.extend([SurfaceRefViewportInitializer]);
|
||||||
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),
|
|
||||||
]);
|
|
||||||
|
|
||||||
const referenceId = this.model.reference;
|
const referenceId = this.model.reference;
|
||||||
const setReferenceXYWH = (xywh: typeof this._referenceXYWH) => {
|
const setReferenceXYWH = (xywh: typeof this._referenceXYWH) => {
|
||||||
@@ -501,7 +474,7 @@ export class SurfaceRefBlockComponent extends BlockComponent<SurfaceRefBlockMode
|
|||||||
refreshViewport();
|
refreshViewport();
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
} else if (referenceElement instanceof GroupElementModel) {
|
} else if (referenceElement instanceof GfxPrimitiveElementModel) {
|
||||||
_disposable.add(
|
_disposable.add(
|
||||||
surfaceModel.elementUpdated.on(({ id, oldValues }) => {
|
surfaceModel.elementUpdated.on(({ id, oldValues }) => {
|
||||||
if (
|
if (
|
||||||
@@ -513,8 +486,6 @@ export class SurfaceRefBlockComponent extends BlockComponent<SurfaceRefBlockMode
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
} else {
|
|
||||||
console.warn('Unsupported reference element type');
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -31,7 +31,8 @@ export const SurfaceBlockSchema = defineBlockSchema({
|
|||||||
'affine:edgeless-text',
|
'affine:edgeless-text',
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
transformer: () => new SurfaceBlockTransformer(),
|
transformer: transformerConfigs =>
|
||||||
|
new SurfaceBlockTransformer(transformerConfigs),
|
||||||
toModel: () => new SurfaceBlockModel(),
|
toModel: () => new SurfaceBlockModel(),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -92,9 +92,18 @@ export class SurfaceBlockTransformer extends BaseBlockTransformer<SurfaceBlockPr
|
|||||||
const snapshot = super.toSnapshot(payload);
|
const snapshot = super.toSnapshot(payload);
|
||||||
const elementsValue = payload.model.elements.getValue();
|
const elementsValue = payload.model.elements.getValue();
|
||||||
const value: Record<string, unknown> = {};
|
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) {
|
if (elementsValue) {
|
||||||
elementsValue.forEach((element, key) => {
|
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 = {
|
snapshot.props = {
|
||||||
|
|||||||
@@ -81,7 +81,8 @@ export const AttachmentBlockSchema = defineBlockSchema({
|
|||||||
'affine:list',
|
'affine:list',
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
transformer: () => new AttachmentBlockTransformer(),
|
transformer: transformerConfigs =>
|
||||||
|
new AttachmentBlockTransformer(transformerConfigs),
|
||||||
toModel: () => new AttachmentBlockModel(),
|
toModel: () => new AttachmentBlockModel(),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -35,7 +35,8 @@ export const ImageBlockSchema = defineBlockSchema({
|
|||||||
version: 1,
|
version: 1,
|
||||||
role: 'content',
|
role: 'content',
|
||||||
},
|
},
|
||||||
transformer: () => new ImageBlockTransformer(),
|
transformer: transformerConfigs =>
|
||||||
|
new ImageBlockTransformer(transformerConfigs),
|
||||||
toModel: () => new ImageBlockModel(),
|
toModel: () => new ImageBlockModel(),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -4,10 +4,9 @@ import { DocModeProvider } from '@blocksuite/affine-shared/services';
|
|||||||
import {
|
import {
|
||||||
isInsideEdgelessEditor,
|
isInsideEdgelessEditor,
|
||||||
isInsidePageEditor,
|
isInsidePageEditor,
|
||||||
isTopLevelBlock,
|
|
||||||
} from '@blocksuite/affine-shared/utils';
|
} from '@blocksuite/affine-shared/utils';
|
||||||
import { type BlockComponent, WidgetComponent } from '@blocksuite/block-std';
|
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 {
|
import {
|
||||||
DisposableGroup,
|
DisposableGroup,
|
||||||
type IVec,
|
type IVec,
|
||||||
@@ -15,8 +14,9 @@ import {
|
|||||||
type Rect,
|
type Rect,
|
||||||
} from '@blocksuite/global/utils';
|
} from '@blocksuite/global/utils';
|
||||||
import { computed, type ReadonlySignal, signal } from '@preact/signals-core';
|
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 { query, state } from 'lit/decorators.js';
|
||||||
|
import { classMap } from 'lit/directives/class-map.js';
|
||||||
import { styleMap } from 'lit/directives/style-map.js';
|
import { styleMap } from 'lit/directives/style-map.js';
|
||||||
|
|
||||||
import type { AFFINE_DRAG_HANDLE_WIDGET } from './consts.js';
|
import type { AFFINE_DRAG_HANDLE_WIDGET } from './consts.js';
|
||||||
@@ -53,12 +53,13 @@ export class AffineDragHandleWidget extends WidgetComponent<RootBlockModel> {
|
|||||||
this.dragHoverRect = null;
|
this.dragHoverRect = null;
|
||||||
this.anchorBlockId.value = null;
|
this.anchorBlockId.value = null;
|
||||||
this.isDragHandleHovered = false;
|
this.isDragHandleHovered = false;
|
||||||
this.isHoverDragHandleVisible = false;
|
|
||||||
this.isTopLevelDragHandleVisible = false;
|
|
||||||
|
|
||||||
this.pointerEventWatcher.reset();
|
this.pointerEventWatcher.reset();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@state()
|
||||||
|
accessor activeDragHandle: 'block' | 'gfx' | null = null;
|
||||||
|
|
||||||
anchorBlockId = signal<string | null>(null);
|
anchorBlockId = signal<string | null>(null);
|
||||||
|
|
||||||
anchorBlockComponent = computed<BlockComponent | null>(() => {
|
anchorBlockComponent = computed<BlockComponent | null>(() => {
|
||||||
@@ -67,16 +68,14 @@ export class AffineDragHandleWidget extends WidgetComponent<RootBlockModel> {
|
|||||||
return this.std.view.getBlock(this.anchorBlockId.value);
|
return this.std.view.getBlock(this.anchorBlockId.value);
|
||||||
});
|
});
|
||||||
|
|
||||||
anchorEdgelessElement: ReadonlySignal<GfxBlockElementModel | null> = computed(
|
anchorEdgelessElement: ReadonlySignal<GfxModel | null> = computed(() => {
|
||||||
() => {
|
if (!this.anchorBlockId.value) return null;
|
||||||
if (!this.anchorBlockId.value) return null;
|
if (this.mode === 'page') return null;
|
||||||
if (this.mode === 'page') return null;
|
|
||||||
|
|
||||||
const crud = this.std.get(EdgelessCRUDIdentifier);
|
const crud = this.std.get(EdgelessCRUDIdentifier);
|
||||||
const edgelessElement = crud.getElementById(this.anchorBlockId.value);
|
const edgelessElement = crud.getElementById(this.anchorBlockId.value);
|
||||||
return isTopLevelBlock(edgelessElement) ? edgelessElement : null;
|
return edgelessElement;
|
||||||
}
|
});
|
||||||
);
|
|
||||||
|
|
||||||
// Single block: drag handle should show on the vertical middle of the first line of element
|
// Single block: drag handle should show on the vertical middle of the first line of element
|
||||||
center: IVec = [0, 0];
|
center: IVec = [0, 0];
|
||||||
@@ -115,15 +114,18 @@ export class AffineDragHandleWidget extends WidgetComponent<RootBlockModel> {
|
|||||||
if (this.dragging && !force) return;
|
if (this.dragging && !force) return;
|
||||||
updateDragHandleClassName();
|
updateDragHandleClassName();
|
||||||
|
|
||||||
this.isHoverDragHandleVisible = false;
|
|
||||||
this.isTopLevelDragHandleVisible = false;
|
|
||||||
this.isDragHandleHovered = false;
|
this.isDragHandleHovered = false;
|
||||||
|
|
||||||
this.anchorBlockId.value = null;
|
this.anchorBlockId.value = null;
|
||||||
|
this.activeDragHandle = null;
|
||||||
|
|
||||||
if (this.dragHandleContainer) {
|
if (this.dragHandleContainer) {
|
||||||
|
this.dragHandleContainer.removeAttribute('style');
|
||||||
this.dragHandleContainer.style.display = 'none';
|
this.dragHandleContainer.style.display = 'none';
|
||||||
}
|
}
|
||||||
|
if (this.dragHandleGrabber) {
|
||||||
|
this.dragHandleGrabber.removeAttribute('style');
|
||||||
|
}
|
||||||
|
|
||||||
if (force) {
|
if (force) {
|
||||||
this._reset();
|
this._reset();
|
||||||
@@ -132,9 +134,13 @@ export class AffineDragHandleWidget extends WidgetComponent<RootBlockModel> {
|
|||||||
|
|
||||||
isDragHandleHovered = false;
|
isDragHandleHovered = false;
|
||||||
|
|
||||||
isHoverDragHandleVisible = false;
|
get isBlockDragHandleVisible() {
|
||||||
|
return this.activeDragHandle === 'block';
|
||||||
|
}
|
||||||
|
|
||||||
isTopLevelDragHandleVisible = false;
|
get isGfxDragHandleVisible() {
|
||||||
|
return this.activeDragHandle === 'gfx';
|
||||||
|
}
|
||||||
|
|
||||||
noteScale = signal(1);
|
noteScale = signal(1);
|
||||||
|
|
||||||
@@ -190,7 +196,7 @@ export class AffineDragHandleWidget extends WidgetComponent<RootBlockModel> {
|
|||||||
|
|
||||||
override render() {
|
override render() {
|
||||||
const hoverRectStyle = styleMap(
|
const hoverRectStyle = styleMap(
|
||||||
this.dragHoverRect
|
this.dragHoverRect && this.activeDragHandle
|
||||||
? {
|
? {
|
||||||
width: `${this.dragHoverRect.width}px`,
|
width: `${this.dragHoverRect.width}px`,
|
||||||
height: `${this.dragHoverRect.height}px`,
|
height: `${this.dragHoverRect.height}px`,
|
||||||
@@ -201,11 +207,27 @@ export class AffineDragHandleWidget extends WidgetComponent<RootBlockModel> {
|
|||||||
display: 'none',
|
display: 'none',
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
const isGfx = this.activeDragHandle === 'gfx';
|
||||||
|
const classes = {
|
||||||
|
'affine-drag-handle-grabber': true,
|
||||||
|
dots: isGfx ? true : false,
|
||||||
|
};
|
||||||
|
|
||||||
return html`
|
return html`
|
||||||
<div class="affine-drag-handle-widget">
|
<div class="affine-drag-handle-widget">
|
||||||
<div class="affine-drag-handle-container" draggable="true">
|
<div class="affine-drag-handle-container">
|
||||||
<div class="affine-drag-handle-grabber"></div>
|
<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>
|
||||||
<div class="affine-drag-hover-rect" style=${hoverRectStyle}></div>
|
<div class="affine-drag-hover-rect" style=${hoverRectStyle}></div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ import {
|
|||||||
|
|
||||||
export class RectHelper {
|
export class RectHelper {
|
||||||
private readonly _getHoveredBlocks = (): BlockComponent[] => {
|
private readonly _getHoveredBlocks = (): BlockComponent[] => {
|
||||||
if (!this.widget.isHoverDragHandleVisible || !this.widget.anchorBlockId)
|
if (!this.widget.isBlockDragHandleVisible || !this.widget.anchorBlockId)
|
||||||
return [];
|
return [];
|
||||||
|
|
||||||
const hoverBlock = this.widget.anchorBlockComponent.peek();
|
const hoverBlock = this.widget.anchorBlockComponent.peek();
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
};
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { unsafeCSSVarV2 } from '@blocksuite/affine-shared/theme';
|
||||||
import { css } from 'lit';
|
import { css } from 'lit';
|
||||||
|
|
||||||
import { DRAG_HANDLE_CONTAINER_WIDTH } from './config.js';
|
import { DRAG_HANDLE_CONTAINER_WIDTH } from './config.js';
|
||||||
@@ -35,6 +36,32 @@ export const styles = css`
|
|||||||
transition: width 0.25s ease;
|
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 {
|
@media print {
|
||||||
.affine-drag-handle-widget {
|
.affine-drag-handle-widget {
|
||||||
display: none;
|
display: none;
|
||||||
|
|||||||
@@ -151,7 +151,7 @@ export const isOutOfNoteBlock = (
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const getParentNoteBlock = (blockComponent: BlockComponent) => {
|
export const getParentNoteBlock = (blockComponent: BlockComponent) => {
|
||||||
return blockComponent.closest('affine-note') ?? null;
|
return blockComponent.closest('affine-note, affine-edgeless-note') ?? null;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getClosestNoteBlock = (
|
export const getClosestNoteBlock = (
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -2,10 +2,7 @@ import {
|
|||||||
EdgelessLegacySlotIdentifier,
|
EdgelessLegacySlotIdentifier,
|
||||||
type SurfaceBlockComponent,
|
type SurfaceBlockComponent,
|
||||||
} from '@blocksuite/affine-block-surface';
|
} from '@blocksuite/affine-block-surface';
|
||||||
import {
|
import { getSelectedRect } from '@blocksuite/affine-shared/utils';
|
||||||
getSelectedRect,
|
|
||||||
isTopLevelBlock,
|
|
||||||
} from '@blocksuite/affine-shared/utils';
|
|
||||||
import {
|
import {
|
||||||
GfxControllerIdentifier,
|
GfxControllerIdentifier,
|
||||||
type GfxToolsFullOptionValue,
|
type GfxToolsFullOptionValue,
|
||||||
@@ -16,19 +13,23 @@ import { effect } from '@preact/signals-core';
|
|||||||
import {
|
import {
|
||||||
DRAG_HANDLE_CONTAINER_OFFSET_LEFT_TOP_LEVEL,
|
DRAG_HANDLE_CONTAINER_OFFSET_LEFT_TOP_LEVEL,
|
||||||
DRAG_HANDLE_CONTAINER_WIDTH_TOP_LEVEL,
|
DRAG_HANDLE_CONTAINER_WIDTH_TOP_LEVEL,
|
||||||
DRAG_HANDLE_GRABBER_BORDER_RADIUS,
|
|
||||||
DRAG_HANDLE_GRABBER_WIDTH_HOVERED,
|
|
||||||
HOVER_AREA_RECT_PADDING_TOP_LEVEL,
|
HOVER_AREA_RECT_PADDING_TOP_LEVEL,
|
||||||
} from '../config.js';
|
} from '../config.js';
|
||||||
import type { AffineDragHandleWidget } from '../drag-handle.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 {
|
export class EdgelessWatcher {
|
||||||
private readonly _handleEdgelessToolUpdated = (
|
private readonly _handleEdgelessToolUpdated = (
|
||||||
newTool: GfxToolsFullOptionValue
|
newTool: GfxToolsFullOptionValue
|
||||||
) => {
|
) => {
|
||||||
// @ts-expect-error FIXME: resolve after gfx tool refactor
|
// @ts-expect-error FIXME: resolve after gfx tool refactor
|
||||||
if (newTool.type === 'default') {
|
if (newTool.type === 'default') {
|
||||||
this.checkTopLevelBlockSelection();
|
this.updateAnchorElement();
|
||||||
} else {
|
} else {
|
||||||
this.widget.hide();
|
this.widget.hide();
|
||||||
}
|
}
|
||||||
@@ -52,17 +53,15 @@ export class EdgelessWatcher {
|
|||||||
this.widget.center = [...center];
|
this.widget.center = [...center];
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.widget.isTopLevelDragHandleVisible) {
|
if (this.widget.isGfxDragHandleVisible) {
|
||||||
this._showDragHandleOnTopLevelBlocks().catch(console.error);
|
this._showDragHandle().catch(console.error);
|
||||||
this._updateDragHoverRectTopLevelBlock();
|
this._updateDragHoverRectTopLevelBlock();
|
||||||
} else {
|
} else if (this.widget.activeDragHandle) {
|
||||||
this.widget.hide();
|
this.widget.hide();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
private readonly _showDragHandleOnTopLevelBlocks = async () => {
|
private readonly _showDragHandle = async () => {
|
||||||
if (this.widget.mode === 'page') return;
|
|
||||||
|
|
||||||
const surfaceModel = this.widget.doc.getBlockByFlavour('affine:surface');
|
const surfaceModel = this.widget.doc.getBlockByFlavour('affine:surface');
|
||||||
const surface = this.widget.std.view.getBlock(
|
const surface = this.widget.std.view.getBlock(
|
||||||
surfaceModel[0]!.id
|
surfaceModel[0]!.id
|
||||||
@@ -75,84 +74,67 @@ export class EdgelessWatcher {
|
|||||||
const grabber = this.widget.dragHandleGrabber;
|
const grabber = this.widget.dragHandleGrabber;
|
||||||
if (!container || !grabber) return;
|
if (!container || !grabber) return;
|
||||||
|
|
||||||
const area = this.hoverAreaTopLevelBlock;
|
const area = this.hoveredElemArea;
|
||||||
if (!area) return;
|
if (!area) return;
|
||||||
|
|
||||||
const height = area.height;
|
|
||||||
|
|
||||||
const posLeft = area.left;
|
|
||||||
|
|
||||||
const posTop = (area.top += area.padding);
|
|
||||||
|
|
||||||
container.style.transition = 'none';
|
container.style.transition = 'none';
|
||||||
container.style.paddingTop = `0px`;
|
container.style.paddingTop = `0px`;
|
||||||
container.style.paddingBottom = `0px`;
|
container.style.paddingBottom = `0px`;
|
||||||
container.style.width = `${area.containerWidth}px`;
|
container.style.left = `${area.left}px`;
|
||||||
container.style.left = `${posLeft}px`;
|
container.style.top = `${area.top}px`;
|
||||||
container.style.top = `${posTop}px`;
|
|
||||||
container.style.display = 'flex';
|
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.handleAnchorModelDisposables();
|
||||||
|
|
||||||
this.widget.isTopLevelDragHandleVisible = true;
|
this.widget.activeDragHandle = 'gfx';
|
||||||
};
|
};
|
||||||
|
|
||||||
private readonly _updateDragHoverRectTopLevelBlock = () => {
|
private readonly _updateDragHoverRectTopLevelBlock = () => {
|
||||||
if (!this.widget.dragHoverRect) return;
|
if (!this.widget.dragHoverRect) return;
|
||||||
|
|
||||||
this.widget.dragHoverRect = this.hoverAreaRectTopLevelBlock;
|
this.widget.dragHoverRect = this.hoveredElemAreaRect;
|
||||||
};
|
};
|
||||||
|
|
||||||
checkTopLevelBlockSelection = () => {
|
get gfx() {
|
||||||
if (!this.widget.isConnected) return;
|
return this.widget.std.get(GfxControllerIdentifier);
|
||||||
|
}
|
||||||
|
|
||||||
|
updateAnchorElement = () => {
|
||||||
|
if (!this.widget.isConnected) return;
|
||||||
if (this.widget.doc.readonly || this.widget.mode === 'page') {
|
if (this.widget.doc.readonly || this.widget.mode === 'page') {
|
||||||
this.widget.hide();
|
this.widget.hide();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const { std } = this.widget;
|
const { selection } = this.gfx;
|
||||||
const gfx = std.get(GfxControllerIdentifier);
|
|
||||||
const { selection } = gfx;
|
|
||||||
const editing = selection.editing;
|
const editing = selection.editing;
|
||||||
const selectedElements = selection.selectedElements;
|
const selectedElements = selection.selectedElements;
|
||||||
if (editing || selectedElements.length !== 1) {
|
|
||||||
|
if (editing || selectedElements.length !== 1 || this.widget.doc.readonly) {
|
||||||
this.widget.hide();
|
this.widget.hide();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const selectedElement = selectedElements[0];
|
const selectedElement = selectedElements[0];
|
||||||
if (!isTopLevelBlock(selectedElement)) {
|
|
||||||
this.widget.hide();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.widget.anchorBlockId.value = selectedElement.id;
|
this.widget.anchorBlockId.value = selectedElement.id;
|
||||||
|
|
||||||
this._showDragHandleOnTopLevelBlocks().catch(console.error);
|
this._showDragHandle().catch(console.error);
|
||||||
};
|
};
|
||||||
|
|
||||||
get hoverAreaRectTopLevelBlock() {
|
get hoveredElemAreaRect() {
|
||||||
const area = this.hoverAreaTopLevelBlock;
|
const area = this.hoveredElemArea;
|
||||||
if (!area) return null;
|
if (!area) return null;
|
||||||
|
|
||||||
return new Rect(area.left, area.top, area.right, area.bottom);
|
return new Rect(area.left, area.top, area.right, area.bottom);
|
||||||
}
|
}
|
||||||
|
|
||||||
get hoverAreaTopLevelBlock() {
|
get hoveredElemArea() {
|
||||||
const edgelessElement = this.widget.anchorEdgelessElement.peek();
|
const edgelessElement = this.widget.anchorEdgelessElement.peek();
|
||||||
|
|
||||||
if (!edgelessElement) return null;
|
if (!edgelessElement) return null;
|
||||||
|
|
||||||
const { std } = this.widget;
|
const { viewport } = this.gfx;
|
||||||
const gfx = std.get(GfxControllerIdentifier);
|
|
||||||
const { viewport } = gfx;
|
|
||||||
const rect = getSelectedRect([edgelessElement]);
|
const rect = getSelectedRect([edgelessElement]);
|
||||||
let [left, top] = viewport.toViewCoord(rect.left, rect.top);
|
let [left, top] = viewport.toViewCoord(rect.left, rect.top);
|
||||||
const scale = this.widget.scale.peek();
|
const scale = this.widget.scale.peek();
|
||||||
@@ -186,6 +168,10 @@ export class EdgelessWatcher {
|
|||||||
constructor(readonly widget: AffineDragHandleWidget) {}
|
constructor(readonly widget: AffineDragHandleWidget) {}
|
||||||
|
|
||||||
watch() {
|
watch() {
|
||||||
|
if (this.widget.mode === 'page') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const { disposables, std } = this.widget;
|
const { disposables, std } = this.widget;
|
||||||
const gfx = std.get(GfxControllerIdentifier);
|
const gfx = std.get(GfxControllerIdentifier);
|
||||||
const { viewport, selection, tool } = gfx;
|
const { viewport, selection, tool } = gfx;
|
||||||
@@ -197,7 +183,19 @@ export class EdgelessWatcher {
|
|||||||
|
|
||||||
disposables.add(
|
disposables.add(
|
||||||
selection.slots.updated.on(() => {
|
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(
|
disposables.add(
|
||||||
edgelessSlots.elementResizeStart.on(() => {
|
edgelessSlots.elementResizeStart.on(() => {
|
||||||
this.widget.hide();
|
this.widget.hide();
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
disposables.add(
|
|
||||||
edgelessSlots.elementResizeEnd.on(() => {
|
|
||||||
this.checkTopLevelBlockSelection();
|
|
||||||
})
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import type { AffineDragHandleWidget } from '../drag-handle.js';
|
|||||||
|
|
||||||
export class HandleEventWatcher {
|
export class HandleEventWatcher {
|
||||||
private readonly _onDragHandlePointerDown = () => {
|
private readonly _onDragHandlePointerDown = () => {
|
||||||
if (!this.widget.isHoverDragHandleVisible || !this.widget.anchorBlockId)
|
if (!this.widget.isBlockDragHandleVisible || !this.widget.anchorBlockId)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
this.widget.dragHoverRect = this.widget.draggingAreaRect.value;
|
this.widget.dragHoverRect = this.widget.draggingAreaRect.value;
|
||||||
@@ -18,7 +18,7 @@ export class HandleEventWatcher {
|
|||||||
const grabber = this.widget.dragHandleGrabber;
|
const grabber = this.widget.dragHandleGrabber;
|
||||||
if (!container || !grabber) return;
|
if (!container || !grabber) return;
|
||||||
|
|
||||||
if (this.widget.isHoverDragHandleVisible && this.widget.anchorBlockId) {
|
if (this.widget.isBlockDragHandleVisible && this.widget.anchorBlockId) {
|
||||||
const block = this.widget.anchorBlockComponent;
|
const block = this.widget.anchorBlockComponent;
|
||||||
if (!block) return;
|
if (!block) return;
|
||||||
|
|
||||||
@@ -35,9 +35,9 @@ export class HandleEventWatcher {
|
|||||||
}px`;
|
}px`;
|
||||||
|
|
||||||
this.widget.isDragHandleHovered = true;
|
this.widget.isDragHandleHovered = true;
|
||||||
} else if (this.widget.isTopLevelDragHandleVisible) {
|
} else if (this.widget.isGfxDragHandleVisible) {
|
||||||
this.widget.dragHoverRect =
|
this.widget.dragHoverRect =
|
||||||
this.widget.edgelessWatcher.hoverAreaRectTopLevelBlock;
|
this.widget.edgelessWatcher.hoveredElemAreaRect;
|
||||||
this.widget.isDragHandleHovered = true;
|
this.widget.isDragHandleHovered = true;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -46,7 +46,7 @@ export class HandleEventWatcher {
|
|||||||
this.widget.isDragHandleHovered = false;
|
this.widget.isDragHandleHovered = false;
|
||||||
this.widget.dragHoverRect = null;
|
this.widget.dragHoverRect = null;
|
||||||
|
|
||||||
if (this.widget.isTopLevelDragHandleVisible) return;
|
if (this.widget.isGfxDragHandleVisible) return;
|
||||||
|
|
||||||
if (this.widget.dragging) return;
|
if (this.widget.dragging) return;
|
||||||
|
|
||||||
@@ -54,7 +54,7 @@ export class HandleEventWatcher {
|
|||||||
};
|
};
|
||||||
|
|
||||||
private readonly _onDragHandlePointerUp = () => {
|
private readonly _onDragHandlePointerUp = () => {
|
||||||
if (!this.widget.isHoverDragHandleVisible) return;
|
if (!this.widget.isBlockDragHandleVisible) return;
|
||||||
this.widget.dragHoverRect = null;
|
this.widget.dragHoverRect = null;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -29,6 +29,9 @@ import {
|
|||||||
updateDragHandleClassName,
|
updateDragHandleClassName,
|
||||||
} from '../utils.js';
|
} from '../utils.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Used to control the drag handle visibility in page mode
|
||||||
|
*/
|
||||||
export class PointerEventWatcher {
|
export class PointerEventWatcher {
|
||||||
private get _gfx() {
|
private get _gfx() {
|
||||||
return this.widget.std.get(GfxControllerIdentifier);
|
return this.widget.std.get(GfxControllerIdentifier);
|
||||||
@@ -51,7 +54,7 @@ export class PointerEventWatcher {
|
|||||||
* Should clear selection if current block is the first selected block
|
* Should clear selection if current block is the first selected block
|
||||||
*/
|
*/
|
||||||
private readonly _clickHandler: UIEventHandler = ctx => {
|
private readonly _clickHandler: UIEventHandler = ctx => {
|
||||||
if (!this.widget.isHoverDragHandleVisible) return;
|
if (!this.widget.isBlockDragHandleVisible) return;
|
||||||
|
|
||||||
const state = ctx.get('pointerState');
|
const state = ctx.get('pointerState');
|
||||||
const { target } = state.raw;
|
const { target } = state.raw;
|
||||||
@@ -152,7 +155,7 @@ export class PointerEventWatcher {
|
|||||||
* And update hover block id and path
|
* And update hover block id and path
|
||||||
*/
|
*/
|
||||||
private readonly _pointerMoveOnBlock = (state: PointerEventState) => {
|
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 point = new Point(state.raw.x, state.raw.y);
|
||||||
const closestBlock = getClosestBlockByPoint(
|
const closestBlock = getClosestBlockByPoint(
|
||||||
@@ -182,7 +185,7 @@ export class PointerEventWatcher {
|
|||||||
this.widget.anchorBlockId.peek(),
|
this.widget.anchorBlockId.peek(),
|
||||||
this._lastHoveredBlockId
|
this._lastHoveredBlockId
|
||||||
) ||
|
) ||
|
||||||
!this.widget.isHoverDragHandleVisible) &&
|
!this.widget.isBlockDragHandleVisible) &&
|
||||||
!this.widget.isDragHandleHovered
|
!this.widget.isDragHandleHovered
|
||||||
) {
|
) {
|
||||||
this.showDragHandleOnHoverBlock();
|
this.showDragHandleOnHoverBlock();
|
||||||
@@ -224,7 +227,7 @@ export class PointerEventWatcher {
|
|||||||
this.widget.hide();
|
this.widget.hide();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (this.widget.isTopLevelDragHandleVisible) return;
|
if (this.widget.isGfxDragHandleVisible) return;
|
||||||
|
|
||||||
const state = ctx.get('pointerState');
|
const state = ctx.get('pointerState');
|
||||||
const { target } = state.raw;
|
const { target } = state.raw;
|
||||||
@@ -263,7 +266,9 @@ export class PointerEventWatcher {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.widget.hide();
|
if (this.widget.activeDragHandle) {
|
||||||
|
this.widget.hide();
|
||||||
|
}
|
||||||
return false;
|
return false;
|
||||||
},
|
},
|
||||||
1000 / 60
|
1000 / 60
|
||||||
@@ -278,7 +283,7 @@ export class PointerEventWatcher {
|
|||||||
const grabber = this.widget.dragHandleGrabber;
|
const grabber = this.widget.dragHandleGrabber;
|
||||||
if (!container || !grabber) return;
|
if (!container || !grabber) return;
|
||||||
|
|
||||||
this.widget.isHoverDragHandleVisible = true;
|
this.widget.activeDragHandle = 'block';
|
||||||
|
|
||||||
const draggingAreaRect = this.widget.draggingAreaRect.peek();
|
const draggingAreaRect = this.widget.draggingAreaRect.peek();
|
||||||
if (!draggingAreaRect) return;
|
if (!draggingAreaRect) return;
|
||||||
|
|||||||
@@ -322,8 +322,9 @@ export class TemplateJob {
|
|||||||
) {
|
) {
|
||||||
const schema =
|
const schema =
|
||||||
this.model.doc.workspace.schema.flavourSchemaMap.get('affine:surface');
|
this.model.doc.workspace.schema.flavourSchemaMap.get('affine:surface');
|
||||||
const surfaceTransformer =
|
const surfaceTransformer = schema?.transformer?.(
|
||||||
schema?.transformer?.() as SurfaceBlockTransformer;
|
new Map()
|
||||||
|
) as SurfaceBlockTransformer;
|
||||||
|
|
||||||
this.model.doc.transact(() => {
|
this.model.doc.transact(() => {
|
||||||
const defered: [string, Record<string, unknown>][] = [];
|
const defered: [string, Record<string, unknown>][] = [];
|
||||||
|
|||||||
@@ -433,6 +433,10 @@ export class SurfaceBlockModel extends BlockModel<SurfaceBlockProps> {
|
|||||||
this._watchGroupRelationChange();
|
this._watchGroupRelationChange();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getConstructor(type: string) {
|
||||||
|
return this._elementCtorMap[type];
|
||||||
|
}
|
||||||
|
|
||||||
addElement<T extends object = Record<string, unknown>>(
|
addElement<T extends object = Record<string, unknown>>(
|
||||||
props: Partial<T> & { type: string }
|
props: Partial<T> & { type: string }
|
||||||
) {
|
) {
|
||||||
|
|||||||
@@ -50,7 +50,7 @@ function createTestOptions() {
|
|||||||
return { id: 'test-collection', idGenerator, schema };
|
return { id: 'test-collection', idGenerator, schema };
|
||||||
}
|
}
|
||||||
|
|
||||||
const transformer = new BaseBlockTransformer();
|
const transformer = new BaseBlockTransformer(new Map());
|
||||||
const blobCRUD = new MemoryBlobCRUD();
|
const blobCRUD = new MemoryBlobCRUD();
|
||||||
const assets = new AssetsManager({ blob: blobCRUD });
|
const assets = new AssetsManager({ blob: blobCRUD });
|
||||||
|
|
||||||
|
|||||||
@@ -40,7 +40,7 @@ export const BlockSchema = z.object({
|
|||||||
}),
|
}),
|
||||||
transformer: z
|
transformer: z
|
||||||
.function()
|
.function()
|
||||||
.args()
|
.args(z.custom<Map<string, unknown>>())
|
||||||
.returns(z.custom<BaseBlockTransformer>())
|
.returns(z.custom<BaseBlockTransformer>())
|
||||||
.optional(),
|
.optional(),
|
||||||
});
|
});
|
||||||
@@ -69,14 +69,14 @@ export function defineBlockSchema<
|
|||||||
metadata: Metadata;
|
metadata: Metadata;
|
||||||
props?: (internalPrimitives: InternalPrimitives) => Props;
|
props?: (internalPrimitives: InternalPrimitives) => Props;
|
||||||
toModel?: () => Model;
|
toModel?: () => Model;
|
||||||
transformer?: () => Transformer;
|
transformer?: (transformerConfig: Map<string, unknown>) => Transformer;
|
||||||
}): {
|
}): {
|
||||||
version: number;
|
version: number;
|
||||||
model: {
|
model: {
|
||||||
props: PropsGetter<Props>;
|
props: PropsGetter<Props>;
|
||||||
flavour: Flavour;
|
flavour: Flavour;
|
||||||
} & Metadata;
|
} & Metadata;
|
||||||
transformer?: () => Transformer;
|
transformer?: (transformerConfig: Map<string, unknown>) => Transformer;
|
||||||
};
|
};
|
||||||
|
|
||||||
export function defineBlockSchema({
|
export function defineBlockSchema({
|
||||||
@@ -96,7 +96,9 @@ export function defineBlockSchema({
|
|||||||
};
|
};
|
||||||
props?: (internalPrimitives: InternalPrimitives) => Record<string, unknown>;
|
props?: (internalPrimitives: InternalPrimitives) => Record<string, unknown>;
|
||||||
toModel?: () => BlockModel;
|
toModel?: () => BlockModel;
|
||||||
transformer?: () => BaseBlockTransformer;
|
transformer?: (
|
||||||
|
transformerConfig: Map<string, unknown>
|
||||||
|
) => BaseBlockTransformer;
|
||||||
}): BlockSchemaType {
|
}): BlockSchemaType {
|
||||||
const schema = {
|
const schema = {
|
||||||
version: metadata.version,
|
version: metadata.version,
|
||||||
|
|||||||
@@ -51,6 +51,8 @@ export class BaseBlockTransformer<Props extends object = object> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
constructor(public readonly transformerConfigs: Map<string, unknown>) {}
|
||||||
|
|
||||||
fromSnapshot({
|
fromSnapshot({
|
||||||
json,
|
json,
|
||||||
}: FromSnapshotPayload): Promise<SnapshotNode<Props>> | SnapshotNode<Props> {
|
}: FromSnapshotPayload): Promise<SnapshotNode<Props>> | SnapshotNode<Props> {
|
||||||
|
|||||||
@@ -82,7 +82,8 @@ type TransformerMiddlewareOptions = {
|
|||||||
assetsManager: AssetsManager;
|
assetsManager: AssetsManager;
|
||||||
slots: TransformerSlots;
|
slots: TransformerSlots;
|
||||||
docCRUD: DocCRUD;
|
docCRUD: DocCRUD;
|
||||||
adapterConfigs: Map<string, string>;
|
adapterConfigs: Map<string, unknown>;
|
||||||
|
transformerConfigs: Map<string, unknown>;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type TransformerMiddleware = (
|
export type TransformerMiddleware = (
|
||||||
|
|||||||
@@ -56,6 +56,8 @@ const BATCH_SIZE = 100;
|
|||||||
export class Transformer {
|
export class Transformer {
|
||||||
private readonly _adapterConfigs = new Map<string, string>();
|
private readonly _adapterConfigs = new Map<string, string>();
|
||||||
|
|
||||||
|
private readonly _transformerConfigs = new Map<string, unknown>();
|
||||||
|
|
||||||
private readonly _assetsManager: AssetsManager;
|
private readonly _assetsManager: AssetsManager;
|
||||||
|
|
||||||
private readonly _schema: Schema;
|
private readonly _schema: Schema;
|
||||||
@@ -72,6 +74,11 @@ export class Transformer {
|
|||||||
blockToSnapshot = (model: DraftModel): BlockSnapshot | undefined => {
|
blockToSnapshot = (model: DraftModel): BlockSnapshot | undefined => {
|
||||||
try {
|
try {
|
||||||
const snapshot = this._blockToSnapshot(model);
|
const snapshot = this._blockToSnapshot(model);
|
||||||
|
|
||||||
|
if (!snapshot) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
BlockSnapshotSchema.parse(snapshot);
|
BlockSnapshotSchema.parse(snapshot);
|
||||||
|
|
||||||
return snapshot;
|
return snapshot;
|
||||||
@@ -354,24 +361,28 @@ export class Transformer {
|
|||||||
docCRUD: this._docCRUD,
|
docCRUD: this._docCRUD,
|
||||||
assetsManager: this._assetsManager,
|
assetsManager: this._assetsManager,
|
||||||
adapterConfigs: this._adapterConfigs,
|
adapterConfigs: this._adapterConfigs,
|
||||||
|
transformerConfigs: this._transformerConfigs,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private _blockToSnapshot(model: DraftModel): BlockSnapshot {
|
private _blockToSnapshot(model: DraftModel): BlockSnapshot | null {
|
||||||
this._slots.beforeExport.emit({
|
this._slots.beforeExport.emit({
|
||||||
type: 'block',
|
type: 'block',
|
||||||
model,
|
model,
|
||||||
});
|
});
|
||||||
|
|
||||||
const schema = this._getSchema(model.flavour);
|
const schema = this._getSchema(model.flavour);
|
||||||
const transformer = this._getTransformer(schema);
|
const transformer = this._getTransformer(schema);
|
||||||
const snapshotLeaf = transformer.toSnapshot({
|
const snapshotLeaf = transformer.toSnapshot({
|
||||||
model,
|
model,
|
||||||
assets: this._assetsManager,
|
assets: this._assetsManager,
|
||||||
});
|
});
|
||||||
const children = model.children.map(child => {
|
const children = model.children
|
||||||
return this._blockToSnapshot(child);
|
.map(child => {
|
||||||
});
|
return this._blockToSnapshot(child);
|
||||||
|
})
|
||||||
|
.filter(Boolean) as BlockSnapshot[];
|
||||||
const snapshot: BlockSnapshot = {
|
const snapshot: BlockSnapshot = {
|
||||||
type: 'block',
|
type: 'block',
|
||||||
...snapshotLeaf,
|
...snapshotLeaf,
|
||||||
@@ -489,7 +500,10 @@ export class Transformer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private _getTransformer(schema: BlockSchemaType) {
|
private _getTransformer(schema: BlockSchemaType) {
|
||||||
return schema.transformer?.() ?? new BaseBlockTransformer();
|
return (
|
||||||
|
schema.transformer?.(this._transformerConfigs) ??
|
||||||
|
new BaseBlockTransformer(this._transformerConfigs)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async _insertBlockTree(
|
private async _insertBlockTree(
|
||||||
|
|||||||
@@ -204,13 +204,13 @@ test.describe('edgeless text block', () => {
|
|||||||
)!;
|
)!;
|
||||||
return container.getBoundingClientRect();
|
return container.getBoundingClientRect();
|
||||||
});
|
});
|
||||||
const model = await page.evaluate(() => {
|
const modelXYWH = await page.evaluate(() => {
|
||||||
const block = window.host.view.getBlock(
|
const block = window.host.view.getBlock(
|
||||||
'4'
|
'4'
|
||||||
) as EdgelessTextBlockComponent;
|
) 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.width).toBeCloseTo(bound.w);
|
||||||
expect(rect.height).toBeCloseTo(bound.h);
|
expect(rect.height).toBeCloseTo(bound.h);
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user