Improved cursor and multiline input field.
This commit is contained in:
+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
|
||||
*/
|
||||
bindEvents() {
|
||||
// Submit command on Enter key
|
||||
// Submit command on Enter key without Shift
|
||||
this.playerInput.addEventListener('keydown', (e) => {
|
||||
if (e.key === 'Enter') {
|
||||
if (e.key === 'Enter' && !e.shiftKey) {
|
||||
e.preventDefault(); // Prevent default to avoid newline
|
||||
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
|
||||
this.playerInput.addEventListener('input', this.updateCursorPosition.bind(this));
|
||||
// Auto-resize textarea 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));
|
||||
|
||||
// Handle cursor visibility based on input focus
|
||||
this.playerInput.addEventListener('focus', () => {
|
||||
document.getElementById('cursor').style.opacity = '1';
|
||||
this.updateCursorPosition();
|
||||
@@ -133,6 +269,21 @@ class AIFiction {
|
||||
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
|
||||
this.speechButton.addEventListener('click', () => {
|
||||
// Check if the handler is available (it should be if button is enabled)
|
||||
@@ -305,6 +456,10 @@ class AIFiction {
|
||||
|
||||
if (command === '') return;
|
||||
|
||||
// Fade out the input field
|
||||
const commandInput = document.getElementById('command_input');
|
||||
commandInput.classList.add('fading');
|
||||
|
||||
// Disable input temporarily
|
||||
this.playerInput.disabled = true;
|
||||
|
||||
@@ -324,11 +479,25 @@ class AIFiction {
|
||||
const cursor = document.getElementById('cursor');
|
||||
if (cursor) {
|
||||
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)
|
||||
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.focus();
|
||||
|
||||
// Remove thinking indicator
|
||||
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
|
||||
*/
|
||||
setupFocusManagement() {
|
||||
// Focus input field when the page loads
|
||||
window.addEventListener('load', () => {
|
||||
// Force immediate focus on load
|
||||
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
|
||||
|
||||
Reference in New Issue
Block a user