diff --git a/blocksuite/blocks/src/_common/utils/drag-and-drop.ts b/blocksuite/affine/shared/src/utils/dnd/calc-drop-target.ts similarity index 84% rename from blocksuite/blocks/src/_common/utils/drag-and-drop.ts rename to blocksuite/affine/shared/src/utils/dnd/calc-drop-target.ts index 6c03d377cb253..2ac5d18ecf770 100644 --- a/blocksuite/blocks/src/_common/utils/drag-and-drop.ts +++ b/blocksuite/affine/shared/src/utils/dnd/calc-drop-target.ts @@ -1,25 +1,14 @@ -import { - getClosestBlockComponentByElement, - getRectByBlockComponent, - matchFlavours, -} from '@blocksuite/affine-shared/utils'; import type { BlockComponent } from '@blocksuite/block-std'; import { type Point, Rect } from '@blocksuite/global/utils'; import type { BlockModel } from '@blocksuite/store'; -import type { EditingState } from '../types.js'; -import { DropFlags, getDropRectByPoint } from './query.js'; - -/** - * A dropping type. - */ -export type DroppingType = 'none' | 'before' | 'after' | 'database'; - -export type DropResult = { - type: DroppingType; - rect: Rect; - modelState: EditingState; -}; +import { + getClosestBlockComponentByElement, + getRectByBlockComponent, +} from '../dom/index.js'; +import { matchFlavours } from '../model/index.js'; +import { getDropRectByPoint } from './get-drop-rect-by-point.js'; +import { DropFlags, type DroppingType, type DropResult } from './types.js'; /** * Calculates the drop target. @@ -32,7 +21,9 @@ export function calcDropTarget( scale: number = 1, flavour: string | null = null // for block-hub ): DropResult | null { - const schema = model.doc.getSchemaByFlavour('affine:database'); + const schema = model.doc.getSchemaByFlavour( + 'affine:database' as BlockSuite.Flavour + ); const children = schema?.model.children ?? []; let shouldAppendToDatabase = true; @@ -47,8 +38,12 @@ export function calcDropTarget( } } - if (!shouldAppendToDatabase && !matchFlavours(model, ['affine:database'])) { - const databaseBlockComponent = element.closest('affine-database'); + if ( + !shouldAppendToDatabase && + !matchFlavours(model, ['affine:database' as BlockSuite.Flavour]) + ) { + const databaseBlockComponent = + element.closest('affine-database'); if (databaseBlockComponent) { element = databaseBlockComponent; model = databaseBlockComponent.model; @@ -57,7 +52,9 @@ export function calcDropTarget( let type: DroppingType = 'none'; const height = 3 * scale; - const { rect: domRect, flag } = getDropRectByPoint(point, model, element); + const dropResult = getDropRectByPoint(point, model, element); + if (!dropResult) return null; + const { rect: domRect, flag } = dropResult; if (flag === DropFlags.EmptyDatabase) { // empty database diff --git a/blocksuite/affine/shared/src/utils/dnd/get-drop-rect-by-point.ts b/blocksuite/affine/shared/src/utils/dnd/get-drop-rect-by-point.ts new file mode 100644 index 0000000000000..bc3cddf89cab7 --- /dev/null +++ b/blocksuite/affine/shared/src/utils/dnd/get-drop-rect-by-point.ts @@ -0,0 +1,161 @@ +import { BLOCK_ID_ATTR } from '@blocksuite/block-std'; +import type { Point } from '@blocksuite/global/utils'; +import type { BlockModel } from '@blocksuite/store'; + +import { getRectByBlockComponent } from '../dom/index.js'; +import { matchFlavours } from '../model/index.js'; +import { DropFlags } from './types.js'; + +const ATTR_SELECTOR = `[${BLOCK_ID_ATTR}]`; + +/** + * Gets the drop rect by block and point. + */ +export function getDropRectByPoint( + point: Point, + model: BlockModel, + element: Element +): null | { + rect: DOMRect; + flag: DropFlags; +} { + const result = { + rect: getRectByBlockComponent(element), + flag: DropFlags.Normal, + }; + + const isDatabase = matchFlavours(model, [ + 'affine:database' as BlockSuite.Flavour, + ]); + + if (isDatabase) { + const table = getDatabaseBlockTableElement(element); + if (!table) { + return result; + } + + let bounds = table.getBoundingClientRect(); + if (model.isEmpty.value) { + result.flag = DropFlags.EmptyDatabase; + + if (point.y < bounds.top) return result; + + const header = getDatabaseBlockColumnHeaderElement(element); + if (!header) { + return null; + } + + bounds = header.getBoundingClientRect(); + result.rect = new DOMRect( + result.rect.left, + bounds.bottom, + result.rect.width, + 1 + ); + + return result; + } + + result.flag = DropFlags.Database; + const rows = getDatabaseBlockRowsElement(element); + if (!rows) { + return null; + } + const rowsBounds = rows.getBoundingClientRect(); + + if (point.y < rowsBounds.top || point.y > rowsBounds.bottom) return result; + + const elements = document.elementsFromPoint(point.x, point.y); + const len = elements.length; + let e; + let i = 0; + for (; i < len; i++) { + e = elements[i]; + + if (e.classList.contains('affine-database-block-row-cell-content')) { + const cellRect = getCellRect(e, bounds); + if (!cellRect) { + return null; + } + result.rect = cellRect; + return result; + } + + if (e.classList.contains('affine-database-block-row')) { + e = e.querySelector(ATTR_SELECTOR); + if (!e) { + return null; + } + const cellRect = getCellRect(e, bounds); + if (!cellRect) { + return null; + } + result.rect = cellRect; + return result; + } + } + + return result; + } + + const parent = element.parentElement; + if (parent?.classList.contains('affine-database-block-row-cell-content')) { + result.flag = DropFlags.Database; + const cellRect = getCellRect(parent); + if (!cellRect) { + return null; + } + result.rect = cellRect; + return result; + } + + return result; +} + +/** + * Gets the table of the database. + */ +function getDatabaseBlockTableElement(element: Element) { + return element.querySelector('.affine-database-block-table'); +} + +/** + * Gets the column header of the database. + */ +function getDatabaseBlockColumnHeaderElement(element: Element) { + return element.querySelector('.affine-database-column-header'); +} + +/** + * Gets the rows of the database. + */ +function getDatabaseBlockRowsElement(element: Element) { + return element.querySelector('.affine-database-block-rows'); +} + +function getCellRect(element: Element, bounds?: DOMRect) { + if (!bounds) { + const table = element.closest('.affine-database-block-table'); + if (!table) { + return null; + } + bounds = table.getBoundingClientRect(); + } + // affine-database-block-row-cell + const col = element.parentElement; + if (!col) { + return null; + } + // affine-database-block-row + const row = col.parentElement; + if (!row) { + return null; + } + const colRect = col.getBoundingClientRect(); + return new DOMRect( + bounds.left, + colRect.top, + colRect.right - bounds.left, + colRect.height + ); +} diff --git a/blocksuite/affine/shared/src/utils/dnd/index.ts b/blocksuite/affine/shared/src/utils/dnd/index.ts index d8f8db3616ece..72862148f6cfd 100644 --- a/blocksuite/affine/shared/src/utils/dnd/index.ts +++ b/blocksuite/affine/shared/src/utils/dnd/index.ts @@ -1 +1,4 @@ +export * from './calc-drop-target.js'; +export * from './get-drop-rect-by-point.js'; export * from './legacy.js'; +export type { DropResult } from './types.js'; diff --git a/blocksuite/affine/shared/src/utils/dnd/types.ts b/blocksuite/affine/shared/src/utils/dnd/types.ts new file mode 100644 index 0000000000000..3aad16d973104 --- /dev/null +++ b/blocksuite/affine/shared/src/utils/dnd/types.ts @@ -0,0 +1,29 @@ +import type { BlockComponent } from '@blocksuite/block-std'; +import type { Rect } from '@blocksuite/global/utils'; +import type { BlockModel } from '@blocksuite/store'; + +export interface EditingState { + element: BlockComponent; + model: BlockModel; + rect: DOMRect; +} + +/** + * Returns a flag for the drop target. + */ +export enum DropFlags { + Normal, + Database, + EmptyDatabase, +} + +/** + * A dropping type. + */ +export type DroppingType = 'none' | 'before' | 'after' | 'database'; + +export type DropResult = { + type: DroppingType; + rect: Rect; + modelState: EditingState; +}; diff --git a/blocksuite/blocks/src/_common/components/file-drop-manager.ts b/blocksuite/blocks/src/_common/components/file-drop-manager.ts index 07d515c8310fd..de31ed5607433 100644 --- a/blocksuite/blocks/src/_common/components/file-drop-manager.ts +++ b/blocksuite/blocks/src/_common/components/file-drop-manager.ts @@ -1,5 +1,7 @@ import type { DragIndicator } from '@blocksuite/affine-components/drag-indicator'; import { + calcDropTarget, + type DropResult, getClosestBlockComponentByPoint, isInsidePageEditor, matchFlavours, @@ -9,8 +11,6 @@ import type { IVec } from '@blocksuite/global/utils'; import { assertExists, Point } from '@blocksuite/global/utils'; import type { BlockModel } from '@blocksuite/store'; -import { calcDropTarget, type DropResult } from '../../_common/utils/index.js'; - export type onDropProps = { files: File[]; targetModel: BlockModel | null; diff --git a/blocksuite/blocks/src/_common/utils/index.ts b/blocksuite/blocks/src/_common/utils/index.ts index ecb289f2ff29a..5ebcc2aff05fb 100644 --- a/blocksuite/blocks/src/_common/utils/index.ts +++ b/blocksuite/blocks/src/_common/utils/index.ts @@ -1,6 +1,5 @@ // Compat with SSR export * from '../types.js'; -export * from './drag-and-drop.js'; export * from './query.js'; export { createButtonPopper, diff --git a/blocksuite/blocks/src/_common/utils/query.ts b/blocksuite/blocks/src/_common/utils/query.ts index f70139ee5ec8e..0c0ef8389ec59 100644 --- a/blocksuite/blocks/src/_common/utils/query.ts +++ b/blocksuite/blocks/src/_common/utils/query.ts @@ -1,16 +1,8 @@ -import { - getRectByBlockComponent, - matchFlavours, -} from '@blocksuite/affine-shared/utils'; -import { BLOCK_ID_ATTR, type EditorHost } from '@blocksuite/block-std'; -import type { Point } from '@blocksuite/global/utils'; -import { assertExists } from '@blocksuite/global/utils'; +import type { EditorHost } from '@blocksuite/block-std'; import type { BlockModel } from '@blocksuite/store'; import type { RootBlockComponent } from '../../index.js'; -const ATTR_SELECTOR = `[${BLOCK_ID_ATTR}]`; - /** * This function is used to build model's "normal" block path. * If this function does not meet your needs, you may need to build path manually to satisfy your needs. @@ -59,150 +51,6 @@ export function getBlockComponentByModel( return editorHost.view.getBlock(model.id); } -function isEdgelessChildNote({ classList }: Element) { - return classList.contains('note-background'); -} - -/** - * Get hovering note with given a point in edgeless mode. - */ -export function getHoveringNote(point: Point) { - return ( - document.elementsFromPoint(point.x, point.y).find(isEdgelessChildNote) || - null - ); -} - -/** - * Gets the table of the database. - */ -function getDatabaseBlockTableElement(element: Element) { - return element.querySelector('.affine-database-block-table'); -} - -/** - * Gets the column header of the database. - */ -function getDatabaseBlockColumnHeaderElement(element: Element) { - return element.querySelector('.affine-database-column-header'); -} - -/** - * Gets the rows of the database. - */ -function getDatabaseBlockRowsElement(element: Element) { - return element.querySelector('.affine-database-block-rows'); -} - -/** - * Returns a flag for the drop target. - */ -export enum DropFlags { - Normal, - Database, - EmptyDatabase, -} - -/** - * Gets the drop rect by block and point. - */ -export function getDropRectByPoint( - point: Point, - model: BlockModel, - element: Element -): { - rect: DOMRect; - flag: DropFlags; -} { - const result = { - rect: getRectByBlockComponent(element), - flag: DropFlags.Normal, - }; - - const isDatabase = matchFlavours(model, ['affine:database']); - - if (isDatabase) { - const table = getDatabaseBlockTableElement(element); - if (!table) { - return result; - } - let bounds = table.getBoundingClientRect(); - if (model.isEmpty.value) { - result.flag = DropFlags.EmptyDatabase; - - if (point.y < bounds.top) return result; - - const header = getDatabaseBlockColumnHeaderElement(element); - assertExists(header); - bounds = header.getBoundingClientRect(); - result.rect = new DOMRect( - result.rect.left, - bounds.bottom, - result.rect.width, - 1 - ); - } else { - result.flag = DropFlags.Database; - const rows = getDatabaseBlockRowsElement(element); - assertExists(rows); - const rowsBounds = rows.getBoundingClientRect(); - - if (point.y < rowsBounds.top || point.y > rowsBounds.bottom) - return result; - - const elements = document.elementsFromPoint(point.x, point.y); - const len = elements.length; - let e; - let i = 0; - for (; i < len; i++) { - e = elements[i]; - - if (e.classList.contains('affine-database-block-row-cell-content')) { - result.rect = getCellRect(e, bounds); - return result; - } - - if (e.classList.contains('affine-database-block-row')) { - e = e.querySelector(ATTR_SELECTOR); - assertExists(e); - result.rect = getCellRect(e, bounds); - return result; - } - } - } - } else { - const parent = element.parentElement; - if (parent?.classList.contains('affine-database-block-row-cell-content')) { - result.flag = DropFlags.Database; - result.rect = getCellRect(parent); - return result; - } - } - - return result; -} - -function getCellRect(element: Element, bounds?: DOMRect) { - if (!bounds) { - const table = element.closest('.affine-database-block-table'); - assertExists(table); - bounds = table.getBoundingClientRect(); - } - // affine-database-block-row-cell - const col = element.parentElement; - assertExists(col); - // affine-database-block-row - const row = col.parentElement; - assertExists(row); - const colRect = col.getBoundingClientRect(); - return new DOMRect( - bounds.left, - colRect.top, - colRect.right - bounds.left, - colRect.height - ); -} - /** * Return `true` if the element has class name in the class list. */ diff --git a/blocksuite/blocks/src/root-block/widgets/drag-handle/drag-handle.ts b/blocksuite/blocks/src/root-block/widgets/drag-handle/drag-handle.ts index 84f117d3d8136..d90e7035d3360 100644 --- a/blocksuite/blocks/src/root-block/widgets/drag-handle/drag-handle.ts +++ b/blocksuite/blocks/src/root-block/widgets/drag-handle/drag-handle.ts @@ -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 { 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 { ); }; - 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 { 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() { diff --git a/blocksuite/blocks/src/root-block/widgets/drag-handle/helpers/rect-helper.ts b/blocksuite/blocks/src/root-block/widgets/drag-handle/helpers/rect-helper.ts index 6562ecf97262e..400dfa35b6bbc 100644 --- a/blocksuite/blocks/src/root-block/widgets/drag-handle/helpers/rect-helper.ts +++ b/blocksuite/blocks/src/root-block/widgets/drag-handle/helpers/rect-helper.ts @@ -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'; diff --git a/blocksuite/blocks/src/root-block/widgets/drag-handle/helpers/selection-helper.ts b/blocksuite/blocks/src/root-block/widgets/drag-handle/helpers/selection-helper.ts index d451772bd4023..b206e3f397df5 100644 --- a/blocksuite/blocks/src/root-block/widgets/drag-handle/helpers/selection-helper.ts +++ b/blocksuite/blocks/src/root-block/widgets/drag-handle/helpers/selection-helper.ts @@ -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'); diff --git a/blocksuite/blocks/src/root-block/widgets/drag-handle/utils.ts b/blocksuite/blocks/src/root-block/widgets/drag-handle/utils.ts index c6d2baaeb523c..4d76056cd9c37 100644 --- a/blocksuite/blocks/src/root-block/widgets/drag-handle/utils.ts +++ b/blocksuite/blocks/src/root-block/widgets/drag-handle/utils.ts @@ -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'); +} diff --git a/blocksuite/blocks/src/root-block/widgets/drag-handle/watchers/drag-event-watcher.ts b/blocksuite/blocks/src/root-block/widgets/drag-handle/watchers/drag-event-watcher.ts index 8b6b6d47dc7b3..60b4a5622aa67 100644 --- a/blocksuite/blocks/src/root-block/widgets/drag-handle/watchers/drag-event-watcher.ts +++ b/blocksuite/blocks/src/root-block/widgets/drag-handle/watchers/drag-event-watcher.ts @@ -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; } diff --git a/blocksuite/blocks/src/root-block/widgets/drag-handle/watchers/edgeless-watcher.ts b/blocksuite/blocks/src/root-block/widgets/drag-handle/watchers/edgeless-watcher.ts index efdb1d37a0236..a4fd747e1365d 100644 --- a/blocksuite/blocks/src/root-block/widgets/drag-handle/watchers/edgeless-watcher.ts +++ b/blocksuite/blocks/src/root-block/widgets/drag-handle/watchers/edgeless-watcher.ts @@ -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); diff --git a/blocksuite/blocks/src/root-block/widgets/drag-handle/watchers/legacy-drag-event-watcher.ts b/blocksuite/blocks/src/root-block/widgets/drag-handle/watchers/legacy-drag-event-watcher.ts deleted file mode 100644 index 97aa5f3673eb3..0000000000000 --- a/blocksuite/blocks/src/root-block/widgets/drag-handle/watchers/legacy-drag-event-watcher.ts +++ /dev/null @@ -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 }); - } -}