Refactored app to include all the ink.js typography.
This commit is contained in:
@@ -0,0 +1,293 @@
|
||||
/**
|
||||
* 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;
|
||||
Reference in New Issue
Block a user