diff --git a/ts_web/elements/wysiwyg/dees-wysiwyg-block.ts b/ts_web/elements/wysiwyg/dees-wysiwyg-block.ts index 6b9b6ef..eba814f 100644 --- a/ts_web/elements/wysiwyg/dees-wysiwyg-block.ts +++ b/ts_web/elements/wysiwyg/dees-wysiwyg-block.ts @@ -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, diff --git a/ts_web/elements/wysiwyg/wysiwyg.selection.ts b/ts_web/elements/wysiwyg/wysiwyg.selection.ts index 37d9462..41a247c 100644 --- a/ts_web/elements/wysiwyg/wysiwyg.selection.ts +++ b/ts_web/elements/wysiwyg/wysiwyg.selection.ts @@ -100,9 +100,9 @@ export class WysiwygSelection { /** * Gets cursor position relative to a specific element */ - static getCursorPositionInElement(element: Element, shadowRoot?: ShadowRoot): number | null { - const selectionInfo = shadowRoot - ? this.getSelectionInfo(shadowRoot) + static getCursorPositionInElement(element: Element, ...shadowRoots: ShadowRoot[]): number | null { + const selectionInfo = shadowRoots.length > 0 + ? this.getSelectionInfo(...shadowRoots) : this.getSelectionInfo(); if (!selectionInfo || !selectionInfo.collapsed) return null; @@ -111,9 +111,28 @@ export class WysiwygSelection { try { const range = document.createRange(); range.selectNodeContents(element); - range.setEnd(selectionInfo.startContainer, selectionInfo.startOffset); - return range.toString().length; + // Handle case where selection is in a text node that's a child of the element + if (element.contains(selectionInfo.startContainer)) { + range.setEnd(selectionInfo.startContainer, selectionInfo.startOffset); + return range.toString().length; + } else { + // Selection might be in shadow DOM or different context + // Try to find the equivalent position in the element + const text = element.textContent || ''; + const selectionText = selectionInfo.startContainer.textContent || ''; + + // If the selection is at the beginning or end, handle those cases + if (selectionInfo.startOffset === 0) { + return 0; + } else if (selectionInfo.startOffset === selectionText.length) { + return text.length; + } + + // For other cases, try to match based on text content + console.warn('Selection container not within element, using text matching fallback'); + return selectionInfo.startOffset; + } } catch (error) { console.warn('Failed to get cursor position:', error); return null;