diff --git a/readme.refactoring-summary.md b/readme.refactoring-summary.md index 0d46aa6..d8443b5 100644 --- a/readme.refactoring-summary.md +++ b/readme.refactoring-summary.md @@ -28,6 +28,16 @@ 3. Added debug logging to track cursor position and content state - **Result**: Backspace now correctly deletes individual characters instead of the whole block +### Arrow Left Navigation Fix ✅ +- **Issue**: "When jumping to the previous block from the beginning of a block with arrow left, the cursor should be at the end of the previous block, not at the start" +- **Root Cause**: Browser's default focus behavior places cursor at the beginning of contenteditable elements, overriding our cursor positioning +- **Solution**: For 'end' position, set up the selection range BEFORE focusing the element: + 1. Create a range pointing to the end of content + 2. Apply the selection + 3. Then focus the element (which preserves the existing selection) + 4. Only use setCursorToEnd for empty blocks +- **Result**: Arrow left navigation now correctly places cursor at the end of the previous block + ## Completed Phases ### Phase 1: Infrastructure ✅ diff --git a/ts_web/elements/wysiwyg/blocks/text/heading.block.ts b/ts_web/elements/wysiwyg/blocks/text/heading.block.ts index 5807183..7eaa2a5 100644 --- a/ts_web/elements/wysiwyg/blocks/text/heading.block.ts +++ b/ts_web/elements/wysiwyg/blocks/text/heading.block.ts @@ -280,6 +280,22 @@ export class HeadingBlockHandler extends BaseBlockHandler { } } + /** + * Helper to get the last text node in an element + */ + private getLastTextNode(element: Node): Text | null { + if (element.nodeType === Node.TEXT_NODE) { + return element as Text; + } + + for (let i = element.childNodes.length - 1; i >= 0; i--) { + const lastText = this.getLastTextNode(element.childNodes[i]); + if (lastText) return lastText; + } + + return null; + } + // Helper methods for heading functionality (mostly the same as paragraph) getCursorPosition(element: HTMLElement, context?: any): number | null { @@ -404,19 +420,40 @@ export class HeadingBlockHandler extends BaseBlockHandler { const headingBlock = element.querySelector(`.block.heading-${this.level}`) as HTMLDivElement; if (!headingBlock) return; + // Ensure element is focusable first if (!headingBlock.hasAttribute('contenteditable')) { headingBlock.setAttribute('contenteditable', 'true'); } - // Focus the element + // For 'end' position, we need to set up selection before focus to prevent browser default + if (position === 'end' && headingBlock.textContent && headingBlock.textContent.length > 0) { + // Set up the selection first + const sel = window.getSelection(); + if (sel) { + const range = document.createRange(); + const lastNode = this.getLastTextNode(headingBlock) || headingBlock; + if (lastNode.nodeType === Node.TEXT_NODE) { + range.setStart(lastNode, lastNode.textContent?.length || 0); + range.setEnd(lastNode, lastNode.textContent?.length || 0); + } else { + range.selectNodeContents(lastNode); + range.collapse(false); + } + sel.removeAllRanges(); + sel.addRange(range); + } + } + + // Now focus the element headingBlock.focus(); - // Set cursor position after focus is established + // Set cursor position after focus is established (for non-end positions) const setCursor = () => { if (position === 'start') { this.setCursorToStart(element, context); - } else if (position === 'end') { + } else if (position === 'end' && (!headingBlock.textContent || headingBlock.textContent.length === 0)) { + // Only call setCursorToEnd for empty blocks this.setCursorToEnd(element, context); } else if (typeof position === 'number') { // Use the selection utility to set cursor position @@ -432,6 +469,13 @@ export class HeadingBlockHandler extends BaseBlockHandler { Promise.resolve().then(() => { if (document.activeElement === headingBlock || element.shadowRoot?.activeElement === headingBlock) { setCursor(); + } else { + // Try again with a small delay - sometimes focus needs more time + setTimeout(() => { + if (document.activeElement === headingBlock || element.shadowRoot?.activeElement === headingBlock) { + setCursor(); + } + }, 10); } }); } diff --git a/ts_web/elements/wysiwyg/blocks/text/paragraph.block.ts b/ts_web/elements/wysiwyg/blocks/text/paragraph.block.ts index 6a0a780..47cfb3a 100644 --- a/ts_web/elements/wysiwyg/blocks/text/paragraph.block.ts +++ b/ts_web/elements/wysiwyg/blocks/text/paragraph.block.ts @@ -246,6 +246,22 @@ export class ParagraphBlockHandler extends BaseBlockHandler { return "Type '/' for commands..."; } + /** + * Helper to get the last text node in an element + */ + private getLastTextNode(element: Node): Text | null { + if (element.nodeType === Node.TEXT_NODE) { + return element as Text; + } + + for (let i = element.childNodes.length - 1; i >= 0; i--) { + const lastText = this.getLastTextNode(element.childNodes[i]); + if (lastText) return lastText; + } + + return null; + } + // Helper methods for paragraph functionality getCursorPosition(element: HTMLElement, context?: any): number | null { @@ -376,19 +392,40 @@ export class ParagraphBlockHandler extends BaseBlockHandler { const paragraphBlock = element.querySelector('.block.paragraph') as HTMLDivElement; if (!paragraphBlock) return; + // Ensure element is focusable first if (!paragraphBlock.hasAttribute('contenteditable')) { paragraphBlock.setAttribute('contenteditable', 'true'); } - // Focus the element + // For 'end' position, we need to set up selection before focus to prevent browser default + if (position === 'end' && paragraphBlock.textContent && paragraphBlock.textContent.length > 0) { + // Set up the selection first + const sel = window.getSelection(); + if (sel) { + const range = document.createRange(); + const lastNode = this.getLastTextNode(paragraphBlock) || paragraphBlock; + if (lastNode.nodeType === Node.TEXT_NODE) { + range.setStart(lastNode, lastNode.textContent?.length || 0); + range.setEnd(lastNode, lastNode.textContent?.length || 0); + } else { + range.selectNodeContents(lastNode); + range.collapse(false); + } + sel.removeAllRanges(); + sel.addRange(range); + } + } + + // Now focus the element paragraphBlock.focus(); - // Set cursor position after focus is established + // Set cursor position after focus is established (for non-end positions) const setCursor = () => { if (position === 'start') { this.setCursorToStart(element, context); - } else if (position === 'end') { + } else if (position === 'end' && (!paragraphBlock.textContent || paragraphBlock.textContent.length === 0)) { + // Only call setCursorToEnd for empty blocks this.setCursorToEnd(element, context); } else if (typeof position === 'number') { // Use the selection utility to set cursor position @@ -404,6 +441,13 @@ export class ParagraphBlockHandler extends BaseBlockHandler { Promise.resolve().then(() => { if (document.activeElement === paragraphBlock || element.shadowRoot?.activeElement === paragraphBlock) { setCursor(); + } else { + // Try again with a small delay - sometimes focus needs more time + setTimeout(() => { + if (document.activeElement === paragraphBlock || element.shadowRoot?.activeElement === paragraphBlock) { + setCursor(); + } + }, 10); } }); } diff --git a/ts_web/elements/wysiwyg/dees-wysiwyg-block.ts b/ts_web/elements/wysiwyg/dees-wysiwyg-block.ts index 22090cf..9d20b15 100644 --- a/ts_web/elements/wysiwyg/dees-wysiwyg-block.ts +++ b/ts_web/elements/wysiwyg/dees-wysiwyg-block.ts @@ -1196,12 +1196,6 @@ export class DeesWysiwygBlock extends DeesElement { } public focusWithCursor(position: 'start' | 'end' | number = 'end'): void { - console.log('focusWithCursor called', { - blockId: this.block?.id, - blockType: this.block?.type, - position - }); - // Check if we have a registered handler for this block type const handler = BlockRegistry.getHandler(this.block.type); if (handler && handler.focusWithCursor) { diff --git a/ts_web/elements/wysiwyg/wysiwyg.keyboardhandler.ts b/ts_web/elements/wysiwyg/wysiwyg.keyboardhandler.ts index 9580276..7f4d79e 100644 --- a/ts_web/elements/wysiwyg/wysiwyg.keyboardhandler.ts +++ b/ts_web/elements/wysiwyg/wysiwyg.keyboardhandler.ts @@ -308,17 +308,6 @@ export class WysiwygKeyboardHandler { const actualContent = blockComponent.getContent ? blockComponent.getContent() : target.textContent; - console.log('Backspace handler cursor position:', { - blockId: block.id, - storedBlockContent: block.content, - actualDOMContent: actualContent, - targetTextContent: target.textContent, - cursorPos, - isAtBeginning: cursorPos === 0, - isStoredEmpty: block.content === '', - isActuallyEmpty: actualContent === '' || actualContent.trim() === '' - }); - // Check if cursor is at the beginning of the block if (cursorPos === 0) { e.preventDefault(); @@ -675,12 +664,6 @@ export class WysiwygKeyboardHandler { e.preventDefault(); const nonEditableTypes = ['divider', 'image', 'youtube', 'markdown', 'html', 'attachment']; const position = nonEditableTypes.includes(prevBlock.type) ? undefined : 'end'; - console.log('ArrowLeft: Navigating to previous block', { - currentBlockId: block.id, - prevBlockId: prevBlock.id, - prevBlockType: prevBlock.type, - focusPosition: position - }); await blockOps.focusBlock(prevBlock.id, position); } } diff --git a/ts_web/elements/wysiwyg/wysiwyg.selection.ts b/ts_web/elements/wysiwyg/wysiwyg.selection.ts index 1227148..41007f7 100644 --- a/ts_web/elements/wysiwyg/wysiwyg.selection.ts +++ b/ts_web/elements/wysiwyg/wysiwyg.selection.ts @@ -125,20 +125,9 @@ export class WysiwygSelection { // Use our Shadow DOM-aware contains method const isContained = this.containsAcrossShadowDOM(element, selectionInfo.startContainer); - console.log('getCursorPositionInElement debug:', { - element: element.tagName, - elementText: element.textContent, - selectionContainer: selectionInfo.startContainer, - selectionOffset: selectionInfo.startOffset, - isContained, - elementShadowRoot: element.getRootNode(), - selectionShadowRoot: selectionInfo.startContainer.getRootNode() - }); - if (isContained) { range.setEnd(selectionInfo.startContainer, selectionInfo.startOffset); const position = range.toString().length; - console.log('Cursor position calculated:', position); return position; } else { // Selection might be in shadow DOM or different context @@ -148,10 +137,8 @@ export class WysiwygSelection { // If the selection is at the beginning or end, handle those cases if (selectionInfo.startOffset === 0) { - console.log('Fallback: returning 0 (beginning)'); return 0; } else if (selectionInfo.startOffset === selectionText.length) { - console.log('Fallback: returning text length:', text.length); return text.length; }