diff --git a/readme.refactoring-summary.md b/readme.refactoring-summary.md index 3e5f602..0d46aa6 100644 --- a/readme.refactoring-summary.md +++ b/readme.refactoring-summary.md @@ -8,6 +8,26 @@ - **Solution**: Changed the selector to be more specific: `.block.${blockType}` which ensures the correct element is found for each block type - **Result**: All block types now highlight consistently when selected +### Enter Key Block Creation Fix ✅ +- **Issue**: "When pressing enter and jumping to new block then typing something: The cursor is not at the beginning of the new block and there is content" +- **Root Cause**: Block handlers were rendering content with template syntax `${block.content || ''}` in their render methods, which violates the static HTML principle +- **Solution**: + - Removed all `${block.content}` from render methods in paragraph, heading, quote, and code block handlers + - Content is now set programmatically in the setup() method only when needed + - Fixed `setCursorToStart` and `setCursorToEnd` to always find elements fresh instead of relying on cached `blockElement` +- **Result**: New empty blocks remain truly empty, cursor positioning works correctly + +### Backspace Key Deletion Fix ✅ +- **Issue**: "After typing in a new block, pressing backspace deletes the whole block instead of just the last character" +- **Root Cause**: + 1. `getCursorPositionInElement` was using `element.contains()` which doesn't work across Shadow DOM boundaries + 2. The backspace handler was checking `block.content === ''` which only contains the stored content, not the actual DOM content +- **Solution**: + 1. Fixed `getCursorPositionInElement` to use `containsAcrossShadowDOM` for proper Shadow DOM support + 2. Updated backspace handler to get actual content from DOM using `blockComponent.getContent()` instead of relying on stored `block.content` + 3. Added debug logging to track cursor position and content state +- **Result**: Backspace now correctly deletes individual characters instead of the whole block + ## Completed Phases ### Phase 1: Infrastructure ✅ diff --git a/ts_web/elements/wysiwyg/blocks/text/code.block.ts b/ts_web/elements/wysiwyg/blocks/text/code.block.ts index 7e37df1..11ee974 100644 --- a/ts_web/elements/wysiwyg/blocks/text/code.block.ts +++ b/ts_web/elements/wysiwyg/blocks/text/code.block.ts @@ -25,7 +25,7 @@ export class CodeBlockHandler extends BaseBlockHandler { data-block-id="${block.id}" data-block-type="${block.type}" spellcheck="false" - >${block.content || ''} + > `; } diff --git a/ts_web/elements/wysiwyg/blocks/text/heading.block.ts b/ts_web/elements/wysiwyg/blocks/text/heading.block.ts index 9358f75..5807183 100644 --- a/ts_web/elements/wysiwyg/blocks/text/heading.block.ts +++ b/ts_web/elements/wysiwyg/blocks/text/heading.block.ts @@ -32,7 +32,7 @@ export class HeadingBlockHandler extends BaseBlockHandler { data-placeholder="${placeholder}" data-block-id="${block.id}" data-block-type="${block.type}" - >${block.content || ''} + > `; } diff --git a/ts_web/elements/wysiwyg/blocks/text/paragraph.block.ts b/ts_web/elements/wysiwyg/blocks/text/paragraph.block.ts index 12d37bf..6a0a780 100644 --- a/ts_web/elements/wysiwyg/blocks/text/paragraph.block.ts +++ b/ts_web/elements/wysiwyg/blocks/text/paragraph.block.ts @@ -25,7 +25,7 @@ export class ParagraphBlockHandler extends BaseBlockHandler { data-placeholder="${placeholder}" data-block-id="${block.id}" data-block-type="${block.type}" - >${block.content || ''} + > `; } diff --git a/ts_web/elements/wysiwyg/blocks/text/quote.block.ts b/ts_web/elements/wysiwyg/blocks/text/quote.block.ts index 3fc91fa..6ab2110 100644 --- a/ts_web/elements/wysiwyg/blocks/text/quote.block.ts +++ b/ts_web/elements/wysiwyg/blocks/text/quote.block.ts @@ -25,7 +25,7 @@ export class QuoteBlockHandler extends BaseBlockHandler { data-placeholder="${placeholder}" data-block-id="${block.id}" data-block-type="${block.type}" - >${block.content || ''} + > `; } diff --git a/ts_web/elements/wysiwyg/dees-wysiwyg-block.ts b/ts_web/elements/wysiwyg/dees-wysiwyg-block.ts index 13e530b..22090cf 100644 --- a/ts_web/elements/wysiwyg/dees-wysiwyg-block.ts +++ b/ts_web/elements/wysiwyg/dees-wysiwyg-block.ts @@ -1196,6 +1196,12 @@ 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) { @@ -1389,9 +1395,10 @@ export class DeesWysiwygBlock extends DeesElement { return handler.setCursorToStart(container, context); } + // Always find the element fresh, don't rely on cached blockElement const editableElement = this.block?.type === 'code' ? this.shadowRoot?.querySelector('.block.code') as HTMLDivElement - : this.blockElement; + : this.shadowRoot?.querySelector('.block') as HTMLDivElement; if (editableElement) { WysiwygBlocks.setCursorToStart(editableElement); } @@ -1406,9 +1413,10 @@ export class DeesWysiwygBlock extends DeesElement { return handler.setCursorToEnd(container, context); } + // Always find the element fresh, don't rely on cached blockElement const editableElement = this.block?.type === 'code' ? this.shadowRoot?.querySelector('.block.code') as HTMLDivElement - : this.blockElement; + : this.shadowRoot?.querySelector('.block') as HTMLDivElement; if (editableElement) { WysiwygBlocks.setCursorToEnd(editableElement); } diff --git a/ts_web/elements/wysiwyg/wysiwyg.keyboardhandler.ts b/ts_web/elements/wysiwyg/wysiwyg.keyboardhandler.ts index 561584a..9580276 100644 --- a/ts_web/elements/wysiwyg/wysiwyg.keyboardhandler.ts +++ b/ts_web/elements/wysiwyg/wysiwyg.keyboardhandler.ts @@ -306,6 +306,19 @@ export class WysiwygKeyboardHandler { const cursorPos = WysiwygSelection.getCursorPositionInElement(target, ...shadowRoots); + 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(); @@ -335,7 +348,8 @@ export class WysiwygKeyboardHandler { if (block.type === 'code' && prevBlock.type !== 'code') { // Can't merge code into non-code block - if (block.content === '') { + const actualContent = blockComponent.getContent ? blockComponent.getContent() : block.content; + if (actualContent === '' || actualContent.trim() === '') { blockOps.removeBlock(block.id); await blockOps.focusBlock(prevBlock.id, 'end'); } @@ -376,16 +390,21 @@ export class WysiwygKeyboardHandler { // Focus previous block at merge point await blockOps.focusBlock(prevBlock.id, mergePoint); } - } else if (block.content === '' && this.component.blocks.length > 1) { - // Empty block - just remove it - e.preventDefault(); - const prevBlock = blockOps.getPreviousBlock(block.id); + } else if (this.component.blocks.length > 1) { + // Check if block is actually empty by getting current content from DOM + const currentContent = blockComponent.getContent ? blockComponent.getContent() : block.content; - if (prevBlock) { - blockOps.removeBlock(block.id); + if (currentContent === '' || currentContent.trim() === '') { + // Empty block - just remove it + e.preventDefault(); + const prevBlock = blockOps.getPreviousBlock(block.id); - if (prevBlock.type !== 'divider') { - await blockOps.focusBlock(prevBlock.id, 'end'); + if (prevBlock) { + blockOps.removeBlock(block.id); + + if (prevBlock.type !== 'divider') { + await blockOps.focusBlock(prevBlock.id, 'end'); + } } } } @@ -655,7 +674,14 @@ export class WysiwygKeyboardHandler { if (prevBlock) { e.preventDefault(); const nonEditableTypes = ['divider', 'image', 'youtube', 'markdown', 'html', 'attachment']; - await blockOps.focusBlock(prevBlock.id, nonEditableTypes.includes(prevBlock.type) ? undefined : 'end'); + 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); } } // Otherwise, let the browser handle normal left arrow navigation diff --git a/ts_web/elements/wysiwyg/wysiwyg.selection.ts b/ts_web/elements/wysiwyg/wysiwyg.selection.ts index 657029b..1227148 100644 --- a/ts_web/elements/wysiwyg/wysiwyg.selection.ts +++ b/ts_web/elements/wysiwyg/wysiwyg.selection.ts @@ -122,9 +122,24 @@ export class WysiwygSelection { range.selectNodeContents(element); // Handle case where selection is in a text node that's a child of the element - if (element.contains(selectionInfo.startContainer)) { + // 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); - return range.toString().length; + const position = range.toString().length; + console.log('Cursor position calculated:', position); + return position; } else { // Selection might be in shadow DOM or different context // Try to find the equivalent position in the element @@ -133,8 +148,10 @@ 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; }