fix(wysiwyg): cursor position

This commit is contained in:
Juergen Kunz
2025-06-24 13:53:47 +00:00
parent 4b2178cedd
commit e0a125c9bd
2 changed files with 165 additions and 62 deletions

View File

@ -332,10 +332,8 @@ export class DeesWysiwygBlock extends DeesElement {
}
}
// Update blockElement reference for code blocks
if (this.block.type === 'code') {
this.blockElement = editableBlock;
}
// For code blocks, we use the nested editableBlock
// The blockElement getter will automatically find the right element
}
render(): TemplateResult {
@ -394,33 +392,43 @@ export class DeesWysiwygBlock extends DeesElement {
public focus(): void {
if (!this.blockElement) return;
// Get the actual editable element (might be nested for code blocks)
const editableElement = this.block?.type === 'code'
? this.shadowRoot?.querySelector('.block.code') as HTMLDivElement
: this.blockElement;
if (!editableElement) return;
// Ensure the element is focusable
if (!this.blockElement.hasAttribute('contenteditable')) {
this.blockElement.setAttribute('contenteditable', 'true');
if (!editableElement.hasAttribute('contenteditable')) {
editableElement.setAttribute('contenteditable', 'true');
}
this.blockElement.focus();
editableElement.focus();
// If focus failed, try again after a microtask
if (document.activeElement !== this.blockElement) {
if (document.activeElement !== editableElement && this.shadowRoot?.activeElement !== editableElement) {
Promise.resolve().then(() => {
this.blockElement.focus();
editableElement.focus();
});
}
}
public focusWithCursor(position: 'start' | 'end' | number = 'end'): void {
if (!this.blockElement) return;
// Get the actual editable element (might be nested for code blocks)
const editableElement = this.block?.type === 'code'
? this.shadowRoot?.querySelector('.block.code') as HTMLDivElement
: this.blockElement;
if (!editableElement) return;
// Ensure element is focusable first
if (!this.blockElement.hasAttribute('contenteditable')) {
this.blockElement.setAttribute('contenteditable', 'true');
if (!editableElement.hasAttribute('contenteditable')) {
editableElement.setAttribute('contenteditable', 'true');
}
// Focus the element
this.blockElement.focus();
editableElement.focus();
// Set cursor position after focus is established
const setCursor = () => {
@ -430,17 +438,17 @@ export class DeesWysiwygBlock extends DeesElement {
this.setCursorToEnd();
} else if (typeof position === 'number') {
// Use the new selection utility to set cursor position
WysiwygSelection.setCursorPosition(this.blockElement, position);
WysiwygSelection.setCursorPosition(editableElement, position);
}
};
// Ensure cursor is set after focus
if (document.activeElement === this.blockElement) {
if (document.activeElement === editableElement || this.shadowRoot?.activeElement === editableElement) {
setCursor();
} else {
// Wait for focus to be established
Promise.resolve().then(() => {
if (document.activeElement === this.blockElement) {
if (document.activeElement === editableElement || this.shadowRoot?.activeElement === editableElement) {
setCursor();
}
});
@ -461,44 +469,64 @@ export class DeesWysiwygBlock extends DeesElement {
}
public getContent(): string {
if (!this.blockElement) return '';
// Get the actual editable element (might be nested for code blocks)
const editableElement = this.block?.type === 'code'
? this.shadowRoot?.querySelector('.block.code') as HTMLDivElement
: this.blockElement;
if (!editableElement) return '';
if (this.block.type === 'list') {
const listItems = this.blockElement.querySelectorAll('li');
const listItems = editableElement.querySelectorAll('li');
return Array.from(listItems).map(li => li.innerHTML || '').join('\n');
} else if (this.block.type === 'code') {
return this.blockElement.textContent || '';
return editableElement.textContent || '';
} else {
return this.blockElement.innerHTML || '';
return editableElement.innerHTML || '';
}
}
public setContent(content: string): void {
if (!this.blockElement) return;
// Get the actual editable element (might be nested for code blocks)
const editableElement = this.block?.type === 'code'
? this.shadowRoot?.querySelector('.block.code') as HTMLDivElement
: this.blockElement;
if (!editableElement) return;
// Store if we have focus
const hadFocus = document.activeElement === this.blockElement;
const hadFocus = document.activeElement === editableElement || this.shadowRoot?.activeElement === editableElement;
if (this.block.type === 'list') {
this.blockElement.innerHTML = WysiwygBlocks.renderListContent(content, this.block.metadata);
editableElement.innerHTML = WysiwygBlocks.renderListContent(content, this.block.metadata);
} else if (this.block.type === 'code') {
this.blockElement.textContent = content;
editableElement.textContent = content;
} else {
this.blockElement.innerHTML = content;
editableElement.innerHTML = content;
}
// Restore focus if we had it
if (hadFocus) {
this.blockElement.focus();
editableElement.focus();
}
}
public setCursorToStart(): void {
WysiwygBlocks.setCursorToStart(this.blockElement);
const editableElement = this.block?.type === 'code'
? this.shadowRoot?.querySelector('.block.code') as HTMLDivElement
: this.blockElement;
if (editableElement) {
WysiwygBlocks.setCursorToStart(editableElement);
}
}
public setCursorToEnd(): void {
WysiwygBlocks.setCursorToEnd(this.blockElement);
const editableElement = this.block?.type === 'code'
? this.shadowRoot?.querySelector('.block.code') as HTMLDivElement
: this.blockElement;
if (editableElement) {
WysiwygBlocks.setCursorToEnd(editableElement);
}
}
public focusListItem(): void {
@ -521,50 +549,84 @@ export class DeesWysiwygBlock extends DeesElement {
blockType: this.block.type
});
// Get selection info using the new utility that handles Shadow DOM
const selectionInfo = WysiwygSelection.getSelectionInfo(this.shadowRoot!);
if (!selectionInfo) {
console.log('getSplitContent: No selection, returning all content as before');
// Direct approach: Get selection from window
const selection = window.getSelection();
if (!selection || selection.rangeCount === 0) {
console.log('getSplitContent: No selection found');
return {
before: fullContent,
after: ''
};
}
// Check if selection is within this block
if (!WysiwygSelection.isSelectionInElement(this.blockElement, this.shadowRoot!)) {
console.log('getSplitContent: Selection not in this block');
const range = selection.getRangeAt(0);
console.log('getSplitContent: Range info:', {
startContainer: range.startContainer,
startOffset: range.startOffset,
collapsed: range.collapsed,
startContainerType: range.startContainer.nodeType,
startContainerText: range.startContainer.textContent?.substring(0, 50)
});
// Check if this block element has focus or contains the selection
const activeElement = this.shadowRoot?.activeElement || document.activeElement;
const hasFocus = this.blockElement === activeElement || this.blockElement?.contains(activeElement as Node);
// For contenteditable, check if selection is in our shadow DOM
let selectionInThisBlock = false;
try {
// Walk up from the selection to see if we reach our block element
let node: Node | null = range.startContainer;
while (node) {
if (node === this.blockElement || node === this.shadowRoot) {
selectionInThisBlock = true;
break;
}
node = node.parentNode || (node as any).host; // Check shadow host too
}
} catch (e) {
console.log('Error checking selection ancestry:', e);
}
console.log('getSplitContent: Focus check:', {
hasFocus,
selectionInThisBlock,
activeElement,
blockElement: this.blockElement
});
if (!hasFocus && !selectionInThisBlock) {
console.log('getSplitContent: Block does not have focus/selection');
return null;
}
// Get cursor position as a number
const cursorPosition = WysiwygSelection.getCursorPositionInElement(this.blockElement, this.shadowRoot!);
console.log('getSplitContent: Cursor position:', {
cursorPosition,
contentLength: fullContent.length,
startContainer: selectionInfo.startContainer,
startOffset: selectionInfo.startOffset,
collapsed: selectionInfo.collapsed
});
// Get the actual editable element (might be nested for code blocks)
const editableElement = this.block.type === 'code'
? this.shadowRoot?.querySelector('.block.code') as HTMLDivElement
: this.blockElement;
if (!editableElement) {
console.log('getSplitContent: No editable element found');
return null;
}
// Handle special cases for different block types
if (this.block.type === 'code') {
// For code blocks, split text content
const fullText = this.blockElement.textContent || '';
const textNode = this.getFirstTextNode(this.blockElement);
const fullText = editableElement.textContent || '';
const textNode = this.getFirstTextNode(editableElement);
if (textNode && selectionInfo.startContainer === textNode) {
const before = fullText.substring(0, selectionInfo.startOffset);
const after = fullText.substring(selectionInfo.startOffset);
if (textNode && range.startContainer === textNode) {
const before = fullText.substring(0, range.startOffset);
const after = fullText.substring(range.startOffset);
console.log('getSplitContent: Code block split result:', {
cursorPosition,
contentLength: fullText.length,
beforeContent: before,
beforeLength: before.length,
afterContent: after,
afterLength: after.length,
startOffset: selectionInfo.startOffset
startOffset: range.startOffset
});
return { before, after };
@ -573,15 +635,38 @@ export class DeesWysiwygBlock extends DeesElement {
// For other block types, extract HTML content
try {
// If selection is not directly in our element, try to find cursor position by text
if (!editableElement.contains(range.startContainer)) {
// Simple approach: split at cursor position in text
const textContent = editableElement.textContent || '';
const cursorPos = range.startOffset; // Simplified cursor position
const beforeText = textContent.substring(0, cursorPos);
const afterText = textContent.substring(cursorPos);
console.log('Splitting by text position (fallback):', {
cursorPos,
beforeText,
afterText,
totalLength: textContent.length
});
// For now, return text-based split
return {
before: beforeText,
after: afterText
};
}
// Create a temporary range to get content before cursor
const beforeRange = document.createRange();
beforeRange.selectNodeContents(this.blockElement);
beforeRange.setEnd(selectionInfo.startContainer, selectionInfo.startOffset);
beforeRange.selectNodeContents(editableElement);
beforeRange.setEnd(range.startContainer, range.startOffset);
// Create a temporary range to get content after cursor
const afterRange = document.createRange();
afterRange.selectNodeContents(this.blockElement);
afterRange.setStart(selectionInfo.startContainer, selectionInfo.startOffset);
afterRange.selectNodeContents(editableElement);
afterRange.setStart(range.startContainer, range.startOffset);
// Clone HTML content (not extract, to avoid modifying the DOM)
const beforeContents = beforeRange.cloneContents();
@ -602,7 +687,6 @@ export class DeesWysiwygBlock extends DeesElement {
};
console.log('getSplitContent: Split result:', {
cursorPosition,
contentLength: fullContent.length,
beforeContent: result.before,
beforeLength: result.before.length,