// Pull-to-Refresh implementation for mobile (function() { 'use strict'; class PullToRefresh { constructor(options = {}) { this.container = options.container || document.querySelector('.content'); this.onRefresh = options.onRefresh || (() => location.reload()); this.threshold = options.threshold || 80; this.max = options.max || 120; this.startY = 0; this.currentY = 0; this.pulling = false; this.refreshing = false; if (this.container && this.isMobile()) { this.init(); } } isMobile() { return window.innerWidth <= 768 || /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent); } init() { // Create pull-to-refresh indicator this.createIndicator(); // Add touch event listeners this.container.addEventListener('touchstart', this.onTouchStart.bind(this), { passive: true }); this.container.addEventListener('touchmove', this.onTouchMove.bind(this), { passive: false }); this.container.addEventListener('touchend', this.onTouchEnd.bind(this)); } createIndicator() { this.indicator = document.createElement('div'); this.indicator.className = 'pull-refresh-indicator'; this.indicator.innerHTML = `
Pull to refresh
`; // Insert at the beginning of container this.container.insertBefore(this.indicator, this.container.firstChild); // Add styles this.addStyles(); } addStyles() { if (document.getElementById('pull-refresh-styles')) return; const styles = ` `; document.head.insertAdjacentHTML('beforeend', styles); } onTouchStart(e) { if (this.refreshing) return; // Only activate at the top of the page if (this.container.scrollTop === 0) { this.startY = e.touches[0].clientY; this.pulling = true; } } onTouchMove(e) { if (!this.pulling || this.refreshing) return; this.currentY = e.touches[0].clientY; const diff = this.currentY - this.startY; if (diff > 0) { e.preventDefault(); // Calculate pull distance with resistance const pullDistance = Math.min(diff * 0.5, this.max); // Update container transform this.container.style.transform = `translateY(${pullDistance}px)`; // Update indicator this.indicator.classList.add('visible'); if (pullDistance >= this.threshold) { this.indicator.classList.add('pulling'); this.updateText('Release to refresh'); } else { this.indicator.classList.remove('pulling'); this.updateText('Pull to refresh'); } } } onTouchEnd() { if (!this.pulling || this.refreshing) return; const diff = this.currentY - this.startY; const pullDistance = Math.min(diff * 0.5, this.max); this.pulling = false; this.container.style.transition = 'transform 0.3s ease'; if (pullDistance >= this.threshold) { // Trigger refresh this.refresh(); } else { // Reset this.reset(); } } refresh() { this.refreshing = true; this.container.style.transform = 'translateY(60px)'; this.indicator.classList.add('refreshing'); this.updateText('Refreshing...'); // Add haptic feedback if available if (navigator.vibrate) { navigator.vibrate(10); } // Call refresh callback Promise.resolve(this.onRefresh()).then(() => { setTimeout(() => { this.reset(); }, 500); }); } reset() { this.container.style.transform = ''; this.indicator.classList.remove('visible', 'pulling', 'refreshing'); setTimeout(() => { this.container.style.transition = ''; this.refreshing = false; this.currentY = 0; }, 300); } updateText(text) { const textEl = this.indicator.querySelector('.pull-refresh-text'); if (textEl) textEl.textContent = text; } } // Auto-initialize on common containers document.addEventListener('DOMContentLoaded', function() { // Time tracking page if (document.querySelector('.time-tracking-container')) { new PullToRefresh({ container: document.querySelector('.time-tracking-container'), onRefresh: () => { // Refresh time entries if (window.loadTimeEntries) { return window.loadTimeEntries(); } } }); } // Notes list if (document.querySelector('.notes-container')) { new PullToRefresh({ container: document.querySelector('.notes-container'), onRefresh: () => { // Refresh notes if (window.refreshNotes) { return window.refreshNotes(); } } }); } }); // Expose globally window.PullToRefresh = PullToRefresh; })();