refactor: remove legacy drag handle logic (#9246)

This commit is contained in:
Saul-Mirone
2024-12-23 08:13:04 +00:00
parent a187f23452
commit 23dcaa9cb7
14 changed files with 237 additions and 718 deletions

View File

@@ -1,8 +1,6 @@
/* eslint-disable @typescript-eslint/no-non-null-assertion */
import type { RootBlockModel } from '@blocksuite/affine-model';
import {
DocModeProvider,
DragHandleConfigIdentifier,
type DropType,
} from '@blocksuite/affine-shared/services';
import {
@@ -49,7 +47,6 @@ import { DragEventWatcher } from './watchers/drag-event-watcher.js';
import { EdgelessWatcher } from './watchers/edgeless-watcher.js';
import { HandleEventWatcher } from './watchers/handle-event-watcher.js';
import { KeyboardEventWatcher } from './watchers/keyboard-event-watcher.js';
import { LegacyDragEventWatcher } from './watchers/legacy-drag-event-watcher.js';
import { PageWatcher } from './watchers/page-watcher.js';
import { PointerEventWatcher } from './watchers/pointer-event-watcher.js';
@@ -143,8 +140,6 @@ export class AffineDragHandleWidget extends WidgetComponent<RootBlockModel> {
private readonly _keyboardEventWatcher = new KeyboardEventWatcher(this);
private readonly _legacyDragEventWatcher = new LegacyDragEventWatcher(this);
private readonly _pageWatcher = new PageWatcher(this);
private readonly _removeDropIndicator = () => {
@@ -360,10 +355,6 @@ export class AffineDragHandleWidget extends WidgetComponent<RootBlockModel> {
);
};
private get _enableNewDnd() {
return this.std.doc.awarenessStore.getFlag('enable_new_dnd') ?? true;
}
get dragHandleContainerOffsetParent() {
return this.dragHandleContainer.parentElement!;
}
@@ -385,17 +376,10 @@ export class AffineDragHandleWidget extends WidgetComponent<RootBlockModel> {
override connectedCallback() {
super.connectedCallback();
this.std.provider.getAll(DragHandleConfigIdentifier).forEach(config => {
this.optionRunner.register(config);
});
this.pointerEventWatcher.watch();
this._keyboardEventWatcher.watch();
if (this._enableNewDnd) {
this._dragEventWatcher.watch();
} else {
this._legacyDragEventWatcher.watch();
}
this._dragEventWatcher.watch();
}
override disconnectedCallback() {

View File

@@ -1,4 +1,3 @@
/* eslint-disable @typescript-eslint/no-non-null-assertion */
import { getCurrentNativeRange } from '@blocksuite/affine-shared/utils';
import type { BlockComponent } from '@blocksuite/block-std';
import { Rect } from '@blocksuite/global/utils';

View File

@@ -1,4 +1,3 @@
/* eslint-disable @typescript-eslint/no-non-null-assertion */
import { findNoteBlockModel } from '@blocksuite/affine-shared/utils';
import type { BlockComponent } from '@blocksuite/block-std';
@@ -47,14 +46,9 @@ export class SelectionHelper {
.filter((block): block is BlockComponent => !!block);
}
get selectedBlockIds() {
return this.selectedBlocks.map(block => block.blockId);
}
get selectedBlocks() {
const selection = this.selection;
// eslint-disable-next-line
return selection.find('text')
? selection.filter('text')
: selection.filter('block');

View File

@@ -10,6 +10,7 @@ import {
getBlockProps,
getClosestBlockComponentByElement,
getClosestBlockComponentByPoint,
getDropRectByPoint,
getRectByBlockComponent,
matchFlavours,
} from '@blocksuite/affine-shared/utils';
@@ -21,10 +22,6 @@ import type {
import { Point, Rect } from '@blocksuite/global/utils';
import type { BlockModel } from '@blocksuite/store';
import {
getDropRectByPoint,
getHoveringNote,
} from '../../../_common/utils/index.js';
import {
DRAG_HANDLE_CONTAINER_HEIGHT,
DRAG_HANDLE_CONTAINER_OFFSET_LEFT,
@@ -197,7 +194,9 @@ export function calcDropTarget(
): DropResult | null {
let type: DropType | 'none' = 'none';
const height = 3 * scale;
const { rect: domRect } = getDropRectByPoint(point, model, element);
const dropRect = getDropRectByPoint(point, model, element);
if (!dropRect) return null;
const { rect: domRect } = dropRect;
const distanceToTop = Math.abs(domRect.top - point.y);
const distanceToBottom = Math.abs(domRect.bottom - point.y);
@@ -348,3 +347,17 @@ export function getDuplicateBlocks(blocks: BlockModel[]) {
}));
return duplicateBlocks;
}
/**
* Get hovering note with given a point in edgeless mode.
*/
function getHoveringNote(point: Point) {
return (
document.elementsFromPoint(point.x, point.y).find(isEdgelessChildNote) ||
null
);
}
function isEdgelessChildNote({ classList }: Element) {
return classList.contains('note-background');
}

View File

@@ -9,7 +9,9 @@ import {
TelemetryProvider,
} from '@blocksuite/affine-shared/services';
import {
calcDropTarget,
captureEventTarget,
type DropResult,
getBlockComponentsExcludeSubtrees,
getClosestBlockComponentByPoint,
matchFlavours,
@@ -25,14 +27,6 @@ import { GfxControllerIdentifier } from '@blocksuite/block-std/gfx';
import { Bound, Point } from '@blocksuite/global/utils';
import { Job, Slice, type SliceSnapshot } from '@blocksuite/store';
import {
HtmlAdapter,
MarkdownAdapter,
} from '../../../../_common/adapters/index.js';
import {
calcDropTarget,
type DropResult,
} from '../../../../_common/utils/index.js';
import type { EdgelessRootBlockComponent } from '../../../edgeless/index.js';
import { addNoteAtPoint } from '../../../edgeless/utils/common.js';
import { DropIndicator } from '../components/drop-indicator.js';
@@ -493,28 +487,7 @@ export class DragEventWatcher {
return slice;
}
const html = dataTransfer.getData('text/html');
if (html) {
// use html parser;
const htmlAdapter = new HtmlAdapter(job);
const slice = await htmlAdapter.toSlice(
{ file: html },
std.doc,
parent,
index
);
return slice;
}
const text = dataTransfer.getData('text/plain');
const textAdapter = new MarkdownAdapter(job);
const slice = await textAdapter.toSlice(
{ file: text },
std.doc,
parent,
index
);
return slice;
return null;
} catch {
return null;
}

View File

@@ -135,13 +135,6 @@ export class EdgelessWatcher {
return;
}
const flavour = selectedElement.flavour;
const dragHandleOptions = this.widget.optionRunner.getOption(flavour);
if (!dragHandleOptions || !dragHandleOptions.edgeless) {
this.widget.hide();
return;
}
this.widget.anchorBlockId.value = selectedElement.id;
this._showDragHandleOnTopLevelBlocks().catch(console.error);

View File

@@ -1,474 +0,0 @@
/* eslint-disable @typescript-eslint/no-non-null-assertion */
import type { NoteBlockModel } from '@blocksuite/affine-model';
import {
captureEventTarget,
findNoteBlockModel,
getBlockComponentsExcludeSubtrees,
matchFlavours,
} from '@blocksuite/affine-shared/utils';
import {
type BlockComponent,
isGfxBlockComponent,
type PointerEventState,
type UIEventHandler,
} from '@blocksuite/block-std';
import { GfxControllerIdentifier } from '@blocksuite/block-std/gfx';
import { IS_MOBILE } from '@blocksuite/global/env';
import { Bound, Point } from '@blocksuite/global/utils';
import type { BlockModel } from '@blocksuite/store';
import { render } from 'lit';
import type { EdgelessRootBlockComponent } from '../../../edgeless/index.js';
import { addNoteAtPoint } from '../../../edgeless/utils/common.js';
import { DropIndicator } from '../components/drop-indicator.js';
import { AFFINE_DRAG_HANDLE_WIDGET } from '../consts.js';
import type { AffineDragHandleWidget } from '../drag-handle.js';
import {
containBlock,
getDuplicateBlocks,
includeTextSelection,
} from '../utils.js';
export class LegacyDragEventWatcher {
private readonly _changeCursorToGrabbing = () => {
document.documentElement.classList.add('affine-drag-preview-grabbing');
};
private readonly _createDropIndicator = () => {
if (!this.widget.dropIndicator) {
this.widget.dropIndicator = new DropIndicator();
this.widget.rootComponent.append(this.widget.dropIndicator);
}
};
/**
* When drag end, should move blocks to drop position
*/
private readonly _dragEndHandler: UIEventHandler = ctx => {
this.widget.clearRaf();
if (!this.widget.dragging || !this.widget.dragPreview) return false;
if (this.widget.draggingElements.length === 0 || this.widget.doc.readonly) {
this.widget.hide(true);
return false;
}
const state = ctx.get('pointerState');
const { target } = state.raw;
if (!this.widget.host.contains(target as Node)) {
this.widget.hide(true);
return true;
}
for (const option of this.widget.optionRunner.options) {
if (
option.onDragEnd?.({
// @ts-expect-error FIXME: ts error
state,
draggingElements: this.widget.draggingElements,
dropBlockId: this.widget.dropBlockId,
dropType: this.widget.dropType,
dragPreview: this.widget.dragPreview,
noteScale: this.widget.noteScale.peek(),
editorHost: this.widget.host,
})
) {
this.widget.hide(true);
if (this.widget.mode === 'edgeless') {
this.widget.edgelessWatcher.checkTopLevelBlockSelection();
}
return true;
}
}
// call default drag end handler if no option return true
this._onDragEnd(state);
if (this.widget.mode === 'edgeless') {
this.widget.edgelessWatcher.checkTopLevelBlockSelection();
}
return true;
};
/**
* When dragging, should:
* Update drag preview position
* Update indicator position
* Update drop block id
*/
private readonly _dragMoveHandler: UIEventHandler = ctx => {
if (
this.widget.isHoverDragHandleVisible ||
this.widget.isTopLevelDragHandleVisible
) {
this.widget.hide();
}
if (!this.widget.dragging || this.widget.draggingElements.length === 0) {
return false;
}
ctx.get('defaultState').event.preventDefault();
const state = ctx.get('pointerState');
for (const option of this.widget.optionRunner.options) {
if (
option.onDragMove?.({
// @ts-expect-error FIXME: ts error
state,
draggingElements: this.widget.draggingElements,
})
) {
return true;
}
}
// call default drag move handler if no option return true
return this._onDragMove(state);
};
/**
* When start dragging, should set dragging elements and create drag preview
*/
private readonly _dragStartHandler: UIEventHandler = ctx => {
const state = ctx.get('pointerState');
// If not click left button to start dragging, should do nothing
const { button } = state.raw;
if (button !== 0) {
return false;
}
// call default drag start handler if no option return true
for (const option of this.widget.optionRunner.options) {
if (
option.onDragStart?.({
// @ts-expect-error FIXME: ts error
state,
// @ts-expect-error FIXME: ts error
startDragging: this._startDragging,
anchorBlockId: this.widget.anchorBlockId.peek() ?? '',
editorHost: this.widget.host,
})
) {
return true;
}
}
return this._onDragStart(state);
};
private readonly _onDragEnd = (state: PointerEventState) => {
const targetBlockId = this.widget.dropBlockId;
const dropType = this.widget.dropType;
const draggingElements = this.widget.draggingElements;
this.widget.hide(true);
// handle drop of blocks from note onto edgeless container
if (!targetBlockId) {
const target = captureEventTarget(state.raw.target);
if (!target) return false;
const isTargetEdgelessContainer =
target.classList.contains('edgeless-container');
if (!isTargetEdgelessContainer) return false;
const selectedBlocks = getBlockComponentsExcludeSubtrees(draggingElements)
.map(element => element.model)
.filter((x): x is BlockModel => !!x);
if (selectedBlocks.length === 0) return false;
const isSurfaceComponent = selectedBlocks.some(block => {
const parent = this.widget.doc.getParent(block.id);
return matchFlavours(parent, ['affine:surface']);
});
if (isSurfaceComponent) return true;
const edgelessRoot = this.widget
.rootComponent as EdgelessRootBlockComponent;
const { left: viewportLeft, top: viewportTop } = edgelessRoot.viewport;
const newNoteId = addNoteAtPoint(
edgelessRoot.std,
new Point(state.raw.x - viewportLeft, state.raw.y - viewportTop),
{
scale: this.widget.noteScale.peek(),
}
);
const newNoteBlock = this.widget.doc.getBlockById(
newNoteId
) as NoteBlockModel;
if (!newNoteBlock) return;
const bound = Bound.deserialize(newNoteBlock.xywh);
bound.h *= this.widget.noteScale.peek();
bound.w *= this.widget.noteScale.peek();
this.widget.doc.updateBlock(newNoteBlock, {
xywh: bound.serialize(),
edgeless: {
...newNoteBlock.edgeless,
scale: this.widget.noteScale.peek(),
},
});
const altKey = state.raw.altKey;
if (altKey) {
const duplicateBlocks = getDuplicateBlocks(selectedBlocks);
this.widget.doc.addBlocks(duplicateBlocks, newNoteBlock);
} else {
this.widget.doc.moveBlocks(selectedBlocks, newNoteBlock);
}
edgelessRoot.service.selection.set({
elements: [newNoteBlock.id],
editing: true,
});
return true;
}
// Should make sure drop block id is not in selected blocks
if (
containBlock(this.widget.selectionHelper.selectedBlockIds, targetBlockId)
) {
return false;
}
const selectedBlocks = getBlockComponentsExcludeSubtrees(draggingElements)
.map(element => element.model)
.filter((x): x is BlockModel => !!x);
if (!selectedBlocks.length) {
return false;
}
const targetBlock = this.widget.doc.getBlockById(targetBlockId);
if (!targetBlock) return;
const shouldInsertIn = dropType === 'in';
const parent = shouldInsertIn
? targetBlock
: this.widget.doc.getParent(targetBlockId);
if (!parent) return;
const altKey = state.raw.altKey;
if (shouldInsertIn) {
if (altKey) {
const duplicateBlocks = getDuplicateBlocks(selectedBlocks);
this.widget.doc.addBlocks(duplicateBlocks, targetBlock);
} else {
this.widget.doc.moveBlocks(selectedBlocks, targetBlock);
}
} else {
if (altKey) {
const duplicateBlocks = getDuplicateBlocks(selectedBlocks);
const parentIndex =
parent.children.indexOf(targetBlock) + (dropType === 'after' ? 1 : 0);
this.widget.doc.addBlocks(duplicateBlocks, parent, parentIndex);
} else {
this.widget.doc.moveBlocks(
selectedBlocks,
parent,
targetBlock,
dropType === 'before'
);
}
}
// TODO: need a better way to update selection
// Should update selection after moving blocks
// In doc page mode, update selected blocks
// In edgeless mode, focus on the first block
setTimeout(() => {
if (!parent) return;
// Need to update selection when moving blocks successfully
// Because the block path may be changed after moving
const parentElement = this.widget.std.view.getBlock(parent.id);
if (parentElement) {
const newSelectedBlocks = selectedBlocks.map(block => {
return this.widget.std.view.getBlock(block.id);
});
if (!newSelectedBlocks) return;
const note = findNoteBlockModel(parentElement.model);
if (!note) return;
this.widget.selectionHelper.setSelectedBlocks(
newSelectedBlocks as BlockComponent[],
note.id
);
}
}, 0);
return true;
};
private readonly _onDragMove = (state: PointerEventState) => {
this.widget.clearRaf();
this.widget.rafID = requestAnimationFrame(() => {
// @ts-expect-error FIXME: ts error
this.widget.edgelessWatcher.updateDragPreviewPosition(state);
// @ts-expect-error FIXME: ts error
this.widget.updateDropIndicator(state, true);
});
return true;
};
private readonly _onDragStart = (state: PointerEventState) => {
// Get current hover block element by path
const hoverBlock = this.widget.anchorBlockComponent.peek();
if (!hoverBlock) return false;
const element = captureEventTarget(state.raw.target);
const dragByHandle = !!element?.closest(AFFINE_DRAG_HANDLE_WIDGET);
const isInSurface = isGfxBlockComponent(hoverBlock);
if (isInSurface && dragByHandle) {
const viewport = this.widget.std.get(GfxControllerIdentifier).viewport;
const zoom = viewport.zoom ?? 1;
const dragPreviewEl = document.createElement('div');
const bound = Bound.deserialize(hoverBlock.model.xywh);
const offset = new Point(bound.x * zoom, bound.y * zoom);
// TODO: not use `dangerouslyRenderModel` to render drag preview
render(
this.widget.std.host.dangerouslyRenderModel(hoverBlock.model),
dragPreviewEl
);
this._startDragging([hoverBlock], state, dragPreviewEl, offset);
return true;
}
const selectBlockAndStartDragging = () => {
this.widget.std.selection.setGroup('note', [
this.widget.std.selection.create('block', {
blockId: hoverBlock.blockId,
}),
]);
this._startDragging([hoverBlock], state);
};
if (this.widget.draggingElements.length === 0) {
const dragByBlock =
hoverBlock.contains(element) && !hoverBlock.model.text;
const canDragByBlock =
matchFlavours(hoverBlock.model, [
'affine:attachment',
'affine:bookmark',
]) || hoverBlock.model.flavour.startsWith('affine:embed-');
if (!isInSurface && dragByBlock && canDragByBlock) {
selectBlockAndStartDragging();
return true;
}
}
// Should only start dragging when pointer down on drag handle
// And current mouse button is left button
if (!dragByHandle) {
this.widget.hide();
return false;
}
if (this.widget.draggingElements.length === 1 && !isInSurface) {
selectBlockAndStartDragging();
return true;
}
if (!this.widget.isHoverDragHandleVisible) return false;
let selections = this.widget.selectionHelper.selectedBlocks;
// When current selection is TextSelection
// Should set BlockSelection for the blocks in native range
if (selections.length > 0 && includeTextSelection(selections)) {
const nativeSelection = document.getSelection();
const rangeManager = this.widget.std.range;
if (nativeSelection && nativeSelection.rangeCount > 0 && rangeManager) {
const range = nativeSelection.getRangeAt(0);
const blocks = rangeManager.getSelectedBlockComponentsByRange(range, {
match: el => el.model.role === 'content',
mode: 'highest',
});
this.widget.selectionHelper.setSelectedBlocks(blocks);
selections = this.widget.selectionHelper.selectedBlocks;
}
}
// When there is no selected blocks
// Or selected blocks not including current hover block
// Set current hover block as selected
if (
selections.length === 0 ||
!containBlock(
selections.map(selection => selection.blockId),
this.widget.anchorBlockId.peek()!
)
) {
const block = this.widget.anchorBlockComponent.peek();
if (block) {
this.widget.selectionHelper.setSelectedBlocks([block]);
}
}
const blocks = this.widget.selectionHelper.selectedBlockComponents;
// This could be skip if we can ensure that all selected blocks are on the same level
// Which means not selecting parent block and child block at the same time
const blocksExcludingChildren = getBlockComponentsExcludeSubtrees(
blocks
) as BlockComponent[];
if (blocksExcludingChildren.length === 0) return false;
this._startDragging(blocksExcludingChildren, state);
this.widget.hide();
return true;
};
private readonly _startDragging = (
blocks: BlockComponent[],
state: PointerEventState,
dragPreviewEl?: HTMLElement,
dragPreviewOffset?: Point
) => {
if (!blocks.length) {
return;
}
this.widget.draggingElements = blocks;
this.widget.dragPreview = this.widget.previewHelper.createDragPreview(
blocks,
// @ts-expect-error FIXME: ts error
state,
dragPreviewEl,
dragPreviewOffset
);
this.widget.dragging = true;
this._changeCursorToGrabbing();
this._createDropIndicator();
this.widget.hide();
};
constructor(readonly widget: AffineDragHandleWidget) {}
watch() {
this.widget.disposables.addFromEvent(this.widget, 'pointerdown', e => {
e.preventDefault();
});
if (IS_MOBILE) return;
this.widget.handleEvent('dragStart', this._dragStartHandler);
this.widget.handleEvent('dragMove', this._dragMoveHandler);
this.widget.handleEvent('dragEnd', this._dragEndHandler, { global: true });
}
}