refactor(editor): extract drag handle widget (#9415)

This commit is contained in:
Saul-Mirone
2024-12-29 06:51:48 +00:00
parent b96a03b283
commit cec4a4b2c0
47 changed files with 277 additions and 92 deletions
@@ -0,0 +1,118 @@
import { SpecProvider } from '@blocksuite/affine-shared/utils';
import {
type BlockComponent,
BlockStdScope,
type DndEventState,
} from '@blocksuite/block-std';
import { Point } from '@blocksuite/global/utils';
import { BlockViewType, type Query } from '@blocksuite/store';
import { DragPreview } from '../components/drag-preview.js';
import type { AffineDragHandleWidget } from '../drag-handle.js';
export class PreviewHelper {
private readonly _calculatePreviewOffset = (
blocks: BlockComponent[],
state: DndEventState
) => {
const { top, left } = blocks[0].getBoundingClientRect();
const previewOffset = new Point(state.raw.x - left, state.raw.y - top);
return previewOffset;
};
private readonly _calculateQuery = (selectedIds: string[]): Query => {
const ids: Array<{ id: string; viewType: BlockViewType }> = selectedIds.map(
id => ({
id,
viewType: BlockViewType.Display,
})
);
// The ancestors of the selected blocks should be rendered as Bypass
selectedIds.forEach(block => {
let parent: string | null = block;
do {
if (!selectedIds.includes(parent)) {
ids.push({ viewType: BlockViewType.Bypass, id: parent });
}
parent = this.widget.doc.blockCollection.crud.getParent(parent);
} while (parent && !ids.map(({ id }) => id).includes(parent));
});
// The children of the selected blocks should be rendered as Display
const addChildren = (id: string) => {
const children = this.widget.doc.getBlock(id)?.model.children ?? [];
children.forEach(child => {
ids.push({ viewType: BlockViewType.Display, id: child.id });
addChildren(child.id);
});
};
selectedIds.forEach(addChildren);
return {
match: ids,
mode: 'strict',
};
};
createDragPreview = (
blocks: BlockComponent[],
state: DndEventState,
dragPreviewEl?: HTMLElement,
dragPreviewOffset?: Point
): DragPreview => {
if (this.widget.dragPreview) {
this.widget.dragPreview.remove();
}
let dragPreview: DragPreview;
if (dragPreviewEl) {
dragPreview = new DragPreview(dragPreviewOffset);
dragPreview.append(dragPreviewEl);
} else {
let width = 0;
blocks.forEach(element => {
width = Math.max(width, element.getBoundingClientRect().width);
});
const selectedIds = blocks.map(block => block.model.id);
const query = this._calculateQuery(selectedIds);
const doc = this.widget.doc.blockCollection.getDoc({ query });
const previewSpec = SpecProvider.getInstance().getSpec('page:preview');
const previewStd = new BlockStdScope({
doc,
extensions: previewSpec.value,
});
const previewTemplate = previewStd.render();
const offset = this._calculatePreviewOffset(blocks, state);
const posX = state.raw.x - offset.x;
const posY = state.raw.y - offset.y;
const altKey = state.raw.altKey;
dragPreview = new DragPreview(offset);
dragPreview.template = previewTemplate;
dragPreview.onRemove = () => {
this.widget.doc.blockCollection.clearQuery(query);
};
dragPreview.style.width = `${width / this.widget.scaleInNote.peek()}px`;
dragPreview.style.transform = `translate(${posX}px, ${posY}px) scale(${this.widget.scaleInNote.peek()})`;
dragPreview.style.opacity = altKey ? '1' : '0.5';
}
this.widget.rootComponent.append(dragPreview);
return dragPreview;
};
removeDragPreview = () => {
if (this.widget.dragPreview) {
this.widget.dragPreview.remove();
this.widget.dragPreview = null;
}
};
constructor(readonly widget: AffineDragHandleWidget) {}
}
@@ -0,0 +1,95 @@
import { getCurrentNativeRange } from '@blocksuite/affine-shared/utils';
import type { BlockComponent } from '@blocksuite/block-std';
import { Rect } from '@blocksuite/global/utils';
import {
DRAG_HANDLE_CONTAINER_WIDTH,
DRAG_HOVER_RECT_PADDING,
} from '../config.js';
import type { AffineDragHandleWidget } from '../drag-handle.js';
import {
containBlock,
getDragHandleLeftPadding,
includeTextSelection,
} from '../utils.js';
export class RectHelper {
private readonly _getHoveredBlocks = (): BlockComponent[] => {
if (!this.widget.isHoverDragHandleVisible || !this.widget.anchorBlockId)
return [];
const hoverBlock = this.widget.anchorBlockComponent.peek();
if (!hoverBlock) return [];
const selections = this.widget.selectionHelper.selectedBlocks;
let blocks: BlockComponent[] = [];
// When current selection is TextSelection, should cover all the blocks in native range
if (selections.length > 0 && includeTextSelection(selections)) {
const range = getCurrentNativeRange();
if (!range) return [];
const rangeManager = this.widget.std.range;
if (!rangeManager) return [];
blocks = rangeManager.getSelectedBlockComponentsByRange(range, {
match: el => el.model.role === 'content',
mode: 'highest',
});
} else {
blocks = this.widget.selectionHelper.selectedBlockComponents;
}
if (
containBlock(
blocks.map(block => block.blockId),
this.widget.anchorBlockId.peek()!
)
) {
return blocks;
}
return [hoverBlock];
};
getDraggingAreaRect = (): Rect | null => {
const block = this.widget.anchorBlockComponent.value;
if (!block) return null;
// When hover block is in selected blocks, should show hover rect on the selected blocks
// Top: the top of the first selected block
// Left: the left of the first selected block
// Right: the largest right of the selected blocks
// Bottom: the bottom of the last selected block
let { left, top, right, bottom } = block.getBoundingClientRect();
const blocks = this._getHoveredBlocks();
blocks.forEach(block => {
left = Math.min(left, block.getBoundingClientRect().left);
top = Math.min(top, block.getBoundingClientRect().top);
right = Math.max(right, block.getBoundingClientRect().right);
bottom = Math.max(bottom, block.getBoundingClientRect().bottom);
});
const offsetLeft = getDragHandleLeftPadding(blocks);
const offsetParentRect =
this.widget.dragHandleContainerOffsetParent.getBoundingClientRect();
if (!offsetParentRect) return null;
left -= offsetParentRect.left;
right -= offsetParentRect.left;
top -= offsetParentRect.top;
bottom -= offsetParentRect.top;
const scaleInNote = this.widget.scaleInNote.value;
// Add padding to hover rect
left -= (DRAG_HANDLE_CONTAINER_WIDTH + offsetLeft) * scaleInNote;
top -= DRAG_HOVER_RECT_PADDING * scaleInNote;
right += DRAG_HOVER_RECT_PADDING * scaleInNote;
bottom += DRAG_HOVER_RECT_PADDING * scaleInNote;
return new Rect(left, top, right, bottom);
};
constructor(readonly widget: AffineDragHandleWidget) {}
}
@@ -0,0 +1,62 @@
import { findNoteBlockModel } from '@blocksuite/affine-shared/utils';
import type { BlockComponent } from '@blocksuite/block-std';
import type { AffineDragHandleWidget } from '../drag-handle.js';
export class SelectionHelper {
/** Check if given block component is selected */
isBlockSelected = (block?: BlockComponent) => {
if (!block) return false;
return this.selectedBlocks.some(
selection => selection.blockId === block.model.id
);
};
setSelectedBlocks = (blocks: BlockComponent[], noteId?: string) => {
const { selection } = this;
const selections = blocks.map(block =>
selection.create('block', {
blockId: block.blockId,
})
);
// When current page is edgeless page
// We need to remain surface selection and set editing as true
if (this.widget.mode === 'edgeless') {
const surfaceElementId = noteId
? noteId
: findNoteBlockModel(blocks[0].model)?.id;
if (!surfaceElementId) return;
const surfaceSelection = selection.create(
'surface',
blocks[0]!.blockId,
[surfaceElementId],
true
);
selections.push(surfaceSelection);
}
selection.set(selections);
};
get selectedBlockComponents() {
return this.selectedBlocks
.map(block => this.widget.std.view.getBlock(block.blockId))
.filter((block): block is BlockComponent => !!block);
}
get selectedBlocks() {
const selection = this.selection;
return selection.find('text')
? selection.filter('text')
: selection.filter('block');
}
get selection() {
return this.widget.std.selection;
}
constructor(readonly widget: AffineDragHandleWidget) {}
}