Files
homeschool/public/js/tablet-utils.js
T
nmemmert 8d37038177 feat: Complete tablet UI overhaul with popup menu system and help bubble
Major Changes:
- 🪟 Popup Menu System: Converted slide-down navigation to draggable, minimizable popup window
- ❓ Help Bubble: Added contextual help system for admin dashboard (accessible via ? bubble)
- 🎨 Universal Theme Engine: All elements now properly match selected theme across all panels
- 📱 Enhanced Tablet Support: Comprehensive touch gestures, handwriting recognition, and Apple Pencil optimization
- 🖊️ Unified Write Mode: Combined handwriting and keyboard input with large canvas interface

Technical Implementation:
- Draggable popup with minimize/restore functionality and boundary constraints
- Dynamic theme switching using CSS variables throughout all components
- Help system with modal overlay and keyboard shortcuts (ESC to close)
- Responsive design optimizations for mobile, tablet, and desktop
- Comprehensive CSS overrides to eliminate hardcoded colors
- JavaScript fixes for canvas stroke colors and input styling

UI/UX Improvements:
- Clean admin dashboard without instruction clutter
- Professional popup interface with smooth animations
- Better contrast and readability across all themes (default, playground, chalkboard, galaxy, nature)
- Improved accessibility with proper focus management
- Enhanced mobile responsiveness with touch-optimized controls

Files Modified:
- Menu system: tablet-menu.js, tablet-menu.css
- Theme engine: styles.css, themes integration
- Admin interface: index.html, ui.js
- Input systems: game.js, unified-write.js, handwriting.js
- Authentication: auth.js with tablet menu cleanup
2025-10-02 14:26:04 -04:00

388 lines
13 KiB
JavaScript

