Improved cursor and multiline input field.
This commit is contained in:
+30
-6
@@ -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
@@ -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
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user