chore: dnd cleanup (#9899)

This commit is contained in:
doouding
2025-01-27 04:29:13 +00:00
parent ffd54c6620
commit 73b4437081
16 changed files with 207 additions and 305 deletions
@@ -1,4 +1,4 @@
import { FileDropConfigExtension } from '@blocksuite/affine-components/drag-indicator';
import { FileDropConfigExtension } from '@blocksuite/affine-components/drop-indicator';
import { AttachmentBlockSchema } from '@blocksuite/affine-model';
import { TelemetryProvider } from '@blocksuite/affine-shared/services';
import {
@@ -4,7 +4,7 @@ import {
popMenu,
popupTargetFromElement,
} from '@blocksuite/affine-components/context-menu';
import { DragIndicator } from '@blocksuite/affine-components/drag-indicator';
import { DropIndicator } from '@blocksuite/affine-components/drop-indicator';
import { PeekViewProvider } from '@blocksuite/affine-components/peek';
import { toast } from '@blocksuite/affine-components/toast';
import type { DatabaseBlockModel } from '@blocksuite/affine-model';
@@ -241,7 +241,7 @@ export class DatabaseBlockComponent extends CaptionedBlockComponent<DatabaseBloc
}
);
indicator = new DragIndicator();
indicator = new DropIndicator();
onDrag = (evt: MouseEvent, id: string): (() => void) => {
const result = getDropResult(evt);
@@ -1,4 +1,4 @@
import { FileDropConfigExtension } from '@blocksuite/affine-components/drag-indicator';
import { FileDropConfigExtension } from '@blocksuite/affine-components/drop-indicator';
import { ImageBlockSchema, MAX_IMAGE_WIDTH } from '@blocksuite/affine-model';
import { TelemetryProvider } from '@blocksuite/affine-shared/services';
import {
+1 -1
View File
@@ -54,7 +54,7 @@
"./caption": "./src/caption/index.ts",
"./context-menu": "./src/context-menu/index.ts",
"./date-picker": "./src/date-picker/index.ts",
"./drag-indicator": "./src/drag-indicator/index.ts",
"./drop-indicator": "./src/drop-indicator/index.ts",
"./filterable-list": "./src/filterable-list/index.ts",
"./virtual-keyboard": "./src/virtual-keyboard/index.ts",
"./toggle-button": "./src/toggle-button/index.ts",
@@ -1,44 +0,0 @@
import type { Rect } from '@blocksuite/global/utils';
import { css, html, LitElement } from 'lit';
import { property } from 'lit/decorators.js';
import { styleMap } from 'lit/directives/style-map.js';
export class DragIndicator extends LitElement {
static override styles = css`
.affine-drag-indicator {
position: absolute;
top: 0;
left: 0;
background: var(--affine-primary-color);
transition-property: width, height, transform;
transition-duration: 100ms;
transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
transition-delay: 0s;
transform-origin: 0 0;
pointer-events: none;
z-index: 2;
}
`;
override render() {
if (!this.rect) {
return null;
}
const { left, top, width, height } = this.rect;
const style = styleMap({
width: `${width}px`,
height: `${height}px`,
transform: `translate(${left}px, ${top}px)`,
});
return html`<div class="affine-drag-indicator" style=${style}></div>`;
}
@property({ attribute: false })
accessor rect: Rect | null = null;
}
declare global {
interface HTMLElementTagNameMap {
'affine-drag-indicator': DragIndicator;
}
}
@@ -25,14 +25,12 @@ export class DropIndicator extends LitElement {
return null;
}
const parentRect = this.parentElement!.getBoundingClientRect();
const { left, top, width, height } = this.rect;
const style = styleMap({
width: `${width}px`,
height: `${height}px`,
top: `${top - parentRect.y}px`,
left: `${left - parentRect.x}px`,
top: `${top}px`,
left: `${left}px`,
});
return html`<div class="affine-drop-indicator" style=${style}></div>`;
@@ -17,7 +17,7 @@ import { Point, throttle } from '@blocksuite/global/utils';
import type { BlockModel, ExtensionType } from '@blocksuite/store';
import { computed, signal } from '@preact/signals-core';
import type { DragIndicator } from './drag-indicator';
import { DropIndicator } from './drop-indicator';
export type DropProps = {
std: BlockStdScope;
@@ -39,20 +39,7 @@ export type FileDropOptions = {
export class FileDropExtension extends LifeCycleWatcher {
static override readonly key = 'FileDropExtension';
static get indicator() {
let indicator = document.querySelector<DragIndicator>(
'affine-drag-indicator'
);
if (!indicator) {
indicator = document.createElement(
'affine-drag-indicator'
) as DragIndicator;
document.body.append(indicator);
}
return indicator;
}
indicator: DropIndicator = new DropIndicator();
dragging$ = signal(false);
@@ -166,10 +153,17 @@ export class FileDropExtension extends LifeCycleWatcher {
return this.std.host;
}
override unmounted(): void {
super.unmounted();
this.indicator.remove();
}
override mounted() {
super.mounted();
const std = this.std;
std.host.ownerDocument.body.append(this.indicator);
std.event.disposables.add(
this.point$.subscribe(
throttle(
@@ -198,7 +192,7 @@ export class FileDropExtension extends LifeCycleWatcher {
std.event.disposables.add(
this.dropTarget$.subscribe(target => {
FileDropExtension.indicator.rect = this._disableIndicator
this.indicator.rect = this._disableIndicator
? null
: (target?.rect ?? null);
})
@@ -1,4 +1,4 @@
import { DragIndicator } from './drag-indicator';
import { DropIndicator } from './drop-indicator';
export {
type DropProps,
FileDropConfigExtension,
@@ -6,8 +6,8 @@ export {
type FileDropOptions,
} from './file-drop-manager';
export { DragIndicator };
export { DropIndicator };
export function effects() {
customElements.define('affine-drag-indicator', DragIndicator);
customElements.define('affine-drop-indicator', DropIndicator);
}
@@ -36,6 +36,9 @@ export class AffineDragHandleWidget extends WidgetComponent<RootBlockModel> {
private _anchorModelDisposables: DisposableGroup | null = null;
/**
* Used to handle drag behavior
*/
private readonly _dragEventWatcher = new DragEventWatcher(this);
private readonly _handleEventWatcher = new HandleEventWatcher(this);
@@ -54,11 +57,6 @@ export class AffineDragHandleWidget extends WidgetComponent<RootBlockModel> {
this.isTopLevelDragHandleVisible = false;
this.pointerEventWatcher.reset();
this._resetCursor();
};
private readonly _resetCursor = () => {
document.documentElement.classList.remove('affine-drag-preview-grabbing');
};
anchorBlockId = signal<string | null>(null);
@@ -1,8 +1,6 @@
import { DropIndicator } from './components/drop-indicator';
import { AFFINE_DRAG_HANDLE_WIDGET } from './consts';
import { AffineDragHandleWidget } from './drag-handle';
export function effects() {
customElements.define('affine-drop-indicator', DropIndicator);
customElements.define(AFFINE_DRAG_HANDLE_WIDGET, AffineDragHandleWidget);
}
@@ -3,6 +3,7 @@ import {
addNoteAtPoint,
getSurfaceBlock,
} from '@blocksuite/affine-block-surface';
import { DropIndicator } from '@blocksuite/affine-components/drop-indicator';
import type { EmbedCardStyle, NoteBlockModel } from '@blocksuite/affine-model';
import {
BLOCK_CHILDREN_CONTAINER_PADDING_LEFT,
@@ -23,7 +24,6 @@ import {
} from '@blocksuite/affine-shared/utils';
import {
type BlockComponent,
BlockSelection,
type BlockStdScope,
type DragFromBlockSuite,
type DragPayload,
@@ -34,7 +34,6 @@ import { GfxControllerIdentifier } from '@blocksuite/block-std/gfx';
import { Bound, last, Point, Rect } from '@blocksuite/global/utils';
import { Slice, type SliceSnapshot } from '@blocksuite/store';
import { DropIndicator } from '../components/drop-indicator.js';
import type { AffineDragHandleWidget } from '../drag-handle.js';
import { PreviewHelper } from '../helpers/preview-helper.js';
import { newIdCrossDoc } from '../middleware/new-id-cross-doc.js';
@@ -66,6 +65,8 @@ export class DragEventWatcher {
previewHelper = new PreviewHelper(this.widget);
dropTargetCleanUps: Map<string, (() => void)[]> = new Map();
get host() {
return this.widget.host;
}
@@ -257,25 +258,6 @@ export class DragEventWatcher {
}
};
private readonly _getDraggedBlock = (draggedBlock: BlockComponent) => {
return this._selectAndSetDraggingBlock(draggedBlock);
};
private readonly _selectAndSetDraggingBlock = (
hoveredBlock: BlockComponent
) => {
this.std.selection.setGroup('note', [
this.std.selection.create(BlockSelection, {
blockId: hoveredBlock.blockId,
}),
]);
return {
models: [hoveredBlock.model],
snapshot: this._toSnapshot([hoveredBlock]),
};
};
private readonly _getSnapshotFromHoveredBlocks = () => {
const hoverBlock = this.widget.anchorBlockComponent.peek()!;
const isInSurface = isGfxBlockComponent(hoverBlock);
@@ -620,6 +602,161 @@ export class DragEventWatcher {
return std === this.std;
}
private _makeDraggable(target: HTMLElement) {
const std = this.std;
return std.dnd.draggable<DragBlockEntity>({
element: target,
canDrag: () => {
const hoverBlock = this.widget.anchorBlockComponent.peek();
return hoverBlock ? true : false;
},
onDragStart: () => {
this.widget.dragging = true;
},
onDrop: () => {
this._cleanup();
},
setDragPreview: ({ source, container }) => {
if (!source.data?.bsEntity?.modelIds.length) {
return;
}
this.previewHelper.renderDragPreview(
source.data?.bsEntity?.modelIds,
container
);
},
setDragData: () => {
const { snapshot } = this._getSnapshotFromHoveredBlocks();
return {
type: 'blocks',
modelIds: snapshot ? extractIdsFromSnapshot(snapshot) : [],
snapshot,
};
},
});
}
private _makeDropTarget(view: BlockComponent) {
if (view.model.role !== 'content' && view.model.role !== 'hub') {
return;
}
const widget = this.widget;
const cleanups: (() => void)[] = [];
cleanups.push(
this.std.dnd.dropTarget<
DragBlockEntity,
{
modelId: string;
}
>({
element: view,
getIsSticky: () => true,
canDrop: ({ source }) => {
if (source.data.bsEntity?.type === 'blocks') {
return (
source.data.from?.docId !== widget.doc.id ||
source.data.bsEntity.modelIds.every(id => id !== view.model.id)
);
}
return false;
},
setDropData: () => {
return {
modelId: view.model.id,
};
},
})
);
if (matchFlavours(view.model, ['affine:attachment', 'affine:bookmark'])) {
cleanups.push(this._makeDraggable(view));
}
if (this.dropTargetCleanUps.has(view.model.id)) {
this.dropTargetCleanUps.get(view.model.id)!.forEach(clean => clean());
}
this.dropTargetCleanUps.set(view.model.id, cleanups);
}
private _monitorBlockDrag() {
return this.std.dnd.monitor<DragBlockEntity>({
canMonitor: ({ source }) => {
const entity = source.data?.bsEntity;
return entity?.type === 'blocks' && !!entity.snapshot;
},
onDropTargetChange: ({ location }) => {
this._clearDropIndicator();
if (
!this._isDropOnCurrentEditor(
(location.current.dropTargets[0]?.element as BlockComponent)?.std
)
) {
return;
}
},
onDrop: ({ location, source }) => {
this._clearDropIndicator();
if (
!this._isDropOnCurrentEditor(
(location.current.dropTargets[0]?.element as BlockComponent)?.std
)
) {
return;
}
const target = location.current.dropTargets[0];
const point = new Point(
location.current.input.clientX,
location.current.input.clientY
);
const dragPayload = source.data;
const dropPayload = target.data;
this._onDrop(
target.element as BlockComponent,
dragPayload,
dropPayload,
point
);
},
onDrag: ({ location, source }) => {
if (
!this._isDropOnCurrentEditor(
(location.current.dropTargets[0]?.element as BlockComponent)?.std
) ||
!location.current.dropTargets[0]
) {
return;
}
const target = location.current.dropTargets[0];
const point = new Point(
location.current.input.clientX,
location.current.input.clientY
);
const dragPayload = source.data;
const dropPayload = target.data;
this._onDragMove(
point,
dragPayload,
dropPayload,
target.element as BlockComponent
);
},
});
}
watch() {
this.widget.handleEvent('pointerDown', ctx => {
const state = ctx.get('pointerState');
@@ -652,41 +789,6 @@ export class DragEventWatcher {
const disposables = widget.disposables;
const scrollable = getScrollContainer(this.host);
disposables.add(
std.dnd.draggable<DragBlockEntity>({
element: this.widget,
canDrag: () => {
const hoverBlock = this.widget.anchorBlockComponent.peek();
return hoverBlock ? true : false;
},
onDragStart: () => {
this.widget.dragging = true;
},
onDrop: () => {
this._cleanup();
},
setDragPreview: ({ source, container }) => {
if (!source.data?.bsEntity?.modelIds.length) {
return;
}
this.previewHelper.renderDragPreview(
source.data?.bsEntity?.modelIds,
container
);
},
setDragData: () => {
const { snapshot } = this._getSnapshotFromHoveredBlocks();
return {
type: 'blocks',
modelIds: snapshot ? extractIdsFromSnapshot(snapshot) : [],
snapshot,
};
},
})
);
if (scrollable) {
disposables.add(
std.dnd.autoScroll<DragBlockEntity>({
@@ -698,174 +800,30 @@ export class DragEventWatcher {
);
}
disposables.add(this._makeDraggable(this.widget));
// used to handle drag move and drop
disposables.add(
std.dnd.monitor<DragBlockEntity>({
canMonitor: ({ source }) => {
const entity = source.data?.bsEntity;
return entity?.type === 'blocks' && !!entity.snapshot;
},
onDropTargetChange: ({ location }) => {
this._clearDropIndicator();
if (
!this._isDropOnCurrentEditor(
(location.current.dropTargets[0]?.element as BlockComponent)?.std
)
) {
return;
}
},
onDrop: ({ location, source }) => {
this._clearDropIndicator();
if (
!this._isDropOnCurrentEditor(
(location.current.dropTargets[0]?.element as BlockComponent)?.std
)
) {
return;
}
const target = location.current.dropTargets[0];
const point = new Point(
location.current.input.clientX,
location.current.input.clientY
);
const dragPayload = source.data;
const dropPayload = target.data;
this._onDrop(
target.element as BlockComponent,
dragPayload,
dropPayload,
point
);
},
onDrag: ({ location, source }) => {
if (
!this._isDropOnCurrentEditor(
(location.current.dropTargets[0]?.element as BlockComponent)?.std
) ||
!location.current.dropTargets[0]
) {
return;
}
const target = location.current.dropTargets[0];
const point = new Point(
location.current.input.clientX,
location.current.input.clientY
);
const dragPayload = source.data;
const dropPayload = target.data;
this._onDragMove(
point,
dragPayload,
dropPayload,
target.element as BlockComponent
);
},
})
);
let dropTargetCleanUps: Map<string, (() => void)[]> = new Map();
const makeBlockComponentDropTarget = (view: BlockComponent) => {
if (view.model.role !== 'content' && view.model.role !== 'hub') {
return;
}
const cleanups: (() => void)[] = [];
cleanups.push(
std.dnd.dropTarget<
DragBlockEntity,
{
modelId: string;
}
>({
element: view,
getIsSticky: () => true,
canDrop: ({ source }) => {
if (source.data.bsEntity?.type === 'blocks') {
return (
source.data.from?.docId !== widget.doc.id ||
source.data.bsEntity.modelIds.every(id => id !== view.model.id)
);
}
return false;
},
setDropData: () => {
return {
modelId: view.model.id,
};
},
})
);
if (matchFlavours(view.model, ['affine:attachment', 'affine:bookmark'])) {
cleanups.push(
std.dnd.draggable<DragBlockEntity>({
element: view,
canDrag: () => {
return !isGfxBlockComponent(view);
},
onDragStart: () => {
this.widget.dragging = true;
},
onDrop: () => {
this._cleanup();
},
setDragPreview: ({ source, container }) => {
if (!source.data?.bsEntity?.modelIds.length) {
return;
}
this.previewHelper.renderDragPreview(
source.data?.bsEntity?.modelIds,
container
);
},
setDragData: () => {
const { snapshot } = this._getDraggedBlock(view);
return {
type: 'blocks',
modelIds: snapshot ? extractIdsFromSnapshot(snapshot) : [],
snapshot,
};
},
})
);
}
dropTargetCleanUps.set(view.model.id, cleanups);
};
disposables.add(this._monitorBlockDrag());
disposables.add(
std.view.viewUpdated.on(payload => {
if (payload.type === 'add') {
makeBlockComponentDropTarget(payload.view);
this._makeDropTarget(payload.view);
} else if (
payload.type === 'delete' &&
dropTargetCleanUps.has(payload.id)
this.dropTargetCleanUps.has(payload.id)
) {
dropTargetCleanUps.get(payload.id)!.forEach(clean => clean());
dropTargetCleanUps.delete(payload.id);
this.dropTargetCleanUps.get(payload.id)!.forEach(clean => clean());
this.dropTargetCleanUps.delete(payload.id);
}
})
);
std.view.views.forEach(block => {
makeBlockComponentDropTarget(block);
});
std.view.views.forEach(block => this._makeDropTarget(block));
disposables.add(() => {
dropTargetCleanUps.forEach(cleanUps => cleanUps.forEach(fn => fn()));
dropTargetCleanUps.clear();
this.dropTargetCleanUps.forEach(cleanUps => cleanUps.forEach(fn => fn()));
this.dropTargetCleanUps.clear();
});
}
}
+2 -2
View File
@@ -22,7 +22,7 @@ import { effects as componentCaptionEffects } from '@blocksuite/affine-component
import { effects as componentColorPickerEffects } from '@blocksuite/affine-components/color-picker';
import { effects as componentContextMenuEffects } from '@blocksuite/affine-components/context-menu';
import { effects as componentDatePickerEffects } from '@blocksuite/affine-components/date-picker';
import { effects as componentDragIndicatorEffects } from '@blocksuite/affine-components/drag-indicator';
import { effects as componentDropIndicatorEffects } from '@blocksuite/affine-components/drop-indicator';
import { FilterableListComponent } from '@blocksuite/affine-components/filterable-list';
import { IconButton } from '@blocksuite/affine-components/icon-button';
import { effects as componentPortalEffects } from '@blocksuite/affine-components/portal';
@@ -213,7 +213,7 @@ export function effects() {
componentPortalEffects();
componentRichTextEffects();
componentToolbarEffects();
componentDragIndicatorEffects();
componentDropIndicatorEffects();
componentToggleButtonEffects();
componentAiItemEffects();
componentColorPickerEffects();
@@ -1,4 +1,4 @@
import { FileDropExtension } from '@blocksuite/affine-components/drag-indicator';
import { FileDropExtension } from '@blocksuite/affine-components/drop-indicator';
import {
DNDAPIExtension,
DocModeService,
@@ -1,4 +1,4 @@
import { FileDropExtension } from '@blocksuite/affine-components/drag-indicator';
import { FileDropExtension } from '@blocksuite/affine-components/drop-indicator';
import {
DNDAPIExtension,
DocModeService,
+1 -1
View File
@@ -96,7 +96,7 @@ const chunkGroups = {
require.resolve('@blocksuite/affine-components/caption'),
require.resolve('@blocksuite/affine-components/context-menu'),
require.resolve('@blocksuite/affine-components/date-picker'),
require.resolve('@blocksuite/affine-components/drag-indicator'),
require.resolve('@blocksuite/affine-components/drop-indicator'),
],
affine: [
require.resolve('@blocksuite/affine-shared'),
+11 -11
View File
@@ -66,7 +66,7 @@ test('move drag handle in paragraphs', async ({ page }) => {
await initThreeParagraphs(page);
await assertRichTexts(page, ['123', '456', '789']);
await dragHandleFromBlockToBlockBottomById(page, '2', '4');
await expect(page.locator('.affine-drag-indicator')).toBeHidden();
await expect(page.locator('.affine-drop-indicator')).toBeHidden();
await assertRichTexts(page, ['456', '789', '123']);
});
@@ -76,7 +76,7 @@ test('move drag handle in list', async ({ page }) => {
await initThreeLists(page);
await assertRichTexts(page, ['123', '456', '789']);
await dragHandleFromBlockToBlockBottomById(page, '5', '3', false);
await expect(page.locator('.affine-drag-indicator')).toBeHidden();
await expect(page.locator('.affine-drop-indicator')).toBeHidden();
await assertRichTexts(page, ['123', '789', '456']);
});
@@ -106,11 +106,11 @@ test('move drag handle in nested block', async ({ page }) => {
await assertRichTexts(page, ['1', '2', '21', '22', '23', '3']);
await dragHandleFromBlockToBlockBottomById(page, '5', '7');
await expect(page.locator('.affine-drag-indicator')).toBeHidden();
await expect(page.locator('.affine-drop-indicator')).toBeHidden();
await assertRichTexts(page, ['1', '2', '22', '23', '21', '3']);
await dragHandleFromBlockToBlockBottomById(page, '3', '8');
await expect(page.locator('.affine-drag-indicator')).toBeHidden();
await expect(page.locator('.affine-drop-indicator')).toBeHidden();
await assertRichTexts(page, ['2', '22', '23', '21', '3', '1']);
});
@@ -146,7 +146,7 @@ test('move drag handle into another block', async ({ page }) => {
true,
2 * BLOCK_CHILDREN_CONTAINER_PADDING_LEFT
);
await expect(page.locator('.affine-drag-indicator')).toBeHidden();
await expect(page.locator('.affine-drop-indicator')).toBeHidden();
await assertRichTexts(page, ['1', '2', '22', '23', '21', '3']);
// FIXME(DND)
// await assertBlockChildrenIds(page, '7', ['5']);
@@ -158,7 +158,7 @@ test('move drag handle into another block', async ({ page }) => {
// true,
// 2 * BLOCK_CHILDREN_CONTAINER_PADDING_LEFT
// );
// await expect(page.locator('.affine-drag-indicator')).toBeHidden();
// await expect(page.locator('.affine-drop-indicator')).toBeHidden();
// await assertRichTexts(page, ['2', '22', '23', '21', '3', '1']);
// await assertBlockChildrenIds(page, '8', ['3']);
});
@@ -193,7 +193,7 @@ test('move to the last block of each level in multi-level nesting', async ({
);
await dragHandleFromBlockToBlockBottomById(page, '3', '9');
await expect(page.locator('.affine-drag-indicator')).toBeHidden();
await expect(page.locator('.affine-drop-indicator')).toBeHidden();
expect(await getPageSnapshot(page, true)).toMatchSnapshot(
`${testInfo.title}_drag_3_9.json`
@@ -206,7 +206,7 @@ test('move to the last block of each level in multi-level nesting', async ({
true,
-(1 * BLOCK_CHILDREN_CONTAINER_PADDING_LEFT)
);
await expect(page.locator('.affine-drag-indicator')).toBeHidden();
await expect(page.locator('.affine-drop-indicator')).toBeHidden();
expect(await getPageSnapshot(page, true)).toMatchSnapshot(
`${testInfo.title}_drag_4_3.json`
@@ -220,7 +220,7 @@ test('move to the last block of each level in multi-level nesting', async ({
true,
-(2 * BLOCK_CHILDREN_CONTAINER_PADDING_LEFT)
);
await expect(page.locator('.affine-drag-indicator')).toBeHidden();
await expect(page.locator('.affine-drop-indicator')).toBeHidden();
// FIXME(DND)
// expect(await getPageSnapshot(page, true)).toMatchSnapshot(
@@ -277,7 +277,7 @@ test.fixme(
await expect(blockSelections).toHaveCount(2);
await dragHandleFromBlockToBlockBottomById(page, '2', '4', true);
await expect(page.locator('.affine-drag-indicator')).toBeHidden();
await expect(page.locator('.affine-drop-indicator')).toBeHidden();
await assertRichTexts(page, ['789', '123', '456']);
@@ -350,7 +350,7 @@ test('should blur rich-text first on starting block selection', async ({
await expect(page.locator('*:focus')).toHaveCount(1);
await dragHandleFromBlockToBlockBottomById(page, '2', '4');
await expect(page.locator('.affine-drag-indicator')).toBeHidden();
await expect(page.locator('.affine-drop-indicator')).toBeHidden();
await assertRichTexts(page, ['456', '789', '123']);
await expect(page.locator('*:focus')).toHaveCount(0);