UNPKG

phx-react

Version:

PHX REACT

280 lines • 12.3 kB
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