← back to Jill Website
public/js/adaptive-loading.js
365 lines
/**
* Adaptive Loading Manager
* Manages adaptive loading of images and videos based on connection speed
*/
class AdaptiveLoadingManager {
constructor() {
this.detector = window.connectionSpeed;
this.loadingImages = new Set();
this.loadedImages = new Set();
this.observers = new Map();
this.init();
}
/**
* Initialize adaptive loading
*/
init() {
// Wait for DOM to be ready
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', () => this.setupAdaptiveLoading());
} else {
this.setupAdaptiveLoading();
}
// Listen for connection changes
window.addEventListener('connectionchange', (event) => {
this.handleConnectionChange(event.detail);
});
}
/**
* Setup adaptive loading for all images and videos
*/
setupAdaptiveLoading() {
console.log('Setting up adaptive loading with speed:', this.detector.speedCategory);
// Process all images
this.processImages();
// Process all videos
this.processVideos();
// Setup intersection observer for lazy loading
this.setupIntersectionObserver();
}
/**
* Process all images on the page
*/
processImages() {
const images = document.querySelectorAll('img[data-adaptive]');
images.forEach(img => {
this.applyAdaptiveImage(img);
});
// Also handle regular images that should be adaptive
const regularImages = document.querySelectorAll('.villa-image img, .hero img, .gallery-item img');
regularImages.forEach(img => {
if (!img.hasAttribute('data-adaptive')) {
this.makeImageAdaptive(img);
}
});
}
/**
* Make a regular image adaptive
*/
makeImageAdaptive(img) {
const originalSrc = img.getAttribute('src');
if (!originalSrc || originalSrc.startsWith('data:')) return;
img.setAttribute('data-src-high', originalSrc);
img.setAttribute('data-adaptive', 'true');
this.applyAdaptiveImage(img);
}
/**
* Apply adaptive loading to an image
*/
applyAdaptiveImage(img) {
const quality = this.detector.getImageQuality();
const highSrc = img.getAttribute('data-src-high') || img.getAttribute('src');
const mediumSrc = img.getAttribute('data-src-medium') || highSrc;
const lowSrc = img.getAttribute('data-src-low') || highSrc;
// Show loading indicator
this.showLoadingIndicator(img);
let targetSrc = highSrc;
switch (quality) {
case 'low':
targetSrc = lowSrc;
break;
case 'medium':
targetSrc = mediumSrc;
break;
case 'high':
targetSrc = highSrc;
break;
}
// Load image with loading state
this.loadImage(img, targetSrc);
}
/**
* Load an image with progress tracking
*/
loadImage(img, src) {
if (this.loadedImages.has(img)) return;
this.loadingImages.add(img);
const tempImg = new Image();
tempImg.onload = () => {
img.src = src;
this.loadingImages.delete(img);
this.loadedImages.add(img);
this.hideLoadingIndicator(img);
img.classList.add('adaptive-loaded');
};
tempImg.onerror = () => {
console.error('Failed to load image:', src);
this.loadingImages.delete(img);
this.hideLoadingIndicator(img);
img.classList.add('adaptive-error');
};
tempImg.src = src;
}
/**
* Process all videos on the page
*/
processVideos() {
const videos = document.querySelectorAll('video, iframe[src*="youtube"], iframe[src*="vimeo"]');
videos.forEach(video => {
this.applyAdaptiveVideo(video);
});
}
/**
* Apply adaptive loading to a video
*/
applyAdaptiveVideo(video) {
const shouldEnable = this.detector.shouldEnableVideos();
const quality = this.detector.getVideoQuality();
if (!shouldEnable || quality === 'disabled') {
// Disable video and show placeholder
this.disableVideo(video);
} else {
// Enable video with appropriate quality
this.enableVideo(video, quality);
}
}
/**
* Disable video and show placeholder
*/
disableVideo(video) {
// Create placeholder
const placeholder = document.createElement('div');
placeholder.className = 'video-placeholder';
placeholder.innerHTML = `
<div class="video-placeholder-content">
<i class="fas fa-video-slash"></i>
<p>Video disabled for slow connection</p>
<button class="btn-secondary video-enable-btn">Load Video Anyway</button>
</div>
`;
// Replace video with placeholder
video.style.display = 'none';
video.parentNode.insertBefore(placeholder, video);
// Add click handler to enable video
const enableBtn = placeholder.querySelector('.video-enable-btn');
enableBtn.addEventListener('click', () => {
placeholder.remove();
video.style.display = '';
this.enableVideo(video, 'medium');
});
}
/**
* Enable video with specified quality
*/
enableVideo(video, quality) {
if (video.tagName === 'VIDEO') {
// Native video element
if (!video.hasAttribute('data-original-autoplay')) {
video.setAttribute('data-original-autoplay', video.autoplay.toString());
}
// Disable autoplay for medium quality
if (quality === '480p' || quality === 'medium') {
video.autoplay = false;
video.preload = 'metadata';
} else {
video.preload = 'auto';
}
} else if (video.tagName === 'IFRAME') {
// YouTube or Vimeo iframe
const src = video.getAttribute('src');
if (src && !src.includes('autoplay=0')) {
// Disable autoplay for medium quality
if (quality === '480p' || quality === 'medium') {
const newSrc = src.includes('?') ? `${src}&autoplay=0` : `${src}?autoplay=0`;
video.setAttribute('src', newSrc);
}
}
}
video.classList.add('adaptive-video-enabled');
}
/**
* Show loading indicator for an element
*/
showLoadingIndicator(element) {
const indicator = document.createElement('div');
indicator.className = 'adaptive-loading-indicator';
indicator.innerHTML = `
<div class="spinner"></div>
<span>Loading...</span>
`;
element.classList.add('adaptive-loading');
// Position indicator
const parent = element.parentElement;
if (parent && !parent.querySelector('.adaptive-loading-indicator')) {
parent.style.position = 'relative';
parent.appendChild(indicator);
}
}
/**
* Hide loading indicator for an element
*/
hideLoadingIndicator(element) {
element.classList.remove('adaptive-loading');
const parent = element.parentElement;
if (parent) {
const indicator = parent.querySelector('.adaptive-loading-indicator');
if (indicator) {
indicator.remove();
}
}
}
/**
* Setup intersection observer for lazy loading
*/
setupIntersectionObserver() {
if (!('IntersectionObserver' in window)) {
// Fallback: Load all images immediately
this.processImages();
return;
}
const observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
const img = entry.target;
if (img.hasAttribute('data-adaptive') && !this.loadedImages.has(img)) {
this.applyAdaptiveImage(img);
}
observer.unobserve(img);
}
});
}, {
rootMargin: '50px'
});
// Observe all adaptive images
document.querySelectorAll('img[data-adaptive]').forEach(img => {
observer.observe(img);
});
this.observers.set('images', observer);
}
/**
* Handle connection speed change
*/
handleConnectionChange(detail) {
console.log('Connection changed:', detail);
// Show notification
this.showConnectionNotification(detail.speedCategory);
// Optionally reload content with new quality
// (commented out to avoid disrupting user experience)
// this.setupAdaptiveLoading();
}
/**
* Show connection speed notification
*/
showConnectionNotification(speedCategory) {
const notification = document.createElement('div');
notification.className = 'connection-notification';
let message = '';
switch (speedCategory) {
case 'slow':
message = 'Slow connection detected. Loading optimized content.';
break;
case 'medium':
message = 'Medium connection detected. Loading balanced content.';
break;
case 'fast':
message = 'Fast connection detected. Loading full quality content.';
break;
}
notification.innerHTML = `
<i class="fas fa-wifi"></i>
<span>${message}</span>
`;
document.body.appendChild(notification);
// Auto-hide after 3 seconds
setTimeout(() => {
notification.classList.add('fade-out');
setTimeout(() => notification.remove(), 500);
}, 3000);
}
/**
* Get loading status
*/
getStatus() {
return {
loading: this.loadingImages.size,
loaded: this.loadedImages.size,
speedCategory: this.detector.speedCategory
};
}
}
// Initialize on page load
window.AdaptiveLoadingManager = AdaptiveLoadingManager;
// Auto-initialize
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', () => {
window.adaptiveLoading = new AdaptiveLoadingManager();
});
} else {
window.adaptiveLoading = new AdaptiveLoadingManager();
}