diff --git a/apps/desktop/package.json b/apps/desktop/package.json index 3ef5e510..285eafcb 100644 --- a/apps/desktop/package.json +++ b/apps/desktop/package.json @@ -106,6 +106,7 @@ "react-router-dom": "^7.1.3", "tailwind-merge": "^2.5.5", "tailwindcss-animate": "^1.0.7", + "tiptap-extension-auto-joiner": "^0.1.3", "ts-pattern": "^5.6.1", "update-electron-app": "^3.1.0", "utf-8-validate": "^6.0.5", diff --git a/apps/desktop/src/renderer/components/documents/document-editor.tsx b/apps/desktop/src/renderer/components/documents/document-editor.tsx index 2e0ff5a8..9eb99074 100644 --- a/apps/desktop/src/renderer/components/documents/document-editor.tsx +++ b/apps/desktop/src/renderer/components/documents/document-editor.tsx @@ -61,6 +61,7 @@ import { TrailingNode, UnderlineMark, DatabaseNode, + AutoJoiner, } from '@/renderer/editor/extensions'; import { ToolbarMenu, ActionMenu } from '@/renderer/editor/menus'; import { @@ -145,6 +146,7 @@ export const DocumentEditor = ({ DeleteControlExtension, DropcursorExtension, DatabaseNode, + AutoJoiner, CommanderExtension.configure({ commands: [ ParagraphCommand, diff --git a/apps/desktop/src/renderer/editor/extensions/auto-joiner.tsx b/apps/desktop/src/renderer/editor/extensions/auto-joiner.tsx new file mode 100644 index 00000000..c1ebacbc --- /dev/null +++ b/apps/desktop/src/renderer/editor/extensions/auto-joiner.tsx @@ -0,0 +1,3 @@ +import AutoJoiner from 'tiptap-extension-auto-joiner'; + +export { AutoJoiner }; diff --git a/apps/desktop/src/renderer/editor/extensions/index.tsx b/apps/desktop/src/renderer/editor/extensions/index.tsx index bc5452bb..0326acbd 100644 --- a/apps/desktop/src/renderer/editor/extensions/index.tsx +++ b/apps/desktop/src/renderer/editor/extensions/index.tsx @@ -35,6 +35,7 @@ import { TaskItemNode } from '@/renderer/editor/extensions/task-item'; import { TaskListNode } from '@/renderer/editor/extensions/task-list'; import { TrailingNode } from '@/renderer/editor/extensions/trailing-node'; import { DatabaseNode } from '@/renderer/editor/extensions/database'; +import { AutoJoiner } from '@/renderer/editor/extensions/auto-joiner'; export { BlockquoteNode, @@ -73,4 +74,5 @@ export { TrailingNode, UnderlineMark, DatabaseNode, + AutoJoiner, }; diff --git a/apps/desktop/src/renderer/editor/menus/action-menu.tsx b/apps/desktop/src/renderer/editor/menus/action-menu.tsx index 26275792..e9922574 100644 --- a/apps/desktop/src/renderer/editor/menus/action-menu.tsx +++ b/apps/desktop/src/renderer/editor/menus/action-menu.tsx @@ -3,7 +3,7 @@ import { GripVertical, Plus } from 'lucide-react'; import { useState, useEffect, useRef } from 'react'; import { Node as ProseMirrorNode } from '@tiptap/pm/model'; import { NodeSelection, TextSelection } from '@tiptap/pm/state'; -// @ts-ignore +// @ts-expect-error - we can just ignore this for now import { __serializeForClipboard } from '@tiptap/pm/view'; import { Editor } from '@tiptap/react'; @@ -22,7 +22,7 @@ type MenuState = { }; export const ActionMenu = ({ editor }: ActionMenuProps) => { - const view = useRef(editor?.view!); + const view = useRef(editor!.view!); const [menuState, setMenuState] = useState({ show: false, }); @@ -42,6 +42,10 @@ export const ActionMenu = ({ editor }: ActionMenuProps) => { }, [menuState.rect, menuState.domNode]); useEffect(() => { + if (editor == null) { + return; + } + const handleMouseMove = (event: MouseEvent) => { const editorBounds = view.current.dom.getBoundingClientRect(); const mouseOverEditor = @@ -74,25 +78,40 @@ export const ActionMenu = ({ editor }: ActionMenuProps) => { let currentPos = pos.pos; let pmNode = null; let domNode = null; + let nodePos = -1; + while (currentPos >= 0) { const node = view.current.state.doc.nodeAt(currentPos); - if (node?.isBlock) { - const nodeDOM = view.current.nodeDOM(currentPos) as HTMLElement; - const nodeDOMElement = - nodeDOM instanceof HTMLElement - ? nodeDOM - : ((nodeDOM as Node)?.parentElement as HTMLElement); - if (nodeDOMElement) { - const nodeRect = nodeDOMElement.getBoundingClientRect(); - // Check if the mouse is horizontally aligned with this block - if ( - event.clientX > nodeRect.left - LEFT_MARGIN && - event.clientX < nodeRect.right - ) { - pmNode = node; - domNode = nodeDOMElement; - break; - } + + if ( + !node || + !node.isBlock || + node.type.name === 'bulletList' || + node.type.name === 'orderedList' + ) { + currentPos--; + continue; + } + + const nodeDOM = view.current.nodeDOM(currentPos) as HTMLElement; + const nodeDOMElement = + nodeDOM instanceof HTMLElement + ? nodeDOM + : ((nodeDOM as Node)?.parentElement as HTMLElement); + + if (nodeDOMElement) { + const nodeRect = nodeDOMElement.getBoundingClientRect(); + + // Are we on the same horizontal axis (vertical range) as the mouse over? + const verticallyAligned = + event.clientY >= nodeRect.top && event.clientY <= nodeRect.bottom; + + if (verticallyAligned) { + pmNode = node; + domNode = nodeDOMElement; + nodePos = currentPos; + } else { + break; } } currentPos--; @@ -105,19 +124,20 @@ export const ActionMenu = ({ editor }: ActionMenuProps) => { return; } - const rect = domNode.getBoundingClientRect(); + const nodeRect = domNode.getBoundingClientRect(); + const editorRect = editor.view.dom.getBoundingClientRect(); const menuRect = DOMRect.fromRect({ - x: rect.x - 10, - y: rect.y, + x: editorRect.x - 10, + y: nodeRect.y, width: 0, - height: rect.height, + height: nodeRect.height, }); setMenuState({ show: true, pmNode, domNode, - pos: currentPos, + pos: nodePos, rect: menuRect, }); }; @@ -128,12 +148,12 @@ export const ActionMenu = ({ editor }: ActionMenuProps) => { }); }; - document.addEventListener('mousemove', handleMouseMove); - document.addEventListener('scroll', handleScroll, true); + editor.view.dom.addEventListener('mousemove', handleMouseMove); + editor.view.dom.addEventListener('scroll', handleScroll, true); return () => { - document.removeEventListener('mousemove', handleMouseMove); - document.removeEventListener('scroll', handleScroll, true); + editor.view.dom.removeEventListener('mousemove', handleMouseMove); + editor.view.dom.removeEventListener('scroll', handleScroll, true); }; }, [editor]); diff --git a/package-lock.json b/package-lock.json index 88b40b6f..9721b038 100644 --- a/package-lock.json +++ b/package-lock.json @@ -104,6 +104,7 @@ "react-router-dom": "^7.1.3", "tailwind-merge": "^2.5.5", "tailwindcss-animate": "^1.0.7", + "tiptap-extension-auto-joiner": "^0.1.3", "ts-pattern": "^5.6.1", "update-electron-app": "^3.1.0", "utf-8-validate": "^6.0.5", @@ -18889,6 +18890,12 @@ "@popperjs/core": "^2.9.0" } }, + "node_modules/tiptap-extension-auto-joiner": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/tiptap-extension-auto-joiner/-/tiptap-extension-auto-joiner-0.1.3.tgz", + "integrity": "sha512-nY3aKeCpVb2WjjVEZkLtEqxsK3KU1zGioyglMhK1sUFNjKDccOfRyz/YDKrHRAVsKJPGnk2A8VA1827iGEAXWQ==", + "license": "MIT" + }, "node_modules/tmp": { "version": "0.2.3", "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.3.tgz",