1/** 2 * ð± Mobile Enhancement Manager 3 * Optimized mobile experience with touch gestures, performance, and UX improvements 4 */ 5 6class MobileEnhancementManager { 7 constructor() { 8 this.touchStartX = 0; 9 this.touchStartY = 0; 10 this.touchStartTime = 0; 11 this.isScrolling = false; 12 this.gestureThreshold = 50; 13 this.swipeTimeout = 300; 14 this.performanceOptimizations = new Set(); 15 16 this.init(); 17 } 18 19 init() { 20 this.setupTouchGestures(); 21 this.optimizeForMobile(); 22 this.setupMobileUI(); 23 this.enhanceInputs(); 24 this.setupPerformanceOptimizations(); 25 this.setupAccessibility(); 26 } 27 28 // ð Setup advanced touch gestures 29 setupTouchGestures() { 30 let touchStarted = false; 31 let lastTap = 0; 32 33 // Enhanced touch event handling 34 document.addEventListener('touchstart', (e) => { 35 touchStarted = true; 36 this.touchStartX = e.touches[0].clientX; 37 this.touchStartY = e.touches[0].clientY; 38 this.touchStartTime = Date.now(); 39 this.isScrolling = false; 40 41 // Double tap detection 42 const currentTime = Date.now(); 43 const tapLength = currentTime - lastTap; 44 if (tapLength < 500 && tapLength > 0) { 45 this.handleDoubleTap(e); 46 } 47 lastTap = currentTime; 48 49 }, { passive: false }); 50 51 document.addEventListener('touchmove', (e) => { 52 if (!touchStarted) return; 53 54 const touchX = e.touches[0].clientX; 55 const touchY = e.touches[0].clientY; 56 const deltaX = touchX - this.touchStartX; 57 const deltaY = touchY - this.touchStartY; 58 59 // Determine if user is scrolling 60 if (Math.abs(deltaY) > Math.abs(deltaX)) { 61 this.isScrolling = true; 62 } 63 64 // Handle horizontal swipes (only if not scrolling vertically) 65 if (!this.isScrolling && Math.abs(deltaX) > this.gestureThreshold) { 66 this.handleSwipe(deltaX > 0 ? 'right' : 'left', e); 67 } 68 69 }, { passive: false }); 70 71 document.addEventListener('touchend', (e) => { 72 touchStarted = false; 73 const touchDuration = Date.now() - this.touchStartTime; 74 75 // Handle long press 76 if (touchDuration > 500 && !this.isScrolling) { 77 this.handleLongPress(e); 78 } 79 80 }, { passive: true }); 81 82 // Pinch to zoom handling 83 document.addEventListener('gesturestart', (e) => { 84 e.preventDefault(); 85 }, { passive: false }); 86 87 document.addEventListener('gesturechange', (e) => { 88 e.preventDefault(); 89 }, { passive: false }); 90 } 91 92 // ð± Optimize UI for mobile devices 93 optimizeForMobile() { 94 document.body.classList.add('mobile-enhanced'); 95 96 // Optimize viewport 97 this.optimizeViewport(); 98 99 // Enhance scrolling 100 this.enhanceScrolling(); 101 102 // Optimize touch targets 103 this.optimizeTouchTargets(); 104 105 // Setup mobile navigation 106 this.setupMobileNavigation(); 107 108 // Optimize images for mobile 109 this.optimizeImages(); 110 } 111 112 // ð¼ï¸ Setup mobile-specific UI components 113 setupMobileUI() { 114 // Create mobile toolbar 115 this.createMobileToolbar(); 116 117 // Setup pull-to-refresh 118 this.setupPullToRefresh(); 119 120 // Create mobile-friendly modals 121 this.enhanceMobileModals(); 122 123 // Setup bottom sheet for actions 124 this.createBottomSheet(); 125 126 // Add mobile-specific buttons 127 this.addMobileButtons(); 128 } 129 130 // ð± Create mobile toolbar 131 createMobileToolbar() { 132 const toolbar = document.createElement('div'); 133 toolbar.id = 'mobile-toolbar'; 134 toolbar.className = 'mobile-toolbar'; 135 toolbar.innerHTML = ` 136 <button class="mobile-tool-btn" id="mobile-menu" title="Menu"> 137 <svg width="20" height="20" viewBox="0 0 24 24" fill="currentColor"> 138 <path d="M3 18h18v-2H3v2zm0-5h18v-2H3v2zm0-7v2h18V6H3z"/> 139 </svg> 140 </button> 141 <button class="mobile-tool-btn" id="mobile-scroll-top" title="Scroll to Top"> 142 <svg width="20" height="20" viewBox="0 0 24 24" fill="currentColor">
143 <path d="M7.41 15.41L12 10.83l4.59 4.58L18 14l-6-6-6 6z"/> 144 </svg> 145 </button> 146 <button class="mobile-tool-btn" id="mobile-voice" title="Voice Input"> 147 <svg width="20" height="20" viewBox="0 0 24 24" fill="currentColor"> 148 <path d="M12 14c1.66 0 2.99-1.34 2.99-3L15 5c0-1.66-1.34-3-3-3S9 3.34 9 5v6c0 1.66 1.34 3 3 3zm5.3-3c0 3-2.54 5.1-5.3 5.1S6.7 14 6.7 11H5c0 3.41 2.72 6.23 6 6.72V21h2v-3.28c3.28-.48 6-3.3 6-6.72h-1.7z"/> 149 </svg> 150 </button> 151 <button class="mobile-tool-btn" id="mobile-share" title="Share"> 152 <svg width="20" height="20" viewBox="0 0 24 24" fill="currentColor"> 153 <path d="M18 16.08c-.76 0-1.44.3-1.96.77L8.91 12.7c.05-.23.09-.46.09-.7s-.04-.47-.09-.7l7.05-4.11c.54.5 1.25.81 2.04.81 1.66 0 3-1.34 3-3s-1.34-3-3-3-3 1.34-3 3c0 .24.04.47.09.7L8.04 9.81C7.5 9.31 6.79 9 6 9c-1.66 0-3 1.34-3 3s1.34 3 3 3c.79 0 1.5-.31 2.04-.81l7.12 4.16c-.05.21-.08.43-.08.65 0 1.61 1.31 2.92 2.92 2.92 1.61 0 2.92-1.31 2.92-2.92s-1.31-2.92-2.92-2.92z"/> 154 </svg> 155 </button> 156 `; 157 158 document.body.appendChild(toolbar); 159 160 // Add event listeners 161 document.getElementById('mobile-menu').addEventListener('click', () => this.toggleMobileMenu()); 162 document.getElementById('mobile-scroll-top').addEventListener('click', () => this.scrollToTop()); 163 document.getElementById('mobile-voice').addEventListener('click', () => this.activateVoiceInput()); 164 document.getElementById('mobile-share').addEventListener('click', () => this.shareContent()); 165 } 166 167 // ð Setup pull-to-refresh 168 setupPullToRefresh() { 169 let startY = 0; 170 let currentY = 0; 171 let pullDistance = 0; 172 const threshold = 80; 173 let isPulling = false; 174 175 const chatContainer = document.getElementById('chat-container'); 176 if (!chatContainer) return; 177 178 // Create pull indicator 179 const pullIndicator = document.createElement('div'); 180 pullIndicator.className = 'pull-indicator'; 181 pullIndicator.innerHTML = ` 182 <div class="pull-icon">â</div> 183 <div class="pull-text">Pull to refresh</div> 184 `; 185 chatContainer.insertBefore(pullIndicator, chatContainer.firstChild); 186 187 chatContainer.addEventListener('touchstart', (e) => { 188 if (chatContainer.scrollTop === 0) { 189 startY = e.touches[0].clientY; 190 isPulling = true; 191 } 192 }); 193 194 chatContainer.addEventListener('touchmove', (e) => { 195 if (!isPulling) return; 196 197 currentY = e.touches[0].clientY; 198 pullDistance = currentY - startY; 199 200 if (pullDistance > 0 && chatContainer.scrollTop === 0) { 201 e.preventDefault(); 202 const progress = Math.min(pullDistance / threshold, 1); 203 pullIndicator.style.transform = `translateY(${pullDistance * 0.5}px)`; 204 pullIndicator.style.opacity = progress; 205 206 if (pullDistance > threshold) { 207 pullIndicator.classList.add('ready'); 208 pullIndicator.querySelector('.pull-text').textContent = 'Release to refresh'; 209 } else { 210 pullIndicator.classList.remove('ready'); 211 pullIndicator.querySelector('.pull-text').textContent = 'Pull to refresh'; 212 } 213 } 214 }); 215 216 chatContainer.addEventListener('touchend', () => { 217 if (isPulling && pullDistance > threshold) { 218 this.refreshContent(); 219 } 220 221 isPulling = false; 222 pullDistance = 0; 223 pullIndicator.style.transform = ''; 224 pullIndicator.style.opacity = ''; 225 pullIndicator.classList.remove('ready'); 226 }); 227 } 228 229 // ð± Enhance inputs for mobile 230 enhanceInputs() { 231 const inputs = document.querySelectorAll('input, textarea'); 232
233 inputs.forEach(input => { 234 // Prevent zoom on iOS 235 if (this.isIOS()) { 236 input.style.fontSize = '16px'; 237 } 238 239 // Add mobile-friendly attributes 240 input.setAttribute('autocomplete', 'off'); 241 input.setAttribute('autocorrect', 'off'); 242 input.setAttribute('autocapitalize', 'off'); 243 input.setAttribute('spellcheck', 'false'); 244 245 // Enhanced focus handling 246 input.addEventListener('focus', () => { 247 this.handleInputFocus(input); 248 }); 249 250 input.addEventListener('blur', () => { 251 this.handleInputBlur(input); 252 }); 253 }); 254 255 // Setup virtual keyboard handling 256 this.setupVirtualKeyboard(); 257 } 258 259 // â¨ï¸ Setup virtual keyboard handling 260 setupVirtualKeyboard() { 261 let initialViewportHeight = window.innerHeight; 262 263 window.addEventListener('resize', () => { 264 const currentHeight = window.innerHeight; 265 const heightDifference = initialViewportHeight - currentHeight; 266 267 // Keyboard is likely open if height decreased significantly 268 if (heightDifference > 150) { 269 document.body.classList.add('keyboard-open'); 270 this.adjustForKeyboard(heightDifference); 271 } else { 272 document.body.classList.remove('keyboard-open'); 273 this.resetKeyboardAdjustments(); 274 } 275 }); 276 } 277 278 // ð¯ Handle swipe gestures 279 handleSwipe(direction, event) { 280 const target = event.target.closest('.message, .swipeable'); 281 282 if (target) { 283 if (direction === 'left') { 284 this.showMessageActions(target); 285 } else if (direction === 'right') { 286 this.hideMessageActions(target); 287 } 288 } 289 290 // Global swipe actions 291 if (direction === 'right' && this.touchStartX < 20) { 292 this.openSideMenu(); 293 } else if (direction === 'left' && this.touchStartX > window.innerWidth - 20) { 294 this.closeSideMenu(); 295 } 296 } 297 298 // ð Handle double tap 299 handleDoubleTap(event) { 300 const target = event.target; 301 302 if (target.classList.contains('message')) { 303 this.selectMessage(target); 304 } else if (target.tagName === 'IMG') { 305 this.zoomImage(target); 306 } else { 307 // Double tap to scroll to top 308 this.scrollToTop(); 309 } 310 } 311 312 // ð Handle long press 313 handleLongPress(event) { 314 const target = event.target; 315 316 if (target.classList.contains('message')) { 317 this.showMessageContextMenu(target, event); 318 } else if (target.tagName === 'IMG') { 319 this.showImageContextMenu(target, event); 320 } 321 322 // Haptic feedback if available 323 if (navigator.vibrate) { 324 navigator.vibrate(50); 325 } 326 } 327 328 // ð§ Performance optimizations 329 setupPerformanceOptimizations() { 330 // Lazy loading for images 331 this.setupLazyLoading(); 332 333 // Virtual scrolling for long lists 334 this.setupVirtualScrolling(); 335 336 // Debounced scroll handling 337 this.setupOptimizedScrolling(); 338 339 // Memory management 340 this.setupMemoryManagement(); 341 342 // Battery optimization 343 this.setupBatteryOptimization(); 344 } 345 346 // ð¼ï¸ Setup lazy loading 347 setupLazyLoading() { 348 const imageObserver = new IntersectionObserver((entries) => { 349 entries.forEach(entry => { 350 if (entry.isIntersecting) { 351 const img = entry.target; 352 if (img.dataset.src) { 353 img.src = img.dataset.src; 354 img.removeAttribute('data-src'); 355 imageObserver.unobserve(img); 356 } 357 } 358 }); 359 }, { rootMargin: '50px' }); 360 361 // Observe existing images 362 document.querySelectorAll('img[data-src]').forEach(img => { 363 imageObserver.observe(img); 364 }); 365 366 // Observe new images 367 const mutationObserver = new MutationObserver(mutations => { 368 mutations.forEach(mutation => {
369 mutation.addedNodes.forEach(node => { 370 if (node.nodeType === 1) { 371 const images = node.querySelectorAll ? node.querySelectorAll('img[data-src]') : []; 372 images.forEach(img => imageObserver.observe(img)); 373 } 374 }); 375 }); 376 }); 377 378 mutationObserver.observe(document.body, { childList: true, subtree: true }); 379 } 380 381 // â¿ Setup accessibility enhancements 382 setupAccessibility() { 383 // Enhanced focus management 384 this.setupFocusManagement(); 385 386 // Screen reader optimizations 387 this.setupScreenReaderOptimizations(); 388 389 // High contrast mode detection 390 this.setupHighContrastMode(); 391 392 // Reduced motion support 393 this.setupReducedMotionSupport(); 394 } 395 396 // ð¯ Utility methods 397 isIOS() { 398 return /iPad|iPhone|iPod/.test(navigator.userAgent); 399 } 400 401 isAndroid() { 402 return /Android/.test(navigator.userAgent); 403 } 404 405 isTouchDevice() { 406 return 'ontouchstart' in window || navigator.maxTouchPoints > 0; 407 } 408 409 getDevicePixelRatio() { 410 return window.devicePixelRatio || 1; 411 } 412 413 // ð± Mobile-specific actions 414 scrollToTop() { 415 const chatContainer = document.getElementById('chat-container'); 416 if (chatContainer) { 417 chatContainer.scrollTo({ top: 0, behavior: 'smooth' }); 418 } 419 } 420 421 toggleMobileMenu() { 422 const menu = document.getElementById('mobile-menu-panel'); 423 if (menu) { 424 menu.classList.toggle('open'); 425 } else { 426 this.createMobileMenu(); 427 } 428 } 429 430 activateVoiceInput() { 431 const voiceButton = document.getElementById('voiceButton'); 432 if (voiceButton) { 433 voiceButton.click(); 434 } 435 } 436 437 shareContent() { 438 if (navigator.share) { 439 navigator.share({ 440 title: 'WikiLive AI Assistant', 441 text: 'Check out this AI-powered Wikipedia assistant!', 442 url: window.location.href 443 }); 444 } else { 445 // Fallback: copy URL to clipboard 446 navigator.clipboard.writeText(window.location.href).then(() => { 447 this.showToast('Link copied to clipboard!'); 448 }); 449 } 450 } 451 452 refreshContent() { 453 // Trigger a refresh of the current conversation 454 const newChatButton = document.getElementById('newChatButton'); 455 if (newChatButton) { 456 newChatButton.click(); 457 } 458 this.showToast('Content refreshed'); 459 } 460 461 showToast(message, duration = 3000) { 462 const toast = document.createElement('div'); 463 toast.className = 'mobile-toast'; 464 toast.textContent = message; 465 document.body.appendChild(toast); 466 467 setTimeout(() => toast.classList.add('show'), 10); 468 setTimeout(() => { 469 toast.classList.remove('show'); 470 setTimeout(() => toast.remove(), 300); 471 }, duration); 472 } 473 474 // ð§ Advanced mobile optimizations 475 optimizeViewport() { 476 // Prevent zoom on double tap 477 document.addEventListener('touchend', (e) => { 478 const now = Date.now(); 479 if (now - this.lastTouchEnd <= 300) { 480 e.preventDefault(); 481 } 482 this.lastTouchEnd = now; 483 }, false); 484 } 485 486 enhanceScrolling() { 487 // Smooth scrolling for mobile 488 document.documentElement.style.scrollBehavior = 'smooth'; 489 490 // Momentum scrolling for iOS 491 document.body.style.webkitOverflowScrolling = 'touch'; 492 493 // Prevent overscroll 494 document.body.style.overscrollBehavior = 'contain'; 495 } 496 497 optimizeTouchTargets() { 498 const buttons = document.querySelectorAll('button, a, [role="button"]'); 499 buttons.forEach(button => { 500 const rect = button.getBoundingClientRect(); 501 if (rect.width < 44 || rect.height < 44) { 502 button.style.minWidth = '44px'; 503 button.style.minHeight = '44px'; 504 button.style.display = 'inline-flex'; 505 button.style.alignItems = 'center'; 506 button.style.justifyContent = 'center'; 507 } 508 }); 509 } 510 511 handleInputFocus(input) { 512 // Scroll input into view 513 setTimeout(() => { 514 input.scrollIntoView({ behavior: 'smooth', block: 'center' }); 515 }, 300); 516 517 // Add focused class for styling 518 input.classList.add('mobile-focused'); 519 } 520 521 handleInputBlur(input) { 522 input.classList.remove('mobile-focused'); 523 } 524 525 adjustForKeyboard(keyboardHeight) { 526 const chatContainer = document.getElementById('chat-container'); 527 if (chatContainer) { 528 chatContainer.style.paddingBottom = `${keyboardHeight}px`; 529 } 530 } 531 532 resetKeyboardAdjustments() { 533 const chatContainer = document.getElementById('chat-container'); 534 if (chatContainer) { 535 chatContainer.style.paddingBottom = ''; 536 } 537 } 538 539 // ð Get mobile status 540 getStatus() { 541 return { 542 isTouch: this.isTouchDevice(), 543 isIOS: this.isIOS(), 544 isAndroid: this.isAndroid(), 545 pixelRatio: this.getDevicePixelRatio(),
546 viewportHeight: window.innerHeight, 547 viewportWidth: window.innerWidth, 548 optimizations: Array.from(this.performanceOptimizations) 549 }; 550 } 551} 552 553// ð Initialize mobile enhancement manager 554window.mobileEnhancementManager = null; 555 556// Auto-initialize for mobile devices 557function initializeMobileEnhancements() { 558 if (window.deviceDetector && (window.deviceDetector.isMobile() || window.deviceDetector.isTablet())) { 559 window.mobileEnhancementManager = new MobileEnhancementManager(); 560 console.log('ð± Mobile enhancement manager initialized'); 561 } 562} 563 564// Initialize when device detector is ready 565if (document.readyState === 'loading') { 566 document.addEventListener('DOMContentLoaded', () => { 567 setTimeout(initializeMobileEnhancements, 100); 568 }); 569} else { 570 setTimeout(initializeMobileEnhancements, 100); 571} 572 573// MobileEnhancementManager is available globally 574// export default MobileEnhancementManager;
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.