furry/static/js/furry-lazy-loading.js

379 lines
11 KiB
JavaScript

/* ===== FURRY LAZY LOADING ===== */
/* Moderne Lazy Loading für kasico.de */
class FurryLazyLoading {
constructor() {
this.images = [];
this.observer = null;
this.init();
}
init() {
// Intersection Observer API Support prüfen
if ('IntersectionObserver' in window) {
this.setupIntersectionObserver();
} else {
// Fallback für ältere Browser
this.setupScrollListener();
}
// Initial alle Bilder finden
this.findImages();
}
setupIntersectionObserver() {
this.observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
this.loadImage(entry.target);
this.observer.unobserve(entry.target);
}
});
}, {
rootMargin: '50px 0px', // 50px vor dem Viewport laden
threshold: 0.01
});
}
setupScrollListener() {
// Fallback für ältere Browser
let ticking = false;
const updateImages = () => {
this.images.forEach(img => {
if (this.isInViewport(img)) {
this.loadImage(img);
this.images = this.images.filter(i => i !== img);
}
});
ticking = false;
};
const requestTick = () => {
if (!ticking) {
requestAnimationFrame(updateImages);
ticking = true;
}
};
window.addEventListener('scroll', requestTick);
window.addEventListener('resize', requestTick);
// Initial check
requestTick();
}
findImages() {
// Alle Bilder mit data-src Attribut finden
const lazyImages = document.querySelectorAll('img[data-src], .furry-lazy-image');
lazyImages.forEach(img => {
// Placeholder hinzufügen
this.addPlaceholder(img);
// Zur Beobachtungsliste hinzufügen
if (this.observer) {
this.observer.observe(img);
} else {
this.images.push(img);
}
});
}
addPlaceholder(img) {
// Furry-Theme Placeholder
const placeholder = document.createElement('div');
placeholder.className = 'furry-lazy-placeholder';
placeholder.innerHTML = `
<div class="furry-loading furry-loading-lg"></div>
<div style="margin-top: 0.5rem; font-size: 0.8rem; color: var(--furry-text-secondary);">
🐾 Lade Bild...
</div>
`;
// Placeholder Styling
placeholder.style.cssText = `
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
background: var(--furry-bg-secondary);
border-radius: 0.5rem;
padding: 2rem;
min-height: 200px;
width: 100%;
height: 100%;
position: absolute;
top: 0;
left: 0;
z-index: 1;
`;
// Container für Bild und Placeholder
const container = document.createElement('div');
container.style.position = 'relative';
container.style.width = '100%';
container.style.height = '100%';
// Bild in Container verschieben
img.parentNode.insertBefore(container, img);
container.appendChild(img);
container.appendChild(placeholder);
// Placeholder Referenz speichern
img.furryPlaceholder = placeholder;
// Bild unsichtbar machen bis geladen
img.style.opacity = '0';
img.style.transition = 'opacity 0.3s ease';
}
loadImage(img) {
const src = img.dataset.src || img.src;
if (!src) return;
// Neues Bild erstellen für Preloading
const newImg = new Image();
newImg.onload = () => {
// Bild erfolgreich geladen
img.src = src;
img.style.opacity = '1';
// Placeholder entfernen
if (img.furryPlaceholder) {
img.furryPlaceholder.style.animation = 'fadeOut 0.3s ease';
setTimeout(() => {
if (img.furryPlaceholder.parentNode) {
img.furryPlaceholder.remove();
}
}, 300);
}
// Event triggern
this.triggerEvent('furry-image-loaded', { img, src });
};
newImg.onerror = () => {
// Fehler beim Laden
this.handleImageError(img);
};
// Bild laden
newImg.src = src;
}
handleImageError(img) {
// Furry-Theme Error Placeholder
const errorPlaceholder = document.createElement('div');
errorPlaceholder.className = 'furry-lazy-error';
errorPlaceholder.innerHTML = `
<div style="font-size: 3rem; margin-bottom: 1rem;">🐾</div>
<div style="color: var(--furry-text-secondary); text-align: center;">
<div style="font-weight: 600; margin-bottom: 0.5rem;">Bild konnte nicht geladen werden</div>
<div style="font-size: 0.8rem;">Versuche es später erneut</div>
</div>
`;
errorPlaceholder.style.cssText = `
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
background: var(--furry-bg-secondary);
border: 2px dashed var(--furry-border);
border-radius: 0.5rem;
padding: 2rem;
min-height: 200px;
width: 100%;
height: 100%;
`;
// Placeholder ersetzen
if (img.furryPlaceholder) {
img.furryPlaceholder.replaceWith(errorPlaceholder);
}
// Event triggern
this.triggerEvent('furry-image-error', { img });
}
isInViewport(element) {
const rect = element.getBoundingClientRect();
return (
rect.top >= 0 &&
rect.left >= 0 &&
rect.bottom <= (window.innerHeight || document.documentElement.clientHeight) &&
rect.right <= (window.innerWidth || document.documentElement.clientWidth)
);
}
// Manuelles Laden eines Bildes
loadImageNow(img) {
if (this.observer) {
this.observer.unobserve(img);
}
this.loadImage(img);
}
// Alle Bilder sofort laden
loadAllImages() {
this.images.forEach(img => this.loadImageNow(img));
this.images = [];
}
// Neue Bilder hinzufügen (für dynamisch geladene Inhalte)
addImages(container = document) {
const newImages = container.querySelectorAll('img[data-src], .furry-lazy-image');
newImages.forEach(img => {
if (!img.furryPlaceholder) {
this.addPlaceholder(img);
if (this.observer) {
this.observer.observe(img);
} else {
this.images.push(img);
}
}
});
}
// Performance-Optimierungen
optimizeImages() {
// WebP Support prüfen
const webpSupported = this.checkWebPSupport();
// Responsive Images
const images = document.querySelectorAll('img[data-srcset]');
images.forEach(img => {
if (webpSupported) {
// WebP Version verwenden
const webpSrcset = img.dataset.webpSrcset;
if (webpSrcset) {
img.srcset = webpSrcset;
}
}
});
}
checkWebPSupport() {
return new Promise(resolve => {
const webP = new Image();
webP.onload = webP.onerror = () => {
resolve(webP.height === 2);
};
webP.src = 'data:image/webp;base64,UklGRjoAAABXRUJQVlA4IC4AAACyAgCdASoCAAIALmk0mk0iIiIiIgBoSygABc6WWgAA/veff/0PP8bA//LwYAAA';
});
}
// Event Handling
triggerEvent(eventName, data = {}) {
const event = new CustomEvent(eventName, { detail: data });
document.dispatchEvent(event);
}
// Cleanup
destroy() {
if (this.observer) {
this.observer.disconnect();
}
// Event Listener entfernen
window.removeEventListener('scroll', this.updateImages);
window.removeEventListener('resize', this.updateImages);
}
}
// Globale Instanz erstellen
const furryLazyLoading = new FurryLazyLoading();
// CSS für Animationen
const lazyLoadingStyle = document.createElement('style');
lazyLoadingStyle.textContent = `
.furry-lazy-image {
opacity: 0;
transition: opacity 0.3s ease;
}
.furry-lazy-image.loaded {
opacity: 1;
}
.furry-lazy-placeholder {
background: linear-gradient(90deg, var(--furry-border) 25%, var(--furry-bg-secondary) 50%, var(--furry-border) 75%);
background-size: 200% 100%;
animation: furry-skeleton-loading 1.5s infinite;
}
@keyframes furry-skeleton-loading {
0% { background-position: 200% 0; }
100% { background-position: -200% 0; }
}
.furry-lazy-error {
background: var(--furry-bg-secondary);
border: 2px dashed var(--furry-border);
color: var(--furry-text-secondary);
}
/* Progressive Image Loading */
.furry-progressive-image {
position: relative;
overflow: hidden;
}
.furry-progressive-image img {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
object-fit: cover;
}
.furry-progressive-image .low-res {
filter: blur(10px);
transform: scale(1.1);
}
.furry-progressive-image .high-res {
opacity: 0;
transition: opacity 0.3s ease;
}
.furry-progressive-image .high-res.loaded {
opacity: 1;
}
`;
document.head.appendChild(lazyLoadingStyle);
// Event Listener für dynamisch geladene Inhalte
document.addEventListener('DOMContentLoaded', () => {
// Initial alle Bilder finden
furryLazyLoading.findImages();
// Mutation Observer für dynamische Inhalte
const observer = new MutationObserver((mutations) => {
mutations.forEach((mutation) => {
if (mutation.type === 'childList') {
mutation.addedNodes.forEach((node) => {
if (node.nodeType === 1) { // Element node
furryLazyLoading.addImages(node);
}
});
}
});
});
observer.observe(document.body, {
childList: true,
subtree: true
});
});
// Export für Module-System
if (typeof module !== 'undefined' && module.exports) {
module.exports = FurryLazyLoading;
}