Files
kidsai/mobile-keyboard-handler.js
root 500bd192d5 Initial commit: KidsAI Explorer with complete functionality
- Complete KidsAI Explorer application
- Multi-language support (English/German)
- AI-powered educational guidance using OpenAI
- Interactive chat interface for children
- Proper placeholder translation fixes
- Mobile-responsive design
- Educational framework for critical thinking
2025-07-13 16:59:42 +02:00

389 lines
18 KiB
JavaScript

// Mobile Keyboard Overlay Handler
// This script dynamically adjusts the layout when the virtual keyboard appears
(function() {
'use strict';
// Only run on mobile devices
if (!/Android|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent)) {
return;
}
let initialViewportHeight = window.innerHeight;
let isKeyboardOpen = false;
let activeInputContainer = null; // Track which input container is currently active
let focusedInput = null; // Track the currently focused input element
// Function to handle viewport changes (keyboard show/hide)
function handleViewportChange() {
const currentHeight = window.innerHeight;
const heightDifference = initialViewportHeight - currentHeight;
const threshold = 150; // Minimum height change to consider keyboard open
const conversationContainer = document.querySelector('.conversation-container');
const allChatInputContainers = document.querySelectorAll('.chat-input-container');
const body = document.body;
// Update debug counter
const visibleInputs = Array.from(allChatInputContainers).filter(
container => container.style.display !== 'none'
).length;
body.setAttribute('data-visible-inputs', visibleInputs);
if (heightDifference > threshold && !isKeyboardOpen) {
// Keyboard opened
isKeyboardOpen = true;
console.log('Keyboard opened, adjusting layout. Found', allChatInputContainers.length, 'input containers');
if (conversationContainer) {
conversationContainer.classList.add('keyboard-open');
conversationContainer.style.height = `${currentHeight * 0.4}px`;
conversationContainer.style.maxHeight = `${currentHeight * 0.4}px`;
}
// Apply keyboard positioning to ALL chat input containers
allChatInputContainers.forEach((chatInputContainer, index) => {
if (chatInputContainer) {
console.log(`Processing input container ${index + 1}/${allChatInputContainers.length}`);
chatInputContainer.style.position = 'fixed';
chatInputContainer.style.bottom = '0';
chatInputContainer.style.left = '0';
chatInputContainer.style.right = '0';
chatInputContainer.style.zIndex = '1000';
chatInputContainer.style.background = 'white';
chatInputContainer.style.borderTop = '1px solid #e2e8f0';
chatInputContainer.style.padding = '15px';
chatInputContainer.style.paddingBottom = '20px';
chatInputContainer.style.boxShadow = '0 -2px 10px rgba(0, 0, 0, 0.1)';
chatInputContainer.style.margin = '0';
chatInputContainer.style.borderRadius = '0';
chatInputContainer.classList.add('keyboard-fixed');
// Ensure the input area is properly styled
const inputArea = chatInputContainer.querySelector('.input-area');
if (inputArea) {
inputArea.style.margin = '0';
inputArea.style.borderRadius = '20px';
inputArea.style.background = '#f8f9fa';
}
}
});
// Hide non-active input containers
if (activeInputContainer) {
allChatInputContainers.forEach(container => {
if (container !== activeInputContainer) {
container.style.display = 'none';
console.log('Hiding non-active input container');
} else {
console.log('Keeping active input container visible');
}
});
}
body.classList.add('keyboard-open');
// Update debug counter after hiding
const visibleAfter = Array.from(allChatInputContainers).filter(
container => container.style.display !== 'none'
).length;
body.setAttribute('data-visible-inputs', visibleAfter);
} else if (heightDifference <= threshold && isKeyboardOpen) {
// Keyboard closed
isKeyboardOpen = false;
console.log('Keyboard closed, restoring layout for', allChatInputContainers.length, 'input containers');
if (conversationContainer) {
conversationContainer.classList.remove('keyboard-open');
conversationContainer.style.removeProperty('height');
conversationContainer.style.removeProperty('max-height');
}
// Reset ALL chat input containers
allChatInputContainers.forEach((chatInputContainer, index) => {
if (chatInputContainer) {
console.log(`Restoring input container ${index + 1}/${allChatInputContainers.length}`);
chatInputContainer.style.removeProperty('position');
chatInputContainer.style.removeProperty('bottom');
chatInputContainer.style.removeProperty('left');
chatInputContainer.style.removeProperty('right');
chatInputContainer.style.removeProperty('z-index');
chatInputContainer.style.removeProperty('background');
chatInputContainer.style.removeProperty('border-top');
chatInputContainer.style.removeProperty('padding');
chatInputContainer.style.removeProperty('padding-bottom');
chatInputContainer.style.removeProperty('box-shadow');
chatInputContainer.style.removeProperty('margin');
chatInputContainer.style.removeProperty('border-radius');
chatInputContainer.style.removeProperty('display');
chatInputContainer.classList.remove('keyboard-fixed');
// Reset input area styles
const inputArea = chatInputContainer.querySelector('.input-area');
if (inputArea) {
inputArea.style.removeProperty('margin');
inputArea.style.removeProperty('border-radius');
inputArea.style.removeProperty('background');
}
}
});
activeInputContainer = null;
body.classList.remove('keyboard-open');
body.removeAttribute('data-visible-inputs');
}
}
// Function to scroll input into view when focused
function scrollInputIntoView(inputElement) {
setTimeout(() => {
// Always ensure input is visible when focused
const elementRect = inputElement.getBoundingClientRect();
const viewportHeight = window.innerHeight;
const keyboardHeight = initialViewportHeight - viewportHeight;
// If there's likely a keyboard (height reduced by significant amount)
if (keyboardHeight > 150) {
const availableHeight = viewportHeight;
const inputBottom = elementRect.bottom;
// If input is in bottom 40% of available viewport, scroll it up
if (inputBottom > availableHeight * 0.6) {
// Scroll the input container's parent into view
const container = inputElement.closest('.conversation-container') ||
inputElement.closest('.thinking-section');
if (container) {
container.scrollTop = container.scrollHeight - availableHeight * 0.5;
}
// Also try scrolling the window
window.scrollTo({
top: window.scrollY + (inputBottom - availableHeight * 0.4),
behavior: 'smooth'
});
}
}
}, 100); // Shorter delay for more responsive feel
}
// Enhanced focus handler for chat textarea
function handleChatInputFocus(event) {
const chatTextarea = event.target;
focusedInput = chatTextarea;
// Add focused class for styling
chatTextarea.classList.add('focused');
// Immediately apply keyboard-friendly positioning
const chatInputContainer = chatTextarea.closest('.chat-input-container');
if (chatInputContainer && window.innerWidth <= 768) {
// Set this as the active input container
activeInputContainer = chatInputContainer;
// Hide all other input containers immediately
const allInputContainers = document.querySelectorAll('.chat-input-container');
allInputContainers.forEach(container => {
if (container !== chatInputContainer) {
container.style.display = 'none';
}
});
// Apply keyboard positioning immediately on focus
setTimeout(() => {
chatInputContainer.style.position = 'fixed';
chatInputContainer.style.bottom = '0';
chatInputContainer.style.left = '0';
chatInputContainer.style.right = '0';
chatInputContainer.style.zIndex = '1001';
chatInputContainer.style.background = 'white';
chatInputContainer.style.borderTop = '1px solid #e2e8f0';
chatInputContainer.style.padding = '15px';
chatInputContainer.style.paddingBottom = '25px';
chatInputContainer.style.boxShadow = '0 -2px 10px rgba(0, 0, 0, 0.1)';
chatInputContainer.style.margin = '0';
chatInputContainer.style.display = 'block'; // Ensure this one is visible
chatInputContainer.classList.add('input-focused');
// Add class to body for CSS targeting (fallback for :has() selector)
document.body.classList.add('input-focused-active');
// Adjust conversation container
const conversationContainer = document.querySelector('.conversation-container');
if (conversationContainer) {
conversationContainer.style.paddingBottom = '80px';
conversationContainer.style.marginBottom = '0';
}
}, 50);
}
// Scroll into view if keyboard is likely to appear
scrollInputIntoView(chatTextarea);
// For iOS, prevent zoom by ensuring font-size is 16px
if (/iPhone|iPad|iPod/i.test(navigator.userAgent)) {
chatTextarea.style.fontSize = '16px';
}
}
function handleChatInputBlur(event) {
event.target.classList.remove('focused');
// Clear focused input reference
if (focusedInput === event.target) {
focusedInput = null;
}
// Clean up keyboard positioning when input loses focus
const chatInputContainer = event.target.closest('.chat-input-container');
if (chatInputContainer && chatInputContainer.classList.contains('input-focused')) {
setTimeout(() => {
// Only remove if keyboard is not detected as open
if (!isKeyboardOpen) {
// Show all input containers again
const allInputContainers = document.querySelectorAll('.chat-input-container');
allInputContainers.forEach(container => {
container.style.removeProperty('display');
});
chatInputContainer.style.removeProperty('position');
chatInputContainer.style.removeProperty('bottom');
chatInputContainer.style.removeProperty('left');
chatInputContainer.style.removeProperty('right');
chatInputContainer.style.removeProperty('z-index');
chatInputContainer.style.removeProperty('background');
chatInputContainer.style.removeProperty('border-top');
chatInputContainer.style.removeProperty('padding');
chatInputContainer.style.removeProperty('padding-bottom');
chatInputContainer.style.removeProperty('box-shadow');
chatInputContainer.style.removeProperty('margin');
chatInputContainer.classList.remove('input-focused');
// Remove body class
document.body.classList.remove('input-focused-active');
// Reset conversation container
const conversationContainer = document.querySelector('.conversation-container');
if (conversationContainer) {
conversationContainer.style.removeProperty('padding-bottom');
conversationContainer.style.removeProperty('margin-bottom');
}
// Clear active input container
if (activeInputContainer === chatInputContainer) {
activeInputContainer = null;
}
}
}, 300); // Delay to avoid flicker
}
}
// Debounced resize handler
let resizeTimeout;
function debouncedViewportChange() {
clearTimeout(resizeTimeout);
resizeTimeout = setTimeout(handleViewportChange, 150);
}
// Initialize when DOM is ready
function initialize() {
console.log('Initializing mobile keyboard handler');
// Store initial viewport height
initialViewportHeight = window.innerHeight;
// Listen for viewport changes
window.addEventListener('resize', debouncedViewportChange);
// Listen for orientation changes
window.addEventListener('orientationchange', () => {
setTimeout(() => {
initialViewportHeight = window.innerHeight;
handleViewportChange();
}, 500); // Wait for orientation change to complete
});
// Add focus/blur handlers for chat inputs (using event delegation for dynamic content)
document.addEventListener('focus', (event) => {
if (event.target.matches('.chat-textarea, #question-input, .step-thinking-space textarea')) {
handleChatInputFocus(event);
}
}, true);
document.addEventListener('blur', (event) => {
if (event.target.matches('.chat-textarea, #question-input, .step-thinking-space textarea')) {
handleChatInputBlur(event);
}
}, true);
// Listen for new chat input containers being added to the DOM
if (window.MutationObserver) {
const observer = new MutationObserver((mutations) => {
mutations.forEach((mutation) => {
mutation.addedNodes.forEach((node) => {
if (node.nodeType === 1) { // Element node
// Check if the added node contains chat input containers
const newInputContainers = node.querySelectorAll ?
node.querySelectorAll('.chat-input-container') : [];
if (newInputContainers.length > 0) {
console.log('New chat input containers detected:', newInputContainers.length);
// If keyboard is currently open and we have an active input, hide the new ones
if (isKeyboardOpen && activeInputContainer) {
newInputContainers.forEach(container => {
if (container !== activeInputContainer) {
container.style.display = 'none';
}
});
}
}
}
});
});
});
// Observe the conversation container for new additions
const conversationContainer = document.querySelector('.conversation-container');
if (conversationContainer) {
observer.observe(conversationContainer, {
childList: true,
subtree: true
});
}
// Also observe the main content area
const mainContent = document.querySelector('.main-content, .thinking-steps');
if (mainContent) {
observer.observe(mainContent, {
childList: true,
subtree: true
});
}
}
// Visual Viewport API support (newer browsers)
if (window.visualViewport) {
window.visualViewport.addEventListener('resize', () => {
const heightDifference = window.innerHeight - window.visualViewport.height;
if (heightDifference > 150 && !isKeyboardOpen) {
handleViewportChange();
} else if (heightDifference <= 150 && isKeyboardOpen) {
handleViewportChange();
}
});
}
}
// Wait for DOM to be ready
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', initialize);
} else {
initialize();
}
// Add utility class for JavaScript
document.documentElement.classList.add('js-mobile-keyboard-handler');
})();