import { BaseModule } from './base-module.js'; import { moduleRegistry } from './module-registry.js'; class UIInputHandler extends BaseModule { constructor() { super('ui-input-handler', 'UI Input Handler'); // Explicitly declare ui-display-handler as a dependency this.dependencies = ['ui-display-handler']; // Input elements this.inputArea = null; this.playerInput = null; this.cursor = null; this.commandHistoryElement = null; // Input state this.inputEnabled = true; this.historyIndex = -1; this.commandHistory = []; this.inputBuffer = ''; // Bind methods using the parent class bindMethods utility this.bindMethods([ 'setupInputElements', 'handlePlayerInput', 'handleInputKeyDown', 'positionCursor', 'handleKeyboardInput', 'submitCommand', 'addToHistory', 'resetCursorPosition' ]); console.log('UIInputHandler: Constructor initialized'); } async initialize() { try { this.reportProgress(0, 'Initializing UI Input Handler'); // Get display handler reference through the parent's getModule method this.displayHandler = this.getModule('ui-display-handler'); if (!this.displayHandler) { console.error('UIInputHandler: Display handler module not found'); return false; } this.reportProgress(30, 'Setting up keyboard listeners'); // Use the parent's addEventListener for automatic cleanup this.addEventListener(document, 'keydown', this.handleKeyboardInput); this.reportProgress(60, 'Setting up input elements'); this.setupInputElements(); this.reportProgress(100, 'UI Input Handler ready'); return true; } catch (error) { console.error('Error initializing UI Input Handler:', error); return false; } } /** * Handle keyboard shortcuts and input globally * @param {KeyboardEvent} event - The keyboard event */ handleKeyboardInput(event) { // Handle global keyboard shortcuts here // This is different from the input field's specific key handling // For example: Escape key to blur the input if (event.key === 'Escape') { if (document.activeElement === this.playerInput) { this.playerInput.blur(); } } } setupInputElements() { console.log("UIInputHandler: Setting up input elements in document flow"); // Find the left page - this is created by the display handler const pageLeft = document.getElementById('page_left'); if (!pageLeft) { console.error('UIInputHandler: Left page not found, cannot create input elements'); return; } // Only create choices container if it doesn't already exist let choicesContainer = document.getElementById('choices'); if (!choicesContainer) { choicesContainer = document.createElement('div'); choicesContainer.id = 'choices'; choicesContainer.className = 'container'; // Use natural document flow, not absolute positioning // Do NOT add a separator here, as it already exists in the CSS pageLeft.appendChild(choicesContainer); } // Create command history container if needed let commandHistory = document.getElementById('command_history'); if (!commandHistory) { commandHistory = document.createElement('div'); commandHistory.id = 'command_history'; choicesContainer.appendChild(commandHistory); this.commandHistoryElement = commandHistory; } else { this.commandHistoryElement = commandHistory; } // Create input container if needed let commandInput = document.getElementById('command_input'); if (!commandInput) { commandInput = document.createElement('div'); commandInput.id = 'command_input'; choicesContainer.appendChild(commandInput); } // Create input wrapper if needed let inputWrapper = commandInput.querySelector('.input-wrapper'); if (!inputWrapper) { inputWrapper = document.createElement('div'); inputWrapper.className = 'input-wrapper'; commandInput.appendChild(inputWrapper); } // Create the textarea if needed let playerInput = document.getElementById('player_input'); if (!playerInput) { playerInput = document.createElement('textarea'); playerInput.id = 'player_input'; playerInput.rows = 1; playerInput.placeholder = 'What will you do?'; playerInput.setAttribute('autocomplete', 'off'); playerInput.setAttribute('spellcheck', 'true'); // Fix horizontal scrolling by ensuring the textbox wraps text playerInput.style.overflowX = 'hidden'; playerInput.style.wordWrap = 'break-word'; playerInput.style.whiteSpace = 'pre-wrap'; inputWrapper.appendChild(playerInput); this.playerInput = playerInput; } // Create the cursor if needed let cursor = document.getElementById('cursor'); if (!cursor) { cursor = document.createElement('span'); cursor.id = 'cursor'; inputWrapper.appendChild(cursor); this.cursor = cursor; } // Set up input event handlers if (playerInput) { playerInput.addEventListener('input', this.handlePlayerInput); playerInput.addEventListener('keydown', this.handleInputKeyDown); // Auto-resize input field playerInput.addEventListener('input', () => { playerInput.style.height = 'auto'; playerInput.style.height = playerInput.scrollHeight + 'px'; }); } // Position the cursor if (playerInput && cursor) { this.positionCursor(playerInput, cursor); // Focus the input to let user start typing immediately setTimeout(() => { playerInput.focus(); }, 100); } console.log('UIInputHandler: Input elements setup complete'); } /** * Handle player input changes * @param {Event} e - Input event */ handlePlayerInput(e) { if (!this.playerInput) return; // Auto-resize the input field based on content this.playerInput.style.height = 'auto'; this.playerInput.style.height = `${this.playerInput.scrollHeight}px`; // Update the cursor position with the current input text if (this.cursor) { this.positionCursor(this.playerInput, this.cursor); } // Use the parent class dispatchEvent method instead of custom _dispatchModuleEvent this.dispatchEvent('ui:input:change', { text: this.playerInput.value }); } /** * Handle key down events in the input field * @param {KeyboardEvent} e - Keyboard event */ handleInputKeyDown(e) { if (!this.playerInput) return; // Check for Enter key if (e.key === 'Enter') { if (!e.shiftKey) { // Prevent default (new line) if not holding shift e.preventDefault(); // Submit command this.submitCommand(); } } } /** * Submit the current input as a command */ submitCommand() { if (!this.playerInput || !this.playerInput.value.trim()) return; const command = this.playerInput.value.trim(); console.log(`UIInputHandler: Submitting command: "${command}"`); this.addToHistory(command); this.dispatchEvent('ui:command', { type: 'input', text: command }); // Clear input field this.playerInput.value = ''; this.playerInput.style.height = 'auto'; // Update cursor position if (this.cursor) { this.positionCursor(this.playerInput, this.cursor); } // Focus input field this.playerInput.focus(); } /** * Add command to history * @param {string} command - Command to add to history */ addToHistory(command) { // Add to history array this.commandHistory.push(command); // Limit history size if (this.commandHistory.length > 50) { this.commandHistory.shift(); } // Reset history index this.historyIndex = -1; // Update visual history if element exists if (this.commandHistoryElement && this.commandHistoryElement.appendChild) { const historyItem = document.createElement('div'); historyItem.className = 'history-item'; historyItem.textContent = `> ${command}`; this.commandHistoryElement.appendChild(historyItem); // Limit visible history items while (this.commandHistoryElement.childElementCount > 10) { this.commandHistoryElement.removeChild(this.commandHistoryElement.firstChild); } // Scroll to bottom this.commandHistoryElement.scrollTop = this.commandHistoryElement.scrollHeight; } } /** * Resets the cursor position to the start. */ resetCursorPosition() { if (this.cursor) { this.cursor.style.left = '0px'; // Adjust top based on computed style padding or a default const computedStyle = window.getComputedStyle(this.playerInput); const paddingTop = parseFloat(computedStyle.paddingTop) || 6; this.cursor.style.top = `${paddingTop}px`; } } /** * Position cursor based on input text position * @param {HTMLTextAreaElement} inputElement - The input element * @param {HTMLElement} cursorElement - The visual cursor element */ positionCursor(inputElement, cursorElement) { if (!inputElement || !cursorElement) return; this.cursor = cursorElement; this.playerInput = inputElement; const updatePosition = () => { try { const input = this.playerInput; const cursor = this.cursor; const caretPosition = input.selectionStart || 0; const inputText = input.value; // If no text, position cursor at the beginning based on padding if (inputText.length === 0 && caretPosition === 0) { this.resetCursorPosition(); return; } // Create a temporary measurement div const div = document.createElement('div'); const style = getComputedStyle(input); // Apply relevant styles from the textarea to the div div.style.position = 'absolute'; div.style.top = '-9999px'; div.style.left = '-9999px'; div.style.width = style.width; div.style.height = 'auto'; div.style.padding = style.padding; div.style.border = style.border; div.style.fontFamily = style.fontFamily; div.style.fontSize = style.fontSize; div.style.fontWeight = style.fontWeight; div.style.lineHeight = style.lineHeight; div.style.whiteSpace = 'pre-wrap'; div.style.wordWrap = 'break-word'; div.style.boxSizing = style.boxSizing; // Create spans for text before and after the caret, and a marker span const preCaretText = document.createTextNode(inputText.substring(0, caretPosition)); const caretMarker = document.createElement('span'); caretMarker.innerHTML = ' '; // Use non-breaking space for measurement const postCaretText = document.createTextNode(inputText.substring(caretPosition)); // Append spans to the div div.appendChild(preCaretText); div.appendChild(caretMarker); div.appendChild(postCaretText); // Append div to body for measurement document.body.appendChild(div); // Get position relative to the div's content box const markerRect = caretMarker.getBoundingClientRect(); const divRect = div.getBoundingClientRect(); // Calculate position relative to the input's top-left, considering scroll const cursorLeft = markerRect.left - divRect.left; const cursorTop = markerRect.top - divRect.top - input.scrollTop; // Set cursor position cursor.style.left = `${cursorLeft}px`; cursor.style.top = `${cursorTop}px`; // Clean up the temporary div document.body.removeChild(div); } catch (error) { console.error('Error positioning cursor:', error); } }; // Update on various events inputElement.addEventListener('input', updatePosition); inputElement.addEventListener('click', updatePosition); inputElement.addEventListener('keyup', updatePosition); inputElement.addEventListener('focus', updatePosition); // Initial position update updatePosition(); } } // Create the singleton instance const uiInputHandler = new UIInputHandler(); // Register with the module registry moduleRegistry.register(uiInputHandler); // Export the module export { uiInputHandler as UIInputHandler }; // Keep a reference in window for loader system console.log('UIInputHandler: Registering with window'); window.UIInputHandler = uiInputHandler;