From c3abd0d5664223592729e37d2c8489fe568fbf8e Mon Sep 17 00:00:00 2001 From: AyushAgrawal-A2 Date: Fri, 8 Dec 2023 18:19:01 +0530 Subject: [PATCH] feat(edgeless): support consistent dragging behavior --- packages/blocks/src/_common/utils/query.ts | 14 + .../src/_common/widgets/drag-handle/config.ts | 10 +- .../src/_common/widgets/drag-handle/index.ts | 465 +++++++++++++----- .../widgets/surface-ref-toolbar/index.ts | 6 +- .../src/attachment-block/attachment-block.ts | 2 +- .../src/bookmark-block/bookmark-block.ts | 2 +- .../src/database-block/database-block.ts | 2 +- .../blocks/src/image-block/image-block.ts | 123 +++-- packages/blocks/src/list-block/list-block.ts | 2 +- packages/blocks/src/note-block/note-block.ts | 39 +- .../block-portal/image/edgeless-image.ts | 4 +- .../rects/edgeless-selected-rect.ts | 2 +- .../blocks/src/page-block/page-service.ts | 2 +- packages/virgo/src/utils/guard.ts | 3 +- tests/edgeless/note.spec.ts | 2 +- 15 files changed, 510 insertions(+), 168 deletions(-) diff --git a/packages/blocks/src/_common/utils/query.ts b/packages/blocks/src/_common/utils/query.ts index 0a39d7e2b9381..7e405e9c8da67 100644 --- a/packages/blocks/src/_common/utils/query.ts +++ b/packages/blocks/src/_common/utils/query.ts @@ -425,6 +425,10 @@ function isEdgelessChildNote({ classList }: Element) { return classList.contains('edgeless-block-portal-note'); } +function isEdgelessChildImage({ classList }: Element) { + return classList.contains('edgeless-block-portal-image'); +} + /** * Returns the closest block element by a point in the rect. * @@ -717,6 +721,16 @@ export function getHoveringNote(point: Point) { ); } +/** + * Get hovering note with given a point in edgeless mode. + */ +export function getHoveringImage(point: Point) { + return ( + document.elementsFromPoint(point.x, point.y).find(isEdgelessChildImage) || + null + ); +} + /** * Gets the table of the database. */ diff --git a/packages/blocks/src/_common/widgets/drag-handle/config.ts b/packages/blocks/src/_common/widgets/drag-handle/config.ts index e5b8ae7f8053b..9ef44b98dcaeb 100644 --- a/packages/blocks/src/_common/widgets/drag-handle/config.ts +++ b/packages/blocks/src/_common/widgets/drag-handle/config.ts @@ -39,7 +39,15 @@ export type DragHandleOption = { ) => boolean; onDragEnd?: ( state: PointerEventState, - draggingElements: BlockElement[] + { + draggingElements, + dropBlockId, + dropType, + }: { + draggingElements: BlockElement[]; + dropBlockId: string; + dropType: DropType | null; + } ) => boolean; }; diff --git a/packages/blocks/src/_common/widgets/drag-handle/index.ts b/packages/blocks/src/_common/widgets/drag-handle/index.ts index 61af47874ec19..21c16a2332ec6 100644 --- a/packages/blocks/src/_common/widgets/drag-handle/index.ts +++ b/packages/blocks/src/_common/widgets/drag-handle/index.ts @@ -2,22 +2,25 @@ import { PathFinder, type PointerEventState, type UIEventHandler, - type UIEventStateContext, } from '@blocksuite/block-std'; -import { assertExists, DisposableGroup } from '@blocksuite/global/utils'; +import { + assertExists, + assertInstanceOf, + DisposableGroup, +} from '@blocksuite/global/utils'; import type { BlockElement } from '@blocksuite/lit'; import { WidgetElement } from '@blocksuite/lit'; -import type { BaseBlockModel } from '@blocksuite/store'; +import { type BaseBlockModel } from '@blocksuite/store'; import { html, render } from 'lit'; import { customElement, query, state } from 'lit/decorators.js'; import { styleMap } from 'lit/directives/style-map.js'; import { - getBlockElementByModel, + buildPath, + type EdgelessElement, getBlockElementsExcludeSubtrees, getCurrentNativeRange, getModelByBlockElement, - isEdgelessPage, isPageMode, matchFlavours, Point, @@ -25,7 +28,13 @@ import { } from '../../../_common/utils/index.js'; import { DocPageBlockComponent } from '../../../page-block/doc/doc-page-block.js'; import { EdgelessPageBlockComponent } from '../../../page-block/edgeless/edgeless-page-block.js'; +import { + getSelectedRect, + isImageBlock, + isNoteBlock, +} from '../../../page-block/edgeless/utils/query.js'; import { autoScroll } from '../../../page-block/text-selection/utils.js'; +import { BLOCK_ID_ATTR } from '../../consts.js'; import { DragPreview } from './components/drag-preview.js'; import { DropIndicator } from './components/drop-indicator.js'; import { @@ -104,6 +113,9 @@ export class AffineDragHandleWidget extends WidgetElement< private _hoveredBlockPath: string[] | null = null; private _lastHoveredBlockPath: string[] | null = null; private _lastShowedBlock: { path: string[]; el: BlockElement } | null = null; + private _selectedEdgelessElement: EdgelessElement | null = null; + private _selectedEdgelessBlock: BaseBlockModel | null = null; + private _edgelessContainerDragging: boolean = false; private _hoverDragHandle = false; private _dragHandlePointerDown = false; @@ -359,13 +371,12 @@ export class AffineDragHandleWidget extends WidgetElement< if (!this._dragHandleContainer) return; this._hoverDragHandle = false; - if (this._dragHandleContainer.style.display !== 'none') - this._dragHandleContainer.style.display = 'none'; + this._dragHandleContainer.style.display = 'none'; if (force) this._reset(); }; - private _handleAnchorModelDisposables(blockElement: BlockElement) { + private _handleAnchorModelDisposables(blockModel: BaseBlockModel) { if (this._anchorModelDisposables) { this._anchorModelDisposables.dispose(); this._anchorModelDisposables = null; @@ -373,12 +384,10 @@ export class AffineDragHandleWidget extends WidgetElement< this._anchorModelDisposables = new DisposableGroup(); this._anchorModelDisposables.add( - blockElement.model.propsUpdated.on(() => this.hide()) + blockModel.propsUpdated.on(() => this.hide()) ); - this._anchorModelDisposables.add( - blockElement.model.deleted.on(() => this.hide()) - ); + this._anchorModelDisposables.add(blockModel.deleted.on(() => this.hide())); } private _getBlockElementFromViewStore(path: string[]) { @@ -426,6 +435,9 @@ export class AffineDragHandleWidget extends WidgetElement< this._hoveredBlockPath = null; this._lastHoveredBlockPath = null; this._lastShowedBlock = null; + this._selectedEdgelessElement = null; + this._selectedEdgelessBlock = null; + this._edgelessContainerDragging = false; this._hoverDragHandle = false; this._dragHandlePointerDown = false; @@ -507,7 +519,7 @@ export class AffineDragHandleWidget extends WidgetElement< } this._resetDragHandleGrabber(); - this._handleAnchorModelDisposables(blockElement); + this._handleAnchorModelDisposables(blockElement.model); if (!isBlockPathEqual(blockElement.path, this._lastShowedBlock?.path)) { this._lastShowedBlock = { path: blockElement.path, @@ -531,6 +543,47 @@ export class AffineDragHandleWidget extends WidgetElement< this._show(blockElement); } + private _showDragHandleTopLevelBlocks() { + const container = this._dragHandleContainer; + const grabber = this._dragHandleGrabber; + if ( + !container || + !grabber || + !this._selectedEdgelessBlock || + !this._selectedEdgelessElement || + isPageMode(this.page) || + this.dragging || + this._edgelessContainerDragging + ) { + return; + } + + const edgelessPage = this.pageBlockElement as EdgelessPageBlockComponent; + + const rect = getSelectedRect([this._selectedEdgelessElement]); + const [left, top] = edgelessPage.surface.toViewCoord(rect.left, rect.top); + const height = rect.height * this.scale; + + const posLeft = + left - + (HOVER_DRAG_HANDLE_GRABBER_WIDTH + 8) * this.scale + + this._viewportOffset.left; + + const posTop = top + this._viewportOffset.top; + + container.style.width = `${HOVER_DRAG_HANDLE_GRABBER_WIDTH * this.scale}px`; + container.style.borderRadius = `${ + DRAG_HANDLE_GRABBER_BORDER_RADIUS * this.scale + }px`; + container.style.left = `${posLeft}px`; + container.style.top = `${posTop}px`; + container.style.display = 'flex'; + container.style.height = `${height}px`; + + this._resetDragHandleGrabber(); + this._handleAnchorModelDisposables(this._selectedEdgelessBlock); + } + private _getHoveredBlocks(): BlockElement[] { if (!this._hoveredBlockPath) return []; const hoverBlock = this._getBlockElementFromViewStore( @@ -601,6 +654,40 @@ export class AffineDragHandleWidget extends WidgetElement< return new Rect(left, top, right, bottom); } + private _getTopLevelBlockDraggingAreaRect() { + if ( + !this._selectedEdgelessBlock || + !this._selectedEdgelessElement || + isPageMode(this.page) + ) { + return; + } + const edgelessPage = this.pageBlockElement as EdgelessPageBlockComponent; + const rect = getSelectedRect([this._selectedEdgelessElement]); + let [left, top] = edgelessPage.surface.toViewCoord(rect.left, rect.top); + const width = rect.width * this.scale; + const height = rect.height * this.scale; + + let [right, bottom] = [left + width, top + height]; + + const offsetLeft = 8 * this.scale; + const paddingLeft = 3 * this.scale; + const padding = 6 * this.scale; + + left -= + paddingLeft + HOVER_DRAG_HANDLE_GRABBER_WIDTH * this.scale + offsetLeft; + top -= padding; + right += padding; + bottom += padding; + + left += this._viewportOffset.left; + top += this._viewportOffset.top; + right += this._viewportOffset.left; + bottom += this._viewportOffset.top; + + return new Rect(left, top, right, bottom); + } + private _setSelectedBlocks(blockElements: BlockElement[], noteId?: string) { const { selection } = this.root; const selections = blockElements.map(blockElement => @@ -611,7 +698,7 @@ export class AffineDragHandleWidget extends WidgetElement< // When current page is edgeless page // We need to remain surface selection and set editing as true - if (isEdgelessPage(this.pageBlockElement)) { + if (!isPageMode(this.page)) { const surfaceElementId = noteId ? noteId : getNoteId(blockElements[0]); const surfaceSelection = selection.getInstance( 'surface', @@ -632,6 +719,7 @@ export class AffineDragHandleWidget extends WidgetElement< private _removeHoverRect() { this._dragHoverRect = null; + this._hoverDragHandle = false; this._dragHandlePointerDown = false; } @@ -649,24 +737,56 @@ export class AffineDragHandleWidget extends WidgetElement< private _scrollToUpdateIndicator = () => { if ( - !this.dragging || - this.draggingElements.length === 0 || - !this.lastDragPointerState - ) + this.dragging && + this.draggingElements.length !== 0 && + this.lastDragPointerState + ) { + const state = this.lastDragPointerState; + this.rafID = requestAnimationFrame(() => + this.updateIndicator(state, false) + ); + } else if (this._selectedEdgelessBlock) { + this._handleEdgelessTopLevelBlockSelection(); + } + }; + + private _handleEdgelessTopLevelBlockSelection = () => { + this.hide(); + + if (isPageMode(this.page) || this._edgelessContainerDragging) { return; + } + const edgelessPage = this.pageBlockElement as EdgelessPageBlockComponent; - const state = this.lastDragPointerState; - this.rafID = requestAnimationFrame(() => - this.updateIndicator(state, false) - ); + if (edgelessPage.selectionManager.editing) { + return; + } + + const { elements } = edgelessPage.selectionManager.state; + if (elements.length !== 1) { + this._selectedEdgelessElement = null; + this._selectedEdgelessBlock = null; + return; + } + + const blockModel = this.page.getBlockById(elements[0]); + assertExists(blockModel); + + if (isNoteBlock(blockModel) || isImageBlock(blockModel)) { + this._selectedEdgelessElement = edgelessPage.selectionManager.elements[0]; + this._selectedEdgelessBlock = blockModel; + this._showDragHandleTopLevelBlocks(); + } else { + this._selectedEdgelessElement = null; + this._selectedEdgelessBlock = null; + } }; /** * When pointer move on block, should show drag handle * And update hover block id and path */ - private _pointerMoveOnBlock = (ctx: UIEventStateContext) => { - const state = ctx.get('pointerState'); + private _pointerMoveOnBlock = (state: PointerEventState) => { const point = getContainerOffsetPoint(state); const closestBlockElement = getClosestBlockByPoint( this.page, @@ -710,11 +830,12 @@ export class AffineDragHandleWidget extends WidgetElement< this.hide(); return; } + const state = ctx.get('pointerState'); const { target } = state.raw; const element = captureEventTarget(target); - // WHen pointer not on block or on dragging, should do nothing + // When pointer not on block or on dragging, should do nothing if (!element || this.dragging) { return; } @@ -731,18 +852,18 @@ export class AffineDragHandleWidget extends WidgetElement< this.page, this.pageBlockElement, point - ); + ) as BlockElement | null; if ( - !closestNoteBlock || - !this._canEditing(closestNoteBlock as BlockElement) || - this.outOfNoteBlock(closestNoteBlock, point) + closestNoteBlock && + this._canEditing(closestNoteBlock) && + !this.outOfNoteBlock(closestNoteBlock, point) ) { + this._pointerMoveOnBlock(state); + return true; + } else if (!this._selectedEdgelessBlock) { this.hide(); - return; } - - this._pointerMoveOnBlock(ctx); - return true; + return false; }; /** @@ -794,92 +915,149 @@ export class AffineDragHandleWidget extends WidgetElement< const event = state.raw; const { target } = event; const element = captureEventTarget(target); - const inside = !!element?.closest('affine-drag-handle-widget'); + const insideDragHandle = !!element?.closest('affine-drag-handle-widget'); // Should only start dragging when pointer down on drag handle // And current mouse button is left button - if (!inside || !this._hoveredBlockId || !this._hoveredBlockPath) { - return false; - } + if (!insideDragHandle) { + if (!isPageMode(this.page)) { + // edgeless top level container dragging + this.hide(); + this._edgelessContainerDragging = true; + } - // Get current hover block element by path - const hoverBlockElement = this._getBlockElementFromViewStore( - this._hoveredBlockPath - ); - if (!hoverBlockElement) { return false; } - let selections = this.selectedBlocks; + if (this._hoveredBlockId && this._hoveredBlockPath) { + // Get current hover block element by path + const hoverBlockElement = this._getBlockElementFromViewStore( + this._hoveredBlockPath + ); + if (!hoverBlockElement) { + return false; + } - // When current selection is TextSelection - // Should set BlockSelection for the blocks in native range - if (selections.length > 0 && includeTextSelection(selections)) { - const nativeSelection = document.getSelection(); - if (nativeSelection && nativeSelection.rangeCount > 0) { - const range = nativeSelection.getRangeAt(0); - const blockElements = - this._rangeManager.getSelectedBlockElementsByRange(range, { - match: el => el.model.role === 'content', - mode: 'highest', - }); - this._setSelectedBlocks(blockElements); - selections = this.selectedBlocks; + let selections = this.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(); + if (nativeSelection && nativeSelection.rangeCount > 0) { + const range = nativeSelection.getRangeAt(0); + const blockElements = + this._rangeManager.getSelectedBlockElementsByRange(range, { + match: el => el.model.role === 'content', + mode: 'highest', + }); + this._setSelectedBlocks(blockElements); + selections = this.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._hoveredBlockId - ) - ) { - const blockElement = this._getBlockElementFromViewStore( - this._hoveredBlockPath - ); - assertExists(blockElement); + // 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._hoveredBlockId + ) + ) { + const blockElement = this._getBlockElementFromViewStore( + this._hoveredBlockPath + ); + assertExists(blockElement); - this._setSelectedBlocks([blockElement]); - } + this._setSelectedBlocks([blockElement]); + } - const blockElements = this.selectedBlocks - .map(selection => { - return this._getBlockElementFromViewStore(selection.path); - }) - .filter((element): element is BlockElement => !!element); + const blockElements = this.selectedBlocks + .map(selection => { + return this._getBlockElementFromViewStore(selection.path); + }) + .filter( + (element): element is BlockElement => !!element + ); - // 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 blockElementsExcludingChildren = getBlockElementsExcludeSubtrees( - blockElements - ) as BlockElement[]; + // 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 blockElementsExcludingChildren = getBlockElementsExcludeSubtrees( + blockElements + ) as BlockElement[]; - if (blockElementsExcludingChildren.length === 0) return false; + if (blockElementsExcludingChildren.length === 0) return false; - this.startDragging(blockElementsExcludingChildren, state); + this.startDragging(blockElementsExcludingChildren, state); - return true; + return true; + } else if (this._selectedEdgelessElement && this._selectedEdgelessBlock) { + // handle drag start of top level block + + // FIXME: get block element using path + // surface selection bug + const blockElement = this.pageBlockElement.querySelector( + `[${BLOCK_ID_ATTR}="${this._selectedEdgelessBlock.id}"]` + ) as BlockElement | null; + assertExists(blockElement); + + this.startDragging([blockElement], state); + + return true; + } + this.hide(); + return false; }; - private _onDragMove = (ctx: UIEventStateContext) => { + private _onDragMove = (state: PointerEventState) => { this.clearRaf(); - const state = ctx.get('pointerState'); this.rafID = requestAnimationFrame(() => this.updateIndicator(state, true)); return true; }; - private _onDragEnd = () => { + private _onDragEnd = (state: PointerEventState) => { const targetBlockId = this.dropBlockId; const dropType = this.dropType; const draggingElements = this.draggingElements; this.hide(true); - if (!targetBlockId) return false; + + if (!targetBlockId) { + // handle drag end on edgeless container + const target = captureEventTarget(state.raw.target); + + const isTargetEdgelessContainer = + target?.classList.contains('edgeless') && + target?.classList.contains('affine-block-children-container'); + + if (!isTargetEdgelessContainer) { + return false; + } + + const selectedBlocks = getBlockElementsExcludeSubtrees(draggingElements) + .map(element => getModelByBlockElement(element)) + .filter((x): x is BaseBlockModel => !!x); + + if (selectedBlocks.length === 0) { + return false; + } + + const edgelessPage = this.pageBlockElement; + assertInstanceOf(edgelessPage, EdgelessPageBlockComponent); + + const newNoteId = edgelessPage.addNoteWithPoint( + new Point(state.x, state.y) + ); + const newNoteBlock = this.page.getBlockById(newNoteId); + assertExists(newNoteBlock); + + this.page.moveBlocks(selectedBlocks, newNoteBlock); + + return true; + } // Should make sure drop block id is not in selected blocks if ( @@ -922,7 +1100,9 @@ export class AffineDragHandleWidget extends WidgetElement< assertExists(parent); // Need to update selection when moving blocks successfully // Because the block path may be changed after moving - const parentElement = getBlockElementByModel(parent); + const parentElement = this._getBlockElementFromViewStore( + buildPath(parent) + ); if (parentElement) { const newSelectedBlocks = selectedBlocks .map(block => parentElement.path.concat(block.id)) @@ -980,7 +1160,7 @@ export class AffineDragHandleWidget extends WidgetElement< } // call default drag move handler if no option return true - return this._onDragMove(ctx); + return this._onDragMove(state); }; /** @@ -997,17 +1177,24 @@ export class AffineDragHandleWidget extends WidgetElement< this.hide(true); return false; } + const state = ctx.get('pointerState'); for (const option of this.optionRunner.options) { - if (option.onDragEnd?.(state, this.draggingElements)) { + if ( + option.onDragEnd?.(state, { + draggingElements: this.draggingElements, + dropBlockId: this.dropBlockId, + dropType: this.dropType, + }) + ) { this.hide(true); return true; } } //call default drag end handler if no option return true - return this._onDragEnd(); + return this._onDragEnd(state); }; private _pointerOutHandler: UIEventHandler = ctx => { @@ -1033,29 +1220,37 @@ export class AffineDragHandleWidget extends WidgetElement< }; private _onDragHandleHover = () => { - if (!this._hoveredBlockPath || !this._dragHandleGrabber) return; - - const blockElement = this._getBlockElementFromViewStore( - this._hoveredBlockPath - ); - if (!blockElement) return; - - const draggingAreaRect = this._getDraggingAreaRect(blockElement); - if (!draggingAreaRect) return; + if (!this._dragHandleGrabber) return; - const padding = 8 * this.scale; - this._dragHandleContainer.style.paddingTop = `${padding}px`; - this._dragHandleContainer.style.paddingBottom = `${padding}px`; - this._dragHandleContainer.style.transition = `padding 0.25s ease`; - - this._dragHandleGrabber.style.width = `${ - HOVER_DRAG_HANDLE_GRABBER_WIDTH * this.scale - }px`; - this._dragHandleGrabber.style.borderRadius = `${ - DRAG_HANDLE_GRABBER_BORDER_RADIUS * this.scale - }px`; - - this._hoverDragHandle = true; + if (this._hoveredBlockPath) { + const blockElement = this._getBlockElementFromViewStore( + this._hoveredBlockPath + ); + if (!blockElement) return; + + const draggingAreaRect = this._getDraggingAreaRect(blockElement); + if (!draggingAreaRect) return; + + const padding = 8 * this.scale; + this._dragHandleContainer.style.paddingTop = `${padding}px`; + this._dragHandleContainer.style.paddingBottom = `${padding}px`; + this._dragHandleContainer.style.transition = `padding 0.25s ease`; + + this._dragHandleGrabber.style.width = `${ + HOVER_DRAG_HANDLE_GRABBER_WIDTH * this.scale + }px`; + this._dragHandleGrabber.style.borderRadius = `${ + DRAG_HANDLE_GRABBER_BORDER_RADIUS * this.scale + }px`; + + this._hoverDragHandle = true; + } else if (this._selectedEdgelessBlock) { + const draggingAreaRect = this._getTopLevelBlockDraggingAreaRect(); + if (!draggingAreaRect) return; + + this._dragHoverRect = draggingAreaRect; + this._hoverDragHandle = true; + } }; private _onDragHandlePointerDown = () => { @@ -1077,7 +1272,7 @@ export class AffineDragHandleWidget extends WidgetElement< }; private _onDragHandlePointerLeave = () => { - if (this._dragHandlePointerDown) this._removeHoverRect(); + this._removeHoverRect(); if (!this._hoveredBlockPath) return; @@ -1088,7 +1283,6 @@ export class AffineDragHandleWidget extends WidgetElement< if (this.dragging) return; this._show(blockElement); - this._hoverDragHandle = false; }; override firstUpdated() { @@ -1123,29 +1317,56 @@ export class AffineDragHandleWidget extends WidgetElement< this._onDragHandlePointerLeave ); - if (isEdgelessPage(this.pageBlockElement)) { - const edgelessPage = this.pageBlockElement; + if (!isPageMode(this.page)) { + const edgelessPage = this.pageBlockElement as EdgelessPageBlockComponent; + this._disposables.add( edgelessPage.slots.edgelessToolUpdated.on(newTool => { if (newTool.type !== 'default') this.hide(); }) ); + this._disposables.add( edgelessPage.slots.viewportUpdated.on(() => { this.hide(); this.scale = edgelessPage.surface.viewport.zoom; - this._scrollToUpdateIndicator(); }) ); + + this._disposables.add( + edgelessPage.selectionManager.slots.updated.on(() => { + this._handleEdgelessTopLevelBlockSelection(); + }) + ); + + this._disposables.add( + edgelessPage.slots.draggingAreaUpdated.on(() => { + this._handleEdgelessTopLevelBlockSelection(); + }) + ); + + this._disposables.add( + edgelessPage.slots.elementResizeStart.on(() => { + this.hide(); + }) + ); + + this._disposables.add( + edgelessPage.slots.elementResizeEnd.on(() => { + this._handleEdgelessTopLevelBlockSelection(); + }) + ); } else { - const docPage = this.pageBlockElement; + const docPage = this.pageBlockElement as DocPageBlockComponent; + this._disposables.add( docPage.slots.viewportUpdated.on(() => this.hide()) ); const viewportElement = docPage.viewportElement; assertExists(viewportElement); + this._disposables.addFromEvent(viewportElement, 'scroll', () => { this._scrollToUpdateIndicator(); }); diff --git a/packages/blocks/src/_common/widgets/surface-ref-toolbar/index.ts b/packages/blocks/src/_common/widgets/surface-ref-toolbar/index.ts index 3dc8b524923f1..0ff697b5b7003 100644 --- a/packages/blocks/src/_common/widgets/surface-ref-toolbar/index.ts +++ b/packages/blocks/src/_common/widgets/surface-ref-toolbar/index.ts @@ -8,10 +8,8 @@ import { HoverController } from '../../../_common/components/hover/controller.js import { PAGE_HEADER_HEIGHT } from '../../../_common/consts.js'; import { downloadBlob } from '../../../_common/utils/filesys.js'; import type { EdgelessElement } from '../../../_common/utils/types.js'; -import { - type SurfaceRefBlockComponent, - type SurfaceRefBlockModel, -} from '../../../surface-ref-block/index.js'; +import type { SurfaceRefBlockComponent } from '../../../surface-ref-block/surface-ref-block.js'; +import type { SurfaceRefBlockModel } from '../../../surface-ref-block/surface-ref-model.js'; import { toast } from '../../components/toast.js'; import { EdgelessModeIcon } from '../../icons/edgeless.js'; import { diff --git a/packages/blocks/src/attachment-block/attachment-block.ts b/packages/blocks/src/attachment-block/attachment-block.ts index 4a985b76d5169..20e4c1fcdb6cd 100644 --- a/packages/blocks/src/attachment-block/attachment-block.ts +++ b/packages/blocks/src/attachment-block/attachment-block.ts @@ -136,7 +136,7 @@ export class AttachmentBlockComponent extends BlockElement AffineDragHandleWidget.registerOption({ flavour: AttachmentBlockSchema.model.flavour, onDragStart: (state, startDragging) => { - // Check if start dragging from the image block + // Check if start dragging from the attachment block const target = captureEventTarget(state.raw.target); const attachmentBlock = target?.closest('affine-attachment'); if (!attachmentBlock) return false; diff --git a/packages/blocks/src/bookmark-block/bookmark-block.ts b/packages/blocks/src/bookmark-block/bookmark-block.ts index 4364791a2109c..2ce773ec8e27e 100644 --- a/packages/blocks/src/bookmark-block/bookmark-block.ts +++ b/packages/blocks/src/bookmark-block/bookmark-block.ts @@ -76,7 +76,7 @@ export class BookmarkBlockComponent extends BlockElement { AffineDragHandleWidget.registerOption({ flavour: BookmarkBlockSchema.model.flavour, onDragStart: (state, startDragging) => { - // Check if start dragging from the image block + // Check if start dragging from the bookmark block const target = captureEventTarget(state.raw.target); const bookmarkBlock = target?.closest('affine-bookmark'); if (!bookmarkBlock) return false; diff --git a/packages/blocks/src/database-block/database-block.ts b/packages/blocks/src/database-block/database-block.ts index 9f6e07fbf52b0..df030002eb4c0 100644 --- a/packages/blocks/src/database-block/database-block.ts +++ b/packages/blocks/src/database-block/database-block.ts @@ -216,7 +216,7 @@ export class DatabaseBlockComponent extends BlockElement { } return false; }, - onDragEnd: (state, draggingElements) => { + onDragEnd: (state, { draggingElements }) => { const target = state.raw.target; const view = this.view; if ( diff --git a/packages/blocks/src/image-block/image-block.ts b/packages/blocks/src/image-block/image-block.ts index 89ece7ff033c9..603f120abafc1 100644 --- a/packages/blocks/src/image-block/image-block.ts +++ b/packages/blocks/src/image-block/image-block.ts @@ -2,6 +2,7 @@ import './image/placeholder/image-not-found.js'; import './image/placeholder/loading-card.js'; import { PathFinder } from '@blocksuite/block-std'; +import { assertExists } from '@blocksuite/global/utils'; import { BlockElement } from '@blocksuite/lit'; import { Text } from '@blocksuite/store'; import { css, html, type PropertyValues } from 'lit'; @@ -9,10 +10,11 @@ import { customElement, query, state } from 'lit/decorators.js'; import { styleMap } from 'lit/directives/style-map.js'; import { stopPropagation } from '../_common/utils/event.js'; +import { matchFlavours, Point } from '../_common/utils/index.js'; import { asyncFocusRichText } from '../_common/utils/selection.js'; import { AffineDragHandleWidget } from '../_common/widgets/drag-handle/index.js'; import { captureEventTarget } from '../_common/widgets/drag-handle/utils.js'; -import { Bound } from '../surface-block/index.js'; +import { Bound, Vec } from '../surface-block/index.js'; import { ImageResizeManager } from './image/image-resize-manager.js'; import { ImageSelectedRectsContainer } from './image/image-selected-rects.js'; import { shouldResizeImage } from './image/utils.js'; @@ -38,6 +40,7 @@ export class ImageBlockComponent extends BlockElement { super.connectedCallback(); const parent = this.root.page.getParent(this.model); this._isInSurface = parent?.flavour === 'affine:surface'; + this._registerDragHandleOption(); } get resizeImg() { @@ -48,6 +51,94 @@ export class ImageBlockComponent extends BlockElement { return this._current.blob; } + private _registerDragHandleOption = () => { + this._disposables.add( + AffineDragHandleWidget.registerOption({ + flavour: ImageBlockSchema.model.flavour, + onDragEnd: (state, { draggingElements, dropBlockId, dropType }) => { + if ( + draggingElements.length !== 1 || + !matchFlavours(draggingElements[0].model, [ + ImageBlockSchema.model.flavour, + ]) + ) { + return false; + } + + const imageBlockElement = draggingElements[0] as ImageBlockComponent; + const pageImageBlockEl = + imageBlockElement.querySelector('affine-page-image'); + const edgelessImageBlockEl = imageBlockElement.querySelector( + 'affine-edgeless-image' + ); + + const imageBlock = imageBlockElement.model; + if (pageImageBlockEl) { + // handle drag end of affine-page-image on the edgeless page + const imageBlock = imageBlockElement.model; + + const element = captureEventTarget(state.raw.target); + const isTargetEdgelessContainer = + element?.classList.contains('edgeless') && + element?.classList.contains('affine-block-children-container'); + if (!isTargetEdgelessContainer) { + return false; + } + + const edgelessPage = element?.closest('affine-edgeless-page'); + assertExists(edgelessPage); + + const imgEl = imageBlockElement.querySelector('img'); + assertExists(imgEl); + + const point = new Point(state.point.x, state.point.y); + + edgelessPage.addImage( + { + sourceId: imageBlock.sourceId, + width: imgEl.width, + height: imgEl.height, + }, + Vec.toVec(point) + ); + this.page.deleteBlock(imageBlock); + + return true; + } else if (edgelessImageBlockEl) { + // handle drag end of affine-edgeless-image on the edgeless note + const targetBlock = this.page.getBlockById(dropBlockId); + if (!targetBlock) { + return true; + } + + const shouldInsertIn = dropType === 'in'; + const parentBlock = shouldInsertIn + ? targetBlock + : this.page.getParent(targetBlock); + assertExists(parentBlock); + const parentIndex = shouldInsertIn + ? 0 + : parentBlock.children.indexOf(targetBlock); + + this.page.addBlock( + this.flavour, + { + sourceId: imageBlock.sourceId, + rotate: imageBlock.rotate, + }, + parentBlock, + parentIndex + ); + this.page.deleteBlock(imageBlock); + + return true; + } + return false; + }, + }) + ); + }; + override render() { if (this._isInSurface) { return html``; @@ -239,7 +331,6 @@ export class ImageBlockPageComponent extends ImageBlock { this._handleSelection(); this._observeDrag(); - this._registerDragHandleOption(); } override firstUpdated(changedProperties: PropertyValues) { @@ -270,34 +361,6 @@ export class ImageBlockPageComponent extends ImageBlock { }); } - private _registerDragHandleOption = () => { - this._disposables.add( - AffineDragHandleWidget.registerOption({ - flavour: ImageBlockSchema.model.flavour, - onDragStart: (state, startDragging) => { - // Check if start dragging from the image block - const target = captureEventTarget(state.raw.target); - const insideImageBlock = target?.closest('.resizable-img'); - if (!insideImageBlock) return false; - - // If start dragging from the image element - // Set selection and take over dragStart event to start dragging - const imageBlock = target?.closest('affine-image'); - if (!imageBlock || shouldResizeImage(imageBlock, target)) - return false; - - this.root.selection.set([ - this.root.selection.getInstance('block', { - path: imageBlock.path, - }), - ]); - startDragging([imageBlock], state); - return true; - }, - }) - ); - }; - private _onInputChange() { this._caption = this._input.value; this.model.page.updateBlock(this.model, { caption: this._caption }); diff --git a/packages/blocks/src/list-block/list-block.ts b/packages/blocks/src/list-block/list-block.ts index 656805fde8c30..935cff102086b 100644 --- a/packages/blocks/src/list-block/list-block.ts +++ b/packages/blocks/src/list-block/list-block.ts @@ -27,7 +27,7 @@ export class ListBlockComponent extends BlockElement { readonly attributeRenderer = affineAttributeRenderer; @state() - private _isCollapsedWhenReadOnly = !!this.model?.collapsed ?? false; + private _isCollapsedWhenReadOnly = !!this.model?.collapsed; private _select() { const selection = this.root.selection; diff --git a/packages/blocks/src/note-block/note-block.ts b/packages/blocks/src/note-block/note-block.ts index 59dca74814bd1..6a26d6811168a 100644 --- a/packages/blocks/src/note-block/note-block.ts +++ b/packages/blocks/src/note-block/note-block.ts @@ -3,8 +3,10 @@ import { BlockElement } from '@blocksuite/lit'; import { css, html } from 'lit'; import { customElement } from 'lit/decorators.js'; +import { isPageMode, matchFlavours } from '../_common/utils/index.js'; +import { AffineDragHandleWidget } from '../_common/widgets/index.js'; import { KeymapController } from './keymap-controller.js'; -import type { NoteBlockModel } from './note-model.js'; +import { type NoteBlockModel, NoteBlockSchema } from './note-model.js'; @customElement('affine-note') export class NoteBlockComponent extends BlockElement { @@ -20,9 +22,44 @@ export class NoteBlockComponent extends BlockElement { keymapController = new KeymapController(this); + private _registerDragHandleOption = () => { + this._disposables.add( + AffineDragHandleWidget.registerOption({ + flavour: NoteBlockSchema.model.flavour, + onDragEnd: (_, { draggingElements, dropBlockId, dropType }) => { + if ( + isPageMode(this.page) || + draggingElements.length !== 1 || + !matchFlavours(draggingElements[0].model, [ + NoteBlockSchema.model.flavour, + ]) + ) { + return false; + } + + const noteBlock = draggingElements[0].model as NoteBlockModel; + const targetBlock = this.page.getBlockById(dropBlockId); + const parentBlock = this.page.getParent(dropBlockId); + if (targetBlock && parentBlock && dropType !== 'in') { + this.page.moveBlocks( + noteBlock.children, + parentBlock, + targetBlock, + dropType === 'before' + ); + this.page.deleteBlock(noteBlock); + } + + return true; + }, + }) + ); + }; + override connectedCallback() { super.connectedCallback(); this.keymapController.bind(); + this._registerDragHandleOption(); } override render() { diff --git a/packages/blocks/src/page-block/edgeless/components/block-portal/image/edgeless-image.ts b/packages/blocks/src/page-block/edgeless/components/block-portal/image/edgeless-image.ts index 144d235104a1e..82b7d7b2240ac 100644 --- a/packages/blocks/src/page-block/edgeless/components/block-portal/image/edgeless-image.ts +++ b/packages/blocks/src/page-block/edgeless/components/block-portal/image/edgeless-image.ts @@ -24,7 +24,9 @@ export class EdgelessBlockPortalImage extends EdgelessPortalBase${this.renderModel(model)} +
+ ${this.renderModel(model)} +
`; } } diff --git a/packages/blocks/src/page-block/edgeless/components/rects/edgeless-selected-rect.ts b/packages/blocks/src/page-block/edgeless/components/rects/edgeless-selected-rect.ts index 4b6819b1c1f19..6835932108b09 100644 --- a/packages/blocks/src/page-block/edgeless/components/rects/edgeless-selected-rect.ts +++ b/packages/blocks/src/page-block/edgeless/components/rects/edgeless-selected-rect.ts @@ -693,7 +693,7 @@ export class EdgelessSelectedRect extends WithDisposable(LitElement) { ); _disposables.add(selection.slots.updated.on(this._updateOnSelectionChange)); - _disposables.add(page.slots.blockUpdated.on(this._updateOnElementChange)); + _disposables.add( page.slots.blockUpdated.on(data => { this._updateOnElementChange(data, true); diff --git a/packages/blocks/src/page-block/page-service.ts b/packages/blocks/src/page-block/page-service.ts index 34a167abab0a8..0d1829df286dd 100644 --- a/packages/blocks/src/page-block/page-service.ts +++ b/packages/blocks/src/page-block/page-service.ts @@ -44,7 +44,7 @@ export class PageService extends BlockService { .add('formatText', formatTextCommand) .add('withRoot', withRootCommand); - this.loadFonts(); + // this.loadFonts(); } loadFonts() { diff --git a/packages/virgo/src/utils/guard.ts b/packages/virgo/src/utils/guard.ts index 16880cadeeb94..ecafc8f918225 100644 --- a/packages/virgo/src/utils/guard.ts +++ b/packages/virgo/src/utils/guard.ts @@ -2,8 +2,7 @@ import { VirgoElement, VirgoLine } from '../components/index.js'; export function isNativeTextInVText(text: unknown): text is Text { return ( - text instanceof Text && - (text.parentElement?.dataset.virgoText === 'true' ?? false) + text instanceof Text && text.parentElement?.dataset.virgoText === 'true' ); } diff --git a/tests/edgeless/note.spec.ts b/tests/edgeless/note.spec.ts index 4aa13924f7d07..35a115c1ddb59 100644 --- a/tests/edgeless/note.spec.ts +++ b/tests/edgeless/note.spec.ts @@ -302,7 +302,7 @@ test('dragging un-selected note', async ({ page }) => { ]); }); -test('drag handle should be shown when a note is actived in default mode or hidden in other modes', async ({ +test('drag handle should be shown when a note is activated in default mode or hidden in other modes', async ({ page, }) => { await enterPlaygroundRoom(page);