mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-02-19 15:26:59 +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 {
|
||||
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');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -31,7 +31,8 @@ export const SurfaceBlockSchema = defineBlockSchema({
|
||||
'affine:edgeless-text',
|
||||
],
|
||||
},
|
||||
transformer: () => new SurfaceBlockTransformer(),
|
||||
transformer: transformerConfigs =>
|
||||
new SurfaceBlockTransformer(transformerConfigs),
|
||||
toModel: () => new SurfaceBlockModel(),
|
||||
});
|
||||
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
@@ -81,7 +81,8 @@ export const AttachmentBlockSchema = defineBlockSchema({
|
||||
'affine:list',
|
||||
],
|
||||
},
|
||||
transformer: () => new AttachmentBlockTransformer(),
|
||||
transformer: transformerConfigs =>
|
||||
new AttachmentBlockTransformer(transformerConfigs),
|
||||
toModel: () => new AttachmentBlockModel(),
|
||||
});
|
||||
|
||||
|
||||
@@ -35,7 +35,8 @@ export const ImageBlockSchema = defineBlockSchema({
|
||||
version: 1,
|
||||
role: 'content',
|
||||
},
|
||||
transformer: () => new ImageBlockTransformer(),
|
||||
transformer: transformerConfigs =>
|
||||
new ImageBlockTransformer(transformerConfigs),
|
||||
toModel: () => new ImageBlockModel(),
|
||||
});
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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 { 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;
|
||||
|
||||
@@ -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 = (
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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();
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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>][] = [];
|
||||
|
||||
@@ -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 }
|
||||
) {
|
||||
|
||||
@@ -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 });
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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> {
|
||||
|
||||
@@ -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 = (
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user