| 1 | "use client"; |
| 2 | |
| 3 | import { DndPlugin, DndScroller } from "@platejs/dnd"; |
| 4 | import { useEditorRef, usePluginOption } from "platejs/react"; |
| 5 | import { useEffect, useRef, type ReactNode } from "react"; |
| 6 | |
| 7 | const SCROLL_CLASS_NAMES = [ |
| 8 | "overflow-auto", |
| 9 | "overflow-scroll", |
| 10 | "overflow-y-auto", |
| 11 | "overflow-y-scroll", |
| 12 | ] as const; |
| 13 | |
| 14 | const SCROLL_OVERFLOW_VALUES = new Set(["auto", "overlay", "scroll"]); |
| 15 | |
| 16 | function hasScrollableClassName(element: HTMLElement): boolean { |
| 17 | const className = |
| 18 | typeof element.className === "string" |
| 19 | ? element.className |
| 20 | : (element.getAttribute("class") ?? ""); |
| 21 | |
| 22 | return SCROLL_CLASS_NAMES.some((name) => className.includes(name)); |
| 23 | } |
| 24 | |
| 25 | function isScrollableAncestor(element: HTMLElement): boolean { |
| 26 | const styles = window.getComputedStyle(element); |
| 27 | const overflowY = styles.overflowY.toLowerCase(); |
| 28 | const overflow = styles.overflow.toLowerCase(); |
| 29 | const allowsScroll = |
| 30 | hasScrollableClassName(element) || |
| 31 | SCROLL_OVERFLOW_VALUES.has(overflowY) || |
| 32 | SCROLL_OVERFLOW_VALUES.has(overflow); |
| 33 | |
| 34 | return allowsScroll && element.scrollHeight > element.clientHeight + 1; |
| 35 | } |
| 36 | |
| 37 | function findScrollableAncestor( |
| 38 | startElement: HTMLElement | null, |
| 39 | ): HTMLElement | null { |
| 40 | let current = startElement; |
| 41 | |
| 42 | while (current) { |
| 43 | if ( |
| 44 | current !== document.body && |
| 45 | current !== document.documentElement && |
| 46 | isScrollableAncestor(current) |
| 47 | ) { |
| 48 | return current; |
| 49 | } |
| 50 | |
| 51 | current = current.parentElement; |
| 52 | } |
| 53 | |
| 54 | const rootScroller = document.scrollingElement; |
| 55 | |
| 56 | return rootScroller instanceof HTMLElement ? rootScroller : null; |
| 57 | } |
| 58 | |
| 59 | export function PlateDndOverlay({ children }: { children: ReactNode }) { |
| 60 | const editor = useEditorRef(); |
| 61 | const isDragging = Boolean(usePluginOption(DndPlugin, "isDragging")); |
| 62 | const containerRef = useRef<HTMLElement | null>(null); |
| 63 | const editorElementRef = useRef<HTMLElement | null>(null); |
| 64 | |
| 65 | useEffect(() => { |
| 66 | if (!isDragging) { |
| 67 | return; |
| 68 | } |
| 69 | |
| 70 | try { |
| 71 | const editorElement = editor.api.toDOMNode(editor); |
| 72 | |
| 73 | if (!(editorElement instanceof HTMLElement)) { |
| 74 | containerRef.current = null; |
| 75 | editorElementRef.current = null; |
| 76 | return; |
| 77 | } |
| 78 | |
| 79 | if ( |
| 80 | editorElementRef.current === editorElement && |
| 81 | containerRef.current?.isConnected |
| 82 | ) { |
| 83 | return; |
| 84 | } |
| 85 | |
| 86 | if ( |
| 87 | containerRef.current?.isConnected && |
| 88 | containerRef.current.contains(editorElement) |
| 89 | ) { |
| 90 | editorElementRef.current = editorElement; |
| 91 | return; |
| 92 | } |
| 93 | |
| 94 | containerRef.current = findScrollableAncestor(editorElement); |
| 95 | editorElementRef.current = editorElement; |
| 96 | } catch { |
| 97 | containerRef.current = null; |
| 98 | editorElementRef.current = null; |
| 99 | } |
| 100 | }, [editor, isDragging]); |
| 101 | |
| 102 | return ( |
| 103 | <> |
| 104 | {children} |
| 105 | <DndScroller |
| 106 | containerRef={containerRef} |
| 107 | height={32} |
| 108 | minStrength={0.05} |
| 109 | strengthMultiplier={16} |
| 110 | /> |
| 111 | </> |
| 112 | ); |
| 113 | } |
| 114 |