Improved cursor and multiline input field.

This commit is contained in:
2025-04-01 11:54:55 +00:00
parent 5cb31a65d9
commit 89b8cf8311
3 changed files with 214 additions and 49 deletions
+30 -6
View File
@@ -259,6 +259,7 @@ cap {
transition: opacity 0.5s; transition: opacity 0.5s;
} }
/* /*
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.)
@@ -579,6 +580,13 @@ ol.choice {
width: 100%; width: 100%;
position: relative; position: relative;
margin-top: 10px; margin-top: 10px;
transition: opacity 0.5s ease; /* Add transition for fading effect */
}
/* Fade out command input when loading */
#command_input.fading {
opacity: 0.3;
pointer-events: none; /* Prevent interaction while faded out */
} }
/* Input wrapper for positioning cursor */ /* Input wrapper for positioning cursor */
@@ -586,9 +594,12 @@ ol.choice {
position: relative; position: relative;
width: 100%; width: 100%;
display: inline-block; display: inline-block;
min-height: 1.5em; /* Minimum height for one line */
max-height: 6em; /* Maximum height - about 4 lines */
overflow: visible; /* Changed from 'auto' to 'visible' to hide scrollbars when not needed */
} }
/* Player input styling */ /* Player input styling - now a textarea for multiline support */
#player_input { #player_input {
width: 100%; width: 100%;
background: transparent; background: transparent;
@@ -601,10 +612,11 @@ ol.choice {
padding: 5px 0; padding: 5px 0;
caret-color: transparent; /* Hide the default caret */ caret-color: transparent; /* Hide the default caret */
box-sizing: border-box; box-sizing: border-box;
} resize: none; /* Disable manual resizing */
overflow: hidden; /* Hide all scrollbars */
#player_input:focus { line-height: 1.2; /* Match paragraph line height */
border-bottom-color: #333; height: auto; /* Allow height to adjust */
min-height: 1.5em; /* Ensure minimum height */
} }
/* Custom cursor styling */ /* Custom cursor styling */
@@ -618,12 +630,14 @@ ol.choice {
left: 0; left: 0;
animation: blink 1s step-end infinite; animation: blink 1s step-end infinite;
pointer-events: none; /* Allow clicks to pass through to the input */ pointer-events: none; /* Allow clicks to pass through to the input */
z-index: 1; /* Ensure cursor appears above text */
} }
/* Placeholder styling - lighter and italic */ /* Placeholder styling - lighter and italic, with padding to avoid cursor overlap */
#player_input::placeholder { #player_input::placeholder {
color: #aaa; color: #aaa;
font-style: italic; font-style: italic;
padding-left: 15px; /* Add padding to move placeholder text to the right */
} }
/* Blinking animation */ /* Blinking animation */
@@ -631,3 +645,13 @@ ol.choice {
0%, 100% { opacity: 1; } 0%, 100% { opacity: 1; }
50% { opacity: 0; } 50% { opacity: 0; }
} }
/* Fade-in animation for input area */
@keyframes fadeInInput {
from { opacity: 0.3; }
to { opacity: 1; }
}
.fade-in-input {
animation: fadeInInput 0.5s ease forwards;
}
+1 -1
View File
@@ -34,7 +34,7 @@
<div id="command_input"> <div id="command_input">
<div class="input-wrapper"> <div class="input-wrapper">
<input type="text" id="player_input" placeholder="Enter your command..." autofocus> <textarea id="player_input" placeholder="Enter your command..." rows="1" autofocus></textarea>
<span id="cursor"></span> <span id="cursor"></span>
</div> </div>
</div> </div>
+183 -42
View File
@@ -107,23 +107,159 @@ class AIFiction {
}); });
} }
/**
* Helper function to get precise caret coordinates in a textarea
* @param {HTMLTextAreaElement} element - The textarea element
* @param {number} position - The caret position
* @return {Object} Object with top and left coordinates
*/
getCaretCoordinates(element, position) {
// Create a range to represent the caret
const range = document.createRange();
const textNode = document.createTextNode(element.value.substring(0, position));
const span = document.createElement('span');
span.appendChild(textNode);
// Create a temporary div
const div = document.createElement('div');
div.style.position = 'absolute';
div.style.top = '-9999px';
div.style.left = '-9999px';
div.style.width = element.offsetWidth + 'px';
div.style.whiteSpace = 'pre-wrap';
div.style.wordWrap = 'break-word';
div.style.fontFamily = window.getComputedStyle(element).fontFamily;
div.style.fontSize = window.getComputedStyle(element).fontSize;
div.style.lineHeight = window.getComputedStyle(element).lineHeight;
div.style.padding = window.getComputedStyle(element).padding;
// Append everything to the DOM
div.appendChild(span);
document.body.appendChild(div);
// Measure the position
const coordinates = {
top: span.offsetTop,
left: span.offsetWidth
};
// Clean up
document.body.removeChild(div);
return coordinates;
}
/**
* Update the custom cursor position based on input text and caret position
* Enhanced version with simpler, more reliable positioning
*/
updateCursorPosition() {
const input = this.playerInput;
const cursor = document.getElementById('cursor');
if (!cursor || !input) 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';
cursor.style.top = '6px'; // Default top position
return;
}
// Auto-adjust textarea height based on content
this.adjustTextareaHeight();
// Use a more reliable method to get cursor position:
// Create a temporary element that exactly duplicates the input's content and styling
const div = document.createElement('div');
div.style.position = 'absolute';
div.style.top = '-9999px';
div.style.left = '-9999px';
div.style.width = getComputedStyle(input).width;
div.style.height = 'auto';
div.style.padding = getComputedStyle(input).padding;
div.style.border = getComputedStyle(input).border;
div.style.fontFamily = getComputedStyle(input).fontFamily;
div.style.fontSize = getComputedStyle(input).fontSize;
div.style.fontWeight = getComputedStyle(input).fontWeight;
div.style.lineHeight = getComputedStyle(input).lineHeight;
div.style.whiteSpace = 'pre-wrap';
div.style.wordWrap = 'break-word';
div.style.boxSizing = getComputedStyle(input).boxSizing;
// Create three spans to help us position the cursor
const preCaretText = document.createElement('span');
preCaretText.textContent = inputText.substring(0, caretPosition);
const caretChar = document.createElement('span');
caretChar.textContent = '|'; // Visible cursor marker for measurement
caretChar.style.display = 'inline-block';
caretChar.style.width = '0';
const postCaretText = document.createElement('span');
postCaretText.textContent = inputText.substring(caretPosition);
// Add all elements to the DOM
div.appendChild(preCaretText);
div.appendChild(caretChar);
div.appendChild(postCaretText);
document.body.appendChild(div);
// Get position of the caret marker
const caretRect = caretChar.getBoundingClientRect();
const inputRect = input.getBoundingClientRect();
// Set cursor position
// We need to account for the input's scroll position
cursor.style.left = (caretRect.left - div.getBoundingClientRect().left) + 'px';
cursor.style.top = (caretRect.top - div.getBoundingClientRect().top - input.scrollTop) + 'px';
// Clean up
document.body.removeChild(div);
}
/**
* Adjust textarea height based on its content
*/
adjustTextareaHeight() {
const textarea = this.playerInput;
if (!textarea) return;
// Reset height to auto to get the correct scrollHeight
textarea.style.height = 'auto';
// Set height to scrollHeight to fit content
textarea.style.height = textarea.scrollHeight + 'px';
}
/** /**
* Bind event handlers to DOM elements * Bind event handlers to DOM elements
*/ */
bindEvents() { bindEvents() {
// Submit command on Enter key // Submit command on Enter key without Shift
this.playerInput.addEventListener('keydown', (e) => { this.playerInput.addEventListener('keydown', (e) => {
if (e.key === 'Enter') { if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault(); // Prevent default to avoid newline
this.submitCommand(); this.submitCommand();
} else if (e.key === 'Enter' && e.shiftKey) {
// Allow Shift+Enter to create a new line
// Default behavior happens, no need to do anything
} }
}); });
// Handle cursor position based on input changes and caret position // Auto-resize textarea on input
this.playerInput.addEventListener('input', this.updateCursorPosition.bind(this)); this.playerInput.addEventListener('input', () => {
this.adjustTextareaHeight();
this.updateCursorPosition();
});
// Update cursor on various events
this.playerInput.addEventListener('click', this.updateCursorPosition.bind(this)); this.playerInput.addEventListener('click', this.updateCursorPosition.bind(this));
this.playerInput.addEventListener('keyup', this.updateCursorPosition.bind(this)); this.playerInput.addEventListener('keyup', this.updateCursorPosition.bind(this));
// Handle cursor visibility based on input focus
this.playerInput.addEventListener('focus', () => { this.playerInput.addEventListener('focus', () => {
document.getElementById('cursor').style.opacity = '1'; document.getElementById('cursor').style.opacity = '1';
this.updateCursorPosition(); this.updateCursorPosition();
@@ -133,6 +269,21 @@ class AIFiction {
document.getElementById('cursor').style.opacity = '0'; document.getElementById('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();
});
// 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)
@@ -305,6 +456,10 @@ class AIFiction {
if (command === '') return; if (command === '') return;
// Fade out the input field
const commandInput = document.getElementById('command_input');
commandInput.classList.add('fading');
// Disable input temporarily // Disable input temporarily
this.playerInput.disabled = true; this.playerInput.disabled = true;
@@ -324,11 +479,25 @@ class AIFiction {
const cursor = document.getElementById('cursor'); const cursor = document.getElementById('cursor');
if (cursor) { if (cursor) {
cursor.style.left = '0px'; cursor.style.left = '0px';
cursor.style.top = '6px';
} }
// Reset textarea height
this.adjustTextareaHeight();
// 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(() => {
// Remove fading class and add fade-in animation
commandInput.classList.remove('fading');
commandInput.classList.add('fade-in-input');
// Remove animation class after it completes
setTimeout(() => {
commandInput.classList.remove('fade-in-input');
}, 500);
this.playerInput.disabled = false; this.playerInput.disabled = false;
this.playerInput.focus();
// Remove thinking indicator // Remove thinking indicator
const thinkingElement = document.getElementById(thinkingId); const thinkingElement = document.getElementById(thinkingId);
@@ -516,49 +685,21 @@ class AIFiction {
} }
} }
/**
* 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 * Set up focus management to ensure input field is always focused
*/ */
setupFocusManagement() { setupFocusManagement() {
// Focus input field when the page loads // Focus input field when the page loads
window.addEventListener('load', () => { window.addEventListener('load', () => {
// Force immediate focus on load
this.playerInput.focus(); this.playerInput.focus();
// Some browsers might need a slight delay
setTimeout(() => this.playerInput.focus(), 100);
// Also adjust textarea height and update cursor position
this.adjustTextareaHeight();
this.updateCursorPosition();
}); });
// Focus input when user clicks anywhere in the document // Focus input when user clicks anywhere in the document