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
388 lines
13 KiB
JavaScript
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;
|
|
} |