PageSourceSearch

https://dynamic-salmiakki-095693.netlify.app/mobile-enhancements.js

js dynamic-salmiakki-095693.netlify.app collected 2026-10-03 10:40:19 UTC 19,722 bytes, 574 lines download raw bytes

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.