var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
    var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
    if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
    else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
    return c > 3 && r && Object.defineProperty(target, key, r), r;
};
var AffineDragHandleWidget_1;
import { PathFinder, } from '@blockeditor/block-std';
import { assertExists, assertInstanceOf, DisposableGroup, throttle, } from '@blockeditor/global/utils';
import { WidgetElement } from '@blockeditor/lit';
import {} from '@blockeditor/store';
import { html, render } from 'lit';
import { customElement, query, state } from 'lit/decorators.js';
import { styleMap } from 'lit/directives/style-map.js';
import { buildPath, getBlockElementsExcludeSubtrees, getCurrentNativeRange, getModelByBlockComponent, isInsideDocEditor, isInsideEdgelessEditor, matchFlavours, Point, Rect, } 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, isTopLevelBlock, } from '../../../page-block/edgeless/utils/query.js';
import { autoScroll } from '../../../page-block/text-selection/utils.js';
import { DragPreview } from './components/drag-preview.js';
import { DropIndicator } from './components/drop-indicator.js';
import { DragHandleOptionsRunner } from './config.js';
import { DRAG_HANDLE_CONTAINER_OFFSET_LEFT_TOP_LEVEL, DRAG_HANDLE_CONTAINER_PADDING, DRAG_HANDLE_CONTAINER_WIDTH, DRAG_HANDLE_CONTAINER_WIDTH_TOP_LEVEL, DRAG_HANDLE_GRABBER_BORDER_RADIUS, DRAG_HANDLE_GRABBER_HEIGHT, DRAG_HANDLE_GRABBER_WIDTH, DRAG_HANDLE_GRABBER_WIDTH_HOVERED, DRAG_HOVER_RECT_PADDING, HOVER_AREA_RECT_PADDING_TOP_LEVEL, } from './config.js';
import { styles } from './styles.js';
import { calcDropTarget, captureEventTarget, containBlock, containChildBlock, getClosestBlockByPoint, getClosestNoteBlock, getDragHandleContainerHeight, getDragHandleLeftPadding, getNoteId, includeTextSelection, insideDatabaseTable, isBlockPathEqual, isOutOfNoteBlock, updateDragHandleClassName, } from './utils.js';
export const AFFINE_DRAG_HANDLE_WIDGET = 'affine-drag-handle-widget';
let AffineDragHandleWidget = AffineDragHandleWidget_1 = class AffineDragHandleWidget extends WidgetElement {
    constructor() {
        super(...arguments);
        this.draggingElements = [];
        this.dropBlockId = '';
        this.dropType = null;
        this.dragging = false;
        this.dragPreview = null;
        this.dropIndicator = null;
        this.lastDragPointerState = null;
        this.scale = 1;
        this.center = [0, 0];
        this.rafID = 0;
        this._dragHoverRect = null;
        this._lastHoveredBlockPath = null;
        this._lastShowedBlock = null;
        this._isHoverDragHandleVisible = false;
        this._isDragHandleHovered = false;
        this._isTopLevelDragHandleVisible = false;
        this._anchorBlockId = '';
        this._anchorBlockPath = null;
        this._anchorModelDisposables = null;
        /**
         * When dragging, should update indicator position and target drop block id
         */
        this._getDropResult = (state) => {
            const point = new Point(state.raw.x, state.raw.y);
            const closestBlockElement = getClosestBlockByPoint(this.host, this.pageBlockElement, point);
            if (!closestBlockElement) {
                return null;
            }
            const blockId = closestBlockElement.model.id;
            const blockPath = closestBlockElement.path;
            const model = closestBlockElement.model;
            const isDatabase = matchFlavours(model, ['affine:database']);
            if (isDatabase) {
                return null;
            }
            // note block can only be dropped into another note block
            // prevent note block from being dropped into other blocks
            const isDraggedElementNote = this.draggingElements.length === 1 &&
                matchFlavours(this.draggingElements[0].model, ['affine:note']);
            if (isDraggedElementNote) {
                const parentElement = this._getBlockElementFromViewStore(PathFinder.parent(closestBlockElement.path));
                if (!parentElement)
                    return null;
                if (!matchFlavours(parentElement.model, ['affine:note']))
                    return null;
            }
            // Should make sure that target drop block is
            // neither within the dragging elements
            // nor a child-block of any dragging elements
            if (containBlock(this.draggingElements.map(blockElement => blockElement.model.id), blockId) ||
                containChildBlock(this.draggingElements, blockPath)) {
                return null;
            }
            let rect = null;
            let dropType = 'before';
            const result = calcDropTarget(point, model, closestBlockElement, this.draggingElements, this.scale);
            if (result) {
                rect = result.rect;
                dropType = result.dropType;
            }
            if (isDraggedElementNote && dropType === 'in')
                return null;
            const dropIndicator = {
                rect,
                dropBlockId: blockId,
                dropType,
            };
            return dropIndicator;
        };
        this._updateDropResult = (dropResult) => {
            assertExists(this.dropIndicator);
            this.dropBlockId = dropResult?.dropBlockId ?? '';
            this.dropType = dropResult?.dropType ?? null;
            if (dropResult?.rect) {
                const { left: viewportLeft, top: viewportTop, scrollLeft, scrollTop, } = this.pageBlockElement.viewport;
                let { left, top } = dropResult.rect;
                left += scrollLeft - viewportLeft;
                top += scrollTop - viewportTop;
                const { width, height } = dropResult.rect;
                const rect = Rect.fromLWTH(left, width, top, height);
                this.dropIndicator.rect = rect;
            }
            else {
                this.dropIndicator.rect = dropResult?.rect ?? null;
            }
        };
        this._resetDropResult = () => {
            this.dropBlockId = '';
            this.dropType = null;
            if (this.dropIndicator)
                this.dropIndicator.rect = null;
        };
        this._createDropIndicator = () => {
            if (!this.dropIndicator) {
                this.dropIndicator = new DropIndicator();
                this.pageBlockElement.append(this.dropIndicator);
            }
        };
        this._updateDropIndicator = (state, shouldAutoScroll = false) => {
            const point = new Point(state.raw.x, state.raw.y);
            const closestNoteBlock = getClosestNoteBlock(this.host, this.pageBlockElement, point);
            if (!closestNoteBlock ||
                isOutOfNoteBlock(this.host, closestNoteBlock, point, this.scale)) {
                this._resetDropResult();
            }
            else {
                const dropResult = this._getDropResult(state);
                this._updateDropResult(dropResult);
            }
            this.lastDragPointerState = state;
            if (this.pageBlockElement instanceof DocPageBlockComponent) {
                if (!shouldAutoScroll)
                    return;
                const result = autoScroll(this.pageBlockElement.viewportElement, state.raw.y);
                if (!result) {
                    this._clearRaf();
                    return;
                }
                this.rafID = requestAnimationFrame(() => this._updateDropIndicator(state, true));
            }
            else {
                this._clearRaf();
            }
        };
        this._updateDropIndicatorOnScroll = () => {
            if (!this.dragging ||
                this.draggingElements.length === 0 ||
                !this.lastDragPointerState)
                return;
            const state = this.lastDragPointerState;
            this.rafID = requestAnimationFrame(() => this._updateDropIndicator(state, false));
        };
        this._removeDropIndicator = () => {
            if (this.dropIndicator) {
                this.dropIndicator.remove();
                this.dropIndicator = null;
            }
        };
        this._calculatePreviewOffset = (blockElements, state) => {
            const { top, left } = blockElements[0].getBoundingClientRect();
            const previewOffset = new Point(state.raw.x - left, state.raw.y - top);
            return previewOffset;
        };
        this._createDragPreview = (blockElements, state, dragPreviewEl, dragPreviewOffset) => {
            let dragPreview;
            if (dragPreviewEl) {
                dragPreview = new DragPreview(dragPreviewOffset);
                dragPreview.appendChild(dragPreviewEl);
            }
            else {
                const fragment = document.createDocumentFragment();
                let width = 0;
                blockElements.forEach(element => {
                    width = Math.max(width, element.getBoundingClientRect().width);
                    const container = document.createElement('div');
                    render(this.host.renderModel(element.model), container);
                    fragment.appendChild(container);
                });
                const offset = this._calculatePreviewOffset(blockElements, state);
                const posX = state.raw.x - offset.x;
                const posY = state.raw.y - offset.y;
                dragPreview = new DragPreview(offset);
                dragPreview.style.width = `${width / this.scale}px`;
                dragPreview.style.transform = `translate(${posX}px, ${posY}px) scale(${this.scale})`;
                dragPreview.appendChild(fragment);
            }
            this.pageBlockElement.appendChild(dragPreview);
            return dragPreview;
        };
        this._updateDragPreviewPosition = (state) => {
            if (!this.dragPreview)
                return;
            const pageBlockElement = this.pageBlockElement;
            const { left, top, scrollLeft, scrollTop, scrollWidth, scrollHeight } = pageBlockElement.viewport;
            const dragPreviewOffset = this.dragPreview.offset;
            const { width, height } = this.dragPreview.getBoundingClientRect();
            let posX = state.raw.x - dragPreviewOffset.x - left + scrollLeft;
            if (posX < 0) {
                posX = 0;
            }
            else if (posX + width > scrollWidth) {
                posX = scrollWidth - width;
            }
            let posY = state.raw.y - dragPreviewOffset.y - top + scrollTop;
            if (posY < 0) {
                posY = 0;
            }
            else if (posY + height > scrollHeight) {
                posY = scrollHeight - height;
            }
            this.dragPreview.style.transform = `translate(${posX}px, ${posY}px) scale(${this.scale})`;
        };
        this._updateDragPreviewOnViewportUpdate = () => {
            if (this.dragPreview && this.lastDragPointerState) {
                this._updateDragPreviewPosition(this.lastDragPointerState);
            }
        };
        this._removeDragPreview = () => {
            if (this.dragPreview) {
                this.dragPreview.remove();
                this.dragPreview = null;
            }
        };
        this._changeCursorToGrabbing = () => {
            document.documentElement.classList.add('affine-drag-preview-grabbing');
        };
        this._resetCursor = () => {
            document.documentElement.classList.remove('affine-drag-preview-grabbing');
        };
        this._startDragging = (blockElements, state, dragPreviewEl, dragPreviewOffset) => {
            if (!blockElements.length)
                return;
            this.draggingElements = blockElements;
            if (this.dragPreview)
                this._removeDragPreview();
            this.dragPreview = this._createDragPreview(blockElements, state, dragPreviewEl, dragPreviewOffset);
            this.dragging = true;
            this._changeCursorToGrabbing();
            this._createDropIndicator();
            this._hide();
        };
        this._hide = (force = false) => {
            updateDragHandleClassName();
            this._isHoverDragHandleVisible = false;
            this._isTopLevelDragHandleVisible = false;
            this._isDragHandleHovered = false;
            this._anchorBlockId = '';
            this._anchorBlockPath = null;
            if (this._dragHandleContainer)
                this._dragHandleContainer.style.display = 'none';
            if (force)
                this._reset();
        };
        this._reset = () => {
            this.draggingElements = [];
            this.dropBlockId = '';
            this.dropType = null;
            this.lastDragPointerState = null;
            this.rafID = 0;
            this.dragging = false;
            this._dragHoverRect = null;
            this._lastHoveredBlockPath = null;
            this._lastShowedBlock = null;
            this._anchorBlockId = '';
            this._anchorBlockPath = null;
            this._isHoverDragHandleVisible = false;
            this._isDragHandleHovered = false;
            this._isTopLevelDragHandleVisible = false;
            this._removeDragPreview();
            this._removeDropIndicator();
            this._resetCursor();
        };
        this._handleAnchorModelDisposables = (blockModel) => {
            if (this._anchorModelDisposables) {
                this._anchorModelDisposables.dispose();
                this._anchorModelDisposables = null;
            }
            this._anchorModelDisposables = new DisposableGroup();
            this._anchorModelDisposables.add(blockModel.propsUpdated.on(() => this._hide()));
            this._anchorModelDisposables.add(blockModel.deleted.on(() => this._hide()));
        };
        this._getBlockElementFromViewStore = (path) => {
            return this.host.view.viewFromPath('block', path);
        };
        // Need to consider block padding and scale
        this._getTopWithBlockElement = (blockElement) => {
            const computedStyle = getComputedStyle(blockElement);
            const { top } = blockElement.getBoundingClientRect();
            const paddingTop = parseInt(computedStyle.paddingTop) * this.scale;
            return top + paddingTop + this._viewportOffset.top;
        };
        /** Check if given blockElement is selected */
        this._isBlockSelected = (block) => {
            if (!block)
                return false;
            return this.selectedBlocks.some(selection => selection.blockId === block.model.id);
        };
        // Single block: drag handle should show on the vertical middle of the first line of element
        // Multiple blocks: drag handle should show on the vertical middle of all blocks
        this._showDragHandleOnHoverBlock = (blockPath) => {
            const blockElement = this._getBlockElementFromViewStore(blockPath);
            assertExists(blockElement);
            const container = this._dragHandleContainer;
            const grabber = this._dragHandleGrabber;
            if (!container || !grabber)
                return;
            let { left } = blockElement.getBoundingClientRect();
            const draggingAreaRect = this._getDraggingAreaRect(blockElement);
            // Some blocks have padding, should consider padding when calculating position
            const computedStyle = getComputedStyle(blockElement);
            const paddingLeft = parseInt(computedStyle.paddingLeft) * this.scale;
            left += paddingLeft;
            const containerHeight = getDragHandleContainerHeight(blockElement.model);
            // Ad-hoc solution for list with toggle icon
            updateDragHandleClassName([blockElement]);
            const offsetLeft = getDragHandleLeftPadding([blockElement]);
            // End of ad-hoc solution
            const posLeft = left -
                (DRAG_HANDLE_CONTAINER_WIDTH + offsetLeft) * this.scale +
                this._viewportOffset.left;
            const posTop = this._getTopWithBlockElement(blockElement);
            const rowPaddingY = ((containerHeight - DRAG_HANDLE_GRABBER_HEIGHT) / 2) * this.scale;
            // use padding to control grabber's height
            const paddingTop = rowPaddingY + posTop - draggingAreaRect.top;
            const paddingBottom = draggingAreaRect.height -
                paddingTop -
                DRAG_HANDLE_GRABBER_HEIGHT * this.scale;
            const applyStyle = (transition) => {
                container.style.transition = transition ? 'padding 0.25s ease' : 'none';
                container.style.paddingTop = `${paddingTop}px`;
                container.style.paddingBottom = `${paddingBottom}px`;
                container.style.width = `${DRAG_HANDLE_CONTAINER_WIDTH * this.scale}px`;
                container.style.left = `${posLeft}px`;
                container.style.top = `${draggingAreaRect.top}px`;
                container.style.display = 'flex';
                container.style.height = `${draggingAreaRect.height}px`;
            };
            if (isBlockPathEqual(blockElement.path, this._lastShowedBlock?.path)) {
                applyStyle(true);
            }
            else if (this.selectedBlocks.length) {
                if (this._isBlockSelected(blockElement))
                    applyStyle(this._isDragHandleHovered &&
                        this._isBlockSelected(this._lastShowedBlock?.el));
                else
                    applyStyle(false);
            }
            else {
                applyStyle(false);
            }
            grabber.style.width = `${DRAG_HANDLE_GRABBER_WIDTH * this.scale}px`;
            grabber.style.borderRadius = `${DRAG_HANDLE_GRABBER_BORDER_RADIUS * this.scale}px`;
            this._handleAnchorModelDisposables(blockElement.model);
            if (!isBlockPathEqual(blockElement.path, this._lastShowedBlock?.path)) {
                this._lastShowedBlock = {
                    path: blockElement.path,
                    el: blockElement,
                };
            }
            this._isHoverDragHandleVisible = true;
        };
        this._showDragHandleOnTopLevelBlocks = async () => {
            if (isInsideDocEditor(this.host))
                return;
            const edgelessPage = this.pageBlockElement;
            await edgelessPage.surface.updateComplete;
            if (!this._anchorBlockPath)
                return;
            const blockElement = this.anchorBlockElement;
            assertExists(blockElement);
            const edgelessElement = edgelessPage.surface.pickById(blockElement.model.id);
            assertExists(edgelessElement);
            const container = this._dragHandleContainer;
            const grabber = this._dragHandleGrabber;
            if (!container || !grabber)
                return;
            const rect = getSelectedRect([edgelessElement]);
            const [left, top] = edgelessPage.surface.toViewCoord(rect.left, rect.top);
            const height = rect.height * this.scale;
            const posLeft = left -
                (DRAG_HANDLE_CONTAINER_WIDTH_TOP_LEVEL +
                    DRAG_HANDLE_CONTAINER_OFFSET_LEFT_TOP_LEVEL) *
                    this.scale;
            const posTop = top;
            container.style.transition = 'none';
            container.style.paddingTop = `0px`;
            container.style.paddingBottom = `0px`;
            container.style.width = `${DRAG_HANDLE_CONTAINER_WIDTH_TOP_LEVEL * this.scale}px`;
            container.style.left = `${posLeft}px`;
            container.style.top = `${posTop}px`;
            container.style.display = 'flex';
            container.style.height = `${height}px`;
            grabber.style.width = `${DRAG_HANDLE_GRABBER_WIDTH_HOVERED * this.scale}px`;
            grabber.style.borderRadius = `${DRAG_HANDLE_GRABBER_BORDER_RADIUS * this.scale}px`;
            this._handleAnchorModelDisposables(blockElement.model);
            this._isTopLevelDragHandleVisible = true;
        };
        this._getHoveredBlocks = () => {
            if (!this._isHoverDragHandleVisible || !this._anchorBlockPath)
                return [];
            const hoverBlock = this.anchorBlockElement;
            assertExists(hoverBlock);
            const selections = this.selectedBlocks;
            let blockElements = [];
            // When current selection is TextSelection, should cover all the blocks in native range
            if (selections.length > 0 && includeTextSelection(selections)) {
                const range = getCurrentNativeRange();
                blockElements = this._rangeManager.getSelectedBlockElementsByRange(range, {
                    match: el => el.model.role === 'content',
                    mode: 'highest',
                });
            }
            else {
                blockElements = this.selectedBlocks
                    .map(block => this._getBlockElementFromViewStore(block.path))
                    .filter((block) => !!block);
            }
            if (containBlock(blockElements.map(block => PathFinder.id(block.path)), PathFinder.id(this._anchorBlockPath))) {
                return blockElements;
            }
            return [hoverBlock];
        };
        this._getDraggingAreaRect = (blockElement) => {
            // When hover block is in selected blocks, should show hover rect on the selected blocks
            // Top: the top of the first selected block
            // Left: the left of the first selected block
            // Right: the largest right of the selected blocks
            // Bottom: the bottom of the last selected block
            let { left, top, right, bottom } = blockElement.getBoundingClientRect();
            const blockElements = this._getHoveredBlocks();
            blockElements.forEach(blockElement => {
                left = Math.min(left, blockElement.getBoundingClientRect().left);
                top = Math.min(top, blockElement.getBoundingClientRect().top);
                right = Math.max(right, blockElement.getBoundingClientRect().right);
                bottom = Math.max(bottom, blockElement.getBoundingClientRect().bottom);
            });
            const offsetLeft = getDragHandleLeftPadding(blockElements);
            // Add padding to hover rect
            left -= (DRAG_HANDLE_CONTAINER_WIDTH + offsetLeft) * this.scale;
            top -= DRAG_HOVER_RECT_PADDING * this.scale;
            right += DRAG_HOVER_RECT_PADDING * this.scale;
            bottom += DRAG_HOVER_RECT_PADDING * this.scale;
            left += this._viewportOffset.left;
            right += this._viewportOffset.left;
            top += this._viewportOffset.top;
            bottom += this._viewportOffset.top;
            return new Rect(left, top, right, bottom);
        };
        this._updateDragHoverRectTopLevelBlock = () => {
            if (!this._dragHoverRect)
                return;
            const edgelessElement = this.anchorEdgelessElement;
            if (edgelessElement) {
                this._dragHoverRect =
                    this._getHoverAreaRectTopLevelBlock(edgelessElement);
            }
        };
        this._setSelectedBlocks = (blockElements, noteId) => {
            const { selection } = this.host;
            const selections = blockElements.map(blockElement => selection.create('block', {
                path: blockElement.path,
            }));
            // When current page is edgeless page
            // We need to remain surface selection and set editing as true
            if (isInsideEdgelessEditor(this.host)) {
                const surfaceElementId = noteId ? noteId : getNoteId(blockElements[0]);
                const surfaceSelection = selection.create('surface', blockElements[0].path, [surfaceElementId], true);
                selections.push(surfaceSelection);
            }
            selection.set(selections);
        };
        this._canEditing = (noteBlock) => {
            if (noteBlock.page.id !== this.page.id)
                return false;
            if (isInsideDocEditor(this.host))
                return true;
            const edgelessPage = this.pageBlockElement;
            const noteBlockId = noteBlock.path[noteBlock.path.length - 1];
            return (edgelessPage.selectionManager.editing &&
                edgelessPage.selectionManager.selectedIds[0] === noteBlockId);
        };
        this._checkTopLevelBlockSelection = () => {
            if (!this.isConnected) {
                return;
            }
            if (this.page.readonly || isInsideDocEditor(this.host)) {
                this._hide();
                return;
            }
            const edgelessPage = this.pageBlockElement;
            const editing = edgelessPage.selectionManager.editing;
            const selectedElements = edgelessPage.selectionManager.elements;
            if (editing || selectedElements.length !== 1) {
                this._hide();
                return;
            }
            const selectedElement = selectedElements[0];
            if (!isTopLevelBlock(selectedElement)) {
                this._hide();
                return;
            }
            const flavour = selectedElement.flavour;
            const dragHandleOptions = this.optionRunner.getOption(flavour);
            if (!dragHandleOptions || !dragHandleOptions.edgeless) {
                this._hide();
                return;
            }
            const selections = edgelessPage.selectionManager.selections;
            this._anchorBlockId = selectedElement.id;
            this._anchorBlockPath = selections[0].path;
            this._showDragHandleOnTopLevelBlocks().catch(console.error);
        };
        /**
         * When pointer move on block, should show drag handle
         * And update hover block id and path
         */
        this._pointerMoveOnBlock = (state) => {
            if (this._isTopLevelDragHandleVisible)
                return;
            const point = new Point(state.raw.x, state.raw.y);
            const closestBlockElement = getClosestBlockByPoint(this.host, this.pageBlockElement, point);
            if (!closestBlockElement) {
                this._anchorBlockId = '';
                this._anchorBlockPath = null;
                return;
            }
            const blockId = closestBlockElement.getAttribute(this.host.blockIdAttr);
            assertExists(blockId);
            this._anchorBlockId = blockId;
            this._anchorBlockPath = closestBlockElement.path;
            if (insideDatabaseTable(closestBlockElement) || this.page.readonly) {
                this._hide();
                return;
            }
            // If current block is not the last hovered block, show drag handle beside the hovered block
            if ((!this._lastHoveredBlockPath ||
                !isBlockPathEqual(this._anchorBlockPath, this._lastHoveredBlockPath) ||
                !this._isHoverDragHandleVisible) &&
                !this._isDragHandleHovered) {
                this._showDragHandleOnHoverBlock(this._anchorBlockPath);
                this._lastHoveredBlockPath = this._anchorBlockPath;
            }
        };
        this._pointerMoveHandler = ctx => {
            if (this.page.readonly || this.dragging || !this.isConnected) {
                this._hide();
                return;
            }
            if (this._isTopLevelDragHandleVisible)
                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
            if (!element)
                return;
            // When pointer on drag handle, should do nothing
            if (element.closest('.affine-drag-handle-container'))
                return;
            // TODO: need to optimize
            // When pointer out of note block hover area or inside database, should hide drag handle
            const point = new Point(state.raw.x, state.raw.y);
            const closestNoteBlock = getClosestNoteBlock(this.host, this.pageBlockElement, point);
            if (closestNoteBlock &&
                this._canEditing(closestNoteBlock) &&
                !isOutOfNoteBlock(this.host, closestNoteBlock, point, this.scale)) {
                this._pointerMoveOnBlock(state);
                return true;
            }
            this._hide();
            return false;
        };
        this._throttledPointerMoveHandler = throttle(this._pointerMoveHandler, 1000 / 60);
        /**
         * When click on drag handle
         * Should select the block and show slash menu if current block is not selected
         * Should clear selection if current block is the first selected block
         */
        this._clickHandler = ctx => {
            if (!this._isHoverDragHandleVisible)
                return;
            const state = ctx.get('pointerState');
            const { target } = state.raw;
            const element = captureEventTarget(target);
            const insideDragHandle = !!element?.closest(AFFINE_DRAG_HANDLE_WIDGET);
            if (!insideDragHandle)
                return;
            if (!this._anchorBlockId || !this._anchorBlockPath)
                return;
            const { selection } = this.host;
            const selectedBlocks = this.selectedBlocks;
            // Should clear selection if current block is the first selected block
            if (selectedBlocks.length > 0 &&
                !includeTextSelection(selectedBlocks) &&
                selectedBlocks[0].blockId === this._anchorBlockId) {
                selection.clear(['block']);
                this._dragHoverRect = null;
                this._showDragHandleOnHoverBlock(this._anchorBlockPath);
                return;
            }
            // Should select the block if current block is not selected
            const blockElement = this.anchorBlockElement;
            assertExists(blockElement);
            if (selectedBlocks.length > 1) {
                this._showDragHandleOnHoverBlock(this._anchorBlockPath);
            }
            this._setSelectedBlocks([blockElement]);
            return true;
        };
        this._onDragStart = (state) => {
            const event = state.raw;
            const { target } = event;
            const element = captureEventTarget(target);
            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 (!insideDragHandle) {
                this._hide();
                return false;
            }
            if (!this._isHoverDragHandleVisible)
                return;
            // Get current hover block element by path
            assertExists(this._anchorBlockId);
            assertExists(this._anchorBlockPath);
            const hoverBlockElement = this.anchorBlockElement;
            if (!hoverBlockElement)
                return false;
            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._anchorBlockId)) {
                const blockElement = this.anchorBlockElement;
                assertExists(blockElement);
                this._setSelectedBlocks([blockElement]);
            }
            const blockElements = this.selectedBlocks
                .map(selection => {
                return this._getBlockElementFromViewStore(selection.path);
            })
                .filter((element) => !!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);
            if (blockElementsExcludingChildren.length === 0)
                return false;
            this._startDragging(blockElementsExcludingChildren, state);
            this._hide();
            return true;
        };
        this._onDragMove = (state) => {
            this._clearRaf();
            this.rafID = requestAnimationFrame(() => {
                this._updateDragPreviewPosition(state);
                this._updateDropIndicator(state, true);
            });
            return true;
        };
        this._onDragEnd = (state) => {
            const targetBlockId = this.dropBlockId;
            const dropType = this.dropType;
            const draggingElements = this.draggingElements;
            this._hide(true);
            // handle drop of blocks from note onto edgeless container
            if (!targetBlockId) {
                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 => getModelByBlockComponent(element))
                    .filter((x) => !!x);
                if (selectedBlocks.length === 0)
                    return false;
                const isSurfaceComponent = selectedBlocks.some(block => {
                    const parent = this.page.getParent(block.id);
                    return matchFlavours(parent, ['affine:surface']);
                });
                if (isSurfaceComponent)
                    return true;
                const edgelessPage = this.pageBlockElement;
                assertInstanceOf(edgelessPage, EdgelessPageBlockComponent);
                const { left: viewportLeft, top: viewportTop } = edgelessPage.viewport;
                const newNoteId = edgelessPage.addNoteWithPoint(new Point(state.raw.x - viewportLeft, state.raw.y - viewportTop));
                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 (containBlock(this.selectedBlocks.map(selection => selection.blockId), targetBlockId))
                return false;
            const selectedBlocks = getBlockElementsExcludeSubtrees(draggingElements)
                .map(element => getModelByBlockComponent(element))
                .filter((x) => !!x);
            if (!selectedBlocks.length)
                return false;
            const targetBlock = this.page.getBlockById(targetBlockId);
            assertExists(targetBlock);
            const shouldInsertIn = dropType === 'in';
            const parent = shouldInsertIn
                ? targetBlock
                : this.page.getParent(targetBlockId);
            assertExists(parent);
            if (shouldInsertIn) {
                this.page.moveBlocks(selectedBlocks, targetBlock);
            }
            else {
                this.page.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(() => {
                assertExists(parent);
                // Need to update selection when moving blocks successfully
                // Because the block path may be changed after moving
                const parentElement = this._getBlockElementFromViewStore(buildPath(parent));
                if (parentElement) {
                    const newSelectedBlocks = selectedBlocks
                        .map(block => parentElement.path.concat(block.id))
                        .map(path => this._getBlockElementFromViewStore(path));
                    if (!newSelectedBlocks)
                        return;
                    const noteId = getNoteId(parentElement);
                    this._setSelectedBlocks(newSelectedBlocks, noteId);
                }
            }, 0);
            return true;
        };
        /**
         * When start dragging, should set dragging elements and create drag preview
         */
        this._dragStartHandler = 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.optionRunner.options) {
                if (option.onDragStart?.({
                    state,
                    startDragging: this._startDragging,
                    anchorBlockId: this._anchorBlockId,
                    anchorBlockPath: this._anchorBlockPath,
                })) {
                    return true;
                }
            }
            return this._onDragStart(state);
        };
        /**
         * When dragging, should:
         * Update drag preview position
         * Update indicator position
         * Update drop block id
         */
        this._dragMoveHandler = ctx => {
            if (this._isHoverDragHandleVisible || this._isTopLevelDragHandleVisible) {
                this._hide();
            }
            if (!this.dragging || this.draggingElements.length === 0) {
                return false;
            }
            ctx.get('defaultState').event.preventDefault();
            const state = ctx.get('pointerState');
            for (const option of this.optionRunner.options) {
                if (option.onDragMove?.(state, this.draggingElements)) {
                    return true;
                }
            }
            // call default drag move handler if no option return true
            return this._onDragMove(state);
        };
        /**
         * When drag end, should move blocks to drop position
         * @returns
         */
        this._dragEndHandler = ctx => {
            this._clearRaf();
            if (!this.dragging || !this.dragPreview)
                return false;
            if (this.draggingElements.length === 0 || this.page.readonly) {
                this._hide(true);
                return false;
            }
            const state = ctx.get('pointerState');
            for (const option of this.optionRunner.options) {
                if (option.onDragEnd?.({
                    state,
                    draggingElements: this.draggingElements,
                    dropBlockId: this.dropBlockId,
                    dropType: this.dropType,
                    dragPreview: this.dragPreview,
                })) {
                    this._hide(true);
                    if (isInsideEdgelessEditor(this.host)) {
                        this._checkTopLevelBlockSelection();
                    }
                    return true;
                }
            }
            // call default drag end handler if no option return true
            this._onDragEnd(state);
            if (isInsideEdgelessEditor(this.host))
                this._checkTopLevelBlockSelection();
            return true;
        };
        this._pointerOutHandler = ctx => {
            const state = ctx.get('pointerState');
            state.raw.preventDefault();
            const { target } = state.raw;
            const element = captureEventTarget(target);
            if (!element)
                return;
            const { relatedTarget } = state.raw;
            // TODO: when pointer out of page viewport, should hide drag handle
            // But the pointer out event is not as expected
            // Need to be optimized
            const relatedElement = captureEventTarget(relatedTarget);
            const outOfPageViewPort = element.classList.contains('affine-doc-viewport');
            const inPage = !!relatedElement?.closest('.affine-doc-viewport');
            const inDragHandle = !!relatedElement?.closest(AFFINE_DRAG_HANDLE_WIDGET);
            if (outOfPageViewPort && !inDragHandle && !inPage)
                this._hide();
        };
        this._onDragHandlePointerEnter = () => {
            const container = this._dragHandleContainer;
            const grabber = this._dragHandleGrabber;
            if (!container || !grabber)
                return;
            if (this._isHoverDragHandleVisible) {
                assertExists(this._anchorBlockPath);
                const blockElement = this.anchorBlockElement;
                if (!blockElement)
                    return;
                const padding = DRAG_HANDLE_CONTAINER_PADDING * this.scale;
                container.style.paddingTop = `${padding}px`;
                container.style.paddingBottom = `${padding}px`;
                container.style.transition = `padding 0.25s ease`;
                grabber.style.width = `${DRAG_HANDLE_GRABBER_WIDTH_HOVERED * this.scale}px`;
                grabber.style.borderRadius = `${DRAG_HANDLE_GRABBER_BORDER_RADIUS * this.scale}px`;
                this._isDragHandleHovered = true;
            }
            else if (this._isTopLevelDragHandleVisible) {
                const edgelessElement = this.anchorEdgelessElement;
                if (!edgelessElement)
                    return;
                this._dragHoverRect =
                    this._getHoverAreaRectTopLevelBlock(edgelessElement);
                this._isDragHandleHovered = true;
            }
        };
        this._onDragHandlePointerDown = () => {
            if (!this._isHoverDragHandleVisible || !this._anchorBlockPath)
                return;
            const blockElement = this.anchorBlockElement;
            if (!blockElement)
                return;
            this._dragHoverRect = this._getDraggingAreaRect(blockElement) ?? null;
        };
        this._onDragHandlePointerUp = () => {
            if (!this._isHoverDragHandleVisible)
                return;
            this._dragHoverRect = null;
        };
        this._onDragHandlePointerLeave = () => {
            this._isDragHandleHovered = false;
            this._dragHoverRect = null;
            if (this._isTopLevelDragHandleVisible)
                return;
            if (this.dragging)
                return;
            if (!this._anchorBlockPath)
                return;
            this._showDragHandleOnHoverBlock(this._anchorBlockPath);
        };
        this._handleEdgelessToolUpdated = (newTool) => {
            if (newTool.type === 'default') {
                this._checkTopLevelBlockSelection();
            }
            else {
                this._hide();
            }
        };
        this._handleEdgelessViewPortUpdated = ({ zoom, center, }) => {
            if (this.scale !== zoom) {
                this.scale = zoom;
                this._updateDragPreviewOnViewportUpdate();
            }
            if (this.center[0] !== center[0] && this.center[1] !== center[1]) {
                this.center = [...center];
                this._updateDropIndicatorOnScroll();
            }
            if (this._isTopLevelDragHandleVisible) {
                this._showDragHandleOnTopLevelBlocks().catch(console.error);
                this._updateDragHoverRectTopLevelBlock();
            }
            else {
                this._hide();
            }
        };
    }
    static registerOption(option) {
        return AffineDragHandleWidget_1.staticOptionRunner.register(option);
    }
    get optionRunner() {
        return AffineDragHandleWidget_1.staticOptionRunner;
    }
    get pageBlockElement() {
        return this.blockElement;
    }
    get selectedBlocks() {
        return this.host.selection.find('text')
            ? this.host.selection.filter('text')
            : this.host.selection.filter('block');
    }
    get anchorBlockElement() {
        if (!this._anchorBlockPath)
            return null;
        return this._getBlockElementFromViewStore(this._anchorBlockPath);
    }
    get anchorEdgelessElement() {
        if (isInsideDocEditor(this.host) || !this._anchorBlockId)
            return null;
        const { surface } = this.pageBlockElement;
        const edgelessElement = surface.pickById(this._anchorBlockId);
        return isTopLevelBlock(edgelessElement) ? edgelessElement : null;
    }
    _clearRaf() {
        if (this.rafID) {
            cancelAnimationFrame(this.rafID);
            this.rafID = 0;
        }
    }
    get _viewportOffset() {
        if (this.blockElement instanceof EdgelessPageBlockComponent) {
            const pageBlock = this.blockElement;
            const rect = pageBlock.getBoundingClientRect();
            return {
                left: -rect.left,
                top: -rect.top,
            };
        }
        else {
            const pageBlock = this.blockElement;
            return {
                left: pageBlock.viewport.scrollLeft - pageBlock.viewport.left,
                top: pageBlock.viewport.scrollTop - pageBlock.viewport.top,
            };
        }
    }
    _getHoverAreaRectTopLevelBlock(edgelessElement) {
        if (isInsideDocEditor(this.host))
            return null;
        const edgelessPage = this.pageBlockElement;
        const rect = getSelectedRect([edgelessElement]);
        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 = DRAG_HANDLE_CONTAINER_OFFSET_LEFT_TOP_LEVEL * this.scale;
        const padding = HOVER_AREA_RECT_PADDING_TOP_LEVEL * this.scale;
        left -= DRAG_HANDLE_CONTAINER_WIDTH_TOP_LEVEL * this.scale + offsetLeft;
        top -= padding;
        right += padding;
        bottom += padding;
        return new Rect(left, top, right, bottom);
    }
    get _rangeManager() {
        assertExists(this.host.rangeManager);
        return this.host.rangeManager;
    }
    firstUpdated() {
        this._hide(true);
        // When pointer enter drag handle grabber
        // Extend drag handle grabber to the height of the hovered block
        this._disposables.addFromEvent(this._dragHandleContainer, 'pointerenter', this._onDragHandlePointerEnter);
        this._disposables.addFromEvent(this._dragHandleContainer, 'pointerdown', this._onDragHandlePointerDown);
        this._disposables.addFromEvent(this._dragHandleContainer, 'pointerup', this._onDragHandlePointerUp);
        // When pointer leave drag handle grabber, should reset drag handle grabber style
        this._disposables.addFromEvent(this._dragHandleContainer, 'pointerleave', this._onDragHandlePointerLeave);
        this._disposables.addFromEvent(this.host, 'pointerleave', () => this._hide());
        if (isInsideDocEditor(this.host)) {
            this._disposables.add(this.page.slots.blockUpdated.on(() => this._hide()));
            const docPage = this.pageBlockElement;
            this._disposables.add(docPage.slots.viewportUpdated.on(() => {
                this._hide();
                if (this.dropIndicator)
                    this.dropIndicator.rect = null;
            }));
            const viewportElement = docPage.viewportElement;
            assertExists(viewportElement);
            this._disposables.addFromEvent(viewportElement, 'scrollend', this._updateDropIndicatorOnScroll);
        }
        else if (isInsideEdgelessEditor(this.host)) {
            const edgelessPage = this.pageBlockElement;
            this._disposables.add(edgelessPage.slots.edgelessToolUpdated.on(this._handleEdgelessToolUpdated));
            this._disposables.add(edgelessPage.slots.viewportUpdated.on(this._handleEdgelessViewPortUpdated));
            this._disposables.add(edgelessPage.selectionManager.slots.updated.on(() => {
                this._checkTopLevelBlockSelection();
            }));
            this._disposables.add(edgelessPage.slots.readonlyUpdated.on(() => {
                this._checkTopLevelBlockSelection();
            }));
            this._disposables.add(edgelessPage.slots.draggingAreaUpdated.on(() => {
                this._checkTopLevelBlockSelection();
            }));
            this._disposables.add(edgelessPage.slots.elementResizeStart.on(() => {
                this._hide();
            }));
            this._disposables.add(edgelessPage.slots.elementResizeEnd.on(() => {
                this._checkTopLevelBlockSelection();
            }));
        }
    }
    connectedCallback() {
        super.connectedCallback();
        this.handleEvent('pointerMove', this._throttledPointerMoveHandler);
        this.handleEvent('click', this._clickHandler);
        this.handleEvent('dragStart', this._dragStartHandler);
        this.handleEvent('dragMove', this._dragMoveHandler);
        this.handleEvent('dragEnd', this._dragEndHandler);
        this.handleEvent('pointerOut', this._pointerOutHandler);
        this.handleEvent('beforeInput', () => this._hide());
    }
    disconnectedCallback() {
        this._hide(true);
        this._disposables.dispose();
        this._anchorModelDisposables?.dispose();
        super.disconnectedCallback();
    }
    render() {
        const hoverRectStyle = styleMap(this._dragHoverRect
            ? {
                width: `${this._dragHoverRect.width}px`,
                height: `${this._dragHoverRect.height}px`,
                top: `${this._dragHoverRect.top}px`,
                left: `${this._dragHoverRect.left}px`,
            }
            : {
                display: 'none',
            });
        return html `
      <div class="affine-drag-handle-widget">
        <div class="affine-drag-handle-container">
          <div class="affine-drag-handle-grabber"></div>
        </div>
        <div class="affine-drag-hover-rect" style=${hoverRectStyle}></div>
      </div>
    `;
    }
};
AffineDragHandleWidget.styles = styles;
AffineDragHandleWidget.staticOptionRunner = new DragHandleOptionsRunner();
__decorate([
    query('.affine-drag-handle-container')
], AffineDragHandleWidget.prototype, "_dragHandleContainer", void 0);
__decorate([
    query('.affine-drag-handle-grabber')
], AffineDragHandleWidget.prototype, "_dragHandleGrabber", void 0);
__decorate([
    state()
], AffineDragHandleWidget.prototype, "_dragHoverRect", void 0);
AffineDragHandleWidget = AffineDragHandleWidget_1 = __decorate([
    customElement(AFFINE_DRAG_HANDLE_WIDGET)
], AffineDragHandleWidget);
export { AffineDragHandleWidget };
//# sourceMappingURL=drag-handle.js.map