Split everything up into dynamically loaded modules.

This commit is contained in:
2025-04-04 00:00:43 +00:00
parent 2f7cda4b6d
commit aa29a6fd93
32 changed files with 8768 additions and 3935 deletions
+449
View File
@@ -0,0 +1,449 @@
import { BaseModule } from './base-module.js';
import { moduleRegistry } from './module-registry.js';
import { ModuleEvent } from './base-module.js';
class UIInputHandler extends BaseModule {
constructor() {
super('ui-input-handler');
// Explicitly declare ui-display-handler as a dependency
this.dependencies = ['ui-display-handler'];
// Reference to display handler
this.displayHandler = null;
// Input elements
this.inputArea = null;
this.playerInput = null;
this.cursor = null;
this.commandHistoryElement = null; // Changed: renamed to avoid conflict
// Input state
this.inputEnabled = true;
this.historyIndex = -1;
this.commandHistory = []; // Now this is clearly the array of previous commands
this.inputBuffer = '';
// Add this method to properly dispatch custom events
this._dispatchModuleEvent = (name, detail) => {
document.dispatchEvent(new CustomEvent(name, {
detail: { moduleId: this.id, ...detail },
bubbles: true
}));
};
// Bind method contexts
this.setupInputElements = this.setupInputElements.bind(this);
this.handlePlayerInput = this.handlePlayerInput.bind(this);
this.handleInputKeyDown = this.handleInputKeyDown.bind(this);
this.positionCursor = this.positionCursor.bind(this);
this.handleKeyboardInput = this.handleKeyboardInput.bind(this);
console.log('UIInputHandler: Constructor initialized');
}
/**
* Wait for dependencies before initializing
* This ensures displayHandler is ready before we try to use it
*/
async waitForDependencies() {
try {
// Explicitly wait for the display handler to be ready
console.log('UIInputHandler: Waiting for display handler to be ready');
// Get reference to the display handler
this.displayHandler = moduleRegistry.getModule('ui-display-handler');
if (!this.displayHandler) {
console.error('UIInputHandler: Display handler dependency not found');
return false;
}
// Wait for display handler to reach FINISHED state
const displayHandlerReady = await moduleRegistry.waitForModule('ui-display-handler');
if (!displayHandlerReady) {
console.error('UIInputHandler: Display handler not ready after waiting');
return false;
}
console.log('UIInputHandler: Display handler is ready');
return true;
} catch (error) {
console.error('UIInputHandler: Error waiting for dependencies:', error);
return false;
}
}
/**
* Initialize input handler
*/
async initialize() {
this.reportProgress(0, 'Initializing UI Input Handler');
try {
// Double-check display handler reference
if (!this.displayHandler) {
this.displayHandler = moduleRegistry.getModule('ui-display-handler');
if (!this.displayHandler) {
console.error('UIInputHandler: Display handler still not available');
return false;
}
}
this.reportProgress(30, 'Setting up keyboard listeners');
// Set up keyboard event listeners
document.addEventListener('keydown', (event) => {
this.handleKeyboardInput(event);
});
this.reportProgress(60, 'Setting up input elements');
// Set 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; // Changed: store in renamed property
} else {
this.commandHistoryElement = commandHistory; // Changed: store in renamed property
}
// 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);
}
// Dispatch event using the properly defined method
this._dispatchModuleEvent('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}"`);
// Add command to history
this.addToHistory(command);
// Dispatch command event
this._dispatchModuleEvent('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;