"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.default = DraggableBlockPlugin;
const tslib_1 = require("tslib");
const LexicalComposerContext_1 = require("@lexical/react/LexicalComposerContext");
const rich_text_1 = require("@lexical/rich-text");
const utils_1 = require("@lexical/utils");
const lexical_1 = require("lexical");
const React = tslib_1.__importStar(require("react"));
const react_1 = require("react");
const react_dom_1 = require("react-dom");
const guard_1 = require("../../utils/guard");
const point_1 = require("../../utils/point");
const rect_1 = require("../../utils/rect");
const SPACE = 4;
const TARGET_LINE_HALF_HEIGHT = 2;
const DRAGGABLE_BLOCK_MENU_CLASSNAME = 'draggable-block-menu';
const DRAG_DATA_FORMAT = 'application/x-lexical-drag-block';
const TEXT_BOX_HORIZONTAL_PADDING = 28;
const Downward = 1;
const Upward = -1;
const Indeterminate = 0;
let prevIndex = Infinity;
function getCurrentIndex(keysLength) {
    if (keysLength === 0) {
        return Infinity;
    }
    if (prevIndex >= 0 && prevIndex < keysLength) {
        return prevIndex;
    }
    return Math.floor(keysLength / 2);
}
function getTopLevelNodeKeys(editor) {
    return editor.getEditorState().read(() => (0, lexical_1.$getRoot)().getChildrenKeys());
}
function getCollapsedMargins(elem) {
    const getMargin = (element, margin) => element ? parseFloat(window.getComputedStyle(element)[margin]) : 0;
    const { marginBottom, marginTop } = window.getComputedStyle(elem);
    const prevElemSiblingMarginBottom = getMargin(elem.previousElementSibling, 'marginBottom');
    const nextElemSiblingMarginTop = getMargin(elem.nextElementSibling, 'marginTop');
    const collapsedTopMargin = Math.max(parseFloat(marginTop), prevElemSiblingMarginBottom);
    const collapsedBottomMargin = Math.max(parseFloat(marginBottom), nextElemSiblingMarginTop);
    return { marginBottom: collapsedBottomMargin, marginTop: collapsedTopMargin };
}
function getBlockElement(anchorElem, editor, event, useEdgeAsDefault = false) {
    const anchorElementRect = anchorElem.getBoundingClientRect();
    const topLevelNodeKeys = getTopLevelNodeKeys(editor);
    let blockElem = null;
    editor.getEditorState().read(() => {
        if (useEdgeAsDefault) {
            const [firstNode, lastNode] = [
                editor.getElementByKey(topLevelNodeKeys[0]),
                editor.getElementByKey(topLevelNodeKeys[topLevelNodeKeys.length - 1]),
            ];
            const [firstNodeRect, lastNodeRect] = [firstNode === null || firstNode === void 0 ? void 0 : firstNode.getBoundingClientRect(), lastNode === null || lastNode === void 0 ? void 0 : lastNode.getBoundingClientRect()];
            if (firstNodeRect && lastNodeRect) {
                if (event.y < firstNodeRect.top) {
                    blockElem = firstNode;
                }
                else if (event.y > lastNodeRect.bottom) {
                    blockElem = lastNode;
                }
                if (blockElem) {
                    return;
                }
            }
        }
        let index = getCurrentIndex(topLevelNodeKeys.length);
        let direction = Indeterminate;
        while (index >= 0 && index < topLevelNodeKeys.length) {
            const key = topLevelNodeKeys[index];
            const elem = editor.getElementByKey(key);
            if (elem === null) {
                break;
            }
            const point = new point_1.Point(event.x, event.y);
            const domRect = rect_1.Rect.fromDOM(elem);
            const { marginBottom, marginTop } = getCollapsedMargins(elem);
            const rect = domRect.generateNewRect({
                bottom: domRect.bottom + marginBottom,
                left: anchorElementRect.left,
                right: anchorElementRect.right,
                top: domRect.top - marginTop,
            });
            const { reason: { isOnBottomSide, isOnTopSide }, result, } = rect.contains(point);
            if (result) {
                blockElem = elem;
                prevIndex = index;
                break;
            }
            if (direction === Indeterminate) {
                if (isOnTopSide) {
                    direction = Upward;
                }
                else if (isOnBottomSide) {
                    direction = Downward;
                }
                else {
                    // stop search block element
                    direction = Infinity;
                }
            }
            index += direction;
        }
    });
    return blockElem;
}
function isOnMenu(element) {
    return !!element.closest(`.${DRAGGABLE_BLOCK_MENU_CLASSNAME}`);
}
function setMenuPosition(targetElem, floatingElem, anchorElem) {
    if (!targetElem) {
        floatingElem.style.opacity = '0';
        floatingElem.style.transform = 'translate(-10000px, -10000px)';
        return;
    }
    const targetRect = targetElem.getBoundingClientRect();
    const targetStyle = window.getComputedStyle(targetElem);
    const floatingElemRect = floatingElem.getBoundingClientRect();
    const anchorElementRect = anchorElem.getBoundingClientRect();
    const top = targetRect.top + (parseInt(targetStyle.lineHeight, 10) - floatingElemRect.height) / 2 - anchorElementRect.top;
    const left = SPACE;
    floatingElem.style.opacity = '1';
    floatingElem.style.transform = `translate(${left}px, ${top}px)`;
}
function setDragImage(dataTransfer, draggableBlockElem) {
    const { transform } = draggableBlockElem.style;
    // Remove dragImage borders
    draggableBlockElem.style.transform = 'translateZ(0)';
    dataTransfer.setDragImage(draggableBlockElem, 0, 0);
    setTimeout(() => {
        draggableBlockElem.style.transform = transform;
    });
}
function setTargetLine(targetLineElem, targetBlockElem, mouseY, anchorElem) {
    const { height: targetBlockElemHeight, top: targetBlockElemTop } = targetBlockElem.getBoundingClientRect();
    const { top: anchorTop, width: anchorWidth } = anchorElem.getBoundingClientRect();
    const { marginBottom, marginTop } = getCollapsedMargins(targetBlockElem);
    let lineTop = targetBlockElemTop;
    if (mouseY >= targetBlockElemTop) {
        lineTop += targetBlockElemHeight + marginBottom / 2;
    }
    else {
        lineTop -= marginTop / 2;
    }
    const top = lineTop - anchorTop - TARGET_LINE_HALF_HEIGHT;
    const left = TEXT_BOX_HORIZONTAL_PADDING - SPACE;
    targetLineElem.style.transform = `translate(${left}px, ${top}px)`;
    targetLineElem.style.width = `${anchorWidth - (TEXT_BOX_HORIZONTAL_PADDING - SPACE) * 2}px`;
    targetLineElem.style.opacity = '.4';
}
function hideTargetLine(targetLineElem) {
    if (targetLineElem) {
        targetLineElem.style.opacity = '0';
        targetLineElem.style.transform = 'translate(-10000px, -10000px)';
    }
}
function useDraggableBlockMenu(editor, anchorElem, isEditable) {
    const scrollerElem = anchorElem.parentElement;
    const menuRef = (0, react_1.useRef)(null);
    const targetLineRef = (0, react_1.useRef)(null);
    const isDraggingBlockRef = (0, react_1.useRef)(false);
    const [draggableBlockElem, setDraggableBlockElem] = (0, react_1.useState)(null);
    (0, react_1.useEffect)(() => {
        function onMouseMove(event) {
            const target = event.target;
            if (!(0, guard_1.isHTMLElement)(target)) {
                setDraggableBlockElem(null);
                return;
            }
            if (isOnMenu(target)) {
                return;
            }
            const _draggableBlockElem = getBlockElement(anchorElem, editor, event);
            setDraggableBlockElem(_draggableBlockElem);
        }
        function onMouseLeave() {
            setDraggableBlockElem(null);
        }
        scrollerElem === null || scrollerElem === void 0 ? void 0 : scrollerElem.addEventListener('mousemove', onMouseMove);
        scrollerElem === null || scrollerElem === void 0 ? void 0 : scrollerElem.addEventListener('mouseleave', onMouseLeave);
        return () => {
            scrollerElem === null || scrollerElem === void 0 ? void 0 : scrollerElem.removeEventListener('mousemove', onMouseMove);
            scrollerElem === null || scrollerElem === void 0 ? void 0 : scrollerElem.removeEventListener('mouseleave', onMouseLeave);
        };
    }, [scrollerElem, anchorElem, editor]);
    (0, react_1.useEffect)(() => {
        if (menuRef.current) {
            setMenuPosition(draggableBlockElem, menuRef.current, anchorElem);
        }
    }, [anchorElem, draggableBlockElem]);
    (0, react_1.useEffect)(() => {
        function onDragover(event) {
            if (!isDraggingBlockRef.current) {
                return false;
            }
            const [isFileTransfer] = (0, rich_text_1.eventFiles)(event);
            if (isFileTransfer) {
                return false;
            }
            const { pageY, target } = event;
            if (!(0, guard_1.isHTMLElement)(target)) {
                return false;
            }
            const targetBlockElem = getBlockElement(anchorElem, editor, event, true);
            const targetLineElem = targetLineRef.current;
            if (targetBlockElem === null || targetLineElem === null) {
                return false;
            }
            setTargetLine(targetLineElem, targetBlockElem, pageY, anchorElem);
            // Prevent default event to be able to trigger onDrop events
            event.preventDefault();
            return true;
        }
        function onDrop(event) {
            if (!isDraggingBlockRef.current) {
                return false;
            }
            const [isFileTransfer] = (0, rich_text_1.eventFiles)(event);
            if (isFileTransfer) {
                return false;
            }
            const { dataTransfer, pageY, target } = event;
            const dragData = (dataTransfer === null || dataTransfer === void 0 ? void 0 : dataTransfer.getData(DRAG_DATA_FORMAT)) || '';
            const draggedNode = (0, lexical_1.$getNodeByKey)(dragData);
            if (!draggedNode) {
                return false;
            }
            if (!(0, guard_1.isHTMLElement)(target)) {
                return false;
            }
            const targetBlockElem = getBlockElement(anchorElem, editor, event, true);
            if (!targetBlockElem) {
                return false;
            }
            const targetNode = (0, lexical_1.$getNearestNodeFromDOMNode)(targetBlockElem);
            if (!targetNode) {
                return false;
            }
            if (targetNode === draggedNode) {
                return true;
            }
            const targetBlockElemTop = targetBlockElem.getBoundingClientRect().top;
            if (pageY >= targetBlockElemTop) {
                targetNode.insertAfter(draggedNode);
            }
            else {
                targetNode.insertBefore(draggedNode);
            }
            setDraggableBlockElem(null);
            return true;
        }
        return (0, utils_1.mergeRegister)(editor.registerCommand(lexical_1.DRAGOVER_COMMAND, (event) => onDragover(event), lexical_1.COMMAND_PRIORITY_LOW), editor.registerCommand(lexical_1.DROP_COMMAND, (event) => onDrop(event), lexical_1.COMMAND_PRIORITY_HIGH));
    }, [anchorElem, editor]);
    function onDragStart(event) {
        const dataTransfer = event.dataTransfer;
        if (!dataTransfer || !draggableBlockElem) {
            return;
        }
        setDragImage(dataTransfer, draggableBlockElem);
        let nodeKey = '';
        editor.update(() => {
            const node = (0, lexical_1.$getNearestNodeFromDOMNode)(draggableBlockElem);
            if (node) {
                nodeKey = node.getKey();
            }
        });
        isDraggingBlockRef.current = true;
        dataTransfer.setData(DRAG_DATA_FORMAT, nodeKey);
    }
    function onDragEnd() {
        isDraggingBlockRef.current = false;
        hideTargetLine(targetLineRef.current);
    }
    return (0, react_dom_1.createPortal)(React.createElement(React.Fragment, null,
        React.createElement("div", { ref: menuRef, className: 'icon draggable-block-menu', draggable: true, onDragEnd: onDragEnd, onDragStart: onDragStart },
            React.createElement("div", { className: isEditable ? 'icon' : '' })),
        React.createElement("div", { ref: targetLineRef, className: 'draggable-block-target-line' })), anchorElem);
}
function DraggableBlockPlugin({ anchorElem = document.body, }) {
    const [editor] = (0, LexicalComposerContext_1.useLexicalComposerContext)();
    return useDraggableBlockMenu(editor, anchorElem, editor._editable);
}
//# sourceMappingURL=index.js.map