fi(wysiwyg): fix navigation

This commit is contained in:
Juergen Kunz
2025-06-24 23:56:40 +00:00
parent 5f86fdba72
commit 0571d5bf4b
6 changed files with 104 additions and 42 deletions

View File

@ -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 ✅

View File

@ -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);
}
});
}

View File

@ -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);
}
});
}

View File

@ -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) {

View File

@ -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);
}
}

View File

@ -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;
}