diff --git a/packages/client/src/tiptap/core/extensions/dragable.ts b/packages/client/src/tiptap/core/extensions/dragable.ts new file mode 100644 index 00000000..d6a8318a --- /dev/null +++ b/packages/client/src/tiptap/core/extensions/dragable.ts @@ -0,0 +1,173 @@ +import { Extension } from '@tiptap/core'; +import { Plugin, PluginKey, Selection } from 'prosemirror-state'; +import { NodeSelection } from 'prosemirror-state'; +import { __serializeForClipboard, EditorView } from 'prosemirror-view'; +import { ActiveNode, removePossibleTable, selectRootNodeByDom } from 'tiptap/prose-utils'; + +export const DragablePluginKey = new PluginKey('dragable'); + +export const Dragable = Extension.create({ + name: 'dragable', + + addProseMirrorPlugins() { + let editorView: EditorView; + let dragHandleDOM: HTMLElement; + let activeNode: ActiveNode; + let activeSelection: Selection; + let dragging = false; + + const createDragHandleDOM = () => { + const dom = document.createElement('div'); + dom.draggable = true; + dom.setAttribute('data-drag-handle', 'true'); + return dom; + }; + + const showDragHandleDOM = () => { + dragHandleDOM.classList.add('show'); + dragHandleDOM.classList.remove('hide'); + }; + + const hideDragHandleDOM = () => { + dragHandleDOM.classList.remove('show'); + dragHandleDOM.classList.add('hide'); + }; + + const renderDragHandleDOM = (view: EditorView, el: HTMLElement) => { + const root = view.dom.parentElement; + + if (!root) return; + + const targetNodeRect = (el).getBoundingClientRect(); + const rootRect = root.getBoundingClientRect(); + const handleRect = dragHandleDOM.getBoundingClientRect(); + + const left = targetNodeRect.left - rootRect.left - handleRect.width - handleRect.width / 2; + const top = targetNodeRect.top - rootRect.top + handleRect.height / 2 + root.scrollTop; + + dragHandleDOM.style.left = `${left}px`; + dragHandleDOM.style.top = `${top}px`; + + showDragHandleDOM(); + }; + + const handleMouseDown = () => { + if (!activeNode) return null; + + if (NodeSelection.isSelectable(activeNode.node)) { + const nodeSelection = NodeSelection.create(editorView.state.doc, activeNode.$pos.pos - activeNode.offset); + editorView.dispatch(editorView.state.tr.setSelection(nodeSelection)); + editorView.focus(); + activeSelection = nodeSelection; + return nodeSelection; + } + + return null; + }; + + const handleMouseUp = () => { + if (!dragging) return; + + dragging = false; + activeSelection = null; + }; + + const handleDragStart = (event) => { + dragging = true; + if (event.dataTransfer && activeSelection) { + const brokenClipboardAPI = false; + const slice = activeSelection.content(); + event.dataTransfer.effectAllowed = 'copyMove'; + const { dom, text } = __serializeForClipboard(editorView, slice); + event.dataTransfer.clearData(); + event.dataTransfer.setData(brokenClipboardAPI ? 'Text' : 'text/html', dom.innerHTML); + if (!brokenClipboardAPI) event.dataTransfer.setData('text/plain', text); + editorView.dragging = { + slice, + move: true, + }; + } + }; + + return [ + new Plugin({ + key: DragablePluginKey, + view: (view) => { + if (view.editable) { + dragHandleDOM = createDragHandleDOM(); + dragHandleDOM.addEventListener('mousedown', handleMouseDown); + dragHandleDOM.addEventListener('mouseup', handleMouseUp); + dragHandleDOM.addEventListener('dragstart', handleDragStart); + view.dom.parentNode?.appendChild(dragHandleDOM); + } + + return { + update(view) { + editorView = view; + }, + destroy: () => { + if (!dragHandleDOM) return; + + dragHandleDOM.remove(); + }, + }; + }, + props: { + handleDOMEvents: { + drop: (view, event: DragEvent) => { + if (!view.editable || !dragHandleDOM) return false; + + if (dragging) { + const tr = removePossibleTable(view, event); + dragging = false; + + if (tr) { + view.dispatch(tr); + event.preventDefault(); + return true; + } + } + + return false; + }, + mousemove: (view, event) => { + if (!view.editable || !dragHandleDOM) return false; + + const dom = event.target; + + if (!(dom instanceof Element)) { + if (dragging) return false; + hideDragHandleDOM(); + return false; + } + + const result = selectRootNodeByDom(dom, view); + activeNode = result; + + if (!result) { + if (dragging) return false; + hideDragHandleDOM(); + return false; + } + + if (result.node.type.name === 'title') { + if (dragging) return false; + hideDragHandleDOM(); + return false; + } + + renderDragHandleDOM(view, result.el); + return false; + }, + keydown: () => { + if (!editorView.editable || !dragHandleDOM) return false; + dragHandleDOM.classList.remove('show'); + hideDragHandleDOM(); + return false; + }, + }, + }, + }), + ]; + }, +}); diff --git a/packages/client/src/tiptap/core/extensions/paragraph.ts b/packages/client/src/tiptap/core/extensions/paragraph.ts index f4cbceb1..92d9eba2 100644 --- a/packages/client/src/tiptap/core/extensions/paragraph.ts +++ b/packages/client/src/tiptap/core/extensions/paragraph.ts @@ -3,20 +3,5 @@ import TitapParagraph from '@tiptap/extension-paragraph'; export const Paragraph = TitapParagraph.extend({ draggable: true, - - renderHTML({ HTMLAttributes }) { - return [ - 'p', - mergeAttributes(this.options.HTMLAttributes, HTMLAttributes), - [ - 'div', - { - 'contentEditable': 'false', - 'draggable': 'true', - 'data-drag-handle': 'true', - }, - ], - ['div', 0], - ]; - }, + selectable: true, }); diff --git a/packages/client/src/tiptap/core/styles/drag.scss b/packages/client/src/tiptap/core/styles/drag.scss index 0910907f..f674fc93 100644 --- a/packages/client/src/tiptap/core/styles/drag.scss +++ b/packages/client/src/tiptap/core/styles/drag.scss @@ -1,91 +1,26 @@ -/* stylelint-disable */ -.ProseMirror { - &.is-editable { - [data-drag-handle] { - position: relative; - display: inline; - opacity: 0; - transition: opacity 0.3s ease-out; - z-index: 100; +[data-drag-handle] { + position: absolute; + z-index: 100; + display: inline; + width: 16px; + height: 16px; + cursor: move; + opacity: 0; + transition: opacity 0.3s ease-out; + background-image: url("data:image/svg+xml;charset=utf-8,%3Csvg width='16' height='16' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Crect x='3' y='1' width='3' height='3' rx='1.5' fill='%23111'/%3E%3Crect x='10' y='1' width='3' height='3' rx='1.5' fill='%23111'/%3E%3Crect x='3' y='6' width='3' height='3' rx='1.5' fill='%23111'/%3E%3Crect x='10' y='6' width='3' height='3' rx='1.5' fill='%23111'/%3E%3Crect x='3' y='11' width='3' height='3' rx='1.5' fill='%23111'/%3E%3Crect x='10' y='11' width='3' height='3' rx='1.5' fill='%23111'/%3E%3C/svg%3E"); + background-size: contain; + background-position: center 0; + background-repeat: no-repeat; - &:hover { - opacity: 1 !important; - } + &.show { + opacity: 0.3; - &::before { - content: ''; - position: absolute; - left: -24px; - top: 2px; - width: 16px; - height: 16px; - text-align: center; - margin-left: auto; - cursor: move; - background-image: url("data:image/svg+xml;charset=utf-8,%3Csvg width='16' height='16' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Crect x='3' y='1' width='3' height='3' rx='1.5' fill='%23111'/%3E%3Crect x='10' y='1' width='3' height='3' rx='1.5' fill='%23111'/%3E%3Crect x='3' y='6' width='3' height='3' rx='1.5' fill='%23111'/%3E%3Crect x='10' y='6' width='3' height='3' rx='1.5' fill='%23111'/%3E%3Crect x='3' y='11' width='3' height='3' rx='1.5' fill='%23111'/%3E%3Crect x='10' y='11' width='3' height='3' rx='1.5' fill='%23111'/%3E%3C/svg%3E"); - background-size: contain; - background-position: center 0; - background-repeat: no-repeat; - filter: var(--invert-filter); - } - } - - p { - [data-drag-handle] { - &::before { - top: 6px; - } - } - - &:hover { - [data-drag-handle] { - opacity: 0.3; - } - } - } - - ul { - li { - [data-drag-handle] { - &::before { - left: -36px; - } - } - } - - &[data-type='taskList'] { - li { - [data-drag-handle] { - &::before { - left: -46px; - } - } - } - } - } - - ol { - li { - [data-drag-handle] { - &::before { - left: -36px; - } - } - } - } - - .drag-container { - position: relative; - - &:hover { - [data-drag-handle] { - opacity: 0.3; - } - } - - .drag-content { - width: 100%; - } + &:hover { + opacity: 1; } } + + &.hide { + opacity: 0; + } } diff --git a/packages/client/src/tiptap/core/styles/selection.scss b/packages/client/src/tiptap/core/styles/selection.scss index c3d32bfd..e14fe53f 100644 --- a/packages/client/src/tiptap/core/styles/selection.scss +++ b/packages/client/src/tiptap/core/styles/selection.scss @@ -1,5 +1,9 @@ /* stylelint-disable */ .ProseMirror { + p.selected-node { + outline: 1px solid var(--node-selected-border-color); + } + hr.selected-node { &::after { background-color: var(--node-selected-border-color); diff --git a/packages/client/src/tiptap/core/wrappers/attachment/index.tsx b/packages/client/src/tiptap/core/wrappers/attachment/index.tsx index ab43659e..49fad80f 100644 --- a/packages/client/src/tiptap/core/wrappers/attachment/index.tsx +++ b/packages/client/src/tiptap/core/wrappers/attachment/index.tsx @@ -154,10 +154,5 @@ export const AttachmentWrapper = ({ editor, node, updateAttributes }) => { } })(); - return ( - -
-
{content}
- - ); + return {content}; }; diff --git a/packages/client/src/tiptap/core/wrappers/callout/index.tsx b/packages/client/src/tiptap/core/wrappers/callout/index.tsx index aa979467..684fc364 100644 --- a/packages/client/src/tiptap/core/wrappers/callout/index.tsx +++ b/packages/client/src/tiptap/core/wrappers/callout/index.tsx @@ -25,29 +25,26 @@ export const CalloutWrapper = ({ editor, node, updateAttributes }) => { ); return ( - -
-
-
+
+ {isEditable ? ( + + {emoji || 'Icon'} + + ) : ( + emoji && {emoji} + )} + - {isEditable ? ( - - {emoji || 'Icon'} - - ) : ( - emoji && {emoji} - )} - -
+ />
); diff --git a/packages/client/src/tiptap/core/wrappers/code-block/index.tsx b/packages/client/src/tiptap/core/wrappers/code-block/index.tsx index 11f3e747..79afa595 100644 --- a/packages/client/src/tiptap/core/wrappers/code-block/index.tsx +++ b/packages/client/src/tiptap/core/wrappers/code-block/index.tsx @@ -4,8 +4,6 @@ import { NodeViewContent, NodeViewWrapper } from '@tiptap/react'; import cls from 'classnames'; import { copy } from 'helpers/copy'; import React, { useRef } from 'react'; -import { CodeBlock } from 'tiptap/core/extensions/code-block'; -import { DragableWrapper } from 'tiptap/core/wrappers/dragable'; import styles from './index.module.scss'; @@ -16,39 +14,36 @@ export const CodeBlockWrapper = ({ editor, node: { attrs }, updateAttributes, ex const $container = useRef(); return ( - -
-
-
- updateAttributes({ language: value })} + className={styles.selectorWrap} + disabled={!isEditable} + filter + > + auto + {extension.options.lowlight.listLanguages().map((lang, index) => ( + + {lang} + + ))} + + +
-
-          
-        
+ type="tertiary" + theme="borderless" + icon={} + onClick={() => copy($container.current.innerText)} + /> +
+
+        
+      
); }; diff --git a/packages/client/src/tiptap/core/wrappers/countdown/index.tsx b/packages/client/src/tiptap/core/wrappers/countdown/index.tsx index 41a7c7c0..2d8eeb47 100644 --- a/packages/client/src/tiptap/core/wrappers/countdown/index.tsx +++ b/packages/client/src/tiptap/core/wrappers/countdown/index.tsx @@ -32,13 +32,10 @@ export const CountdownWrapper = ({ editor, node }) => { const { title, date } = node.attrs; return ( - -
-
-
- {title} - -
+ +
+ {title} +
); diff --git a/packages/client/src/tiptap/core/wrappers/document-children/index.tsx b/packages/client/src/tiptap/core/wrappers/document-children/index.tsx index cc79793f..a2bf1a7f 100644 --- a/packages/client/src/tiptap/core/wrappers/document-children/index.tsx +++ b/packages/client/src/tiptap/core/wrappers/document-children/index.tsx @@ -35,48 +35,48 @@ export const DocumentChildrenWrapper = ({ editor, node, updateAttributes }) => { }, [node.attrs, wikiId, documentId, updateAttributes]); return ( - -
-
-
- 子文档 -
- {wikiId || documentId ? ( - { - if (!documents || !documents.length) { - return ; - } - return ( -
- {documents.map((doc) => { - return ( - - - - {doc.title} - - - ); - })} -
- ); - }} - /> - ) : ( - 当前页面无法使用子文档 - )} + +
+ 子文档
+ {wikiId || documentId ? ( + { + if (!documents || !documents.length) { + return ; + } + return ( +
+ {documents.map((doc) => { + return ( + + + + {doc.title} + + + ); + })} +
+ ); + }} + /> + ) : ( + 当前页面无法使用子文档 + )}
); }; diff --git a/packages/client/src/tiptap/core/wrappers/document-reference/index.tsx b/packages/client/src/tiptap/core/wrappers/document-reference/index.tsx index 67101808..5127181e 100644 --- a/packages/client/src/tiptap/core/wrappers/document-reference/index.tsx +++ b/packages/client/src/tiptap/core/wrappers/document-reference/index.tsx @@ -50,9 +50,8 @@ export const DocumentReferenceWrapper = ({ editor, node, updateAttributes }) => }, [organizationId, wikiId, documentId, isEditable, isShare, title]); return ( - -
-
{content}
+ + {content} ); }; diff --git a/packages/client/src/tiptap/core/wrappers/flow/index.tsx b/packages/client/src/tiptap/core/wrappers/flow/index.tsx index 99fc5475..719ac12f 100644 --- a/packages/client/src/tiptap/core/wrappers/flow/index.tsx +++ b/packages/client/src/tiptap/core/wrappers/flow/index.tsx @@ -95,48 +95,45 @@ export const FlowWrapper = ({ editor, node, updateAttributes }) => { }, [toggleLoading, data]); return ( - -
-
- - -
- {loading && ( -
- - {/* FIXME: semi-design 的问题,不加 div,文字会换行! */} -
-
-
- )} + + + +
+ {loading && ( +
+ + {/* FIXME: semi-design 的问题,不加 div,文字会换行! */} +
+
+
+ )} - {error && {(error && error.message) || '未知错误'}} + {error && {(error && error.message) || '未知错误'}} - {!loading && !error && visible &&
} -
+ {!loading && !error && visible &&
} +
-
- - - - - 流程图 - -
+
+ + + + + 流程图 + +
-
- -
-
-
-
+
+ +
+ + ); }; diff --git a/packages/client/src/tiptap/core/wrappers/iframe/index.tsx b/packages/client/src/tiptap/core/wrappers/iframe/index.tsx index 26091d65..7a03f710 100644 --- a/packages/client/src/tiptap/core/wrappers/iframe/index.tsx +++ b/packages/client/src/tiptap/core/wrappers/iframe/index.tsx @@ -22,23 +22,20 @@ export const IframeWrapper = ({ editor, node, updateAttributes }) => { ); return ( - -
-
- -
- {url ? ( -
- -
- ) : ( -
- 请设置外链地址 -
- )} -
-
-
+ + +
+ {url ? ( +
+ +
+ ) : ( +
+ 请设置外链地址 +
+ )} +
+
); }; diff --git a/packages/client/src/tiptap/core/wrappers/image/index.tsx b/packages/client/src/tiptap/core/wrappers/image/index.tsx index 3d5b293c..cdcb2e9f 100644 --- a/packages/client/src/tiptap/core/wrappers/image/index.tsx +++ b/packages/client/src/tiptap/core/wrappers/image/index.tsx @@ -69,33 +69,30 @@ export const ImageWrapper = ({ editor, node, updateAttributes }) => { }, [src, hasTrigger, selectFile, updateAttributes]); return ( - -
-
- - {error ? ( -
- {error} -
- ) : !src ? ( -
- - {loading ? '正在上传中' : '请选择图片'} - - -
- ) : ( - - )} -
-
+ + + {error ? ( +
+ {error} +
+ ) : !src ? ( +
+ + {loading ? '正在上传中' : '请选择图片'} + + +
+ ) : ( + + )} +
); }; diff --git a/packages/client/src/tiptap/core/wrappers/katex/index.tsx b/packages/client/src/tiptap/core/wrappers/katex/index.tsx index a7b369ac..6efa2e07 100644 --- a/packages/client/src/tiptap/core/wrappers/katex/index.tsx +++ b/packages/client/src/tiptap/core/wrappers/katex/index.tsx @@ -35,15 +35,12 @@ export const KatexWrapper = ({ node, editor }) => { return ( -
-
-
{content}
-
+
{content}
); }; diff --git a/packages/client/src/tiptap/core/wrappers/mind/index.tsx b/packages/client/src/tiptap/core/wrappers/mind/index.tsx index 5e025dd7..f914af24 100644 --- a/packages/client/src/tiptap/core/wrappers/mind/index.tsx +++ b/packages/client/src/tiptap/core/wrappers/mind/index.tsx @@ -108,69 +108,60 @@ export const MindWrapper = ({ editor, node, updateAttributes }) => { }, [width, height, setCenter]); return ( - -
-
- - -
- {error && ( -
- {error.message || error} -
- )} - - {loading && } - - {!loading && !error && visible && ( -
- )} - -
- - - - - 思维导图 - + + + +
+ {error && ( +
+ {error.message || error}
+ )} -
- -
+ {loading && } + + {!loading && !error && visible && ( +
+ )} + +
+ + + + + 思维导图 +
- - -
+ +
+ +
+
+ + ); }; diff --git a/packages/client/src/tiptap/editor/collaboration/kit.ts b/packages/client/src/tiptap/editor/collaboration/kit.ts index aa4d19b4..0d2ff3a3 100644 --- a/packages/client/src/tiptap/editor/collaboration/kit.ts +++ b/packages/client/src/tiptap/editor/collaboration/kit.ts @@ -16,6 +16,7 @@ import { Countdown } from 'tiptap/core/extensions/countdown'; import { Document } from 'tiptap/core/extensions/document'; import { DocumentChildren } from 'tiptap/core/extensions/document-children'; import { DocumentReference } from 'tiptap/core/extensions/document-reference'; +import { Dragable } from 'tiptap/core/extensions/dragable'; import { Dropcursor } from 'tiptap/core/extensions/dropcursor'; import { Emoji } from 'tiptap/core/extensions/emoji'; import { EventEmitter } from 'tiptap/core/extensions/event-emitter'; @@ -156,4 +157,5 @@ export const CollaborationKit = [ }), Title, DocumentWithTitle, + Dragable, ]; diff --git a/packages/client/src/tiptap/prose-utils/index.ts b/packages/client/src/tiptap/prose-utils/index.ts index c2e676e6..8e585bcb 100644 --- a/packages/client/src/tiptap/prose-utils/index.ts +++ b/packages/client/src/tiptap/prose-utils/index.ts @@ -18,6 +18,7 @@ export * from './markdown-source-map'; export * from './mention'; export * from './node'; export * from './position'; +export * from './select-node-by-dom'; export * from './table'; export * from './text'; export * from './type'; diff --git a/packages/client/src/tiptap/prose-utils/select-node-by-dom.ts b/packages/client/src/tiptap/prose-utils/select-node-by-dom.ts new file mode 100644 index 00000000..08b1e445 --- /dev/null +++ b/packages/client/src/tiptap/prose-utils/select-node-by-dom.ts @@ -0,0 +1,87 @@ +import { Node, ResolvedPos } from 'prosemirror-model'; +import { EditorView } from 'prosemirror-view'; + +export type ActiveNode = Readonly<{ + $pos: ResolvedPos; + node: Node; + el: HTMLElement; + offset: number; +}>; + +const nodeIsNotBlock = (node: Node) => !node.type.isBlock; + +const nodeIsFirstChild = (pos: ResolvedPos) => { + let parent = pos.parent; + const node = pos.node(); + + if (parent === node) { + parent = pos.node(pos.depth - 1); + } + if (!parent || parent.type.name === 'doc') return false; + + return parent.firstChild === node; +}; + +const getDOMByPos = (view: EditorView, root: HTMLElement, $pos: ResolvedPos) => { + const { node } = view.domAtPos($pos.pos); + + let el: HTMLElement = node as HTMLElement; + let parent = el.parentElement; + while (parent && parent !== root && $pos.pos === view.posAtDOM(parent, 0)) { + el = parent; + parent = parent.parentElement; + } + + return el; +}; + +export const selectRootNodeByDom = (dom: Element, view: EditorView): ActiveNode | null => { + const root = view.dom.parentElement; + + if (!root) return null; + + let pos = view.posAtDOM(dom, 0); + + /** + * img 节点修正 + */ + if (dom.tagName === 'IMG') { + pos -= 1; + } + + if (pos === 0) return null; + + let $pos = view.state.doc.resolve(pos); + let node = $pos.node(); + + /** + * 自定义节点修正 + */ + if (node.type.name === 'doc') { + const nodeAtPos = view.state.doc.nodeAt(pos); + + if (nodeAtPos && nodeAtPos.type.name !== 'doc' && nodeAtPos.type.name !== 'text') { + node = nodeAtPos; + $pos = view.state.doc.resolve(pos); + const el = view.nodeDOM(pos); + return { node, $pos, el, offset: 0 }; + } + } + + while (node && (nodeIsNotBlock(node) || nodeIsFirstChild($pos))) { + $pos = view.state.doc.resolve($pos.before()); + node = $pos.node(); + } + + if (node.type.name.includes('table')) { + while (node.type.name !== 'table') { + $pos = view.state.doc.resolve($pos.before()); + node = $pos.node(); + } + } + + $pos = view.state.doc.resolve($pos.pos - $pos.parentOffset); + const el = getDOMByPos(view, root, $pos); + + return { node, $pos, el, offset: 1 }; +}; diff --git a/packages/client/src/tiptap/prose-utils/table.ts b/packages/client/src/tiptap/prose-utils/table.ts index bc95df50..4ea04e7b 100644 --- a/packages/client/src/tiptap/prose-utils/table.ts +++ b/packages/client/src/tiptap/prose-utils/table.ts @@ -2,6 +2,7 @@ import { findParentNode } from '@tiptap/core'; import { Node, ResolvedPos } from 'prosemirror-model'; import { Selection, Transaction } from 'prosemirror-state'; import { CellSelection, TableMap } from 'prosemirror-tables'; +import { EditorView } from 'prosemirror-view'; export const isRectSelected = (rect: any) => (selection: CellSelection) => { const map = TableMap.get(selection.$anchorCell.node(-1)); @@ -220,3 +221,62 @@ export const selectTable = (tr: Transaction) => { } return tr; }; + +function dropPoint(doc, pos, slice) { + const $pos = doc.resolve(pos); + if (!slice.content.size) { + return pos; + } + let content = slice.content; + for (let i = 0; i < slice.openStart; i++) { + content = content.firstChild.content; + } + for (let pass = 1; pass <= (slice.openStart == 0 && slice.size ? 2 : 1); pass++) { + for (let d = $pos.depth; d >= 0; d--) { + const bias = d == $pos.depth ? 0 : $pos.pos <= ($pos.start(d + 1) + $pos.end(d + 1)) / 2 ? -1 : 1; + const insertPos = $pos.index(d) + (bias > 0 ? 1 : 0); + const parent = $pos.node(d); + let fits = false; + if (pass == 1) { + fits = parent.canReplace(insertPos, insertPos, content); + } else { + const wrapping = parent.contentMatchAt(insertPos).findWrapping(content.firstChild.type); + fits = wrapping && parent.canReplaceWith(insertPos, insertPos, wrapping[0]); + } + if (fits) { + return bias == 0 ? $pos.pos : bias < 0 ? $pos.before(d + 1) : $pos.after(d + 1); + } + } + } + return null; +} + +export const removePossibleTable = (view: EditorView, event: DragEvent): Transaction | null => { + const { state } = view; + + const $pos = state.selection.$anchor; + for (let d = $pos.depth; d > 0; d--) { + const node = $pos.node(d); + if (node.type.spec['tableRole'] == 'table') { + const eventPos = view.posAtCoords({ left: event.clientX, top: event.clientY }); + if (!eventPos) return null; + const slice = view.dragging?.slice; + if (!slice) return null; + + const $mouse = view.state.doc.resolve(eventPos.pos); + const insertPos = dropPoint(view.state.doc, $mouse.pos, slice); + if (!insertPos) return null; + + let tr = state.tr; + tr = tr.delete($pos.before(d), $pos.after(d)); + + const pos = tr.mapping.map(insertPos); + + tr = tr.replaceRange(pos, pos, slice).scrollIntoView(); + + return tr; + } + } + + return null; +};