phx-react
Version:
PHX REACT
280 lines • 12.3 kB
JavaScript
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext';
import { eventFiles } from '@lexical/rich-text';
import { mergeRegister } from '@lexical/utils';
import { $getNearestNodeFromDOMNode, $getNodeByKey, $getRoot, COMMAND_PRIORITY_HIGH, COMMAND_PRIORITY_LOW, DRAGOVER_COMMAND, DROP_COMMAND, } from 'lexical';
import * as React from 'react';
import { useEffect, useRef, useState } from 'react';
import { createPortal } from 'react-dom';
import { isHTMLElement } from '../../utils/guard';
import { Point } from '../../utils/point';
import { Rect } from '../../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(() => $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(event.x, event.y);
const domRect = 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 = useRef(null);
const targetLineRef = useRef(null);
const isDraggingBlockRef = useRef(false);
const [draggableBlockElem, setDraggableBlockElem] = useState(null);
useEffect(() => {
function onMouseMove(event) {
const target = event.target;
if (!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]);
useEffect(() => {
if (menuRef.current) {
setMenuPosition(draggableBlockElem, menuRef.current, anchorElem);
}
}, [anchorElem, draggableBlockElem]);
useEffect(() => {
function onDragover(event) {
if (!isDraggingBlockRef.current) {
return false;
}
const [isFileTransfer] = eventFiles(event);
if (isFileTransfer) {
return false;
}
const { pageY, target } = event;
if (!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] = 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 = $getNodeByKey(dragData);
if (!draggedNode) {
return false;
}
if (!isHTMLElement(target)) {
return false;
}
const targetBlockElem = getBlockElement(anchorElem, editor, event, true);
if (!targetBlockElem) {
return false;
}
const targetNode = $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 mergeRegister(editor.registerCommand(DRAGOVER_COMMAND, (event) => onDragover(event), COMMAND_PRIORITY_LOW), editor.registerCommand(DROP_COMMAND, (event) => onDrop(event), 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 = $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 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);
}
export default function DraggableBlockPlugin({ anchorElem = document.body, }) {
const [editor] = useLexicalComposerContext();
return useDraggableBlockMenu(editor, anchorElem, editor._editable);
}
//# sourceMappingURL=index.js.map