diff --git a/ts_web/elements/wysiwyg/dees-input-wysiwyg.ts b/ts_web/elements/wysiwyg/dees-input-wysiwyg.ts index 23d94f6..07e7c10 100644 --- a/ts_web/elements/wysiwyg/dees-input-wysiwyg.ts +++ b/ts_web/elements/wysiwyg/dees-input-wysiwyg.ts @@ -77,14 +77,6 @@ export class DeesInputWysiwyg extends DeesInputBase { public editorContentRef: HTMLDivElement; public isComposing: boolean = false; - private selectionChangeTimeout: any; - private selectionChangeHandler = () => { - // Throttle selection change events - if (this.selectionChangeTimeout) { - clearTimeout(this.selectionChangeTimeout); - } - this.selectionChangeTimeout = setTimeout(() => this.handleSelectionChange(), 50); - }; // Handler instances public blockOperations: WysiwygBlockOperations; @@ -115,8 +107,7 @@ export class DeesInputWysiwyg extends DeesInputBase { async disconnectedCallback() { await super.disconnectedCallback(); - // Remove selection listener - document.removeEventListener('selectionchange', this.selectionChangeHandler); + // Selection listeners are now handled at block level // Clean up handlers this.inputHandler?.destroy(); // Clean up blur timeout @@ -124,56 +115,61 @@ export class DeesInputWysiwyg extends DeesInputBase { clearTimeout(this.blurTimeout); this.blurTimeout = null; } - // Clean up selection change timeout - if (this.selectionChangeTimeout) { - clearTimeout(this.selectionChangeTimeout); - this.selectionChangeTimeout = null; - } } async firstUpdated() { this.updateValue(); this.editorContentRef = this.shadowRoot!.querySelector('.editor-content') as HTMLDivElement; - // Add global selection listener - console.log('Adding selectionchange listener'); - document.addEventListener('selectionchange', this.selectionChangeHandler); - - // Also add listener to our shadow root if supported - if (this.shadowRoot && 'addEventListener' in this.shadowRoot) { - this.shadowRoot.addEventListener('selectionchange', this.selectionChangeHandler); - } + // We now rely on block-level selection detection + // No global selection listener needed // Listen for custom selection events from blocks this.addEventListener('block-text-selected', (e: CustomEvent) => { console.log('Received block-text-selected event:', e.detail); - if (!this.slashMenu.visible) { - if (e.detail.hasSelection && e.detail.text.length > 0) { - this.selectedText = e.detail.text; + if (!this.slashMenu.visible && e.detail.hasSelection && e.detail.text.length > 0) { + this.selectedText = e.detail.text; + + // Use the rect from the event if available + if (e.detail.rect) { + const coords = { + x: e.detail.rect.left + (e.detail.rect.width / 2), + y: Math.max(45, e.detail.rect.top - 45) + }; - // Use the rect from the event if available - if (e.detail.rect) { - const coords = { - x: e.detail.rect.left + (e.detail.rect.width / 2), - y: Math.max(45, e.detail.rect.top - 45) - }; - - // Show the formatting menu at the calculated position - this.formattingMenu.show( - coords, - async (command: string) => await this.applyFormat(command) - ); - } else { - this.updateFormattingMenuPosition(); - } - } else { - // Clear selection - this.hideFormattingMenu(); + console.log('Showing formatting menu at:', coords); + + // Show the formatting menu at the calculated position + this.formattingMenu.show( + coords, + async (command: string) => await this.applyFormat(command) + ); } } }); + // Hide formatting menu when clicking outside + document.addEventListener('mousedown', (e) => { + // Check if click is on the formatting menu itself + const formattingMenuElement = this.formattingMenu.shadowRoot?.querySelector('.formatting-menu'); + if (formattingMenuElement && formattingMenuElement.contains(e.target as Node)) { + return; + } + + // Check if we have an active selection + const selection = window.getSelection(); + if (selection && selection.toString().trim().length > 0) { + // Don't hide if we still have a selection + return; + } + + // Hide the menu + if (this.formattingMenu.visible) { + this.hideFormattingMenu(); + } + }); + // Add global keyboard listener for undo/redo this.addEventListener('keydown', (e: KeyboardEvent) => { // Check if the event is from within our editor @@ -727,133 +723,6 @@ export class DeesInputWysiwyg extends DeesInputBase { // Let the block component handle selection via custom event } - private handleSelectionChange(): void { - console.log('=== handleSelectionChange called ==='); - - const selection = window.getSelection(); - if (!selection || selection.rangeCount === 0) { - if (this.formattingMenu.visible) { - console.log('No selection, hiding menu'); - this.hideFormattingMenu(); - } - return; - } - - const selectedText = selection.toString().trim(); - console.log('Selected text:', selectedText); - - if (selectedText.length === 0) { - if (this.formattingMenu.visible) { - console.log('No text selected, hiding menu'); - this.hideFormattingMenu(); - } - return; - } - - // Get all shadow roots in our component tree - const shadowRoots: ShadowRoot[] = []; - if (this.shadowRoot) shadowRoots.push(this.shadowRoot); - - // Find all block shadow roots - const blockWrappers = this.shadowRoot?.querySelectorAll('.block-wrapper'); - console.log('Found block wrappers:', blockWrappers?.length || 0); - - blockWrappers?.forEach(wrapper => { - const blockComponent = wrapper.querySelector('dees-wysiwyg-block'); - if (blockComponent?.shadowRoot) { - shadowRoots.push(blockComponent.shadowRoot); - } - }); - - console.log('Shadow roots collected:', shadowRoots.length); - - // Try using getComposedRanges if available - let ranges: Range[] | StaticRange[] = []; - let selectionInEditor = false; - - if ('getComposedRanges' in selection && typeof selection.getComposedRanges === 'function') { - console.log('Using getComposedRanges with shadow roots'); - try { - // According to MDN, pass shadow roots in options object - ranges = selection.getComposedRanges({ shadowRoots }); - console.log('getComposedRanges returned', ranges.length, 'ranges'); - - if (ranges.length > 0) { - const range = ranges[0]; - // Check if the range is within our editor - selectionInEditor = this.isRangeInEditor(range); - } - } catch (error) { - console.warn('getComposedRanges failed:', error); - } - } - - // Fallback to regular selection API - if (ranges.length === 0 && selection.rangeCount > 0) { - console.log('Falling back to getRangeAt'); - const range = selection.getRangeAt(0); - ranges = [range]; - selectionInEditor = this.isRangeInEditor(range); - } - - console.log('Selection in editor:', selectionInEditor); - - if (selectionInEditor && selectedText !== this.selectedText) { - console.log('✅ Text selected in editor:', selectedText); - this.selectedText = selectedText; - this.updateFormattingMenuPosition(); - } else if (!selectionInEditor && this.formattingMenu.visible) { - console.log('Selection not in editor, hiding menu'); - this.hideFormattingMenu(); - } - } - - private isRangeInEditor(range: Range | StaticRange): boolean { - // Check if the selection is within one of our blocks - const blockWrappers = this.shadowRoot?.querySelectorAll('.block-wrapper'); - if (!blockWrappers) return false; - - // Check each block - for (let i = 0; i < blockWrappers.length; i++) { - const wrapper = blockWrappers[i]; - const blockComponent = wrapper.querySelector('dees-wysiwyg-block'); - if (blockComponent?.shadowRoot) { - const editableElements = blockComponent.shadowRoot.querySelectorAll('.block, .block.code'); - for (let j = 0; j < editableElements.length; j++) { - const elem = editableElements[j]; - if (elem) { - // For StaticRange, we need to check differently - if ('startContainer' in range) { - // Check if the range nodes are within this element - const startInElement = this.isNodeInElement(range.startContainer, elem); - const endInElement = this.isNodeInElement(range.endContainer, elem); - - if (startInElement || endInElement) { - console.log('Selection found in block:', wrapper.getAttribute('data-block-id')); - return true; - } - } - } - } - } - } - - console.log('Selection not in any block. Range:', range); - return false; - } - - /** - * Check if a node is within an element (handles shadow DOM) - */ - private isNodeInElement(node: Node, element: Element): boolean { - let current: Node | null = node; - while (current) { - if (current === element) return true; - // Walk up the tree, including shadow host if in shadow DOM - current = current.parentNode || (current as any).host; - } - return false; - } private updateFormattingMenuPosition(): void { diff --git a/ts_web/elements/wysiwyg/dees-wysiwyg-block.ts b/ts_web/elements/wysiwyg/dees-wysiwyg-block.ts index 404c211..f64f28a 100644 --- a/ts_web/elements/wysiwyg/dees-wysiwyg-block.ts +++ b/ts_web/elements/wysiwyg/dees-wysiwyg-block.ts @@ -349,8 +349,10 @@ export class DeesWysiwygBlock extends DeesElement { console.log('Cursor position after mouseup:', pos); } - // Check for text selection - this.checkForTextSelection(); + // Check for text selection with a longer delay + setTimeout(() => { + this.checkForTextSelection(); + }, 50); }, 0); this.handleMouseUp(e); @@ -368,6 +370,31 @@ export class DeesWysiwygBlock extends DeesElement { }, 0); }); + // Add select event listener + editableBlock.addEventListener('selectstart', () => { + console.log('Selection started in block'); + }); + + // Listen for selection changes with a mutation observer + let selectionCheckTimeout: any = null; + const checkSelectionDebounced = () => { + if (selectionCheckTimeout) clearTimeout(selectionCheckTimeout); + selectionCheckTimeout = setTimeout(() => { + this.checkForTextSelection(); + }, 100); + }; + + // Check selection on various events + editableBlock.addEventListener('mouseup', checkSelectionDebounced); + editableBlock.addEventListener('keyup', checkSelectionDebounced); + document.addEventListener('selectionchange', () => { + // Check if this block has focus + if (document.activeElement === editableBlock || + this.shadowRoot?.activeElement === editableBlock) { + checkSelectionDebounced(); + } + }); + // Set initial content if needed if (this.block.content) { if (this.block.type === 'code') { @@ -798,57 +825,51 @@ export class DeesWysiwygBlock extends DeesElement { const editableElement = this.block?.type === 'code' ? this.shadowRoot?.querySelector('.block.code') as HTMLDivElement : this.shadowRoot?.querySelector('.block') as HTMLDivElement; - if (!editableElement) return; - - const selection = window.getSelection(); - if (!selection || selection.rangeCount === 0) { - // Dispatch event to clear selection - this.dispatchEvent(new CustomEvent('block-text-selected', { - detail: { - text: '', - blockId: this.block.id, - hasSelection: false - }, - bubbles: true, - composed: true - })); + if (!editableElement) { + console.log('checkForTextSelection: No editable element found'); return; } - const selectedText = selection.toString().trim(); + const selection = window.getSelection(); + if (!selection || selection.rangeCount === 0) { + console.log('checkForTextSelection: No selection or range count is 0'); + return; + } + + const selectedText = selection.toString(); + console.log('checkForTextSelection: Selected text raw:', selectedText, 'length:', selectedText.length); // Only proceed if we have selected text - if (selectedText.length === 0) { - // Dispatch event to clear selection - this.dispatchEvent(new CustomEvent('block-text-selected', { - detail: { - text: '', - blockId: this.block.id, - hasSelection: false - }, - bubbles: true, - composed: true - })); + if (selectedText.trim().length === 0) { + console.log('checkForTextSelection: Selected text is empty after trim'); return; } // Check if the selection is within this block const range = selection.getRangeAt(0); + console.log('checkForTextSelection: Range:', { + startContainer: range.startContainer, + endContainer: range.endContainer, + collapsed: range.collapsed + }); // Check if both start and end are within our editable element const startInBlock = editableElement.contains(range.startContainer); const endInBlock = editableElement.contains(range.endContainer); + console.log('checkForTextSelection: Start in block:', startInBlock, 'End in block:', endInBlock); + if (startInBlock && endInBlock) { - console.log('Block detected text selection:', selectedText); + console.log('✅ Block detected text selection:', selectedText.trim()); // Get the bounding rect of the selection const rect = range.getBoundingClientRect(); + console.log('Selection rect:', rect); // Dispatch event to parent with selection details this.dispatchEvent(new CustomEvent('block-text-selected', { detail: { - text: selectedText, + text: selectedText.trim(), blockId: this.block.id, range: range, rect: rect, @@ -857,6 +878,8 @@ export class DeesWysiwygBlock extends DeesElement { bubbles: true, composed: true })); + } else { + console.log('checkForTextSelection: Selection not contained in block'); } } } \ No newline at end of file