From fdbf691a8d1b11811d44a814228275156994175f Mon Sep 17 00:00:00 2001 From: Sandro Date: Tue, 27 Oct 2026 21:17:12 +0100 Subject: [PATCH] feat(workspace): split bulletpoint paragraph at cursor position on Enter --- .../src/features/workspace/OutlinerBlock.tsx | 27 ++++++++++++++++--- .../src/features/workspace/OutlinerTree.tsx | 6 ++--- 2 files changed, 26 insertions(+), 7 deletions(-) diff --git a/frontend/src/features/workspace/OutlinerBlock.tsx b/frontend/src/features/workspace/OutlinerBlock.tsx index da0fbb9..db804b4 100644 --- a/frontend/src/features/workspace/OutlinerBlock.tsx +++ b/frontend/src/features/workspace/OutlinerBlock.tsx @@ -55,7 +55,7 @@ export interface OutlinerBlockProps { levelOffset?: number; onZoomBlock?: (blockId: string) => void; onUpdateBlock: (updated: DocumentBlock, immediate?: boolean) => void; - onCreateBlockBelow: (afterBlockId: string, currentContent: string) => void; + onCreateBlockBelow: (afterBlockId: string, newContent?: string) => void; onDeleteBlock: (blockId: string, deleteSubtree?: boolean) => void; onIndentBlock: (blockId: string, delta: 1 | -1) => void; onNavigateFocus: (blockId: string, direction: 'up' | 'down') => void; @@ -366,7 +366,7 @@ export const OutlinerBlock: React.FC = ({ // 2. Enter (without Shift) -> If block is completely empty: // - If level > minLevel, outdent (Shift+Tab behaviour) // - If already at minLevel and is the last block, do NOT create another empty block below - // - Otherwise create new block below + // - Otherwise split paragraph at cursor and create new block below if (e.key === 'Enter' && !e.shiftKey && block.type !== 'code') { e.preventDefault(); e.stopPropagation(); @@ -385,8 +385,27 @@ export const OutlinerBlock: React.FC = ({ } } - flushDebounce(localContent); - onCreateBlockBelowRef.current(block.id, localContent); + const start = e.currentTarget.selectionStart ?? localContent.length; + const end = e.currentTarget.selectionEnd ?? start; + let beforeText = localContent.slice(0, start); + let afterText = localContent.slice(end); + + // Clean linebreaks at the split boundary if present + if (beforeText.endsWith('\r\n')) { + beforeText = beforeText.slice(0, -2); + } else if (beforeText.endsWith('\n')) { + beforeText = beforeText.slice(0, -1); + } + if (afterText.startsWith('\r\n')) { + afterText = afterText.slice(2); + } else if (afterText.startsWith('\n')) { + afterText = afterText.slice(1); + } + + setLocalContent(beforeText); + localContentRef.current = beforeText; + flushDebounce(beforeText); + onCreateBlockBelowRef.current(block.id, afterText); return; } diff --git a/frontend/src/features/workspace/OutlinerTree.tsx b/frontend/src/features/workspace/OutlinerTree.tsx index 9a0ef79..c217670 100644 --- a/frontend/src/features/workspace/OutlinerTree.tsx +++ b/frontend/src/features/workspace/OutlinerTree.tsx @@ -196,7 +196,7 @@ export const OutlinerTree = forwardRef(({ }, [blocks, levelOffset]); const handleCreateBlockBelow = useCallback( - (afterBlockId: string, _currentContent: string) => { + (afterBlockId: string, newContent: string = '') => { if (!canEdit) return; const idx = blocks.findIndex((b) => b.id === afterBlockId); const currentBlock = idx >= 0 ? blocks[idx] : null; @@ -207,7 +207,7 @@ export const OutlinerTree = forwardRef(({ id: `blk_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 7)}`, documentId: docId, type: inheritedType, - content: '', + content: newContent, level, checked: false, collapsed: false, @@ -464,7 +464,7 @@ export const OutlinerTree = forwardRef(({ } else { const lastBlock = blocks[blocks.length - 1]; if (lastBlock.content && lastBlock.content.trim() !== '') { - handleCreateBlockBelow(lastBlock.id, lastBlock.content); + handleCreateBlockBelow(lastBlock.id, ''); } else { focusBlockInput(lastBlock.id, 'end'); }