perf(editor): optimize the search for the closest element (#9587)

Closes: [BS-2275](https://linear.app/affine-design/issue/BS-2275/拖拽-indicator-查找优化)
This commit is contained in:
fundon
2025-01-10 08:21:10 +00:00
parent 18ff7500c8
commit 0b3dba614d
11 changed files with 211 additions and 187 deletions
@@ -21,12 +21,11 @@ export class AttachmentBlockService extends BlockService {
export const AttachmentDropOption = FileDropConfigExtension({ export const AttachmentDropOption = FileDropConfigExtension({
flavour: AttachmentBlockSchema.model.flavour, flavour: AttachmentBlockSchema.model.flavour,
onDrop: ({ files, targetModel, place, point, std }) => { onDrop: ({ files, targetModel, placement, point, std }) => {
// generic attachment block for all files except images // generic attachment block for all files except images
const attachmentFiles = files.filter( const attachmentFiles = files.filter(
file => !file.type.startsWith('image/') file => !file.type.startsWith('image/')
); );
if (!attachmentFiles.length) return false; if (!attachmentFiles.length) return false;
if (targetModel && !matchFlavours(targetModel, ['affine:surface'])) { if (targetModel && !matchFlavours(targetModel, ['affine:surface'])) {
@@ -36,7 +35,7 @@ export const AttachmentDropOption = FileDropConfigExtension({
// TODO: use max file size from service // TODO: use max file size from service
maxFileSize, maxFileSize,
targetModel, targetModel,
place placement
).catch(console.error); ).catch(console.error);
return true; return true;
@@ -262,7 +262,7 @@ export class DatabaseBlockComponent extends CaptionedBlockComponent<
const model = this.doc.getBlock(id)?.model; const model = this.doc.getBlock(id)?.model;
const target = result.modelState.model; const target = result.modelState.model;
let parent = this.doc.getParent(target.id); let parent = this.doc.getParent(target.id);
const shouldInsertIn = result.type === 'in'; const shouldInsertIn = result.placement === 'in';
if (shouldInsertIn) { if (shouldInsertIn) {
parent = target; parent = target;
} }
@@ -274,7 +274,7 @@ export class DatabaseBlockComponent extends CaptionedBlockComponent<
[model], [model],
parent, parent,
target, target,
result.type === 'before' result.placement === 'before'
); );
} }
} }
@@ -24,7 +24,7 @@ export class ImageBlockService extends BlockService {
export const ImageDropOption = FileDropConfigExtension({ export const ImageDropOption = FileDropConfigExtension({
flavour: ImageBlockSchema.model.flavour, flavour: ImageBlockSchema.model.flavour,
onDrop: ({ files, targetModel, place, point, std }) => { onDrop: ({ files, targetModel, placement, point, std }) => {
const imageFiles = files.filter(file => file.type.startsWith('image/')); const imageFiles = files.filter(file => file.type.startsWith('image/'));
if (!imageFiles.length) return false; if (!imageFiles.length) return false;
@@ -35,7 +35,7 @@ export const ImageDropOption = FileDropConfigExtension({
// TODO: use max file size from service // TODO: use max file size from service
maxFileSize, maxFileSize,
targetModel, targetModel,
place placement
); );
return true; return true;
} }
@@ -1,40 +1,44 @@
import { import {
calcDropTarget, calcDropTarget,
type DropResult, type DropTarget,
getClosestBlockComponentByPoint, getClosestBlockComponentByPoint,
isInsidePageEditor, isInsidePageEditor,
matchFlavours, matchFlavours,
} from '@blocksuite/affine-shared/utils'; } from '@blocksuite/affine-shared/utils';
import { import {
type BlockComponent,
type BlockStdScope, type BlockStdScope,
type EditorHost, type EditorHost,
LifeCycleWatcher, LifeCycleWatcher,
} from '@blocksuite/block-std'; } from '@blocksuite/block-std';
import { createIdentifier } from '@blocksuite/global/di'; import { createIdentifier } from '@blocksuite/global/di';
import type { IVec } from '@blocksuite/global/utils'; import type { IVec } from '@blocksuite/global/utils';
import { Point } from '@blocksuite/global/utils'; import { Point, throttle } from '@blocksuite/global/utils';
import type { BlockModel, ExtensionType } from '@blocksuite/store'; import type { BlockModel, ExtensionType } from '@blocksuite/store';
import { computed, signal } from '@preact/signals-core';
import type { DragIndicator } from './index.js'; import type { DragIndicator } from './drag-indicator';
export type onDropProps = { export type DropProps = {
std: BlockStdScope; std: BlockStdScope;
files: File[]; files: File[];
targetModel: BlockModel | null; targetModel: BlockModel | null;
place: 'before' | 'after'; placement: 'before' | 'after';
point: IVec; point: IVec;
}; };
export type FileDropOptions = { export type FileDropOptions = {
flavour: string; flavour: string;
onDrop?: (onDropProps: onDropProps) => boolean; onDrop?: (props: DropProps) => boolean;
}; };
/**
* Handles resources from outside.
* Uses `drag over` to handle it.
*/
export class FileDropExtension extends LifeCycleWatcher { export class FileDropExtension extends LifeCycleWatcher {
static override readonly key = 'FileDropExtension'; static override readonly key = 'FileDropExtension';
static dropResult: DropResult | null = null;
static get indicator() { static get indicator() {
let indicator = document.querySelector<DragIndicator>( let indicator = document.querySelector<DragIndicator>(
'affine-drag-indicator' 'affine-drag-indicator'
@@ -50,73 +54,103 @@ export class FileDropExtension extends LifeCycleWatcher {
return indicator; return indicator;
} }
onDragLeave = () => { point$ = signal<Point>(new Point(0, 0));
FileDropExtension.dropResult = null;
FileDropExtension.indicator.rect = null;
};
onDragMove = (event: DragEvent) => { closestElement$ = signal<BlockComponent | null>(null);
event.preventDefault();
dropTarget$ = computed<DropTarget | null>(() => {
let target = null;
const element = this.closestElement$.value;
if (!element) return null;
const model = element.model;
const parent = this.std.store.getParent(model);
if (!matchFlavours(parent, ['affine:surface' as BlockSuite.Flavour])) {
const point = this.point$.value;
target = calcDropTarget(point, model, element);
}
return target;
});
getDropTargetModel(model: BlockModel | null) {
// Existed or In Edgeless
if (model || !isInsidePageEditor(this.editorHost)) return model;
const rootModel = this.doc.root;
if (!rootModel) return null;
let lastNote = rootModel.children[rootModel.children.length - 1];
if (!lastNote || !matchFlavours(lastNote, ['affine:note'])) {
const newNoteId = this.doc.addBlock('affine:note', {}, rootModel.id);
const newNote = this.doc.getBlock(newNoteId)?.model;
if (!newNote) return null;
lastNote = newNote;
}
const lastItem = lastNote.children[lastNote.children.length - 1];
if (lastItem) {
model = lastItem;
} else {
const newParagraphId = this.doc.addBlock(
'affine:paragraph',
{},
lastNote,
0
);
model = this.doc.getBlock(newParagraphId)?.model ?? null;
}
return model;
}
shouldIgnoreEvent = (event: DragEvent, shouldCheckFiles?: boolean) => {
const dataTransfer = event.dataTransfer; const dataTransfer = event.dataTransfer;
if (!dataTransfer) return; if (!dataTransfer) return true;
const effectAllowed = dataTransfer.effectAllowed; const effectAllowed = dataTransfer.effectAllowed;
if (effectAllowed === 'none') return; if (effectAllowed === 'none') return true;
const { clientX, clientY } = event; if (!shouldCheckFiles) return false;
const point = new Point(clientX, clientY);
const element = getClosestBlockComponentByPoint(point.clone());
let result: DropResult | null = null; const droppedFiles = dataTransfer.files;
if (element) { if (!droppedFiles || !droppedFiles.length) return true;
const model = element.model;
const parent = this.std.store.getParent(model); return false;
if (!matchFlavours(parent, ['affine:surface' as BlockSuite.Flavour])) {
result = calcDropTarget(point, model, element);
}
}
if (result) {
FileDropExtension.dropResult = result;
FileDropExtension.indicator.rect = result.rect;
} else {
FileDropExtension.dropResult = null;
FileDropExtension.indicator.rect = null;
}
}; };
get targetModel(): BlockModel | null { updatePoint = (event: DragEvent) => {
let targetModel = FileDropExtension.dropResult?.modelState.model || null; const { clientX, clientY } = event;
const oldPoint = this.point$.peek();
if (!targetModel && isInsidePageEditor(this.editorHost)) { if (
const rootModel = this.doc.root; Math.round(oldPoint.x) === Math.round(clientX) &&
if (!rootModel) return null; Math.round(oldPoint.y) === Math.round(clientY)
)
return;
let lastNote = rootModel.children[rootModel.children.length - 1]; this.point$.value = new Point(clientX, clientY);
if (!lastNote || !matchFlavours(lastNote, ['affine:note'])) { };
const newNoteId = this.doc.addBlock('affine:note', {}, rootModel.id);
const newNote = this.doc.getBlockById(newNoteId);
if (!newNote) return null;
lastNote = newNote;
}
const lastItem = lastNote.children[lastNote.children.length - 1]; onDragLeave = () => {
if (lastItem) { this.closestElement$.value = null;
targetModel = lastItem; };
} else {
const newParagraphId = this.doc.addBlock( onDragOver = (event: DragEvent) => {
'affine:paragraph', event.preventDefault();
{},
lastNote, if (this.shouldIgnoreEvent(event)) return;
0
); this.updatePoint(event);
const newParagraph = this.doc.getBlockById(newParagraphId); };
if (!newParagraph) return null;
targetModel = newParagraph; onDrop = (event: DragEvent) => {
} event.preventDefault();
}
return targetModel; if (this.shouldIgnoreEvent(event, true)) return;
}
this.updatePoint(event);
};
get doc() { get doc() {
return this.std.store; return this.std.store;
@@ -126,66 +160,34 @@ export class FileDropExtension extends LifeCycleWatcher {
return this.std.host; return this.std.host;
} }
get type(): 'before' | 'after' {
return !FileDropExtension.dropResult ||
FileDropExtension.dropResult.type !== 'before'
? 'after'
: 'before';
}
private readonly _onDrop = (event: DragEvent, options: FileDropOptions) => {
FileDropExtension.indicator.rect = null;
const { onDrop } = options;
if (!onDrop) return;
const dataTransfer = event.dataTransfer;
if (!dataTransfer) return;
const effectAllowed = dataTransfer.effectAllowed;
if (effectAllowed === 'none') return;
const droppedFiles = dataTransfer.files;
if (!droppedFiles || !droppedFiles.length) return;
const { clientX, clientY } = event;
const point = new Point(clientX, clientY);
const element = getClosestBlockComponentByPoint(point.clone());
let result: DropResult | null = null;
if (element) {
const model = element.model;
const parent = this.std.store.getParent(model);
if (!matchFlavours(parent, ['affine:surface' as BlockSuite.Flavour])) {
result = calcDropTarget(point, model, element);
}
}
FileDropExtension.dropResult = result;
const { x, y } = event;
const { targetModel, type: place } = this;
const drop = onDrop({
std: this.std,
files: [...droppedFiles],
targetModel,
place,
point: [x, y],
});
if (drop) {
event.preventDefault();
}
return drop;
};
override mounted() { override mounted() {
super.mounted(); super.mounted();
const std = this.std; const std = this.std;
std.event.disposables.add( std.event.disposables.add(
std.event.add('nativeDragMove', context => { this.point$.subscribe(
throttle(
value => {
if (value.x * value.y === 0) return;
this.closestElement$.value = getClosestBlockComponentByPoint(value);
},
233,
{ leading: true, trailing: true }
)
)
);
std.event.disposables.add(
this.dropTarget$.subscribe(target => {
FileDropExtension.indicator.rect = target?.rect ?? null;
})
);
std.event.disposables.add(
std.event.add('nativeDragOver', context => {
const event = context.get('dndState'); const event = context.get('dndState');
this.onDragMove(event.raw); this.onDragOver(event.raw);
}) })
); );
std.event.disposables.add( std.event.disposables.add(
@@ -195,19 +197,43 @@ export class FileDropExtension extends LifeCycleWatcher {
); );
std.event.disposables.add( std.event.disposables.add(
std.event.add('nativeDrop', context => { std.event.add('nativeDrop', context => {
const event = context.get('dndState').raw;
const { x, y, dataTransfer } = event;
const droppedFiles = dataTransfer?.files;
if (!droppedFiles || !droppedFiles.length) {
this.onDragLeave();
return;
}
this.onDrop(event);
const target = this.dropTarget$.peek();
const std = this.std;
const targetModel = this.getDropTargetModel(
target?.modelState.model ?? null
);
const placement = target?.placement === 'before' ? 'before' : 'after';
const values = std.provider const values = std.provider
.getAll(FileDropConfigExtensionIdentifier) .getAll(FileDropConfigExtensionIdentifier)
.values(); .values();
for (const value of values) { for (const ext of values) {
if (value.onDrop) { if (!ext.onDrop) continue;
const event = context.get('dndState');
const drop = this._onDrop(event.raw, value); const options = {
if (drop) { std,
return; files: [...droppedFiles],
} targetModel,
} placement,
point: [x, y],
} satisfies DropProps;
if (ext.onDrop(options)) break;
} }
this.onDragLeave();
}) })
); );
} }
@@ -1,10 +1,10 @@
import { DragIndicator } from './drag-indicator.js'; import { DragIndicator } from './drag-indicator';
export { export {
type DropProps,
FileDropConfigExtension, FileDropConfigExtension,
FileDropExtension, FileDropExtension,
type FileDropOptions, type FileDropOptions,
type onDropProps, } from './file-drop-manager';
} from './file-drop-manager.js';
export { DragIndicator }; export { DragIndicator };
@@ -9,7 +9,7 @@ import {
} from '../dom/index.js'; } from '../dom/index.js';
import { matchFlavours } from '../model/index.js'; import { matchFlavours } from '../model/index.js';
import { getDropRectByPoint } from './get-drop-rect-by-point.js'; import { getDropRectByPoint } from './get-drop-rect-by-point.js';
import { DropFlags, type DroppingType, type DropResult } from './types.js'; import { DropFlags, type DropPlacement, type DropTarget } from './types.js';
function getVisiblePreviousElementSibling(element: Element | null) { function getVisiblePreviousElementSibling(element: Element | null) {
if (!element) return null; if (!element) return null;
@@ -43,7 +43,7 @@ export function calcDropTarget(
* Allow the dragging block to be dropped as sublist * Allow the dragging block to be dropped as sublist
*/ */
allowSublist: boolean = true allowSublist: boolean = true
): DropResult | null { ): DropTarget | null {
const schema = model.doc.getSchemaByFlavour('affine:database'); const schema = model.doc.getSchemaByFlavour('affine:database');
const children = schema?.model.children ?? []; const children = schema?.model.children ?? [];
@@ -64,7 +64,7 @@ export function calcDropTarget(
} }
} }
let type: DroppingType = 'none'; let placement: DropPlacement = 'none';
const height = 3 * scale; const height = 3 * scale;
const dropResult = getDropRectByPoint(point, model, element); const dropResult = getDropRectByPoint(point, model, element);
if (!dropResult) return null; if (!dropResult) return null;
@@ -75,10 +75,10 @@ export function calcDropTarget(
const rect = Rect.fromDOMRect(domRect); const rect = Rect.fromDOMRect(domRect);
rect.top -= height / 2; rect.top -= height / 2;
rect.height = height; rect.height = height;
type = 'database'; placement = 'database';
return { return {
type, placement,
rect, rect,
modelState: { modelState: {
model, model,
@@ -91,10 +91,10 @@ export function calcDropTarget(
const distanceToTop = Math.abs(domRect.top - point.y); const distanceToTop = Math.abs(domRect.top - point.y);
const distanceToBottom = Math.abs(domRect.bottom - point.y); const distanceToBottom = Math.abs(domRect.bottom - point.y);
const before = distanceToTop < distanceToBottom; const before = distanceToTop < distanceToBottom;
type = before ? 'before' : 'after'; placement = before ? 'before' : 'after';
return { return {
type, placement,
rect: Rect.fromLWTH( rect: Rect.fromLWTH(
domRect.left, domRect.left,
domRect.width, domRect.width,
@@ -113,10 +113,10 @@ export function calcDropTarget(
const distanceToBottom = Math.abs(domRect.bottom - point.y); const distanceToBottom = Math.abs(domRect.bottom - point.y);
const before = distanceToTop < distanceToBottom; const before = distanceToTop < distanceToBottom;
type = before ? 'before' : 'after'; placement = before ? 'before' : 'after';
let offsetY = 4; let offsetY = 4;
if (type === 'before') { if (placement === 'before') {
// before // before
let prev; let prev;
let prevRect; let prevRect;
@@ -127,7 +127,7 @@ export function calcDropTarget(
draggingElements.length && draggingElements.length &&
prev === draggingElements[draggingElements.length - 1] prev === draggingElements[draggingElements.length - 1]
) { ) {
type = 'none'; placement = 'none';
} else { } else {
prevRect = getRectByBlockComponent(prev); prevRect = getRectByBlockComponent(prev);
} }
@@ -153,7 +153,7 @@ export function calcDropTarget(
!hasChild && !hasChild &&
point.x > domRect.x + BLOCK_CHILDREN_CONTAINER_PADDING_LEFT point.x > domRect.x + BLOCK_CHILDREN_CONTAINER_PADDING_LEFT
) { ) {
type = 'in'; placement = 'in';
} }
// after // after
let next; let next;
@@ -162,11 +162,11 @@ export function calcDropTarget(
next = getVisibleNextElementSibling(element); next = getVisibleNextElementSibling(element);
if (next) { if (next) {
if ( if (
type === 'after' && placement === 'after' &&
draggingElements.length && draggingElements.length &&
next === draggingElements[0] next === draggingElements[0]
) { ) {
type = 'none'; placement = 'none';
next = null; next = null;
} }
} else { } else {
@@ -181,22 +181,22 @@ export function calcDropTarget(
} }
} }
if (type === 'none') return null; if (placement === 'none') return null;
let top = domRect.top; let top = domRect.top;
if (type === 'before') { if (placement === 'before') {
top -= offsetY; top -= offsetY;
} else { } else {
top += domRect.height + offsetY; top += domRect.height + offsetY;
} }
if (type === 'in') { if (placement === 'in') {
domRect.x += BLOCK_CHILDREN_CONTAINER_PADDING_LEFT; domRect.x += BLOCK_CHILDREN_CONTAINER_PADDING_LEFT;
domRect.width -= BLOCK_CHILDREN_CONTAINER_PADDING_LEFT; domRect.width -= BLOCK_CHILDREN_CONTAINER_PADDING_LEFT;
} }
return { return {
type, placement,
rect: Rect.fromLWTH(domRect.left, domRect.width, top - height / 2, height), rect: Rect.fromLWTH(domRect.left, domRect.width, top - height / 2, height),
modelState: { modelState: {
model, model,
@@ -18,12 +18,12 @@ export enum DropFlags {
} }
/** /**
* A dropping type. * A drop placement.
*/ */
export type DroppingType = 'none' | 'before' | 'after' | 'database' | 'in'; export type DropPlacement = 'none' | 'before' | 'after' | 'database' | 'in';
export type DropResult = { export type DropTarget = {
type: DroppingType; placement: DropPlacement;
rect: Rect; rect: Rect;
modelState: EditingState; modelState: EditingState;
}; };
@@ -146,8 +146,8 @@ export function getClosestBlockComponentByPoint(
} }
} }
} else { } else {
// Indented paragraphs or list
bounds = getRectByBlockComponent(element); bounds = getRectByBlockComponent(element);
// Indented paragraphs or list
childBounds = element childBounds = element
.querySelector('.affine-block-children-container') .querySelector('.affine-block-children-container')
?.firstElementChild?.getBoundingClientRect(); ?.firstElementChild?.getBoundingClientRect();
@@ -263,8 +263,8 @@ export function getClosestBlockComponentByElement(
* https://github.com/toeverything/blocksuite/pull/1121 * https://github.com/toeverything/blocksuite/pull/1121
*/ */
export function getRectByBlockComponent(element: Element | BlockComponent) { export function getRectByBlockComponent(element: Element | BlockComponent) {
if (isDatabase(element)) return element.getBoundingClientRect(); if (!isDatabase(element)) element = element.firstElementChild ?? element;
return (element.firstElementChild ?? element).getBoundingClientRect(); return element.getBoundingClientRect();
} }
/** /**
@@ -303,9 +303,8 @@ function findBlockComponent(elements: Element[], parent?: Element) {
if (hasBlockId(element) && isBlock(element)) return element; if (hasBlockId(element) && isBlock(element)) return element;
if (isImage(element)) { if (isImage(element)) {
const element = elements[i]; const element = elements[i];
if (i < len && hasBlockId(element) && isBlock(element)) { if (!element) return null;
return elements[i]; if (hasBlockId(element) && isBlock(element)) return element;
}
return getClosestBlockComponentByElement(element); return getClosestBlockComponentByElement(element);
} }
} }
@@ -4,8 +4,8 @@ import { DocModeProvider } from '@blocksuite/affine-shared/services';
import { import {
autoScroll, autoScroll,
calcDropTarget, calcDropTarget,
type DroppingType, type DropPlacement,
type DropResult, type DropTarget,
getScrollContainer, getScrollContainer,
isInsideEdgelessEditor, isInsideEdgelessEditor,
isInsidePageEditor, isInsidePageEditor,
@@ -61,9 +61,9 @@ export class AffineDragHandleWidget extends WidgetComponent<RootBlockModel> {
/** /**
* When dragging, should update indicator position and target drop block id * When dragging, should update indicator position and target drop block id
*/ */
private readonly _getDropResult = ( private readonly _getDropTarget = (
state: DndEventState state: DndEventState
): DropResult | null => { ): DropTarget | null => {
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(
this.host, this.host,
@@ -114,7 +114,7 @@ export class AffineDragHandleWidget extends WidgetComponent<RootBlockModel> {
isDraggedElementNote === false isDraggedElementNote === false
); );
if (isDraggedElementNote && result?.type === 'in') return null; if (isDraggedElementNote && result?.placement === 'in') return null;
return result; return result;
}; };
@@ -135,7 +135,7 @@ export class AffineDragHandleWidget extends WidgetComponent<RootBlockModel> {
private readonly _reset = () => { private readonly _reset = () => {
this.draggingElements = []; this.draggingElements = [];
this.dropBlockId = ''; this.dropBlockId = '';
this.dropType = null; this.dropPlacement = null;
this.lastDragPointerState = null; this.lastDragPointerState = null;
this.rafID = 0; this.rafID = 0;
this.dragging = false; this.dragging = false;
@@ -157,29 +157,29 @@ export class AffineDragHandleWidget extends WidgetComponent<RootBlockModel> {
document.documentElement.classList.remove('affine-drag-preview-grabbing'); document.documentElement.classList.remove('affine-drag-preview-grabbing');
}; };
private readonly _resetDropResult = () => { private readonly _resetDropTarget = () => {
this.dropBlockId = ''; this.dropBlockId = '';
this.dropType = null; this.dropPlacement = null;
if (this.dropIndicator) this.dropIndicator.rect = null; if (this.dropIndicator) this.dropIndicator.rect = null;
}; };
private readonly _updateDropResult = (dropResult: DropResult | null) => { private readonly _updateDropTarget = (dropTarget: DropTarget | null) => {
if (!this.dropIndicator) return; if (!this.dropIndicator) return;
this.dropBlockId = dropResult?.modelState.model.id ?? ''; this.dropBlockId = dropTarget?.modelState.model.id ?? '';
this.dropType = dropResult?.type ?? null; this.dropPlacement = dropTarget?.placement ?? null;
if (dropResult?.rect) { if (dropTarget?.rect) {
const offsetParentRect = const offsetParentRect =
this.dragHandleContainerOffsetParent.getBoundingClientRect(); this.dragHandleContainerOffsetParent.getBoundingClientRect();
let { left, top } = dropResult.rect; let { left, top } = dropTarget.rect;
left -= offsetParentRect.left; left -= offsetParentRect.left;
top -= offsetParentRect.top; top -= offsetParentRect.top;
const { width, height } = dropResult.rect; const { width, height } = dropTarget.rect;
const rect = Rect.fromLWTH(left, width, top, height); const rect = Rect.fromLWTH(left, width, top, height);
this.dropIndicator.rect = rect; this.dropIndicator.rect = rect;
} else { } else {
this.dropIndicator.rect = dropResult?.rect ?? null; this.dropIndicator.rect = dropTarget?.rect ?? null;
} }
}; };
@@ -221,7 +221,7 @@ export class AffineDragHandleWidget extends WidgetComponent<RootBlockModel> {
dropIndicator: DropIndicator | null = null; dropIndicator: DropIndicator | null = null;
dropType: DroppingType | null = null; dropPlacement: DropPlacement | null = null;
edgelessWatcher = new EdgelessWatcher(this); edgelessWatcher = new EdgelessWatcher(this);
@@ -298,10 +298,10 @@ export class AffineDragHandleWidget extends WidgetComponent<RootBlockModel> {
!closestNoteBlock || !closestNoteBlock ||
isOutOfNoteBlock(this.host, closestNoteBlock, point, this.scale.peek()) isOutOfNoteBlock(this.host, closestNoteBlock, point, this.scale.peek())
) { ) {
this._resetDropResult(); this._resetDropTarget();
} else { } else {
const dropResult = this._getDropResult(state); const dropTarget = this._getDropTarget(state);
this._updateDropResult(dropResult); this._updateDropTarget(dropTarget);
} }
this.lastDragPointerState = state; this.lastDragPointerState = state;
@@ -3,7 +3,7 @@ import type { ParagraphBlockModel } from '@blocksuite/affine-model';
import { DocModeProvider } from '@blocksuite/affine-shared/services'; import { DocModeProvider } from '@blocksuite/affine-shared/services';
import { import {
calcDropTarget, calcDropTarget,
type DropResult, type DropTarget,
findClosestBlockComponent, findClosestBlockComponent,
getBlockProps, getBlockProps,
getClosestBlockComponentByPoint, getClosestBlockComponentByPoint,
@@ -174,7 +174,7 @@ export const getClosestBlockByPoint = (
export const getDropResult = ( export const getDropResult = (
event: MouseEvent, event: MouseEvent,
scale: number = 1 scale: number = 1
): DropResult | null => { ): DropTarget | null => {
let dropIndicator = null; let dropIndicator = null;
const point = new Point(event.x, event.y); const point = new Point(event.x, event.y);
const closestBlock = getClosestBlockComponentByPoint(point) as BlockComponent; const closestBlock = getClosestBlockComponentByPoint(point) as BlockComponent;
@@ -16,7 +16,7 @@ import {
import { import {
calcDropTarget, calcDropTarget,
captureEventTarget, captureEventTarget,
type DropResult, type DropTarget,
getBlockComponentsExcludeSubtrees, getBlockComponentsExcludeSubtrees,
getClosestBlockComponentByPoint, getClosestBlockComponentByPoint,
matchFlavours, matchFlavours,
@@ -290,11 +290,11 @@ export class DragEventWatcher {
if (matchFlavours(parent, ['affine:surface'])) { if (matchFlavours(parent, ['affine:surface'])) {
return; return;
} }
const result: DropResult | null = calcDropTarget(point, model, element); const target: DropTarget | null = calcDropTarget(point, model, element);
if (!result) return; if (!target) return;
const index = const index =
parent.children.indexOf(model) + (result.type === 'before' ? 0 : 1); parent.children.indexOf(model) + (target.placement === 'before' ? 0 : 1);
if (matchFlavours(parent, ['affine:note'])) { if (matchFlavours(parent, ['affine:note'])) {
const snapshot = this._deserializeSnapshot(state); const snapshot = this._deserializeSnapshot(state);