import { type IBlock } from './wysiwyg.types.js'; import { WysiwygShortcuts } from './wysiwyg.shortcuts.js'; import { WysiwygBlocks } from './wysiwyg.blocks.js'; import { WysiwygBlockOperations } from './wysiwyg.blockoperations.js'; export class WysiwygInputHandler { private component: any; private saveTimeout: any = null; constructor(component: any) { this.component = component; } /** * Handles input events for blocks */ handleBlockInput(e: InputEvent, block: IBlock): void { if (this.component.isComposing) return; const target = e.target as HTMLDivElement; const textContent = target.textContent || ''; // Update block content based on type this.updateBlockContent(block, target); // Check for block type transformations const detectedType = this.detectBlockTypeIntent(textContent); if (detectedType && detectedType.type !== block.type) { e.preventDefault(); this.handleBlockTransformation(block, detectedType, target); return; } // Handle slash commands this.handleSlashCommand(textContent, target); // Schedule auto-save this.scheduleAutoSave(); } /** * Updates block content based on its type */ private updateBlockContent(block: IBlock, target: HTMLDivElement): void { // Get the block component for proper content extraction const wrapperElement = target.closest('.block-wrapper'); const blockComponent = wrapperElement?.querySelector('dees-wysiwyg-block') as any; if (blockComponent) { // Use the block component's getContent method for consistency block.content = blockComponent.getContent(); // Update list metadata if needed if (block.type === 'list') { const listElement = target.querySelector('ol, ul'); if (listElement) { block.metadata = { listType: listElement.tagName.toLowerCase() === 'ol' ? 'ordered' : 'bullet' }; } } } else { // Fallback if block component not found if (block.type === 'list') { const listItems = target.querySelectorAll('li'); // Use innerHTML to preserve formatting block.content = Array.from(listItems).map(li => li.innerHTML || '').join('\n'); const listElement = target.querySelector('ol, ul'); if (listElement) { block.metadata = { listType: listElement.tagName.toLowerCase() === 'ol' ? 'ordered' : 'bullet' }; } } else if (block.type === 'code') { block.content = target.textContent || ''; } else { block.content = target.innerHTML || ''; } } } /** * Detects if the user is trying to create a specific block type */ private detectBlockTypeIntent(content: string): { type: IBlock['type'], listType?: 'bullet' | 'ordered' } | null { // Check heading patterns const headingResult = WysiwygShortcuts.checkHeadingShortcut(content); if (headingResult) { return headingResult; } // Check list patterns const listResult = WysiwygShortcuts.checkListShortcut(content); if (listResult) { return listResult; } // Check quote pattern if (WysiwygShortcuts.checkQuoteShortcut(content)) { return { type: 'quote' }; } // Check code pattern if (WysiwygShortcuts.checkCodeShortcut(content)) { return { type: 'code' }; } // Check divider pattern if (WysiwygShortcuts.checkDividerShortcut(content)) { return { type: 'divider' }; } return null; } /** * Handles block type transformation */ private async handleBlockTransformation( block: IBlock, detectedType: { type: IBlock['type'], listType?: 'bullet' | 'ordered' }, target: HTMLDivElement ): Promise { const blockOps = this.component.blockOperations; if (detectedType.type === 'list') { block.type = 'list'; block.content = ''; block.metadata = { listType: detectedType.listType }; const listTag = detectedType.listType === 'ordered' ? 'ol' : 'ul'; target.innerHTML = `<${listTag}>
  • `; this.component.updateValue(); this.component.requestUpdate(); setTimeout(() => { WysiwygBlocks.focusListItem(target); }, 0); } else if (detectedType.type === 'divider') { block.type = 'divider'; block.content = ' '; const newBlock = blockOps.createBlock(); blockOps.insertBlockAfter(block, newBlock); this.component.updateValue(); this.component.requestUpdate(); } else if (detectedType.type === 'code') { const language = await this.component.showLanguageSelectionModal(); if (language) { block.type = 'code'; block.content = ''; block.metadata = { language }; target.textContent = ''; this.component.updateValue(); this.component.requestUpdate(); } } else { block.type = detectedType.type; block.content = ''; target.textContent = ''; this.component.updateValue(); this.component.requestUpdate(); } } /** * Handles slash command detection and menu display */ private handleSlashCommand(textContent: string, target: HTMLDivElement): void { const slashMenu = this.component.slashMenu; const isSlashMenuVisible = slashMenu && slashMenu.visible; if (textContent === '/' || (textContent.startsWith('/') && isSlashMenuVisible)) { if (!isSlashMenuVisible && textContent === '/') { // Get position for menu based on cursor location const rect = this.getCaretCoordinates(target); // Show the slash menu at the cursor position slashMenu.show( { x: rect.left, y: rect.bottom + 4 }, (type: string) => { this.component.insertBlock(type); } ); // Ensure the block maintains focus requestAnimationFrame(() => { if (document.activeElement !== target) { target.focus(); } }); } // Update filter if (slashMenu) { slashMenu.updateFilter(textContent.slice(1)); } } else if (!textContent.startsWith('/')) { this.component.closeSlashMenu(); } } /** * Gets the coordinates of the caret/cursor */ private getCaretCoordinates(element: HTMLElement): DOMRect { const selection = window.getSelection(); if (selection && selection.rangeCount > 0) { const range = selection.getRangeAt(0); const rect = range.getBoundingClientRect(); if (rect.width > 0 || rect.height > 0) { return rect; } } // Fallback to element position return element.getBoundingClientRect(); } /** * Schedules auto-save after a delay */ private scheduleAutoSave(): void { if (this.saveTimeout) { clearTimeout(this.saveTimeout); } // Don't auto-save if slash menu is open if (this.component.slashMenu && this.component.slashMenu.visible) { return; } this.saveTimeout = setTimeout(() => { this.component.updateValue(); }, 1000); } /** * Cleans up resources */ destroy(): void { if (this.saveTimeout) { clearTimeout(this.saveTimeout); } } }