Improve text input styling and behavior to match book design theme. Changes include: 1. Update input styling to span full width with subtle bottom border. 2. Add custom blinking cursor with terminal-like behavior. 3. Implement auto-focus functionality for better UX. 4. Reset cursor position after command submission.

This commit is contained in:
2025-04-01 11:11:10 +00:00
parent 1882acac8c
commit 5cb31a65d9
3 changed files with 161 additions and 47 deletions
+104 -12
View File
@@ -8,7 +8,6 @@ class AIFiction {
this.storyContainer = document.getElementById('story');
this.commandHistoryContainer = document.getElementById('command_history');
this.playerInput = document.getElementById('player_input');
this.submitButton = document.getElementById('submit_command');
this.speechButton = document.getElementById('speech');
this.rewindButton = document.getElementById('rewind');
this.saveButton = document.getElementById('save');
@@ -33,9 +32,6 @@ class AIFiction {
this.typingSpeed = 30; // Default value, will be adjusted by slider
this.typingTimeout = null;
// Check for kokoro-js being loaded (Now handled by factory)
// this.checkForKokoroJs(); // No longer needed here
// Bind event handlers
this.bindEvents();
@@ -47,6 +43,9 @@ class AIFiction {
// Listen for TTS readiness
this.listenForTTSReady();
// Set up focus management
this.setupFocusManagement();
}
/**
@@ -112,9 +111,6 @@ class AIFiction {
* Bind event handlers to DOM elements
*/
bindEvents() {
// Submit command on button click
this.submitButton.addEventListener('click', () => this.submitCommand());
// Submit command on Enter key
this.playerInput.addEventListener('keydown', (e) => {
if (e.key === 'Enter') {
@@ -122,6 +118,21 @@ class AIFiction {
}
});
// Handle cursor position based on input changes and caret position
this.playerInput.addEventListener('input', this.updateCursorPosition.bind(this));
this.playerInput.addEventListener('click', this.updateCursorPosition.bind(this));
this.playerInput.addEventListener('keyup', this.updateCursorPosition.bind(this));
// Handle cursor visibility based on input focus
this.playerInput.addEventListener('focus', () => {
document.getElementById('cursor').style.opacity = '1';
this.updateCursorPosition();
});
this.playerInput.addEventListener('blur', () => {
document.getElementById('cursor').style.opacity = '0';
});
// Toggle speech
this.speechButton.addEventListener('click', () => {
// Check if the handler is available (it should be if button is enabled)
@@ -133,7 +144,6 @@ class AIFiction {
// Set user activation flag for the handler
window.ttsHandler.hasUserActivation = true;
const enabled = window.ttsHandler.toggle();
this.updateSpeechButton(enabled);
@@ -249,11 +259,10 @@ class AIFiction {
// Scroll to bottom and focus input
this.scrollToBottom();
this.playerInput.focus();
this.playerInput.focus();
// Re-enable input (failsafe)
this.playerInput.disabled = false;
this.submitButton.disabled = false;
});
// Game saved confirmation
@@ -298,7 +307,6 @@ class AIFiction {
// Disable input temporarily
this.playerInput.disabled = true;
this.submitButton.disabled = true;
// Add command to history
this.addUserCommand(command);
@@ -312,10 +320,15 @@ class AIFiction {
// Clear input
this.playerInput.value = '';
// Reset cursor position to the start
const cursor = document.getElementById('cursor');
if (cursor) {
cursor.style.left = '0px';
}
// Re-enable input field after a short delay (or after 8 seconds as failsafe)
const timeout = setTimeout(() => {
this.playerInput.disabled = false;
this.submitButton.disabled = false;
// Remove thinking indicator
const thinkingElement = document.getElementById(thinkingId);
@@ -502,6 +515,85 @@ class AIFiction {
container.scrollTop = container.scrollHeight;
}
}
/**
* Update the custom cursor position based on input text and caret position
*/
updateCursorPosition() {
const input = this.playerInput;
const cursor = document.getElementById('cursor');
if (!cursor) return;
// Get the current caret position
const caretPosition = input.selectionStart || 0;
const inputText = input.value;
if (inputText.length === 0) {
// If no text, position cursor at the beginning (placeholder visible)
cursor.style.left = '0px';
return;
}
// Create a temporary span to measure text width
const tempSpan = document.createElement('span');
tempSpan.style.font = window.getComputedStyle(input).font;
tempSpan.style.position = 'absolute';
tempSpan.style.visibility = 'hidden';
tempSpan.style.whiteSpace = 'pre';
tempSpan.textContent = inputText.substring(0, caretPosition);
document.body.appendChild(tempSpan);
// Set cursor position based on the width of text before caret
const textWidth = tempSpan.getBoundingClientRect().width;
cursor.style.left = `${textWidth}px`;
// Clean up
document.body.removeChild(tempSpan);
}
/**
* Set up focus management to ensure input field is always focused
*/
setupFocusManagement() {
// Focus input field when the page loads
window.addEventListener('load', () => {
this.playerInput.focus();
});
// Focus input when user clicks anywhere in the document
document.addEventListener('click', (e) => {
// Don't steal focus if user is clicking on a button or link
if (
e.target.tagName !== 'BUTTON' &&
e.target.tagName !== 'A' &&
!e.target.classList.contains('suggestions') &&
!e.target.closest('.suggestions')
) {
this.playerInput.focus();
}
});
// Re-focus input when user returns to this browser tab
window.addEventListener('focus', () => {
this.playerInput.focus();
});
// Re-focus input when user returns to the window
window.addEventListener('visibilitychange', () => {
if (document.visibilityState === 'visible') {
setTimeout(() => this.playerInput.focus(), 100);
}
});
// Focus on input after narrative is added
const originalAddNarrative = this.addNarrative.bind(this);
this.addNarrative = (text) => {
originalAddNarrative(text);
// Short timeout to ensure rendering completes
setTimeout(() => this.playerInput.focus(), 10);
};
}
}
// Create the application when the DOM is fully loaded