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:
+53
-33
@@ -259,20 +259,6 @@ cap {
|
|||||||
transition: opacity 0.5s;
|
transition: opacity 0.5s;
|
||||||
}
|
}
|
||||||
|
|
||||||
#choices {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: repeat(3, 1fr);
|
|
||||||
width: calc(var(--book-width) * 0.39)px;
|
|
||||||
}
|
|
||||||
|
|
||||||
#choices *:first-child {
|
|
||||||
grid-column: 1 / -1;
|
|
||||||
}
|
|
||||||
|
|
||||||
#choices ol.categorized {
|
|
||||||
list-style-type: lower-alpha;
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
Class applied to all choices
|
Class applied to all choices
|
||||||
(Will always appear inside <p> element by default.)
|
(Will always appear inside <p> element by default.)
|
||||||
@@ -582,32 +568,66 @@ ol.choice {
|
|||||||
|
|
||||||
/* Input area */
|
/* Input area */
|
||||||
#input_area {
|
#input_area {
|
||||||
display: flex;
|
display: block;
|
||||||
margin-bottom: 15px;
|
margin-top: 15px;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Command input container */
|
||||||
|
#command_input {
|
||||||
|
width: 100%;
|
||||||
|
position: relative;
|
||||||
|
margin-top: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Input wrapper for positioning cursor */
|
||||||
|
.input-wrapper {
|
||||||
|
position: relative;
|
||||||
|
width: 100%;
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Player input styling */
|
||||||
#player_input {
|
#player_input {
|
||||||
flex: 1;
|
width: 100%;
|
||||||
padding: 8px 12px;
|
background: transparent;
|
||||||
border: 1px solid #d1c8b9;
|
border: none;
|
||||||
background: rgba(255, 255, 255, 0.8);
|
border-bottom: 1px solid #8b7765;
|
||||||
font-family: 'EB Garamond', serif;
|
font-family: 'EB Garamond', serif;
|
||||||
font-size: 16px;
|
font-size: 1.1rem;
|
||||||
|
color: #333;
|
||||||
outline: none;
|
outline: none;
|
||||||
border-radius: 4px 0 0 4px;
|
padding: 5px 0;
|
||||||
|
caret-color: transparent; /* Hide the default caret */
|
||||||
|
box-sizing: border-box;
|
||||||
}
|
}
|
||||||
|
|
||||||
#submit_command {
|
#player_input:focus {
|
||||||
background-color: #8b7765;
|
border-bottom-color: #333;
|
||||||
border: 1px solid #8b7765;
|
|
||||||
color: white;
|
|
||||||
padding: 8px 12px;
|
|
||||||
cursor: pointer;
|
|
||||||
font-family: 'EB Garamond', serif;
|
|
||||||
border-radius: 0 4px 4px 0;
|
|
||||||
transition: background-color 0.2s;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#submit_command:hover {
|
/* Custom cursor styling */
|
||||||
background-color: #6d5d4d;
|
#cursor {
|
||||||
|
position: absolute;
|
||||||
|
display: inline-block;
|
||||||
|
width: 8px;
|
||||||
|
height: 1.2em;
|
||||||
|
background-color: #333;
|
||||||
|
top: 6px;
|
||||||
|
left: 0;
|
||||||
|
animation: blink 1s step-end infinite;
|
||||||
|
pointer-events: none; /* Allow clicks to pass through to the input */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Placeholder styling - lighter and italic */
|
||||||
|
#player_input::placeholder {
|
||||||
|
color: #aaa;
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Blinking animation */
|
||||||
|
@keyframes blink {
|
||||||
|
0%, 100% { opacity: 1; }
|
||||||
|
50% { opacity: 0; }
|
||||||
}
|
}
|
||||||
|
|||||||
+4
-2
@@ -33,8 +33,10 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="command_input">
|
<div id="command_input">
|
||||||
<input type="text" id="player_input" placeholder="Enter your command..." autofocus>
|
<div class="input-wrapper">
|
||||||
<button id="submit_command">Submit</button>
|
<input type="text" id="player_input" placeholder="Enter your command..." autofocus>
|
||||||
|
<span id="cursor"></span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="l10n-remark" id="remark"><i><sup>*</sup>click on page or press spacebar to fast forward text animation</i></div>
|
<div class="l10n-remark" id="remark"><i><sup>*</sup>click on page or press spacebar to fast forward text animation</i></div>
|
||||||
|
|||||||
+104
-12
@@ -8,7 +8,6 @@ class AIFiction {
|
|||||||
this.storyContainer = document.getElementById('story');
|
this.storyContainer = document.getElementById('story');
|
||||||
this.commandHistoryContainer = document.getElementById('command_history');
|
this.commandHistoryContainer = document.getElementById('command_history');
|
||||||
this.playerInput = document.getElementById('player_input');
|
this.playerInput = document.getElementById('player_input');
|
||||||
this.submitButton = document.getElementById('submit_command');
|
|
||||||
this.speechButton = document.getElementById('speech');
|
this.speechButton = document.getElementById('speech');
|
||||||
this.rewindButton = document.getElementById('rewind');
|
this.rewindButton = document.getElementById('rewind');
|
||||||
this.saveButton = document.getElementById('save');
|
this.saveButton = document.getElementById('save');
|
||||||
@@ -33,9 +32,6 @@ class AIFiction {
|
|||||||
this.typingSpeed = 30; // Default value, will be adjusted by slider
|
this.typingSpeed = 30; // Default value, will be adjusted by slider
|
||||||
this.typingTimeout = null;
|
this.typingTimeout = null;
|
||||||
|
|
||||||
// Check for kokoro-js being loaded (Now handled by factory)
|
|
||||||
// this.checkForKokoroJs(); // No longer needed here
|
|
||||||
|
|
||||||
// Bind event handlers
|
// Bind event handlers
|
||||||
this.bindEvents();
|
this.bindEvents();
|
||||||
|
|
||||||
@@ -47,6 +43,9 @@ class AIFiction {
|
|||||||
|
|
||||||
// Listen for TTS readiness
|
// Listen for TTS readiness
|
||||||
this.listenForTTSReady();
|
this.listenForTTSReady();
|
||||||
|
|
||||||
|
// Set up focus management
|
||||||
|
this.setupFocusManagement();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -112,9 +111,6 @@ class AIFiction {
|
|||||||
* Bind event handlers to DOM elements
|
* Bind event handlers to DOM elements
|
||||||
*/
|
*/
|
||||||
bindEvents() {
|
bindEvents() {
|
||||||
// Submit command on button click
|
|
||||||
this.submitButton.addEventListener('click', () => this.submitCommand());
|
|
||||||
|
|
||||||
// Submit command on Enter key
|
// Submit command on Enter key
|
||||||
this.playerInput.addEventListener('keydown', (e) => {
|
this.playerInput.addEventListener('keydown', (e) => {
|
||||||
if (e.key === 'Enter') {
|
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
|
// Toggle speech
|
||||||
this.speechButton.addEventListener('click', () => {
|
this.speechButton.addEventListener('click', () => {
|
||||||
// Check if the handler is available (it should be if button is enabled)
|
// 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
|
// Set user activation flag for the handler
|
||||||
window.ttsHandler.hasUserActivation = true;
|
window.ttsHandler.hasUserActivation = true;
|
||||||
|
|
||||||
const enabled = window.ttsHandler.toggle();
|
const enabled = window.ttsHandler.toggle();
|
||||||
this.updateSpeechButton(enabled);
|
this.updateSpeechButton(enabled);
|
||||||
|
|
||||||
@@ -249,11 +259,10 @@ class AIFiction {
|
|||||||
|
|
||||||
// Scroll to bottom and focus input
|
// Scroll to bottom and focus input
|
||||||
this.scrollToBottom();
|
this.scrollToBottom();
|
||||||
this.playerInput.focus();
|
this.playerInput.focus();
|
||||||
|
|
||||||
// Re-enable input (failsafe)
|
// Re-enable input (failsafe)
|
||||||
this.playerInput.disabled = false;
|
this.playerInput.disabled = false;
|
||||||
this.submitButton.disabled = false;
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// Game saved confirmation
|
// Game saved confirmation
|
||||||
@@ -298,7 +307,6 @@ class AIFiction {
|
|||||||
|
|
||||||
// Disable input temporarily
|
// Disable input temporarily
|
||||||
this.playerInput.disabled = true;
|
this.playerInput.disabled = true;
|
||||||
this.submitButton.disabled = true;
|
|
||||||
|
|
||||||
// Add command to history
|
// Add command to history
|
||||||
this.addUserCommand(command);
|
this.addUserCommand(command);
|
||||||
@@ -312,10 +320,15 @@ class AIFiction {
|
|||||||
// Clear input
|
// Clear input
|
||||||
this.playerInput.value = '';
|
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)
|
// Re-enable input field after a short delay (or after 8 seconds as failsafe)
|
||||||
const timeout = setTimeout(() => {
|
const timeout = setTimeout(() => {
|
||||||
this.playerInput.disabled = false;
|
this.playerInput.disabled = false;
|
||||||
this.submitButton.disabled = false;
|
|
||||||
|
|
||||||
// Remove thinking indicator
|
// Remove thinking indicator
|
||||||
const thinkingElement = document.getElementById(thinkingId);
|
const thinkingElement = document.getElementById(thinkingId);
|
||||||
@@ -502,6 +515,85 @@ class AIFiction {
|
|||||||
container.scrollTop = container.scrollHeight;
|
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
|
// Create the application when the DOM is fully loaded
|
||||||
|
|||||||
Reference in New Issue
Block a user