294 lines
9.1 KiB
JavaScript
294 lines
9.1 KiB
JavaScript
/**
|
|
* Input Handler Module
|
|
* Manages the multi-line text input field with a custom cursor.
|
|
*/
|
|
class InputHandler {
|
|
constructor(inputId = 'player_input', cursorId = 'cursor') {
|
|
this.playerInput = document.getElementById(inputId);
|
|
this.cursor = document.getElementById(cursorId);
|
|
this.commandInputContainer = document.getElementById('command_input'); // Assuming this container exists
|
|
|
|
if (!this.playerInput || !this.cursor || !this.commandInputContainer) {
|
|
console.error('InputHandler: Required DOM elements not found.');
|
|
return;
|
|
}
|
|
|
|
this.commandSubmitCallback = null; // Callback for when a command is submitted
|
|
|
|
this.bindEvents();
|
|
this.adjustTextareaHeight(); // Initial adjustment
|
|
this.updateCursorPosition(); // Initial position
|
|
|
|
// Setup handler for window load event to ensure proper initialization
|
|
window.addEventListener('load', () => {
|
|
console.log('InputHandler: Window loaded, adjusting text area height and cursor position');
|
|
this.adjustTextareaHeight();
|
|
this.updateCursorPosition();
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Register a callback function to be called when a command is submitted.
|
|
* @param {function(string)} callback - The function to call with the command text.
|
|
*/
|
|
onCommandSubmit(callback) {
|
|
this.commandSubmitCallback = callback;
|
|
}
|
|
|
|
/**
|
|
* Bind event handlers to the input element.
|
|
*/
|
|
bindEvents() {
|
|
// Submit command on Enter key without Shift
|
|
this.playerInput.addEventListener('keydown', (e) => {
|
|
if (e.key === 'Enter' && !e.shiftKey) {
|
|
e.preventDefault(); // Prevent default to avoid newline
|
|
this.submitCommand();
|
|
}
|
|
// Allow Shift+Enter for new lines (default behavior)
|
|
});
|
|
|
|
// Auto-resize textarea and update cursor on input
|
|
this.playerInput.addEventListener('input', () => {
|
|
this.adjustTextareaHeight();
|
|
this.updateCursorPosition();
|
|
});
|
|
|
|
// Update cursor on various events
|
|
this.playerInput.addEventListener('click', this.updateCursorPosition.bind(this));
|
|
this.playerInput.addEventListener('keyup', this.updateCursorPosition.bind(this));
|
|
|
|
// Show/hide cursor on focus/blur
|
|
this.playerInput.addEventListener('focus', () => {
|
|
if (this.cursor) this.cursor.style.opacity = '1';
|
|
this.updateCursorPosition();
|
|
});
|
|
this.playerInput.addEventListener('blur', () => {
|
|
if (this.cursor) this.cursor.style.opacity = '0';
|
|
});
|
|
|
|
// Handle paste events
|
|
this.playerInput.addEventListener('paste', () => {
|
|
// Use setTimeout to let the paste complete before adjusting
|
|
setTimeout(() => {
|
|
this.adjustTextareaHeight();
|
|
this.updateCursorPosition();
|
|
}, 10);
|
|
});
|
|
|
|
// Handle window resize
|
|
window.addEventListener('resize', () => {
|
|
this.adjustTextareaHeight();
|
|
this.updateCursorPosition();
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Submit the current command.
|
|
*/
|
|
submitCommand() {
|
|
const command = this.playerInput.value.trim();
|
|
if (command === '' || !this.commandSubmitCallback) return;
|
|
|
|
// Fade out the input field container
|
|
if (this.commandInputContainer) {
|
|
this.commandInputContainer.classList.add('fading');
|
|
}
|
|
|
|
// Disable input temporarily
|
|
this.playerInput.disabled = true;
|
|
|
|
// Call the registered callback
|
|
this.commandSubmitCallback(command);
|
|
|
|
// Clear input
|
|
this.clearInput();
|
|
}
|
|
|
|
/**
|
|
* Clears the input field and resets its state.
|
|
*/
|
|
clearInput() {
|
|
this.playerInput.value = '';
|
|
this.resetCursorPosition();
|
|
this.adjustTextareaHeight();
|
|
}
|
|
|
|
/**
|
|
* Re-enables the input field after a command submission or response.
|
|
*/
|
|
enableInput() {
|
|
if (this.commandInputContainer) {
|
|
// Remove fading class and add fade-in animation
|
|
this.commandInputContainer.classList.remove('fading');
|
|
this.commandInputContainer.classList.add('fade-in-input');
|
|
|
|
// Remove animation class after it completes
|
|
setTimeout(() => {
|
|
if (this.commandInputContainer) {
|
|
this.commandInputContainer.classList.remove('fade-in-input');
|
|
}
|
|
}, 500); // Match CSS animation duration
|
|
}
|
|
|
|
this.playerInput.disabled = false;
|
|
this.focus();
|
|
}
|
|
|
|
/**
|
|
* Focuses the input field.
|
|
*/
|
|
focus() {
|
|
this.playerInput.focus();
|
|
// Ensure cursor is visible and positioned correctly after focus
|
|
setTimeout(() => {
|
|
if (this.cursor) this.cursor.style.opacity = '1';
|
|
this.updateCursorPosition();
|
|
}, 10);
|
|
}
|
|
|
|
/**
|
|
* Gets the current value of the input field.
|
|
* @returns {string} The input text.
|
|
*/
|
|
getValue() {
|
|
return this.playerInput.value;
|
|
}
|
|
|
|
/**
|
|
* Sets the value of the input field.
|
|
* @param {string} value - The text to set.
|
|
*/
|
|
setValue(value) {
|
|
this.playerInput.value = value;
|
|
this.adjustTextareaHeight();
|
|
this.updateCursorPosition();
|
|
this.focus(); // Focus after setting value
|
|
}
|
|
|
|
/**
|
|
* 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`;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Update the custom cursor position based on input text and caret position.
|
|
* Uses a temporary div for accurate measurement.
|
|
*/
|
|
updateCursorPosition() {
|
|
if (!this.cursor || !this.playerInput) return;
|
|
|
|
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);
|
|
}
|
|
|
|
/**
|
|
* Adjust textarea height based on its content.
|
|
*/
|
|
adjustTextareaHeight() {
|
|
if (!this.playerInput) return;
|
|
const textarea = this.playerInput;
|
|
// Temporarily reset height to accurately measure scrollHeight
|
|
textarea.style.height = 'auto';
|
|
// Set height to scrollHeight to fit content, adding a small buffer if needed
|
|
textarea.style.height = `${textarea.scrollHeight}px`;
|
|
}
|
|
|
|
/**
|
|
* Sets up focus management to keep the input field focused.
|
|
* Note: Some parts might be better handled by the main application logic
|
|
* depending on overall focus requirements (e.g., clicking outside input).
|
|
*/
|
|
setupFocusManagement() {
|
|
// Focus input field when the handler is initialized
|
|
this.focus();
|
|
|
|
// Re-focus input when user returns to this browser tab/window
|
|
window.addEventListener('focus', () => this.focus());
|
|
window.addEventListener('visibilitychange', () => {
|
|
if (document.visibilityState === 'visible') {
|
|
setTimeout(() => this.focus(), 100);
|
|
}
|
|
});
|
|
|
|
// Optional: Add a listener to the document to refocus if needed,
|
|
// but be careful not to interfere with other interactive elements.
|
|
/*
|
|
document.addEventListener('click', (e) => {
|
|
// Example: Refocus if click is not on specific elements
|
|
if (!e.target.closest('button, a, .interactive-ui-element')) {
|
|
this.focus();
|
|
}
|
|
});
|
|
*/
|
|
}
|
|
}
|
|
|
|
// Export the class if using modules (optional, depends on build setup)
|
|
// export default InputHandler;
|