// Tablet optimization utilities for Spelling Practice App
class TabletOptimizations {
constructor() {
this.isTablet = this.detectTablet();
this.touchStartTime = 0;
this.touchStartPos = { x: 0, y: 0 };
if (this.isTablet) {
this.init();
}
}
detectTablet() {
// Basic tablet detection
const userAgent = navigator.userAgent.toLowerCase();
const isTouch = 'ontouchstart' in window || navigator.maxTouchPoints > 0;
const isTabletSize = window.innerWidth >= 768 && window.innerWidth <= 1024;
const isTabletUA = /tablet|ipad|playbook|silk/i.test(userAgent);
return isTouch && (isTabletSize || isTabletUA);
}
init() {
console.log('📱 Initializing tablet optimizations');
this.addTabletClasses();
this.optimizeButtons();
this.improveScrolling();
this.addTouchFeedback();
this.optimizeKeyboard();
this.handleOrientationChange();
// Initialize after DOM is ready
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', () => {
this.setupTabletUI();
});
} else {
this.setupTabletUI();
}
}
addTabletClasses() {
document.documentElement.classList.add('tablet-device');
document.body.classList.add('tablet-optimized');
}
optimizeButtons() {
// Add enhanced button interactions
document.addEventListener('touchstart', (e) => {
if (e.target.tagName === 'BUTTON' || e.target.closest('button')) {
const button = e.target.tagName === 'BUTTON' ? e.target : e.target.closest('button');
button.classList.add('touch-active');
this.touchStartTime = Date.now();
this.touchStartPos = { x: e.touches[0].clientX, y: e.touches[0].clientY };
}
}, { passive: true });
document.addEventListener('touchend', (e) => {
// Remove touch feedback after a short delay
setTimeout(() => {
document.querySelectorAll('.touch-active').forEach(btn => {
btn.classList.remove('touch-active');
});
}, 150);
}, { passive: true });
// Handle long press for additional options
document.addEventListener('touchstart', (e) => {
if (e.target.classList.contains('activity-option')) {
this.longPressTimer = setTimeout(() => {
this.showActivityOptions(e.target);
}, 800);
}
}, { passive: true });
document.addEventListener('touchend', () => {
if (this.longPressTimer) {
clearTimeout(this.longPressTimer);
}
}, { passive: true });
}
improveScrolling() {
// Smooth scrolling for tablets
document.documentElement.style.scrollBehavior = 'smooth';
// Prevent bounce scrolling on iOS
document.body.style.overscrollBehavior = 'none';
// Add momentum scrolling
const scrollableElements = document.querySelectorAll('.scrollable, .card, .adminTab');
scrollableElements.forEach(el => {
el.style.webkitOverflowScrolling = 'touch';
});
}
addTouchFeedback() {
// Add haptic feedback (if available)
window.vibrate = window.vibrate || window.webkitVibrate || window.mozVibrate;
// Visual touch feedback
const style = document.createElement('style');
style.textContent = `
.tablet-device .touch-active {
transform: scale(0.98) !important;
transition: transform 0.1s ease-out !important;
background-color: rgba(59, 130, 246, 0.1) !important;
}
.tablet-device button:active,
.tablet-device .activity-option:active {
transform: scale(0.95);
transition: transform 0.1s ease-out;
}
.tablet-device input:focus,
.tablet-device textarea:focus,
.tablet-device canvas:focus {
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.2) !important;
border-color: var(--primary) !important;
}
`;
document.head.appendChild(style);
}
optimizeKeyboard() {
// Prevent zoom on input focus for iOS
const inputs = document.querySelectorAll('input, textarea, select');
inputs.forEach(input => {
if (input.style.fontSize === '' || parseFloat(input.style.fontSize) < 16) {
input.style.fontSize = '16px';
}
});
// Auto-scroll to input when keyboard appears
inputs.forEach(input => {
input.addEventListener('focus', () => {
setTimeout(() => {
input.scrollIntoView({
behavior: 'smooth',
block: 'center',
inline: 'nearest'
});
}, 300); // Wait for keyboard animation
});
});
}
handleOrientationChange() {
let orientationTimeout;
window.addEventListener('orientationchange', () => {
// Clear existing timeout
if (orientationTimeout) {
clearTimeout(orientationTimeout);
}
// Wait for orientation change to complete
orientationTimeout = setTimeout(() => {
this.adjustForOrientation();
// Trigger canvas resize if handwriting is active
if (window.handwritingRecognition && window.handwritingRecognition.resizeCanvas) {
window.handwritingRecognition.resizeCanvas();
}
// Re-focus current input if any
const activeElement = document.activeElement;
if (activeElement && (activeElement.tagName === 'INPUT' || activeElement.tagName === 'TEXTAREA')) {
activeElement.blur();
setTimeout(() => activeElement.focus(), 100);
}
}, 500);
});
}
adjustForOrientation() {
const isLandscape = window.innerWidth > window.innerHeight;
if (isLandscape) {
document.body.classList.add('tablet-landscape');
document.body.classList.remove('tablet-portrait');
} else {
document.body.classList.add('tablet-portrait');
document.body.classList.remove('tablet-landscape');
}
// Adjust game section for landscape
const gameSection = document.getElementById('gameSection');
const wordBox = document.getElementById('wordBox');
if (gameSection && wordBox) {
if (isLandscape) {
gameSection.style.minHeight = '70vh';
gameSection.style.display = 'flex';
gameSection.style.flexDirection = 'column';
gameSection.style.justifyContent = 'center';
} else {
gameSection.style.minHeight = 'auto';
gameSection.style.display = 'block';
}
}
}
setupTabletUI() {
// Enhance activity options with better touch targets
const activityOptions = document.querySelectorAll('.activity-option');
activityOptions.forEach((option, index) => {
// Add touch ripple effect
option.addEventListener('touchstart', (e) => {
this.createRipple(e, option);
});
// Add accessibility improvements
option.setAttribute('tabindex', '0');
option.setAttribute('role', 'button');
// Keyboard navigation
option.addEventListener('keydown', (e) => {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault();
option.click();
}
});
});
// Improve admin tabs for tablet
const adminTabs = document.getElementById('adminTabs');
if (adminTabs) {
adminTabs.classList.add('tablet-optimized-tabs');
}
// Add swipe gestures for navigation (optional)
this.addSwipeGestures();
}
createRipple(event, element) {
const ripple = document.createElement('span');
const rect = element.getBoundingClientRect();
const size = Math.max(rect.width, rect.height);
const x = event.touches[0].clientX - rect.left - size / 2;
const y = event.touches[0].clientY - rect.top - size / 2;
ripple.style.cssText = `
position: absolute;
width: ${size}px;
height: ${size}px;
left: ${x}px;
top: ${y}px;
background: rgba(255, 255, 255, 0.3);
border-radius: 50%;
transform: scale(0);
animation: ripple-animation 0.6s ease-out;
pointer-events: none;
z-index: 1000;
`;
// Add ripple animation if not exists
if (!document.getElementById('ripple-style')) {
const style = document.createElement('style');
style.id = 'ripple-style';
style.textContent = `
@keyframes ripple-animation {
to {
transform: scale(2);
opacity: 0;
}
}
`;
document.head.appendChild(style);
}
element.style.position = 'relative';
element.style.overflow = 'hidden';
element.appendChild(ripple);
setTimeout(() => {
ripple.remove();
}, 600);
}
addSwipeGestures() {
let startX, startY, startTime;
document.addEventListener('touchstart', (e) => {
startX = e.touches[0].clientX;
startY = e.touches[0].clientY;
startTime = Date.now();
}, { passive: true });
document.addEventListener('touchend', (e) => {
if (!startX || !startY) return;
const endX = e.changedTouches[0].clientX;
const endY = e.changedTouches[0].clientY;
const endTime = Date.now();
const deltaX = endX - startX;
const deltaY = endY - startY;
const deltaTime = endTime - startTime;
// Check if it's a swipe (fast, directional movement)
if (deltaTime < 300 && Math.abs(deltaX) > 100 && Math.abs(deltaY) < 80) {
if (deltaX > 0) {
// Swipe right
this.handleSwipeRight();
} else {
// Swipe left
this.handleSwipeLeft();
}
}
startX = startY = null;
}, { passive: true });
}
handleSwipeLeft() {
// Navigate forward or show next content
console.log('👈 Swipe left detected');
}
handleSwipeRight() {
// Navigate back or show previous content
console.log('👉 Swipe right detected');
// Go back to dashboard if in game/typing mode
const gameSection = document.getElementById('gameSection');
const typingSection = document.getElementById('typingSection');
const bibleSection = document.getElementById('bibleSection');
if (!gameSection.classList.contains('hidden') && typeof backToStudentDashboard === 'function') {
backToStudentDashboard();
} else if (!typingSection.classList.contains('hidden') && typeof returnToStudentPanel === 'function') {
returnToStudentPanel();
} else if (!bibleSection.classList.contains('hidden') && typeof returnToStudentPanel === 'function') {
returnToStudentPanel();
}
}
showActivityOptions(element) {
// Show additional options on long press
if (window.vibrate) {
window.vibrate(50); // Haptic feedback
}
// Add visual feedback
element.style.transform = 'scale(1.05)';
element.style.boxShadow = '0 8px 25px rgba(0,0,0,0.2)';
setTimeout(() => {
element.style.transform = '';
element.style.boxShadow = '';
}, 200);
console.log('📋 Long press detected on activity option');
}
// Public methods
vibrate(pattern = 50) {
if (window.vibrate && this.isTablet) {
window.vibrate(pattern);
}
}
isTabletDevice() {
return this.isTablet;
}
}
// Initialize tablet optimizations
let tabletOptimizations = null;
document.addEventListener('DOMContentLoaded', () => {
tabletOptimizations = new TabletOptimizations();
// Make available globally
window.tabletOptimizations = tabletOptimizations;
console.log(`📱 Tablet optimization ${tabletOptimizations.isTabletDevice() ? 'enabled' : 'not needed'}`);
});
// Export for module usage
if (typeof module !== 'undefined' && module.exports) {
module.exports = TabletOptimizations;
}