diff --git a/src/components/Editor.css b/src/components/Editor.css index 0fea8e0b..049c1aa9 100644 --- a/src/components/Editor.css +++ b/src/components/Editor.css @@ -31,6 +31,7 @@ display: flex; justify-content: center; position: relative; + cursor: text; } /* Drag-over state: subtle border highlight */ diff --git a/src/components/Editor.test.tsx b/src/components/Editor.test.tsx index 587038bf..2ebaa1b0 100644 --- a/src/components/Editor.test.tsx +++ b/src/components/Editor.test.tsx @@ -13,6 +13,8 @@ const mockEditor = vi.hoisted(() => ({ prosemirrorView: {} as Record, blocksToHTMLLossy: vi.fn(() => ''), _tiptapEditor: { commands: { setContent: vi.fn() } }, + focus: vi.fn(), + setTextCursorPosition: vi.fn(), })) // Mock BlockNote components @@ -358,6 +360,68 @@ describe('Editor', () => { }) }) +describe('click empty editor space', () => { + it('focuses editor at end of last block when clicking empty space below content', () => { + mockEditor.focus.mockClear() + mockEditor.setTextCursorPosition.mockClear() + + render( + + ) + + const container = document.querySelector('.editor__blocknote-container') + expect(container).toBeTruthy() + + // Click directly on the container (simulates clicking empty space below content) + fireEvent.click(container!) + + expect(mockEditor.setTextCursorPosition).toHaveBeenCalledWith('1', 'end') + expect(mockEditor.focus).toHaveBeenCalled() + }) + + it('does not interfere with clicks on contenteditable elements', () => { + mockEditor.focus.mockClear() + mockEditor.setTextCursorPosition.mockClear() + + render( + + ) + + // Simulate clicking on a contenteditable child (which ProseMirror would handle) + const container = document.querySelector('.editor__blocknote-container')! + const editableDiv = document.createElement('div') + editableDiv.setAttribute('contenteditable', 'true') + container.appendChild(editableDiv) + + fireEvent.click(editableDiv) + + expect(mockEditor.setTextCursorPosition).not.toHaveBeenCalled() + // Clean up + container.removeChild(editableDiv) + }) + + it('does not focus editor when note is not editable (trashed)', () => { + mockEditor.focus.mockClear() + mockEditor.setTextCursorPosition.mockClear() + + const trashedEntry: VaultEntry = { ...mockEntry, trashed: true, trashedAt: Date.now() / 1000 } + render( + + ) + + const container = document.querySelector('.editor__blocknote-container') + expect(container).toBeTruthy() + fireEvent.click(container!) + + expect(mockEditor.setTextCursorPosition).not.toHaveBeenCalled() + expect(mockEditor.focus).not.toHaveBeenCalled() + }) +}) + describe('archived note behavior', () => { it('shows archive banner immediately when entry changes to archived (reactive)', () => { const { rerender } = render( diff --git a/src/components/SingleEditorView.tsx b/src/components/SingleEditorView.tsx index c0ca86e7..27f0751b 100644 --- a/src/components/SingleEditorView.tsx +++ b/src/components/SingleEditorView.tsx @@ -39,6 +39,17 @@ export function SingleEditorView({ editor, entries, onNavigateWikilink, onChange const onImageUrl = useInsertImageCallback(editor) const { isDragOver } = useImageDrop({ containerRef, onImageUrl, vaultPath }) + const handleContainerClick = useCallback((e: React.MouseEvent) => { + if (!editable) return + const target = e.target as HTMLElement + if (target.closest('[contenteditable="true"]')) return + const blocks = editor.document + if (blocks.length > 0) { + editor.setTextCursorPosition(blocks[blocks.length - 1].id, 'end') + } + editor.focus() + }, [editor, editable]) + useEffect(() => { _wikilinkEntriesRef.current = entries }, [entries]) @@ -94,7 +105,7 @@ export function SingleEditorView({ editor, entries, onNavigateWikilink, onChange }, [baseItems, insertWikilink, typeEntryMap]) return ( -
+
{isDragOver && (
Drop image here