<!-- Lightbox для изображений в статьях --> <script> class ContentLightbox { constructor() { this.modal = null; this.modalImage = null; this.caption = null; this.images = []; this.currentIndex = 0; this.debug = true; this.initialized = false; this.init(); } init() { this.createModal(); if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', () => { setTimeout(() => this.setup(), 500); }); } else { setTimeout(() => this.setup(), 500); } } createModal() { this.modal = document.createElement('div'); this.modal.className = 'content-lightbox'; this.modal.setAttribute('aria-hidden', 'true'); this.modal.style.cssText = ` position: fixed; top: 0; left: 0; width: 100%; height: 100%; background-color: rgba(0, 0, 0, 0.45); display: flex; align-items: center; justify-content: center; z-index: 9999; opacity: 0; visibility: hidden; transition: opacity 0.3s ease, visibility 0.3s ease; backdrop-filter: blur(8px); `; const container = document.createElement('div'); container.className = 'content-lightbox__container'; container.style.cssText = ` position: relative; max-width: 95vw; max-height: 95vh; margin: 20px; `; const closeBtn = document.createElement('button'); closeBtn.className = 'content-lightbox__close'; closeBtn.innerHTML = '×'; closeBtn.setAttribute('aria-label', 'Закрыть'); closeBtn.style.cssText = ` position: absolute; top: -45px; right: 0; background: rgba(255, 255, 255, 0.15); border: none; color: white; font-size: 32px; cursor: pointer; line-height: 1; width: 36px; height: 36px; border-radius: 50%; display: flex; align-items: center; justify-content: center; z-index: 10; transition: all 0.2s ease; backdrop-filter: blur(5px); `; closeBtn.addEventListener('mouseenter', () => { closeBtn.style.background = 'rgba(255, 255, 255, 0.25)'; closeBtn.style.transform = 'scale(1.1)'; }); closeBtn.addEventListener('mouseleave', () => { closeBtn.style.background = 'rgba(255, 255, 255, 0.15)'; closeBtn.style.transform = 'scale(1)'; }); closeBtn.addEventListener('click', (e) => { e.stopPropagation(); this.close(); }); this.modalImage = document.createElement('img'); this.modalImage.className = 'content-lightbox__image'; this.modalImage.style.cssText = ` display: block; max-width: 100%; max-height: 85vh; margin: 0 auto; border-radius: 4px; box-shadow: 0 25px 50px rgba(0, 0, 0, 0.5); cursor: default; transform: scale(0.96); opacity: 0; transition: transform 0.4s cubic-bezier(0.175, 0.885, 0.32, 1.275), opacity 0.4s ease; `; this.caption = document.createElement('div'); this.caption.className = 'content-lightbox__caption'; this.caption.style.cssText = ` text-align: center; color: rgba(255, 255, 255, 0.9); font-size: 15px; margin-top: 15px; padding: 8px 16px; background: rgba(255, 255, 255, 0.08); border-radius: 4px; backdrop-filter: blur(10px); opacity: 0; transform: translateY(10px); transition: opacity 0.4s ease, transform 0.4s ease; `; container.appendChild(closeBtn); container.appendChild(this.modalImage); container.appendChild(this.caption); this.modal.appendChild(container); document.body.appendChild(this.modal); this.modal.addEventListener('click', (e) => { if (e.target === this.modal) { this.close(); } }); document.addEventListener('keydown', (e) => { if (e.key === 'Escape' && this.isOpen()) { this.close(); } }); document.addEventListener('keydown', (e) => { if (!this.isOpen()) return; if (e.key === 'ArrowLeft') { e.preventDefault(); this.prev(); } else if (e.key === 'ArrowRight') { e.preventDefault(); this.next(); } }); } setup() { if (this.initialized) return; this.initialized = true; const allLightboxImages = document.querySelectorAll('img[data-lightbox="true"]'); if (this.debug) { console.log('=== DEBUG LIGHTBOX ==='); console.log('Всего изображений с data-lightbox="true":', allLightboxImages.length); } const imagePromises = Array.from(allLightboxImages).map(img => { return new Promise((resolve) => { if (img.complete && img.naturalWidth > 0) { resolve(img); } else if (img.complete && img.naturalWidth === 0) { if (this.debug) console.log('⚠️ Изображение не загружено:', img.src); resolve(null); } else { img.onload = () => resolve(img); img.onerror = () => { if (this.debug) console.log('❌ Ошибка загрузки изображения:', img.src); resolve(null); }; setTimeout(() => { if (img.naturalWidth === 0) { if (this.debug) console.log('⚠️ Таймаут загрузки изображения:', img.src); } resolve(img); }, 1000); } }); }); Promise.all(imagePromises).then(loadedImages => { const validImages = loadedImages.filter(img => img !== null); if (this.debug) { console.log('Загруженных изображений:', validImages.length); console.log('Не загруженных:', loadedImages.length - validImages.length); } this.images = validImages.filter(img => { const naturalWidth = img.naturalWidth || 0; const naturalHeight = img.naturalHeight || 0; const displayedWidth = img.offsetWidth || img.width || 0; const displayedHeight = img.offsetHeight || img.height || 0; if (this.debug) { console.log('--- Проверка изображения ---'); console.log('Src:', img.src.substring(0, 100)); console.log('Natural размер:', naturalWidth, '×', naturalHeight); console.log('Displayed размер:', displayedWidth, '×', displayedHeight); console.log('Complete:', img.complete); console.log('Сжатие:', naturalWidth > 0 ? Math.round((displayedWidth / naturalWidth) * 100) + '%' : 'N/A'); } if (naturalWidth === 0 && naturalHeight === 0) { if (this.debug) console.log('❌ Пропущено: размеры 0×0 (еще не загружено)'); return false; } const isNotIcon = !this.isIcon(img); if (!isNotIcon) { if (this.debug) console.log('❌ Отфильтровано: иконка'); return false; } // УЛУЧШЕННАЯ ЛОГИКА: учитываем и оригинальные, и отображаемые размеры const MIN_WIDTH = 400; const MIN_HEIGHT = 200; const MIN_DISPLAYED_WIDTH = 300; // Минимальная отображаемая ширина const MIN_DISPLAYED_HEIGHT = 150; // Минимальная отображаемая высота // Проверяем пропорции оригинала const naturalAspectRatio = naturalWidth / naturalHeight; const isNaturalHorizontal = naturalAspectRatio > 3; const isNaturalVeryHorizontal = naturalAspectRatio > 5; // Проверяем пропорции отображаемого const displayedAspectRatio = displayedWidth / displayedHeight; const isDisplayedHorizontal = displayedAspectRatio > 3; const isDisplayedVeryHorizontal = displayedAspectRatio > 5; // Коэффициент сжатия (насколько изображение уменьшилось для отображения) const compressionRatio = naturalWidth > 0 ? displayedWidth / naturalWidth : 1; const isCompressed = compressionRatio < 0.9; // Сжато более чем на 10% if (this.debug) { console.log(`Пропорции оригинала: ${naturalAspectRatio.toFixed(2)}:1`); console.log(`Пропорции отображаемого: ${displayedAspectRatio.toFixed(2)}:1`); console.log(`Сжатие: ${Math.round(compressionRatio * 100)}% (${isCompressed ? 'сильно сжато' : 'почти оригинал'})`); } // Проверка №1: Достаточно ли большой оригинал? let isNaturalLargeEnough; if (isNaturalVeryHorizontal) { // Для очень горизонтальных оригиналов (панорамы, таблицы) isNaturalLargeEnough = naturalWidth >= MIN_WIDTH * 1.5 && naturalHeight >= MIN_HEIGHT * 0.6; } else if (isNaturalHorizontal) { // Для горизонтальных оригиналов isNaturalLargeEnough = naturalWidth >= MIN_WIDTH && naturalHeight >= MIN_HEIGHT * 0.7; } else { // Для обычных оригиналов isNaturalLargeEnough = naturalWidth >= MIN_WIDTH && naturalHeight >= MIN_HEIGHT; } // Проверка №2: Достаточно ли большой отображаемый размер? let isDisplayedLargeEnough; if (isDisplayedVeryHorizontal) { // Для очень горизонтальных отображаемых isDisplayedLargeEnough = displayedWidth >= MIN_DISPLAYED_WIDTH * 1.2 && displayedHeight >= MIN_DISPLAYED_HEIGHT * 0.8; } else if (isDisplayedHorizontal) { // Для горизонтальных отображаемых isDisplayedLargeEnough = displayedWidth >= MIN_DISPLAYED_WIDTH && displayedHeight >= MIN_DISPLAYED_HEIGHT * 0.9; } else { // Для обычных отображаемых isDisplayedLargeEnough = displayedWidth >= MIN_DISPLAYED_WIDTH && displayedHeight >= MIN_DISPLAYED_HEIGHT; } // Итоговая проверка: изображение можно увеличивать если: // 1. Оригинал достаточно большой ИЛИ // 2. Отображаемый размер достаточно большой (особенно если изображение сжато) const isLargeEnough = isNaturalLargeEnough || (isCompressed && isDisplayedLargeEnough); if (this.debug) { console.log(`Оригинал достаточно большой: ${isNaturalLargeEnough}`); console.log(`Отображаемый достаточно большой: ${isDisplayedLargeEnough}`); console.log(`Сжатое изображение: ${isCompressed}`); console.log(`Итог: достаточно большой = ${isLargeEnough}`); } if (!isLargeEnough) { if (this.debug) console.log('❌ Отфильтровано: слишком маленькое для увеличения'); return false; } // Проверка для документации const currentPath = window.location.pathname.toLowerCase(); const isDocsPage = currentPath.includes('/manuals/') || currentPath.includes('/docs/') || currentPath.includes('/laboratory/'); if (isDocsPage) { if (this.debug) console.log('✅ Страница документации - разрешаем увеличение'); return true; } // Для обычных страниц - проверяем, полностью ли видно const isNotFullyVisible = !this.isFullyVisible(img, displayedWidth, displayedHeight); if (!isNotFullyVisible) { if (this.debug) console.log('❌ Отфильтровано: полностью видно на обычной странице'); return false; } if (this.debug) console.log('✅ Изображение доступно для увеличения'); return true; }); if (this.debug) { console.log('=== ИТОГО ==='); console.log('Доступно для увеличения:', this.images.length, 'из', validImages.length); console.log('Процент:', Math.round((this.images.length / validImages.length) * 100) + '%'); } allLightboxImages.forEach((img) => { const isZoomable = this.images.includes(img); if (isZoomable) { img.style.cursor = 'zoom-in'; img.classList.add('is-zoomable'); img.addEventListener('mouseenter', () => { img.style.opacity = '0.92'; img.style.transform = 'translateY(-3px)'; }); img.addEventListener('mouseleave', () => { img.style.opacity = '1'; img.style.transform = 'translateY(0)'; }); img.addEventListener('click', (e) => { e.preventDefault(); e.stopPropagation(); const filteredIndex = this.images.indexOf(img); if (filteredIndex !== -1) { this.currentIndex = filteredIndex; this.open(img.dataset.originalSrc || img.src, img.alt || img.title || ''); } }); if (this.debug) { console.log(`✅ ${img.src.substring(0, 80)}... - МОЖНО увеличивать`); } } else { img.style.cursor = 'default'; img.classList.remove('content-image--zoomable'); img.classList.add('not-zoomable'); img.onclick = null; if (this.debug && img.naturalWidth > 0) { console.log(`❌ ${img.src.substring(0, 80)}... - НЕЛЬЗЯ увеличивать`); } } }); setTimeout(() => { this.checkLateLoadingImages(allLightboxImages); }, 3000); }).catch(error => { console.error('Ошибка при загрузке изображений:', error); }); } checkLateLoadingImages(allImages) { const lateImages = Array.from(allImages).filter(img => { const naturalWidth = img.naturalWidth || 0; const naturalHeight = img.naturalHeight || 0; const displayedWidth = img.offsetWidth || img.width || 0; const displayedHeight = img.offsetHeight || img.height || 0; if (!this.images.includes(img) && naturalWidth > 0 && naturalHeight > 0 && !this.isIcon(img)) { // Та же улучшенная логика проверки const MIN_WIDTH = 400; const MIN_HEIGHT = 200; const MIN_DISPLAYED_WIDTH = 300; const MIN_DISPLAYED_HEIGHT = 150; const naturalAspectRatio = naturalWidth / naturalHeight; const isNaturalHorizontal = naturalAspectRatio > 3; const isNaturalVeryHorizontal = naturalAspectRatio > 5; const displayedAspectRatio = displayedWidth / displayedHeight; const isDisplayedHorizontal = displayedAspectRatio > 3; const isDisplayedVeryHorizontal = displayedAspectRatio > 5; const compressionRatio = naturalWidth > 0 ? displayedWidth / naturalWidth : 1; const isCompressed = compressionRatio < 0.9; let isNaturalLargeEnough; if (isNaturalVeryHorizontal) { isNaturalLargeEnough = naturalWidth >= MIN_WIDTH * 1.5 && naturalHeight >= MIN_HEIGHT * 0.6; } else if (isNaturalHorizontal) { isNaturalLargeEnough = naturalWidth >= MIN_WIDTH && naturalHeight >= MIN_HEIGHT * 0.7; } else { isNaturalLargeEnough = naturalWidth >= MIN_WIDTH && naturalHeight >= MIN_HEIGHT; } let isDisplayedLargeEnough; if (isDisplayedVeryHorizontal) { isDisplayedLargeEnough = displayedWidth >= MIN_DISPLAYED_WIDTH * 1.2 && displayedHeight >= MIN_DISPLAYED_HEIGHT * 0.8; } else if (isDisplayedHorizontal) { isDisplayedLargeEnough = displayedWidth >= MIN_DISPLAYED_WIDTH && displayedHeight >= MIN_DISPLAYED_HEIGHT * 0.9; } else { isDisplayedLargeEnough = displayedWidth >= MIN_DISPLAYED_WIDTH && displayedHeight >= MIN_DISPLAYED_HEIGHT; } return isNaturalLargeEnough || (isCompressed && isDisplayedLargeEnough); } return false; }); if (lateImages.length > 0 && this.debug) { console.log('=== ПОЗДНЕЕ ОБНАРУЖЕНИЕ ==='); console.log('Найдено поздно загруженных изображений:', lateImages.length); } lateImages.forEach(img => { if (!this.images.includes(img)) { this.images.push(img); img.style.cursor = 'zoom-in'; img.classList.add('is-zoomable'); img.classList.remove('not-zoomable'); img.addEventListener('click', (e) => { e.preventDefault(); e.stopPropagation(); const filteredIndex = this.images.indexOf(img); if (filteredIndex !== -1) { this.currentIndex = filteredIndex; this.open(img.dataset.originalSrc || img.src, img.alt || img.title || ''); } }); if (this.debug) { console.log(`✅ Позднее обнаружение: ${img.src.substring(0, 80)}...`); } } }); } isIcon(img) { const src = (img.src || '').toLowerCase(); const alt = (img.alt || '').toLowerCase(); const classes = (img.className || '').toLowerCase(); const iconKeywords = [ 'icon', 'logo', 'avatar', 'favicon', 'sprite', 'emoji', 'svg', 'ico', 'png-16', 'png-32', 'png-64', 'icon-', '-icon' ]; const naturalWidth = img.naturalWidth || img.offsetWidth || 0; const naturalHeight = img.naturalHeight || img.offsetHeight || 0; const isVerySmall = naturalWidth < 50 && naturalHeight < 50; const isSquareAndSmall = Math.abs(naturalWidth - naturalHeight) < 10 && naturalWidth < 100; const hasIconKeyword = iconKeywords.some(keyword => src.includes(keyword) || alt.includes(keyword) || classes.includes(keyword) ); return isVerySmall || (isSquareAndSmall && hasIconKeyword); } isFullyVisible(img, displayedWidth = null, displayedHeight = null) { const currentPath = window.location.pathname.toLowerCase(); const isDocsPage = currentPath.includes('/manuals/') || currentPath.includes('/docs/') || currentPath.includes('/laboratory/'); if (isDocsPage) { return false; } const imgWidth = displayedWidth || img.offsetWidth || img.naturalWidth; const container = img.parentElement; if (!container) return false; const containerWidth = container.clientWidth; return imgWidth < containerWidth * 0.90; } open(src, caption) { if (this.debug) { console.log('Открываем lightbox:', src.substring(0, 100)); } this.modalImage.src = src; this.modalImage.alt = caption; this.caption.textContent = caption; this.modal.setAttribute('aria-hidden', 'false'); this.modal.style.visibility = 'visible'; requestAnimationFrame(() => { this.modal.style.opacity = '1'; setTimeout(() => { this.modalImage.style.opacity = '1'; this.modalImage.style.transform = 'scale(1)'; this.caption.style.opacity = '1'; this.caption.style.transform = 'translateY(0)'; }, 50); }); document.body.style.overflow = 'hidden'; document.documentElement.style.overflow = 'hidden'; } close() { this.modalImage.style.opacity = '0'; this.modalImage.style.transform = 'scale(0.96)'; this.caption.style.opacity = '0'; this.caption.style.transform = 'translateY(10px)'; setTimeout(() => { this.modal.style.opacity = '0'; this.modal.style.visibility = 'hidden'; this.modal.setAttribute('aria-hidden', 'true'); document.body.style.overflow = ''; document.documentElement.style.overflow = ''; }, 400); } isOpen() { return this.modal.getAttribute('aria-hidden') === 'false'; } next() { if (this.images.length > 1) { this.currentIndex = (this.currentIndex + 1) % this.images.length; const nextImg = this.images[this.currentIndex]; this.open(nextImg.dataset.originalSrc || nextImg.src, nextImg.alt || nextImg.title || ''); } } prev() { if (this.images.length > 1) { this.currentIndex = (this.currentIndex - 1 + this.images.length) % this.images.length; const prevImg = this.images[this.currentIndex]; this.open(prevImg.dataset.originalSrc || prevImg.src, prevImg.alt || prevImg.title || ''); } } }
document.addEventListener('DOMContentLoaded', () => { setTimeout(() => { window.contentLightbox = new ContentLightbox(); }, 100); const style = document.createElement('style'); style.textContent = ` .content-image { max-width: 100%; height: auto; display: block; margin: 1.5rem auto; border-radius: 4px; transition: all 0.25s ease; } .content-image.is-zoomable { cursor: zoom-in; } .content-image.not-zoomable { cursor: default; } .content-image--shadow.is-zoomable { box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1); } .content-image:not(.is-zoomable), .content-image.not-zoomable { box-shadow: none !important; } .content-image.is-zoomable:hover { box-shadow: 0 8px 24px rgba(0, 0, 0, 0.15); opacity: 0.95; } .content-image.not-zoomable:hover { transform: none !important; opacity: 1 !important; box-shadow: none !important; } .content-image__caption { text-align: center; font-size: 0.875rem; color: #666; margin-top: 0.5rem; line-height: 1.4; font-style: italic; } .content-image__wrapper { margin: 1.5rem 0; text-align: center; } @media (max-width: 768px) { .content-image { margin: 1rem auto; border-radius: 3px; } .content-image--shadow.is-zoomable { box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); } .content-image.is-zoomable:hover { box-shadow: 0 4px 16px rgba(0, 0, 0, 0.12); } .content-lightbox__close { top: 15px; right: 15px; width: 32px; height: 32px; font-size: 28px; } } @media (hover: none) { .content-image:hover { transform: none !important; } } `; document.head.appendChild(style); });
let scrollCheckTimeout = null; window.addEventListener('scroll', () => { if (scrollCheckTimeout) clearTimeout(scrollCheckTimeout); scrollCheckTimeout = setTimeout(() => { if (window.contentLightbox && window.contentLightbox.initialized) { const visibleImages = document.querySelectorAll('img[data-lightbox="true"]:not(.is-zoomable):not(.not-zoomable)'); if (visibleImages.length > 0) { window.contentLightbox.checkLateLoadingImages(visibleImages); } } }, 500); });
document.addEventListener('visibilitychange', () => { if (document.visibilityState === 'visible') { setTimeout(() => { if (window.contentLightbox) { window.contentLightbox.setup(); } }, 1000); } }); </script>Добавлено (2025-12-19, 11:31) --------------------------------------------- <!-- Lightbox для изображений в статьях --> <script> class ContentLightbox { constructor() { this.modal = null; this.modalImage = null; this.caption = null; this.images = []; this.currentIndex = 0; this.debug = true; this.initialized = false; this.observer = null; this.pendingImages = new Set(); // Изображения ожидающие загрузки this.init(); } init() { this.createModal(); this.setupIntersectionObserver(); if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', () => { setTimeout(() => this.setup(), 500); }); } else { setTimeout(() => this.setup(), 500); } } createModal() { this.modal = document.createElement('div'); this.modal.className = 'content-lightbox'; this.modal.setAttribute('aria-hidden', 'true'); this.modal.style.cssText = ` position: fixed; top: 0; left: 0; width: 100%; height: 100%; background-color: rgba(0, 0, 0, 0.45); display: flex; align-items: center; justify-content: center; z-index: 9999; opacity: 0; visibility: hidden; transition: opacity 0.3s ease, visibility 0.3s ease; backdrop-filter: blur(8px); `; const container = document.createElement('div'); container.className = 'content-lightbox__container'; container.style.cssText = ` position: relative; max-width: 95vw; max-height: 95vh; margin: 20px; `; const closeBtn = document.createElement('button'); closeBtn.className = 'content-lightbox__close'; closeBtn.innerHTML = '×'; closeBtn.setAttribute('aria-label', 'Закрыть'); closeBtn.style.cssText = ` position: absolute; top: -45px; right: 0; background: rgba(255, 255, 255, 0.15); border: none; color: white; font-size: 32px; cursor: pointer; line-height: 1; width: 36px; height: 36px; border-radius: 50%; display: flex; align-items: center; justify-content: center; z-index: 10; transition: all 0.2s ease; backdrop-filter: blur(5px); `; closeBtn.addEventListener('mouseenter', () => { closeBtn.style.background = 'rgba(255, 255, 255, 0.25)'; closeBtn.style.transform = 'scale(1.1)'; }); closeBtn.addEventListener('mouseleave', () => { closeBtn.style.background = 'rgba(255, 255, 255, 0.15)'; closeBtn.style.transform = 'scale(1)'; }); closeBtn.addEventListener('click', (e) => { e.stopPropagation(); this.close(); }); this.modalImage = document.createElement('img'); this.modalImage.className = 'content-lightbox__image'; this.modalImage.style.cssText = ` display: block; max-width: 100%; max-height: 85vh; margin: 0 auto; border-radius: 4px; box-shadow: 0 25px 50px rgba(0, 0, 0, 0.5); cursor: default; transform: scale(0.96); opacity: 0; transition: transform 0.4s cubic-bezier(0.175, 0.885, 0.32, 1.275), opacity 0.4s ease; `; this.caption = document.createElement('div'); this.caption.className = 'content-lightbox__caption'; this.caption.style.cssText = ` text-align: center; color: rgba(255, 255, 255, 0.9); font-size: 15px; margin-top: 15px; padding: 8px 16px; background: rgba(255, 255, 255, 0.08); border-radius: 4px; backdrop-filter: blur(10px); opacity: 0; transform: translateY(10px); transition: opacity 0.4s ease, transform 0.4s ease; `; container.appendChild(closeBtn); container.appendChild(this.modalImage); container.appendChild(this.caption); this.modal.appendChild(container); document.body.appendChild(this.modal); this.modal.addEventListener('click', (e) => { if (e.target === this.modal) { this.close(); } }); document.addEventListener('keydown', (e) => { if (e.key === 'Escape' && this.isOpen()) { this.close(); } }); document.addEventListener('keydown', (e) => { if (!this.isOpen()) return; if (e.key === 'ArrowLeft') { e.preventDefault(); this.prev(); } else if (e.key === 'ArrowRight') { e.preventDefault(); this.next(); } }); } setupIntersectionObserver() { if ('IntersectionObserver' in window) { this.observer = new IntersectionObserver((entries) => { entries.forEach(entry => { if (entry.isIntersecting) { const img = entry.target; this.observer.unobserve(img); if (img.dataset.originalSrc) { // Для lazy loading изображений if (!img.src || img.src !== img.dataset.originalSrc) { img.src = img.dataset.originalSrc; } } // Проверяем изображение после появления в viewport this.checkSingleImage(img); } }); }, { rootMargin: '200px', // Начинаем загружать за 200px до появления threshold: 0.1 }); } } setup() { if (this.initialized) return; this.initialized = true; const allLightboxImages = document.querySelectorAll('img[data-lightbox="true"]'); if (this.debug) { console.log('=== DEBUG LIGHTBOX ==='); console.log('Всего изображений с data-lightbox="true":', allLightboxImages.length); } // Разделяем изображения на видимые и невидимые const visibleImages = []; const invisibleImages = []; allLightboxImages.forEach(img => { const rect = img.getBoundingClientRect(); const isVisible = ( rect.top < window.innerHeight && rect.bottom > 0 && rect.left < window.innerWidth && rect.right > 0 ); if (isVisible) { visibleImages.push(img); } else { invisibleImages.push(img); } }); if (this.debug) { console.log('Видимых изображений:', visibleImages.length); console.log('Невидимых изображений:', invisibleImages.length); } // Обрабатываем видимые изображения сразу this.processImageBatch(visibleImages, 'visible'); // Начинаем отслеживать невидимые изображения invisibleImages.forEach(img => { if (this.observer) { this.observer.observe(img); this.pendingImages.add(img); } else { // Fallback для старых браузеров this.processImageBatch([img], 'invisible-fallback'); } }); // Также запускаем проверку всех изображений через 3 секунды setTimeout(() => { this.checkAllImages(allLightboxImages); }, 3000); // И через 10 секунд на всякий случай setTimeout(() => { this.finalCheck(allLightboxImages); }, 10000); } processImageBatch(images, batchType = '') { if (this.debug && batchType) { console.log(`--- Обработка ${batchType} изображений (${images.length}) ---`); } images.forEach(img => { if (this.pendingImages.has(img)) { this.pendingImages.delete(img); } this.checkSingleImage(img); }); } checkSingleImage(img) { return new Promise((resolve) => { const checkImage = () => { const naturalWidth = img.naturalWidth || 0; const naturalHeight = img.naturalHeight || 0; const displayedWidth = img.offsetWidth || img.width || 0; const displayedHeight = img.offsetHeight || img.height || 0; if (this.debug) { console.log('--- Проверка одного изображения ---'); console.log('Src:', img.src.substring(0, 100)); console.log('Natural размер:', naturalWidth, '×', naturalHeight); console.log('Displayed размер:', displayedWidth, '×', displayedHeight); console.log('Complete:', img.complete); } // Если изображение еще не загружено, ждем if (!img.complete || (naturalWidth === 0 && displayedWidth === 0)) { if (this.debug) console.log('🕐 Изображение еще не загружено, ждем...'); if (!img.complete) { img.addEventListener('load', () => { setTimeout(() => checkImage(), 100); }); img.addEventListener('error', () => { if (this.debug) console.log('❌ Ошибка загрузки изображения'); resolve(false); }); } else { setTimeout(() => checkImage(), 500); } return; } if (naturalWidth === 0 && naturalHeight === 0 && displayedWidth === 0 && displayedHeight === 0) { if (this.debug) console.log('❌ Размеры 0×0, пропускаем'); resolve(false); return; } const isNotIcon = !this.isIcon(img); if (!isNotIcon) { if (this.debug) console.log('❌ Отфильтровано: иконка'); this.markAsNotZoomable(img); resolve(false); return; } // Логика проверки размеров (та же что и раньше) const MIN_WIDTH = 400; const MIN_HEIGHT = 200; const MIN_DISPLAYED_WIDTH = 300; const MIN_DISPLAYED_HEIGHT = 150; const naturalAspectRatio = naturalWidth / naturalHeight; const isNaturalHorizontal = naturalAspectRatio > 3; const isNaturalVeryHorizontal = naturalAspectRatio > 5; const displayedAspectRatio = displayedWidth / displayedHeight; const isDisplayedHorizontal = displayedAspectRatio > 3; const isDisplayedVeryHorizontal = displayedAspectRatio > 5; const compressionRatio = naturalWidth > 0 ? displayedWidth / naturalWidth : 1; const isCompressed = compressionRatio < 0.9; let isNaturalLargeEnough; if (isNaturalVeryHorizontal) { isNaturalLargeEnough = naturalWidth >= MIN_WIDTH * 1.5 && naturalHeight >= MIN_HEIGHT * 0.6; } else if (isNaturalHorizontal) { isNaturalLargeEnough = naturalWidth >= MIN_WIDTH && naturalHeight >= MIN_HEIGHT * 0.7; } else { isNaturalLargeEnough = naturalWidth >= MIN_WIDTH && naturalHeight >= MIN_HEIGHT; } let isDisplayedLargeEnough; if (isDisplayedVeryHorizontal) { isDisplayedLargeEnough = displayedWidth >= MIN_DISPLAYED_WIDTH * 1.2 && displayedHeight >= MIN_DISPLAYED_HEIGHT * 0.8; } else if (isDisplayedHorizontal) { isDisplayedLargeEnough = displayedWidth >= MIN_DISPLAYED_WIDTH && displayedHeight >= MIN_DISPLAYED_HEIGHT * 0.9; } else { isDisplayedLargeEnough = displayedWidth >= MIN_DISPLAYED_WIDTH && displayedHeight >= MIN_DISPLAYED_HEIGHT; } const isLargeEnough = isNaturalLargeEnough || (isCompressed && isDisplayedLargeEnough); if (!isLargeEnough) { if (this.debug) console.log('❌ Отфильтровано: слишком маленькое'); this.markAsNotZoomable(img); resolve(false); return; } const currentPath = window.location.pathname.toLowerCase(); const isDocsPage = currentPath.includes('/manuals/') || currentPath.includes('/docs/') || currentPath.includes('/laboratory/'); if (!isDocsPage) { const isNotFullyVisible = !this.isFullyVisible(img, displayedWidth, displayedHeight); if (!isNotFullyVisible) { if (this.debug) console.log('❌ Отфильтровано: полностью видно на обычной странице'); this.markAsNotZoomable(img); resolve(false); return; } } // Изображение прошло все проверки if (!this.images.includes(img)) { this.images.push(img); } this.markAsZoomable(img); if (this.debug) { console.log('✅ Изображение доступно для увеличения'); } resolve(true); }; checkImage(); }); } markAsZoomable(img) { img.style.cursor = 'zoom-in'; img.classList.add('is-zoomable'); img.classList.remove('not-zoomable'); // Удаляем старые обработчики если есть const newImg = img.cloneNode(true); img.parentNode.replaceChild(newImg, img); // Добавляем новые обработчики newImg.addEventListener('mouseenter', () => { newImg.style.opacity = '0.92'; newImg.style.transform = 'translateY(-3px)'; }); newImg.addEventListener('mouseleave', () => { newImg.style.opacity = '1'; newImg.style.transform = 'translateY(0)'; }); newImg.addEventListener('click', (e) => { e.preventDefault(); e.stopPropagation(); const filteredIndex = this.images.indexOf(img); if (filteredIndex !== -1) { this.currentIndex = filteredIndex; this.open(img.dataset.originalSrc || img.src, img.alt || img.title || ''); } }); // Обновляем data-lightbox чтобы он остался newImg.setAttribute('data-lightbox', 'true'); if (img.dataset.originalSrc) { newImg.setAttribute('data-original-src', img.dataset.originalSrc); } return newImg; } markAsNotZoomable(img) { img.style.cursor = 'default'; img.classList.remove('content-image--zoomable'); img.classList.add('not-zoomable'); img.onclick = null; } checkAllImages(allImages) { if (this.debug) { console.log('=== ПРОВЕРКА ВСЕХ ИЗОБРАЖЕНИЙ ЧЕРЕЗ 3С ==='); } const uncheckedImages = Array.from(allImages).filter(img => !this.images.includes(img) && !img.classList.contains('not-zoomable') ); if (uncheckedImages.length > 0) { this.processImageBatch(uncheckedImages, 'late-batch-3s'); } } finalCheck(allImages) { if (this.debug) { console.log('=== ФИНАЛЬНАЯ ПРОВЕРКА ЧЕРЕЗ 10С ==='); console.log('Осталось в ожидании:', this.pendingImages.size); } // Принудительно проверяем все оставшиеся изображения Array.from(allImages).forEach(img => { if (!this.images.includes(img) && !img.classList.contains('not-zoomable')) { this.checkSingleImage(img); } }); // Очищаем pending this.pendingImages.clear(); } isIcon(img) { const src = (img.src || '').toLowerCase(); const alt = (img.alt || '').toLowerCase(); const classes = (img.className || '').toLowerCase(); const iconKeywords = [ 'icon', 'logo', 'avatar', 'favicon', 'sprite', 'emoji', 'svg', 'ico', 'png-16', 'png-32', 'png-64', 'icon-', '-icon' ]; const naturalWidth = img.naturalWidth || img.offsetWidth || 0; const naturalHeight = img.naturalHeight || img.offsetHeight || 0; const isVerySmall = naturalWidth < 50 && naturalHeight < 50; const isSquareAndSmall = Math.abs(naturalWidth - naturalHeight) < 10 && naturalWidth < 100; const hasIconKeyword = iconKeywords.some(keyword => src.includes(keyword) || alt.includes(keyword) || classes.includes(keyword) ); return isVerySmall || (isSquareAndSmall && hasIconKeyword); } isFullyVisible(img, displayedWidth = null, displayedHeight = null) { const currentPath = window.location.pathname.toLowerCase(); const isDocsPage = currentPath.includes('/manuals/') || currentPath.includes('/docs/') || currentPath.includes('/laboratory/'); if (isDocsPage) { return false; } const imgWidth = displayedWidth || img.offsetWidth || img.naturalWidth; const container = img.parentElement; if (!container) return false; const containerWidth = container.clientWidth; return imgWidth < containerWidth * 0.90; } open(src, caption) { if (this.debug) { console.log('Открываем lightbox:', src.substring(0, 100)); } this.modalImage.src = src; this.modalImage.alt = caption; this.caption.textContent = caption; this.modal.setAttribute('aria-hidden', 'false'); this.modal.style.visibility = 'visible'; requestAnimationFrame(() => { this.modal.style.opacity = '1'; setTimeout(() => { this.modalImage.style.opacity = '1'; this.modalImage.style.transform = 'scale(1)'; this.caption.style.opacity = '1'; this.caption.style.transform = 'translateY(0)'; }, 50); }); document.body.style.overflow = 'hidden'; document.documentElement.style.overflow = 'hidden'; } close() { this.modalImage.style.opacity = '0'; this.modalImage.style.transform = 'scale(0.96)'; this.caption.style.opacity = '0'; this.caption.style.transform = 'translateY(10px)'; setTimeout(() => { this.modal.style.opacity = '0'; this.modal.style.visibility = 'hidden'; this.modal.setAttribute('aria-hidden', 'true'); document.body.style.overflow = ''; document.documentElement.style.overflow = ''; }, 400); } isOpen() { return this.modal.getAttribute('aria-hidden') === 'false'; } next() { if (this.images.length > 1) { this.currentIndex = (this.currentIndex + 1) % this.images.length; const nextImg = this.images[this.currentIndex]; this.open(nextImg.dataset.originalSrc || nextImg.src, nextImg.alt || nextImg.title || ''); } } prev() { if (this.images.length > 1) { this.currentIndex = (this.currentIndex - 1 + this.images.length) % this.images.length; const prevImg = this.images[this.currentIndex]; this.open(prevImg.dataset.originalSrc || prevImg.src, prevImg.alt || prevImg.title || ''); } } }
document.addEventListener('DOMContentLoaded', () => { setTimeout(() => { window.contentLightbox = new ContentLightbox(); }, 100); const style = document.createElement('style'); style.textContent = ` .content-image { max-width: 100%; height: auto; display: block; margin: 1.5rem auto; border-radius: 4px; transition: all 0.25s ease; } .content-image.is-zoomable { cursor: zoom-in; } .content-image.not-zoomable { cursor: default; } .content-image--shadow.is-zoomable { box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1); } .content-image:not(.is-zoomable), .content-image.not-zoomable { box-shadow: none !important; } .content-image.is-zoomable:hover { box-shadow: 0 8px 24px rgba(0, 0, 0, 0.15); opacity: 0.95; } .content-image.not-zoomable:hover { transform: none !important; opacity: 1 !important; box-shadow: none !important; } .content-image__caption { text-align: center; font-size: 0.875rem; color: #666; margin-top: 0.5rem; line-height: 1.4; font-style: italic; } .content-image__wrapper { margin: 1.5rem 0; text-align: center; } /* Стили для lazy loading */ .content-image[loading="lazy"] { opacity: 0; transition: opacity 0.3s ease; } .content-image[loading="lazy"].loaded { opacity: 1; } @media (max-width: 768px) { .content-image { margin: 1rem auto; border-radius: 3px; } .content-image--shadow.is-zoomable { box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); } .content-image.is-zoomable:hover { box-shadow: 0 4px 16px rgba(0, 0, 0, 0.12); } .content-lightbox__close { top: 15px; right: 15px; width: 32px; height: 32px; font-size: 28px; } } @media (hover: none) { .content-image:hover { transform: none !important; } } `; document.head.appendChild(style); });
// Автоматическая загрузка lazy images при скролле let lazyLoadTimeout; window.addEventListener('scroll', () => { if (lazyLoadTimeout) clearTimeout(lazyLoadTimeout); lazyLoadTimeout = setTimeout(() => { // Находим все lazy изображения рядом с viewport const lazyImages = document.querySelectorAll('img[data-lightbox="true"][loading="lazy"]'); lazyImages.forEach(img => { const rect = img.getBoundingClientRect(); const isNearViewport = ( rect.top < window.innerHeight + 500 && rect.bottom > -500 ); if (isNearViewport && !img.classList.contains('loaded')) { img.classList.add('loaded'); if (window.contentLightbox) { window.contentLightbox.checkSingleImage(img); } } }); }, 250); });
// Принудительная проверка при изменении размера окна window.addEventListener('resize', () => { setTimeout(() => { if (window.contentLightbox) { const allImages = document.querySelectorAll('img[data-lightbox="true"]'); window.contentLightbox.checkAllImages(allImages); } }, 500); });
// Проверка при возвращении на вкладку document.addEventListener('visibilitychange', () => { if (document.visibilityState === 'visible') { setTimeout(() => { if (window.contentLightbox) { const allImages = document.querySelectorAll('img[data-lightbox="true"]'); window.contentLightbox.checkAllImages(allImages); } }, 1000); } }); </script> Добавлено (2025-12-19, 11:37) --------------------------------------------- <!-- Lightbox для изображений в статьях --> <script> class ContentLightbox { constructor() { this.modal = null; this.modalImage = null; this.caption = null; this.images = []; this.currentIndex = 0; this.debug = true; this.initialized = false; this.init(); } init() { this.createModal(); if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', () => { setTimeout(() => this.setup(), 100); }); } else { setTimeout(() => this.setup(), 100); } } createModal() { this.modal = document.createElement('div'); this.modal.className = 'content-lightbox'; this.modal.setAttribute('aria-hidden', 'true'); this.modal.style.cssText = ` position: fixed; top: 0; left: 0; width: 100%; height: 100%; background-color: rgba(0, 0, 0, 0.45); display: flex; align-items: center; justify-content: center; z-index: 9999; opacity: 0; visibility: hidden; transition: opacity 0.3s ease, visibility 0.3s ease; backdrop-filter: blur(8px); `; const container = document.createElement('div'); container.className = 'content-lightbox__container'; container.style.cssText = ` position: relative; max-width: 95vw; max-height: 95vh; margin: 20px; display: flex; flex-direction: column; align-items: center; `; // ИСПРАВЛЕННАЯ КНОПКА ЗАКРЫТИЯ const closeBtn = document.createElement('button'); closeBtn.className = 'content-lightbox__close'; closeBtn.innerHTML = '×'; closeBtn.setAttribute('aria-label', 'Закрыть'); closeBtn.style.cssText = ` position: absolute; top: -50px; right: 0; background: rgba(255, 255, 255, 0.15); border: none; color: white; font-size: 34px; cursor: pointer; line-height: 36px; width: 40px; height: 40px; border-radius: 50%; display: flex; align-items: center; justify-content: center; z-index: 10; transition: all 0.2s ease; backdrop-filter: blur(5px); padding: 0; margin: 0; `; closeBtn.addEventListener('mouseenter', () => { closeBtn.style.background = 'rgba(255, 255, 255, 0.25)'; closeBtn.style.transform = 'scale(1.1)'; }); closeBtn.addEventListener('mouseleave', () => { closeBtn.style.background = 'rgba(255, 255, 255, 0.15)'; closeBtn.style.transform = 'scale(1)'; }); closeBtn.addEventListener('click', (e) => { e.stopPropagation(); this.close(); }); this.modalImage = document.createElement('img'); this.modalImage.className = 'content-lightbox__image'; this.modalImage.style.cssText = ` display: block; max-width: 100%; max-height: 85vh; margin: 0 auto; border-radius: 4px; box-shadow: 0 25px 50px rgba(0, 0, 0, 0.5); cursor: default; transform: scale(0.96); opacity: 0; transition: transform 0.4s cubic-bezier(0.175, 0.885, 0.32, 1.275), opacity 0.4s ease; `; this.caption = document.createElement('div'); this.caption.className = 'content-lightbox__caption'; this.caption.style.cssText = ` text-align: center; color: rgba(255, 255, 255, 0.9); font-size: 15px; margin-top: 15px; padding: 8px 16px; background: rgba(255, 255, 255, 0.08); border-radius: 4px; backdrop-filter: blur(10px); opacity: 0; transform: translateY(10px); transition: opacity 0.4s ease, transform 0.4s ease; `; container.appendChild(closeBtn); container.appendChild(this.modalImage); container.appendChild(this.caption); this.modal.appendChild(container); document.body.appendChild(this.modal); this.modal.addEventListener('click', (e) => { if (e.target === this.modal) { this.close(); } }); document.addEventListener('keydown', (e) => { if (e.key === 'Escape' && this.isOpen()) { this.close(); } }); document.addEventListener('keydown', (e) => { if (!this.isOpen()) return; if (e.key === 'ArrowLeft') { e.preventDefault(); this.prev(); } else if (e.key === 'ArrowRight') { e.preventDefault(); this.next(); } }); } setup() { if (this.initialized) return; this.initialized = true; const allLightboxImages = document.querySelectorAll('img[data-lightbox="true"]'); if (this.debug) { console.log('=== DEBUG LIGHTBOX ==='); console.log('Всего изображений с data-lightbox="true":', allLightboxImages.length); } // Обрабатываем ВСЕ изображения сразу this.processAllImages(allLightboxImages); // Дополнительная проверка через 1 секунду для lazy images setTimeout(() => { this.checkLazyImages(allLightboxImages); }, 1000); // Еще одна проверка через 3 секунды setTimeout(() => { this.finalCheck(allLightboxImages); }, 3000); } processAllImages(allImages) { const imagePromises = Array.from(allImages).map(img => { return new Promise((resolve) => { // Если изображение уже загружено if (img.complete && img.naturalWidth > 0) { this.processImage(img); resolve(true); } // Если изображение еще не загружено, но имеет src else if (img.src || img.dataset.originalSrc) { // Ждем загрузки const onLoad = () => { img.removeEventListener('load', onLoad); img.removeEventListener('error', onError); setTimeout(() => { this.processImage(img); resolve(true); }, 100); }; const onError = () => { img.removeEventListener('load', onLoad); img.removeEventListener('error', onError); if (this.debug) console.log('❌ Ошибка загрузки:', img.src); resolve(false); }; img.addEventListener('load', onLoad); img.addEventListener('error', onError); // Если изображение lazy, загружаем его if (img.loading === 'lazy' && img.dataset.originalSrc && !img.src) { img.src = img.dataset.originalSrc; } } else { resolve(false); } }); }); Promise.all(imagePromises).then(results => { const successful = results.filter(r => r).length; if (this.debug) { console.log(`Обработано изображений: ${successful}/${allImages.length}`); } }); } processImage(img) { const naturalWidth = img.naturalWidth || 0; const naturalHeight = img.naturalHeight || 0; const displayedWidth = img.offsetWidth || img.width || 0; const displayedHeight = img.offsetHeight || img.height || 0; if (this.debug) { console.log('--- Обработка изображения ---'); console.log('Src:', img.src.substring(0, 100)); console.log('Natural:', naturalWidth, '×', naturalHeight); console.log('Displayed:', displayedWidth, '×', displayedHeight); } // Проверяем минимальные размеры if (naturalWidth === 0 && displayedWidth === 0) { if (this.debug) console.log('🕐 Изображение еще не загрузилось, откладываем'); return false; } const isNotIcon = !this.isIcon(img); if (!isNotIcon) { this.markAsNotZoomable(img); return false; } // УЛУЧШЕННАЯ ПРОВЕРКА РАЗМЕРОВ const MIN_WIDTH = 400; const MIN_HEIGHT = 200; const MIN_DISPLAYED_WIDTH = 300; const MIN_DISPLAYED_HEIGHT = 150; const naturalAspectRatio = naturalWidth / naturalHeight; const isNaturalHorizontal = naturalAspectRatio > 3; const isNaturalVeryHorizontal = naturalAspectRatio > 5; const displayedAspectRatio = displayedWidth / displayedHeight; const isDisplayedHorizontal = displayedAspectRatio > 3; const isDisplayedVeryHorizontal = displayedAspectRatio > 5; const compressionRatio = naturalWidth > 0 ? displayedWidth / naturalWidth : 1; const isCompressed = compressionRatio < 0.9; let isNaturalLargeEnough; if (isNaturalVeryHorizontal) { isNaturalLargeEnough = naturalWidth >= MIN_WIDTH * 1.5 && naturalHeight >= MIN_HEIGHT * 0.6; } else if (isNaturalHorizontal) { isNaturalLargeEnough = naturalWidth >= MIN_WIDTH && naturalHeight >= MIN_HEIGHT * 0.7; } else { isNaturalLargeEnough = naturalWidth >= MIN_WIDTH && naturalHeight >= MIN_HEIGHT; } let isDisplayedLargeEnough; if (isDisplayedVeryHorizontal) { isDisplayedLargeEnough = displayedWidth >= MIN_DISPLAYED_WIDTH * 1.2 && displayedHeight >= MIN_DISPLAYED_HEIGHT * 0.8; } else if (isDisplayedHorizontal) { isDisplayedLargeEnough = displayedWidth >= MIN_DISPLAYED_WIDTH && displayedHeight >= MIN_DISPLAYED_HEIGHT * 0.9; } else { isDisplayedLargeEnough = displayedWidth >= MIN_DISPLAYED_WIDTH && displayedHeight >= MIN_DISPLAYED_HEIGHT; } const isLargeEnough = isNaturalLargeEnough || (isCompressed && isDisplayedLargeEnough); if (!isLargeEnough) { this.markAsNotZoomable(img); return false; } const currentPath = window.location.pathname.toLowerCase(); const isDocsPage = currentPath.includes('/manuals/') || currentPath.includes('/docs/') || currentPath.includes('/laboratory/'); if (!isDocsPage) { const isNotFullyVisible = !this.isFullyVisible(img, displayedWidth, displayedHeight); if (!isNotFullyVisible) { this.markAsNotZoomable(img); return false; } } // Изображение прошло все проверки if (!this.images.includes(img)) { this.images.push(img); } this.markAsZoomable(img); if (this.debug) { console.log('✅ Доступно для увеличения'); } return true; } markAsZoomable(img) { img.style.cursor = 'zoom-in'; img.classList.add('is-zoomable'); img.classList.remove('not-zoomable'); // Удаляем старые обработчики const oldOnClick = img.onclick; img.onclick = null; // Добавляем новые обработчики img.addEventListener('click', (e) => { e.preventDefault(); e.stopPropagation(); const filteredIndex = this.images.indexOf(img); if (filteredIndex !== -1) { this.currentIndex = filteredIndex; this.open(img.dataset.originalSrc || img.src, img.alt || img.title || ''); } }); // Эффекты при наведении img.addEventListener('mouseenter', () => { img.style.opacity = '0.92'; img.style.transform = 'translateY(-3px)'; }); img.addEventListener('mouseleave', () => { img.style.opacity = '1'; img.style.transform = 'translateY(0)'; }); } markAsNotZoomable(img) { img.style.cursor = 'default'; img.classList.remove('content-image--zoomable'); img.classList.add('not-zoomable'); img.onclick = null; } checkLazyImages(allImages) { const lazyImages = Array.from(allImages).filter(img => (img.loading === 'lazy' || !img.complete) && !this.images.includes(img) && !img.classList.contains('not-zoomable') ); if (lazyImages.length > 0 && this.debug) { console.log('Проверка lazy images:', lazyImages.length); } lazyImages.forEach(img => { this.processImage(img); }); } finalCheck(allImages) { const unprocessedImages = Array.from(allImages).filter(img => !this.images.includes(img) && !img.classList.contains('not-zoomable') ); if (unprocessedImages.length > 0 && this.debug) { console.log('Финальная проверка:', unprocessedImages.length, 'изображений'); } unprocessedImages.forEach(img => { this.processImage(img); }); if (this.debug) { console.log('=== ИТОГО ==='); console.log('Доступно для увеличения:', this.images.length, 'из', allImages.length); } } isIcon(img) { const src = (img.src || '').toLowerCase(); const alt = (img.alt || '').toLowerCase(); const classes = (img.className || '').toLowerCase(); const iconKeywords = [ 'icon', 'logo', 'avatar', 'favicon', 'sprite', 'emoji', 'svg', 'ico', 'png-16', 'png-32', 'png-64', 'icon-', '-icon' ]; const naturalWidth = img.naturalWidth || img.offsetWidth || 0; const naturalHeight = img.naturalHeight || img.offsetHeight || 0; const isVerySmall = naturalWidth < 50 && naturalHeight < 50; const isSquareAndSmall = Math.abs(naturalWidth - naturalHeight) < 10 && naturalWidth < 100; const hasIconKeyword = iconKeywords.some(keyword => src.includes(keyword) || alt.includes(keyword) || classes.includes(keyword) ); return isVerySmall || (isSquareAndSmall && hasIconKeyword); } isFullyVisible(img, displayedWidth = null, displayedHeight = null) { const currentPath = window.location.pathname.toLowerCase(); const isDocsPage = currentPath.includes('/manuals/') || currentPath.includes('/docs/') || currentPath.includes('/laboratory/'); if (isDocsPage) { return false; } const imgWidth = displayedWidth || img.offsetWidth || img.naturalWidth; const container = img.parentElement; if (!container) return false; const containerWidth = container.clientWidth; return imgWidth < containerWidth * 0.90; } open(src, caption) { if (this.debug) { console.log('Открываем lightbox:', src.substring(0, 100)); } this.modalImage.src = src; this.modalImage.alt = caption; this.caption.textContent = caption; this.modal.setAttribute('aria-hidden', 'false'); this.modal.style.visibility = 'visible'; requestAnimationFrame(() => { this.modal.style.opacity = '1'; setTimeout(() => { this.modalImage.style.opacity = '1'; this.modalImage.style.transform = 'scale(1)'; this.caption.style.opacity = '1'; this.caption.style.transform = 'translateY(0)'; }, 50); }); document.body.style.overflow = 'hidden'; document.documentElement.style.overflow = 'hidden'; } close() { this.modalImage.style.opacity = '0'; this.modalImage.style.transform = 'scale(0.96)'; this.caption.style.opacity = '0'; this.caption.style.transform = 'translateY(10px)'; setTimeout(() => { this.modal.style.opacity = '0'; this.modal.style.visibility = 'hidden'; this.modal.setAttribute('aria-hidden', 'true'); document.body.style.overflow = ''; document.documentElement.style.overflow = ''; }, 400); } isOpen() { return this.modal.getAttribute('aria-hidden') === 'false'; } next() { if (this.images.length > 1) { this.currentIndex = (this.currentIndex + 1) % this.images.length; const nextImg = this.images[this.currentIndex]; this.open(nextImg.dataset.originalSrc || nextImg.src, nextImg.alt || nextImg.title || ''); } } prev() { if (this.images.length > 1) { this.currentIndex = (this.currentIndex - 1 + this.images.length) % this.images.length; const prevImg = this.images[this.currentIndex]; this.open(prevImg.dataset.originalSrc || prevImg.src, prevImg.alt || prevImg.title || ''); } } }
// Инициализация document.addEventListener('DOMContentLoaded', () => { setTimeout(() => { window.contentLightbox = new ContentLightbox(); }, 50); const style = document.createElement('style'); style.textContent = ` .content-image { max-width: 100%; height: auto; display: block; margin: 1.5rem auto; border-radius: 4px; transition: all 0.25s ease; } .content-image.is-zoomable { cursor: zoom-in; } .content-image.not-zoomable { cursor: default; } .content-image--shadow.is-zoomable { box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1); } .content-image:not(.is-zoomable), .content-image.not-zoomable { box-shadow: none !important; } .content-image.is-zoomable:hover { box-shadow: 0 8px 24px rgba(0, 0, 0, 0.15); opacity: 0.95; } .content-image.not-zoomable:hover { transform: none !important; opacity: 1 !important; box-shadow: none !important; } .content-image__caption { text-align: center; font-size: 0.875rem; color: #666; margin-top: 0.5rem; line-height: 1.4; font-style: italic; } .content-image__wrapper { margin: 1.5rem 0; text-align: center; } /* Стили для lazy loading */ .content-image[loading="lazy"] { min-height: 100px; background: #f5f5f5; } @media (max-width: 768px) { .content-image { margin: 1rem auto; border-radius: 3px; } .content-image--shadow.is-zoomable { box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); } .content-image.is-zoomable:hover { box-shadow: 0 4px 16px rgba(0, 0, 0, 0.12); } .content-lightbox__close { top: 10px; right: 10px; width: 36px; height: 36px; font-size: 30px; } } @media (hover: none) { .content-image:hover { transform: none !important; } } `; document.head.appendChild(style); });
// Проверка при скролле для lazy images window.addEventListener('scroll', () => { setTimeout(() => { if (window.contentLightbox && window.contentLightbox.initialized) { const allImages = document.querySelectorAll('img[data-lightbox="true"]'); window.contentLightbox.checkLazyImages(allImages); } }, 500); });
// Проверка при изменении размера окна window.addEventListener('resize', () => { setTimeout(() => { if (window.contentLightbox) { const allImages = document.querySelectorAll('img[data-lightbox="true"]'); window.contentLightbox.finalCheck(allImages); } }, 300); }); </script> Добавлено (2025-12-19, 11:45) --------------------------------------------- <!-- Lightbox для изображений в статьях --> <script> class ContentLightbox { constructor() { // ... весь JavaScript код остается без изменений ... // Просто вставьте предыдущий JavaScript код полностью } }
// Инициализация document.addEventListener('DOMContentLoaded', () => { setTimeout(() => { window.contentLightbox = new ContentLightbox(); }, 50); const style = document.createElement('style'); style.textContent = ` /* Базовые стили для всех изображений */ .content-image { max-width: 100%; height: auto; display: block; margin: 1.5rem auto; border-radius: 4px; transition: all 0.25s ease; } /* Стили для изображений, которые можно увеличивать */ .content-image.is-zoomable { cursor: zoom-in; } /* Для изображений, которые нельзя увеличивать */ .content-image.not-zoomable { cursor: default; } /* Тень только для zoomable изображений */ .content-image--shadow.is-zoomable { box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1); } /* Маленькие/не-zoomable изображения - без тени */ .content-image:not(.is-zoomable), .content-image.not-zoomable { box-shadow: none !important; } /* Эффект при наведении на zoomable изображения */ .content-image.is-zoomable:hover { box-shadow: 0 8px 24px rgba(0, 0, 0, 0.15); opacity: 0.95; } /* Отключаем hover эффекты для не-zoomable изображений */ .content-image.not-zoomable:hover { transform: none !important; opacity: 1 !important; box-shadow: none !important; } /* Подпись к изображениям */ .content-image__caption { text-align: center; font-size: 0.875rem; color: #666; margin-top: 0.5rem; line-height: 1.4; font-style: italic; } /* Обертка для изображений с подписью */ .content-image__wrapper { margin: 1.5rem 0; text-align: center; } /* УБИРАЕМ min-height для lazy loading - это вызывало растягивание */ /* .content-image[loading="lazy"] { min-height: 100px; background: #f5f5f5; } */ /* Вместо этого делаем плавную загрузку */ .content-image { opacity: 1; } .content-image[loading="lazy"] { opacity: 0; transition: opacity 0.3s ease; } .content-image[loading="lazy"].loaded { opacity: 1; } /* Стили для мобильных устройств */ @media (max-width: 768px) { .content-image { margin: 1rem auto; border-radius: 3px; } .content-image--shadow.is-zoomable { box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); } .content-image.is-zoomable:hover { box-shadow: 0 4px 16px rgba(0, 0, 0, 0.12); } .content-lightbox__close { top: 10px; right: 10px; width: 36px; height: 36px; font-size: 30px; } } /* Отключение hover на тач-устройствах */ @media (hover: none) { .content-image:hover { transform: none !important; } } `; document.head.appendChild(style); });
// Обновляем JavaScript для добавления класса loaded window.addEventListener('load', () => { const lazyImages = document.querySelectorAll('img[data-lightbox="true"][loading="lazy"]'); lazyImages.forEach(img => { if (img.complete) { img.classList.add('loaded'); } else { img.addEventListener('load', () => { img.classList.add('loaded'); }); } }); });
// Проверка при скролле для lazy images window.addEventListener('scroll', () => { setTimeout(() => { if (window.contentLightbox && window.contentLightbox.initialized) { const allImages = document.querySelectorAll('img[data-lightbox="true"]'); window.contentLightbox.checkLazyImages(allImages); } }, 500); });
// Проверка при изменении размера окна window.addEventListener('resize', () => { setTimeout(() => { if (window.contentLightbox) { const allImages = document.querySelectorAll('img[data-lightbox="true"]'); window.contentLightbox.finalCheck(allImages); } }, 300); }); </script>
|
<!-- Lightbox для изображений в статьях --> <script> class ContentLightbox { constructor() { this.modal = null; this.modalImage = null; this.caption = null; this.images = []; this.currentIndex = 0; this.debug = true; this.initialized = false; this.init(); } init() { this.createModal(); if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', () => { setTimeout(() => this.setup(), 100); }); } else { setTimeout(() => this.setup(), 100); } } createModal() { this.modal = document.createElement('div'); this.modal.className = 'content-lightbox'; this.modal.setAttribute('aria-hidden', 'true'); this.modal.style.cssText = ` position: fixed; top: 0; left: 0; width: 100%; height: 100%; background-color: rgba(0, 0, 0, 0.45); display: flex; align-items: center; justify-content: center; z-index: 9999; opacity: 0; visibility: hidden; transition: opacity 0.3s ease, visibility 0.3s ease; backdrop-filter: blur(8px); `; const container = document.createElement('div'); container.className = 'content-lightbox__container'; container.style.cssText = ` position: relative; max-width: 95vw; max-height: 95vh; margin: 20px; `; // ИСПРАВЛЕННАЯ КНОПКА ЗАКРЫТИЯ const closeBtn = document.createElement('button'); closeBtn.className = 'content-lightbox__close'; closeBtn.innerHTML = '×'; closeBtn.setAttribute('aria-label', 'Закрыть'); closeBtn.style.cssText = ` position: absolute; top: -50px; right: 0; background: rgba(255, 255, 255, 0.15); border: none; color: white; font-size: 34px; cursor: pointer; width: 40px; height: 40px; border-radius: 50%; display: flex; align-items: center; justify-content: center; z-index: 10; transition: all 0.2s ease; backdrop-filter: blur(5px); padding: 0; margin: 0; line-height: 40px; `; closeBtn.addEventListener('mouseenter', () => { closeBtn.style.background = 'rgba(255, 255, 255, 0.25)'; closeBtn.style.transform = 'scale(1.1)'; }); closeBtn.addEventListener('mouseleave', () => { closeBtn.style.background = 'rgba(255, 255, 255, 0.15)'; closeBtn.style.transform = 'scale(1)'; }); closeBtn.addEventListener('click', (e) => { e.stopPropagation(); this.close(); }); this.modalImage = document.createElement('img'); this.modalImage.className = 'content-lightbox__image'; this.modalImage.style.cssText = ` display: block; max-width: 100%; max-height: 85vh; margin: 0 auto; border-radius: 4px; box-shadow: 0 25px 50px rgba(0, 0, 0, 0.5); cursor: default; transform: scale(0.96); opacity: 0; transition: transform 0.4s cubic-bezier(0.175, 0.885, 0.32, 1.275), opacity 0.4s ease; `; this.caption = document.createElement('div'); this.caption.className = 'content-lightbox__caption'; this.caption.style.cssText = ` text-align: center; color: rgba(255, 255, 255, 0.9); font-size: 15px; margin-top: 15px; padding: 8px 16px; background: rgba(255, 255, 255, 0.08); border-radius: 4px; backdrop-filter: blur(10px); opacity: 0; transform: translateY(10px); transition: opacity 0.4s ease, transform 0.4s ease; `; container.appendChild(closeBtn); container.appendChild(this.modalImage); container.appendChild(this.caption); this.modal.appendChild(container); document.body.appendChild(this.modal); this.modal.addEventListener('click', (e) => { if (e.target === this.modal) { this.close(); } }); document.addEventListener('keydown', (e) => { if (e.key === 'Escape' && this.isOpen()) { this.close(); } }); document.addEventListener('keydown', (e) => { if (!this.isOpen()) return; if (e.key === 'ArrowLeft') { e.preventDefault(); this.prev(); } else if (e.key === 'ArrowRight') { e.preventDefault(); this.next(); } }); } setup() { if (this.initialized) return; this.initialized = true; const allLightboxImages = document.querySelectorAll('img[data-lightbox="true"]'); if (this.debug) { console.log('=== DEBUG LIGHTBOX ==='); console.log('Всего изображений с data-lightbox="true":', allLightboxImages.length); } // Обрабатываем ВСЕ изображения сразу this.processAllImages(allLightboxImages); // Дополнительная проверка через 1 секунду для lazy images setTimeout(() => { this.checkLazyImages(allLightboxImages); }, 1000); // Еще одна проверка через 3 секунды setTimeout(() => { this.finalCheck(allLightboxImages); }, 3000); } processAllImages(allImages) { const imagePromises = Array.from(allImages).map(img => { return new Promise((resolve) => { // Если изображение уже загружено if (img.complete && img.naturalWidth > 0) { this.processImage(img); resolve(true); } // Если изображение еще не загружено, но имеет src else if (img.src || img.dataset.originalSrc) { // Ждем загрузки const onLoad = () => { img.removeEventListener('load', onLoad); img.removeEventListener('error', onError); setTimeout(() => { this.processImage(img); resolve(true); }, 100); }; const onError = () => { img.removeEventListener('load', onLoad); img.removeEventListener('error', onError); if (this.debug) console.log('❌ Ошибка загрузки:', img.src); resolve(false); }; img.addEventListener('load', onLoad); img.addEventListener('error', onError); // Если изображение lazy, загружаем его if (img.loading === 'lazy' && img.dataset.originalSrc && !img.src) { img.src = img.dataset.originalSrc; } } else { resolve(false); } }); }); Promise.all(imagePromises).then(results => { const successful = results.filter(r => r).length; if (this.debug) { console.log(`Обработано изображений: ${successful}/${allImages.length}`); } }); } processImage(img) { const naturalWidth = img.naturalWidth || 0; const naturalHeight = img.naturalHeight || 0; const displayedWidth = img.offsetWidth || img.width || 0; const displayedHeight = img.offsetHeight || img.height || 0; if (this.debug) { console.log('--- Обработка изображения ---'); console.log('Src:', img.src.substring(0, 100)); console.log('Natural:', naturalWidth, '×', naturalHeight); console.log('Displayed:', displayedWidth, '×', displayedHeight); } // Проверяем минимальные размеры if (naturalWidth === 0 && displayedWidth === 0) { if (this.debug) console.log('🕐 Изображение еще не загрузилось, откладываем'); return false; } const isNotIcon = !this.isIcon(img); if (!isNotIcon) { this.markAsNotZoomable(img); return false; } // УЛУЧШЕННАЯ ПРОВЕРКА РАЗМЕРОВ - ОСЛАБЛЯЕМ ТРЕБОВАНИЯ К ВЫСОТЕ ДЛЯ ГОРИЗОНТАЛЬНЫХ const MIN_WIDTH = 400; const MIN_HEIGHT = 150; const MIN_DISPLAYED_WIDTH = 300; const MIN_DISPLAYED_HEIGHT = 100; const naturalAspectRatio = naturalWidth / naturalHeight; const isNaturalHorizontal = naturalAspectRatio > 3; const isNaturalVeryHorizontal = naturalAspectRatio > 5; const isNaturalExtremelyHorizontal = naturalAspectRatio > 10; const displayedAspectRatio = displayedWidth / displayedHeight; const isDisplayedHorizontal = displayedAspectRatio > 3; const isDisplayedVeryHorizontal = displayedAspectRatio > 5; const isDisplayedExtremelyHorizontal = displayedAspectRatio > 10; const compressionRatio = naturalWidth > 0 ? displayedWidth / naturalWidth : 1; const isCompressed = compressionRatio < 0.9; let isNaturalLargeEnough; if (isNaturalExtremelyHorizontal) { // Для ОЧЕНЬ горизонтальных изображений (ширина > 10× высоты) // Требуем только ширину, высота почти не важна isNaturalLargeEnough = naturalWidth >= MIN_WIDTH * 1.2 && naturalHeight >= 50; } else if (isNaturalVeryHorizontal) { // Для очень горизонтальных (ширина > 5× высоты) isNaturalLargeEnough = naturalWidth >= MIN_WIDTH * 1.5 && naturalHeight >= MIN_HEIGHT * 0.6; } else if (isNaturalHorizontal) { // Для горизонтальных (ширина > 3× высоты) isNaturalLargeEnough = naturalWidth >= MIN_WIDTH && naturalHeight >= MIN_HEIGHT * 0.7; } else { // Для обычных и вертикальных изображений isNaturalLargeEnough = naturalWidth >= MIN_WIDTH && naturalHeight >= MIN_HEIGHT; } let isDisplayedLargeEnough; if (isDisplayedExtremelyHorizontal) { // Для ОЧЕНЬ горизонтальных отображаемых isDisplayedLargeEnough = displayedWidth >= MIN_DISPLAYED_WIDTH * 1.5 && displayedHeight >= 40; } else if (isDisplayedVeryHorizontal) { // Для очень горизонтальных отображаемых isDisplayedLargeEnough = displayedWidth >= MIN_DISPLAYED_WIDTH * 1.2 && displayedHeight >= MIN_DISPLAYED_HEIGHT * 0.8; } else if (isDisplayedHorizontal) { // Для горизонтальных отображаемых isDisplayedLargeEnough = displayedWidth >= MIN_DISPLAYED_WIDTH && displayedHeight >= MIN_DISPLAYED_HEIGHT * 0.9; } else { // Для обычных отображаемых isDisplayedLargeEnough = displayedWidth >= MIN_DISPLAYED_WIDTH && displayedHeight >= MIN_DISPLAYED_HEIGHT; } // Итоговая проверка: изображение можно увеличивать если: // 1. Оригинал достаточно большой ИЛИ // 2. Отображаемый размер достаточно большой (особенно если изображение сжато) const isLargeEnough = isNaturalLargeEnough || (isCompressed && isDisplayedLargeEnough); if (this.debug) { console.log(`Пропорции: ${naturalAspectRatio.toFixed(2)}:1`); console.log(`Тип: ${isNaturalExtremelyHorizontal ? 'очень-очень горизонтальное' : isNaturalVeryHorizontal ? 'очень горизонтальное' : isNaturalHorizontal ? 'горизонтальное' : 'обычное'}`); console.log(`Сжатие: ${Math.round(compressionRatio * 100)}%`); console.log(`Оригинал достаточно большой: ${isNaturalLargeEnough}`); console.log(`Отображаемый достаточно большой: ${isDisplayedLargeEnough}`); console.log(`Итог: достаточно большой = ${isLargeEnough}`); } if (!isLargeEnough) { this.markAsNotZoomable(img); return false; } const currentPath = window.location.pathname.toLowerCase(); const isDocsPage = currentPath.includes('/manuals/') || currentPath.includes('/docs/') || currentPath.includes('/laboratory/'); if (!isDocsPage) { const isNotFullyVisible = !this.isFullyVisible(img, displayedWidth, displayedHeight); if (!isNotFullyVisible) { this.markAsNotZoomable(img); return false; } } // Изображение прошло все проверки if (!this.images.includes(img)) { this.images.push(img); } this.markAsZoomable(img); if (this.debug) { console.log('✅ Доступно для увеличения'); } return true; } markAsZoomable(img) { img.style.cursor = 'zoom-in'; img.classList.add('is-zoomable'); img.classList.remove('not-zoomable'); // Удаляем старые обработчики const oldOnClick = img.onclick; img.onclick = null; // Добавляем новые обработчики img.addEventListener('click', (e) => { e.preventDefault(); e.stopPropagation(); const filteredIndex = this.images.indexOf(img); if (filteredIndex !== -1) { this.currentIndex = filteredIndex; this.open(img.dataset.originalSrc || img.src, img.alt || img.title || ''); } }); // Эффекты при наведении img.addEventListener('mouseenter', () => { img.style.opacity = '0.92'; img.style.transform = 'translateY(-3px)'; }); img.addEventListener('mouseleave', () => { img.style.opacity = '1'; img.style.transform = 'translateY(0)'; }); } markAsNotZoomable(img) { img.style.cursor = 'default'; img.classList.remove('content-image--zoomable'); img.classList.add('not-zoomable'); img.onclick = null; } checkLazyImages(allImages) { const lazyImages = Array.from(allImages).filter(img => (img.loading === 'lazy' || !img.complete) && !this.images.includes(img) && !img.classList.contains('not-zoomable') ); if (lazyImages.length > 0 && this.debug) { console.log('Проверка lazy images:', lazyImages.length); } lazyImages.forEach(img => { this.processImage(img); }); } finalCheck(allImages) { const unprocessedImages = Array.from(allImages).filter(img => !this.images.includes(img) && !img.classList.contains('not-zoomable') ); if (unprocessedImages.length > 0 && this.debug) { console.log('Финальная проверка:', unprocessedImages.length, 'изображений'); } unprocessedImages.forEach(img => { this.processImage(img); }); if (this.debug) { console.log('=== ИТОГО ==='); console.log('Доступно для увеличения:', this.images.length, 'из', allImages.length); } } isIcon(img) { const src = (img.src || '').toLowerCase(); const alt = (img.alt || '').toLowerCase(); const classes = (img.className || '').toLowerCase(); const iconKeywords = [ 'icon', 'logo', 'avatar', 'favicon', 'sprite', 'emoji', 'svg', 'ico', 'png-16', 'png-32', 'png-64', 'icon-', '-icon' ]; const naturalWidth = img.naturalWidth || img.offsetWidth || 0; const naturalHeight = img.naturalHeight || img.offsetHeight || 0; const isVerySmall = naturalWidth < 50 && naturalHeight < 50; const isSquareAndSmall = Math.abs(naturalWidth - naturalHeight) < 10 && naturalWidth < 100; const hasIconKeyword = iconKeywords.some(keyword => src.includes(keyword) || alt.includes(keyword) || classes.includes(keyword) ); return isVerySmall || (isSquareAndSmall && hasIconKeyword); } isFullyVisible(img, displayedWidth = null, displayedHeight = null) { const currentPath = window.location.pathname.toLowerCase(); const isDocsPage = currentPath.includes('/manuals/') || currentPath.includes('/docs/') || currentPath.includes('/laboratory/'); if (isDocsPage) { return false; } const imgWidth = displayedWidth || img.offsetWidth || img.naturalWidth; const container = img.parentElement; if (!container) return false; const containerWidth = container.clientWidth; return imgWidth < containerWidth * 0.90; } open(src, caption) { if (this.debug) { console.log('Открываем lightbox:', src.substring(0, 100)); } this.modalImage.src = src; this.modalImage.alt = caption; this.caption.textContent = caption; this.modal.setAttribute('aria-hidden', 'false'); this.modal.style.visibility = 'visible'; requestAnimationFrame(() => { this.modal.style.opacity = '1'; setTimeout(() => { this.modalImage.style.opacity = '1'; this.modalImage.style.transform = 'scale(1)'; this.caption.style.opacity = '1'; this.caption.style.transform = 'translateY(0)'; }, 50); }); document.body.style.overflow = 'hidden'; document.documentElement.style.overflow = 'hidden'; } close() { this.modalImage.style.opacity = '0'; this.modalImage.style.transform = 'scale(0.96)'; this.caption.style.opacity = '0'; this.caption.style.transform = 'translateY(10px)'; setTimeout(() => { this.modal.style.opacity = '0'; this.modal.style.visibility = 'hidden'; this.modal.setAttribute('aria-hidden', 'true'); document.body.style.overflow = ''; document.documentElement.style.overflow = ''; }, 400); } isOpen() { return this.modal.getAttribute('aria-hidden') === 'false'; } next() { if (this.images.length > 1) { this.currentIndex = (this.currentIndex + 1) % this.images.length; const nextImg = this.images[this.currentIndex]; this.open(nextImg.dataset.originalSrc || nextImg.src, nextImg.alt || nextImg.title || ''); } } prev() { if (this.images.length > 1) { this.currentIndex = (this.currentIndex - 1 + this.images.length) % this.images.length; const prevImg = this.images[this.currentIndex]; this.open(prevImg.dataset.originalSrc || prevImg.src, prevImg.alt || prevImg.title || ''); } } }
// Инициализация document.addEventListener('DOMContentLoaded', () => { setTimeout(() => { window.contentLightbox = new ContentLightbox(); }, 50); const style = document.createElement('style'); style.textContent = ` /* Базовые стили для всех изображений */ .content-image { max-width: 100%; height: auto; display: block; margin: 1.5rem auto; border-radius: 4px; transition: all 0.25s ease; } /* Стили для изображений, которые можно увеличивать */ .content-image.is-zoomable { cursor: zoom-in; } /* Для изображений, которые нельзя увеличивать */ .content-image.not-zoomable { cursor: default; } /* Тень только для zoomable изображений */ .content-image--shadow.is-zoomable { box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1); } /* Маленькие/не-zoomable изображения - без тени */ .content-image:not(.is-zoomable), .content-image.not-zoomable { box-shadow: none !important; } /* Эффект при наведении на zoomable изображения */ .content-image.is-zoomable:hover { box-shadow: 0 8px 24px rgba(0, 0, 0, 0.15); opacity: 0.95; } /* Отключаем hover эффекты для не-zoomable изображений */ .content-image.not-zoomable:hover { transform: none !important; opacity: 1 !important; box-shadow: none !important; } /* Подпись к изображениям */ .content-image__caption { text-align: center; font-size: 0.875rem; color: #666; margin-top: 0.5rem; line-height: 1.4; font-style: italic; } /* Обертка для изображений с подписью */ .content-image__wrapper { margin: 1.5rem 0; text-align: center; } /* Стили для lazy loading */ .content-image { opacity: 1; } .content-image[loading="lazy"] { opacity: 0; transition: opacity 0.3s ease; } .content-image[loading="lazy"].loaded { opacity: 1; } /* Стили для мобильных устройств */ @media (max-width: 768px) { .content-image { margin: 1rem auto; border-radius: 3px; } .content-image--shadow.is-zoomable { box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); } .content-image.is-zoomable:hover { box-shadow: 0 4px 16px rgba(0, 0, 0, 0.12); } .content-lightbox__close { top: 10px; right: 10px; width: 36px; height: 36px; font-size: 30px; line-height: 36px; } } /* Отключение hover на тач-устройствах */ @media (hover: none) { .content-image:hover { transform: none !important; } } `; document.head.appendChild(style); });
// Добавляем класс loaded для lazy images window.addEventListener('load', () => { const lazyImages = document.querySelectorAll('img[data-lightbox="true"][loading="lazy"]'); lazyImages.forEach(img => { if (img.complete) { img.classList.add('loaded'); } else { img.addEventListener('load', () => { img.classList.add('loaded'); }); } }); });
// Проверка при скролле для lazy images window.addEventListener('scroll', () => { setTimeout(() => { if (window.contentLightbox && window.contentLightbox.initialized) { const allImages = document.querySelectorAll('img[data-lightbox="true"]'); window.contentLightbox.checkLazyImages(allImages); } }, 500); });
// Проверка при изменении размера окна window.addEventListener('resize', () => { setTimeout(() => { if (window.contentLightbox) { const allImages = document.querySelectorAll('img[data-lightbox="true"]'); window.contentLightbox.finalCheck(allImages); } }, 300); }); </script>Добавлено (2025-12-19, 12:24) --------------------------------------------- Проблема в том, что на некоторых страницах изображения не получают data-lightbox="true" из-за условий в render-image.html. Давайте упростим логику - сделаем так, чтобы ВСЕ изображения в статьях (кроме главной и разделов) получали lightbox, а фильтрацию оставим на JavaScript.
1. Сначала обновите render-image.html:
```html {{- $alt := .PlainText | safeHTML -}} {{- $lazyLoading := .Page.Site.Params.enableImageLazyLoading | default true -}} {{- $dest := .Destination -}}
{{- $isLocal := not (urls.Parse $dest).Scheme -}} {{- $isPage := and (eq .Page.Kind "page") (not .Page.BundleType) -}} {{- $startsWithSlash := hasPrefix $dest "/" -}} {{- $startsWithRelative := hasPrefix $dest "../" -}}
{{- if and $dest $isLocal -}} {{- if $startsWithSlash -}} {{- $dest = (relURL (strings.TrimPrefix "/" $dest)) -}} {{- else if and $isPage (not $startsWithRelative) -}} {{ $dest = (printf "../%s" $dest) }} {{- end -}} {{- end -}}
{{/* ПРОСТАЯ ЛОГИКА: ВСЕГДА добавляем lightbox для страниц, кроме home и section */}} {{- $addLightbox := true -}} {{- $addShadow := true -}}
{{/* ИСКЛЮЧАЕМ ТОЛЬКО главную и разделы */}} {{- if or (eq .Page.Kind "home") (eq .Page.Kind "section") -}} {{- $addLightbox = false -}} {{- $addShadow = false -}} {{- end -}}
{{/* ВСЕ остальные страницы (page, taxonomy, term) получают lightbox */}}
{{- $imageClass := "content-image" -}} {{- if $addLightbox -}} {{- $imageClass = printf "%s %s" $imageClass "content-image--zoomable" -}} {{- end -}} {{- if $addShadow -}} {{- $imageClass = printf "%s %s" $imageClass "content-image--shadow" -}} {{- end -}}
{{- with .Title -}} <figure class="content-image__wrapper"> <img src="{{ $dest | safeURL }}" title="{{ . }}" alt="{{ $alt }}" {{ if $lazyLoading }}loading="lazy"{{ end }} class="{{ $imageClass }}" data-lightbox="{{ $addLightbox }}" data-original-src="{{ $dest | safeURL }}" /> <figcaption class="content-image__caption">{{ . }}</figcaption> </figure> {{- else -}} <img src="{{ $dest | safeURL }}" alt="{{ $alt }}" {{ if $lazyLoading }}loading="lazy"{{ end }} class="{{ $imageClass }}" data-lightbox="{{ $addLightbox }}" data-original-src="{{ $dest | safeURL }}" /> {{- end -}} ```
2. Теперь обновите JavaScript - добавьте fallback для изображений без data-lightbox:
Добавьте этот код в начало setup() метода (после if (this.initialized) return;):
```javascript setup() { if (this.initialized) return; this.initialized = true; // Сначала находим все изображения с data-lightbox="true" let allLightboxImages = document.querySelectorAll('img[data-lightbox="true"]'); // FALLBACK: Если не нашли изображений с data-lightbox="true", // ищем все изображения в основном контенте if (allLightboxImages.length === 0) { if (this.debug) { console.log('Не найдено изображений с data-lightbox="true", ищем в контенте...'); } // Находим основной контент статьи const contentSelectors = [ 'main', 'article', '.content', '.post-content', '.article-content', '.entry-content', '.prose', '[role="main"]' ]; let contentArea = null; for (const selector of contentSelectors) { const element = document.querySelector(selector); if (element) { contentArea = element; break; } } if (contentArea) { // Находим все изображения в контенте (кроме иконок в шапке/футере) const allImagesInContent = contentArea.querySelectorAll('img'); allImagesInContent.forEach(img => { // Проверяем, что это не иконка в навигации/шапке const isInHeader = img.closest('header, .header, nav, .nav, .navbar'); const isInFooter = img.closest('footer, .footer'); const isInSidebar = img.closest('aside, .sidebar, .aside'); if (!isInHeader && !isInFooter && !isInSidebar) { // Добавляем data-lightbox если его нет if (!img.hasAttribute('data-lightbox')) { img.setAttribute('data-lightbox', 'true'); if (img.src && !img.hasAttribute('data-original-src')) { img.setAttribute('data-original-src', img.src); } // Добавляем класс для стилей img.classList.add('content-image', 'content-image--zoomable', 'content-image--shadow'); } } }); // Обновляем список allLightboxImages = document.querySelectorAll('img[data-lightbox="true"]'); if (this.debug) { console.log('Добавлено изображений через fallback:', allLightboxImages.length); } } } if (this.debug) { console.log('=== DEBUG LIGHTBOX ==='); console.log('Всего изображений для обработки:', allLightboxImages.length); console.log('Текущая страница:', window.location.pathname); console.log('Page Kind (если доступно):', document.body.dataset.pageKind || 'не определен'); } // Обрабатываем ВСЕ изображения this.processAllImages(allLightboxImages); // Дополнительная проверка через 1 секунду для lazy images setTimeout(() => { this.checkLazyImages(allLightboxImages); }, 1000); // Еще одна проверка через 3 секунды setTimeout(() => { this.finalCheck(allLightboxImages); }, 3000); } ```
3. Полный исправленный JavaScript код (с fallback):
Если нужно весь код, вот ключевая часть с fallback. Полный код слишком длинный, но вот что важно добавить в существующий код:
```javascript // В начале setup() метода, после проверки initialized setup() { if (this.initialized) return; this.initialized = true; // === НАЧАЛО FALLBACK ЛОГИКИ === let allLightboxImages = document.querySelectorAll('img[data-lightbox="true"]'); // Если на странице вообще нет изображений с data-lightbox="true" // Проверяем, может быть это страница, где render-image.html не сработал if (allLightboxImages.length === 0) { console.warn('⚠️ Lightbox: Не найдено изображений с data-lightbox="true"'); console.warn('⚠️ Проверяем, может быть это страница типа:', window.location.pathname); // Определяем тип страницы по URL или структуре const path = window.location.pathname.toLowerCase(); const isPossibleArticlePage = ( path.includes('/posts/') || path.includes('/blog/') || path.includes('/articles/') || path.includes('/manuals/') || path.includes('/docs/') || path.includes('/laboratory/') || path.includes('/page/') || /\/\d{4}\/\d{2}\/\d{2}\//.test(path) // Дата в URL (например /2023/12/01/) ); if (isPossibleArticlePage) { console.log('ℹ️ Это похоже на страницу статьи, добавляем lightbox ко всем изображениям в контенте'); // Находим контент статьи const contentSelectors = [ 'main article', 'article .content', '.post-content', '.article-content', '.entry-content', '.prose', 'main .content', 'article', 'main' ]; for (const selector of contentSelectors) { const content = document.querySelector(selector); if (content) { // Добавляем data-lightbox ко всем изображениям в контенте const images = content.querySelectorAll('img'); images.forEach(img => { // Пропускаем совсем маленькие (иконки) if (img.width > 100 || img.height > 100) { img.setAttribute('data-lightbox', 'true'); if (!img.hasAttribute('data-original-src') && img.src) { img.setAttribute('data-original-src', img.src); } // Добавляем базовые классы if (!img.classList.contains('content-image')) { img.classList.add('content-image'); } } }); break; } } // Обновляем список allLightboxImages = document.querySelectorAll('img[data-lightbox="true"]'); console.log(`✅ Добавлено ${allLightboxImages.length} изображений через fallback`); } } // === КОНЕЦ FALLBACK ЛОГИКИ === // Дальше ваш существующий код продолжается... if (this.debug) { console.log('=== DEBUG LIGHTBOX ==='); console.log('Всего изображений для обработки:', allLightboxImages.length); } // ... остальной код setup() } ```
4. Альтернативный простой вариант - обработка ВСЕХ изображений:
Если хотите совсем простой вариант, замените начало setup() на:
```javascript setup() { if (this.initialized) return; this.initialized = true; // ПРОСТОЙ ВАРИАНТ: обрабатываем ВСЕ изображения в основном контенте // 1. Находим основной контент const content = document.querySelector('main, article, .content, .post-content') || document.body; // 2. Находим все изображения (кроме явных иконок) const allImages = Array.from(content.querySelectorAll('img')).filter(img => { // Пропускаем иконки if (this.isIcon(img)) return false; // Пропускаем изображения в шапке/футере const isInHeader = img.closest('header, .header, nav, .navbar'); const isInFooter = img.closest('footer, .footer'); return !isInHeader && !isInFooter; }); // 3. Добавляем необходимые атрибуты allImages.forEach(img => { if (!img.hasAttribute('data-lightbox')) { img.setAttribute('data-lightbox', 'true'); } if (!img.hasAttribute('data-original-src') && img.src) { img.setAttribute('data-original-src', img.src); } if (!img.classList.contains('content-image')) { img.classList.add('content-image'); } }); // 4. Получаем окончательный список const allLightboxImages = document.querySelectorAll('img[data-lightbox="true"]'); // ... дальше ваш существующий код } ```
Что делать:
1. Сначала обновите render-image.html с упрощенной логикой 2. Добавьте fallback логику в JavaScript (первый вариант выше) 3. Проверьте проблемные страницы - теперь изображения должны обрабатываться
Проблема в том, что на некоторых страницах .Page.Kind или другие условия в render-image.html не срабатывают как ожидалось. Fallback в JavaScript решает эту проблему, обрабатывая изображения даже если они не получили data-lightbox="true" из шаблона. Добавлено (2025-12-19, 12:30) --------------------------------------------- <!-- Lightbox для изображений в статьях --> <script> class ContentLightbox { constructor() { this.modal = null; this.modalImage = null; this.caption = null; this.images = []; this.currentIndex = 0; this.debug = true; this.initialized = false; this.init(); } init() { this.createModal(); if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', () => { setTimeout(() => this.setup(), 100); }); } else { setTimeout(() => this.setup(), 100); } } createModal() { this.modal = document.createElement('div'); this.modal.className = 'content-lightbox'; this.modal.setAttribute('aria-hidden', 'true'); this.modal.style.cssText = ` position: fixed; top: 0; left: 0; width: 100%; height: 100%; background-color: rgba(0, 0, 0, 0.45); display: flex; align-items: center; justify-content: center; z-index: 9999; opacity: 0; visibility: hidden; transition: opacity 0.3s ease, visibility 0.3s ease; backdrop-filter: blur(8px); `; const container = document.createElement('div'); container.className = 'content-lightbox__container'; container.style.cssText = ` position: relative; max-width: 95vw; max-height: 95vh; margin: 20px; `; const closeBtn = document.createElement('button'); closeBtn.className = 'content-lightbox__close'; closeBtn.innerHTML = '×'; closeBtn.setAttribute('aria-label', 'Закрыть'); closeBtn.style.cssText = ` position: absolute; top: -50px; right: 0; background: rgba(255, 255, 255, 0.15); border: none; color: white; font-size: 34px; cursor: pointer; width: 40px; height: 40px; border-radius: 50%; display: flex; align-items: center; justify-content: center; z-index: 10; transition: all 0.2s ease; backdrop-filter: blur(5px); padding: 0; margin: 0; line-height: 40px; `; closeBtn.addEventListener('mouseenter', () => { closeBtn.style.background = 'rgba(255, 255, 255, 0.25)'; closeBtn.style.transform = 'scale(1.1)'; }); closeBtn.addEventListener('mouseleave', () => { closeBtn.style.background = 'rgba(255, 255, 255, 0.15)'; closeBtn.style.transform = 'scale(1)'; }); closeBtn.addEventListener('click', (e) => { e.stopPropagation(); this.close(); }); this.modalImage = document.createElement('img'); this.modalImage.className = 'content-lightbox__image'; this.modalImage.style.cssText = ` display: block; max-width: 100%; max-height: 85vh; margin: 0 auto; border-radius: 4px; box-shadow: 0 25px 50px rgba(0, 0, 0, 0.5); cursor: default; transform: scale(0.96); opacity: 0; transition: transform 0.4s cubic-bezier(0.175, 0.885, 0.32, 1.275), opacity 0.4s ease; `; this.caption = document.createElement('div'); this.caption.className = 'content-lightbox__caption'; this.caption.style.cssText = ` text-align: center; color: rgba(255, 255, 255, 0.9); font-size: 15px; margin-top: 15px; padding: 8px 16px; background: rgba(255, 255, 255, 0.08); border-radius: 4px; backdrop-filter: blur(10px); opacity: 0; transform: translateY(10px); transition: opacity 0.4s ease, transform 0.4s ease; `; container.appendChild(closeBtn); container.appendChild(this.modalImage); container.appendChild(this.caption); this.modal.appendChild(container); document.body.appendChild(this.modal); this.modal.addEventListener('click', (e) => { if (e.target === this.modal) { this.close(); } }); document.addEventListener('keydown', (e) => { if (e.key === 'Escape' && this.isOpen()) { this.close(); } }); document.addEventListener('keydown', (e) => { if (!this.isOpen()) return; if (e.key === 'ArrowLeft') { e.preventDefault(); this.prev(); } else if (e.key === 'ArrowRight') { e.preventDefault(); this.next(); } }); } setup() { if (this.initialized) return; this.initialized = true; // === FALLBACK ЛОГИКА: обрабатываем все изображения в контенте === let allLightboxImages = document.querySelectorAll('img[data-lightbox="true"]'); // Если нет изображений с data-lightbox="true", пробуем найти в контенте if (allLightboxImages.length === 0) { if (this.debug) { console.log('⚠️ Не найдено изображений с data-lightbox="true", ищем в контенте...'); } // Определяем, является ли это страницей статьи const path = window.location.pathname.toLowerCase(); const isArticleLikePage = ( !path.endsWith('/') || // Не главная страница path.includes('/posts/') || path.includes('/blog/') || path.includes('/articles/') || path.includes('/manuals/') || path.includes('/docs/') || path.includes('/laboratory/') || path.includes('/page/') || /\/\d{4}\/\d{2}\/\d{2}\//.test(path) // Дата в URL ); if (isArticleLikePage) { // Находим основной контент const contentSelectors = [ 'main', 'article', '.content', '.post-content', '.article-content', '.entry-content', '.prose', '[role="main"]' ]; let contentArea = null; for (const selector of contentSelectors) { const element = document.querySelector(selector); if (element) { contentArea = element; break; } } if (contentArea) { // Находим все изображения в контенте const imagesInContent = contentArea.querySelectorAll('img'); imagesInContent.forEach(img => { // Пропускаем явные иконки if (this.isIcon(img)) return; // Пропускаем изображения в шапке/футере const isInHeader = img.closest('header, .header, nav, .nav, .navbar, .menu'); const isInFooter = img.closest('footer, .footer'); const isInSidebar = img.closest('aside, .sidebar, .aside'); if (!isInHeader && !isInFooter && !isInSidebar) { // Добавляем необходимые атрибуты img.setAttribute('data-lightbox', 'true'); if (img.src && !img.hasAttribute('data-original-src')) { img.setAttribute('data-original-src', img.src); } // Добавляем базовые классы if (!img.classList.contains('content-image')) { img.classList.add('content-image', 'content-image--zoomable', 'content-image--shadow'); } } }); // Обновляем список allLightboxImages = document.querySelectorAll('img[data-lightbox="true"]'); if (this.debug) { console.log(`✅ Добавлено ${allLightboxImages.length} изображений через fallback`); } } } } if (this.debug) { console.log('=== DEBUG LIGHTBOX ==='); console.log('Всего изображений для обработки:', allLightboxImages.length); console.log('URL:', window.location.pathname); } // Обрабатываем ВСЕ изображения this.processAllImages(allLightboxImages); // Дополнительная проверка через 1 секунду для lazy images setTimeout(() => { this.checkLazyImages(allLightboxImages); }, 1000); // Еще одна проверка через 3 секунды setTimeout(() => { this.finalCheck(allLightboxImages); }, 3000); } processAllImages(allImages) { const imagePromises = Array.from(allImages).map(img => { return new Promise((resolve) => { // Если изображение уже загружено if (img.complete && img.naturalWidth > 0) { this.processImage(img); resolve(true); } // Если изображение еще не загружено, но имеет src else if (img.src || img.dataset.originalSrc) { // Ждем загрузки const onLoad = () => { img.removeEventListener('load', onLoad); img.removeEventListener('error', onError); setTimeout(() => { this.processImage(img); resolve(true); }, 100); }; const onError = () => { img.removeEventListener('load', onLoad); img.removeEventListener('error', onError); if (this.debug) console.log('❌ Ошибка загрузки:', img.src); resolve(false); }; img.addEventListener('load', onLoad); img.addEventListener('error', onError); // Если изображение lazy, загружаем его if (img.loading === 'lazy' && img.dataset.originalSrc && !img.src) { img.src = img.dataset.originalSrc; } } else { resolve(false); } }); }); Promise.all(imagePromises).then(results => { const successful = results.filter(r => r).length; if (this.debug) { console.log(`Обработано изображений: ${successful}/${allImages.length}`); } }); } processImage(img) { const naturalWidth = img.naturalWidth || 0; const naturalHeight = img.naturalHeight || 0; const displayedWidth = img.offsetWidth || img.width || 0; const displayedHeight = img.offsetHeight || img.height || 0; if (this.debug) { console.log('--- Обработка изображения ---'); console.log('Src:', img.src.substring(0, 100)); console.log('Natural:', naturalWidth, '×', naturalHeight); console.log('Displayed:', displayedWidth, '×', displayedHeight); } // Проверяем минимальные размеры if (naturalWidth === 0 && displayedWidth === 0) { if (this.debug) console.log('🕐 Изображение еще не загрузилось, откладываем'); return false; } const isNotIcon = !this.isIcon(img); if (!isNotIcon) { this.markAsNotZoomable(img); return false; } // УЛУЧШЕННАЯ ПРОВЕРКА РАЗМЕРОВ const MIN_WIDTH = 400; const MIN_HEIGHT = 150; const MIN_DISPLAYED_WIDTH = 300; const MIN_DISPLAYED_HEIGHT = 100; const naturalAspectRatio = naturalWidth / naturalHeight; const isNaturalHorizontal = naturalAspectRatio > 3; const isNaturalVeryHorizontal = naturalAspectRatio > 5; const isNaturalExtremelyHorizontal = naturalAspectRatio > 10; const displayedAspectRatio = displayedWidth / displayedHeight; const isDisplayedHorizontal = displayedAspectRatio > 3; const isDisplayedVeryHorizontal = displayedAspectRatio > 5; const isDisplayedExtremelyHorizontal = displayedAspectRatio > 10; const compressionRatio = naturalWidth > 0 ? displayedWidth / naturalWidth : 1; const isCompressed = compressionRatio < 0.9; let isNaturalLargeEnough; if (isNaturalExtremelyHorizontal) { // Для ОЧЕНЬ горизонтальных изображений (ширина > 10× высоты) isNaturalLargeEnough = naturalWidth >= MIN_WIDTH * 1.2 && naturalHeight >= 50; } else if (isNaturalVeryHorizontal) { // Для очень горизонтальных (ширина > 5× высоты) isNaturalLargeEnough = naturalWidth >= MIN_WIDTH * 1.5 && naturalHeight >= MIN_HEIGHT * 0.6; } else if (isNaturalHorizontal) { // Для горизонтальных (ширина > 3× высоты) isNaturalLargeEnough = naturalWidth >= MIN_WIDTH && naturalHeight >= MIN_HEIGHT * 0.7; } else { // Для обычных и вертикальных изображений isNaturalLargeEnough = naturalWidth >= MIN_WIDTH && naturalHeight >= MIN_HEIGHT; } let isDisplayedLargeEnough; if (isDisplayedExtremelyHorizontal) { // Для ОЧЕНЬ горизонтальных отображаемых isDisplayedLargeEnough = displayedWidth >= MIN_DISPLAYED_WIDTH * 1.5 && displayedHeight >= 40; } else if (isDisplayedVeryHorizontal) { // Для очень горизонтальных отображаемых isDisplayedLargeEnough = displayedWidth >= MIN_DISPLAYED_WIDTH * 1.2 && displayedHeight >= MIN_DISPLAYED_HEIGHT * 0.8; } else if (isDisplayedHorizontal) { // Для горизонтальных отображаемых isDisplayedLargeEnough = displayedWidth >= MIN_DISPLAYED_WIDTH && displayedHeight >= MIN_DISPLAYED_HEIGHT * 0.9; } else { // Для обычных отображаемых isDisplayedLargeEnough = displayedWidth >= MIN_DISPLAYED_WIDTH && displayedHeight >= MIN_DISPLAYED_HEIGHT; } const isLargeEnough = isNaturalLargeEnough || (isCompressed && isDisplayedLargeEnough); if (this.debug) { console.log(`Пропорции: ${naturalAspectRatio.toFixed(2)}:1`); console.log(`Тип: ${isNaturalExtremelyHorizontal ? 'очень-очень горизонтальное' : isNaturalVeryHorizontal ? 'очень горизонтальное' : isNaturalHorizontal ? 'горизонтальное' : 'обычное'}`); console.log(`Сжатие: ${Math.round(compressionRatio * 100)}%`); console.log(`Итог: достаточно большой = ${isLargeEnough}`); } if (!isLargeEnough) { this.markAsNotZoomable(img); return false; } const currentPath = window.location.pathname.toLowerCase(); const isDocsPage = currentPath.includes('/manuals/') || currentPath.includes('/docs/') || currentPath.includes('/laboratory/'); if (!isDocsPage) { const isNotFullyVisible = !this.isFullyVisible(img, displayedWidth, displayedHeight); if (!isNotFullyVisible) { this.markAsNotZoomable(img); return false; } } // Изображение прошло все проверки if (!this.images.includes(img)) { this.images.push(img); } this.markAsZoomable(img); if (this.debug) { console.log('✅ Доступно для увеличения'); } return true; } markAsZoomable(img) { img.style.cursor = 'zoom-in'; img.classList.add('is-zoomable'); img.classList.remove('not-zoomable'); // Удаляем старые обработчики img.onclick = null; // Добавляем новые обработчики img.addEventListener('click', (e) => { e.preventDefault(); e.stopPropagation(); const filteredIndex = this.images.indexOf(img); if (filteredIndex !== -1) { this.currentIndex = filteredIndex; this.open(img.dataset.originalSrc || img.src, img.alt || img.title || ''); } }); // Эффекты при наведении img.addEventListener('mouseenter', () => { img.style.opacity = '0.92'; img.style.transform = 'translateY(-3px)'; }); img.addEventListener('mouseleave', () => { img.style.opacity = '1'; img.style.transform = 'translateY(0)'; }); } markAsNotZoomable(img) { img.style.cursor = 'default'; img.classList.remove('content-image--zoomable'); img.classList.add('not-zoomable'); img.onclick = null; } checkLazyImages(allImages) { const lazyImages = Array.from(allImages).filter(img => (img.loading === 'lazy' || !img.complete) && !this.images.includes(img) && !img.classList.contains('not-zoomable') ); if (lazyImages.length > 0 && this.debug) { console.log('Проверка lazy images:', lazyImages.length); } lazyImages.forEach(img => { this.processImage(img); }); } finalCheck(allImages) { const unprocessedImages = Array.from(allImages).filter(img => !this.images.includes(img) && !img.classList.contains('not-zoomable') ); if (unprocessedImages.length > 0 && this.debug) { console.log('Финальная проверка:', unprocessedImages.length, 'изображений'); } unprocessedImages.forEach(img => { this.processImage(img); }); if (this.debug) { console.log('=== ИТОГО ==='); console.log('Доступно для увеличения:', this.images.length, 'из', allImages.length); } } isIcon(img) { const src = (img.src || '').toLowerCase(); const alt = (img.alt || '').toLowerCase(); const classes = (img.className || '').toLowerCase(); const iconKeywords = [ 'icon', 'logo', 'avatar', 'favicon', 'sprite', 'emoji', 'svg', 'ico', 'png-16', 'png-32', 'png-64', 'icon-', '-icon' ]; const naturalWidth = img.naturalWidth || img.offsetWidth || 0; const naturalHeight = img.naturalHeight || img.offsetHeight || 0; const isVerySmall = naturalWidth < 50 && naturalHeight < 50; const isSquareAndSmall = Math.abs(naturalWidth - naturalHeight) < 10 && naturalWidth < 100; const hasIconKeyword = iconKeywords.some(keyword => src.includes(keyword) || alt.includes(keyword) || classes.includes(keyword) ); return isVerySmall || (isSquareAndSmall && hasIconKeyword); } isFullyVisible(img, displayedWidth = null, displayedHeight = null) { const currentPath = window.location.pathname.toLowerCase(); const isDocsPage = currentPath.includes('/manuals/') || currentPath.includes('/docs/') || currentPath.includes('/laboratory/'); if (isDocsPage) { return false; } const imgWidth = displayedWidth || img.offsetWidth || img.naturalWidth; const container = img.parentElement; if (!container) return false; const containerWidth = container.clientWidth; return imgWidth < containerWidth * 0.90; } open(src, caption) { if (this.debug) { console.log('Открываем lightbox:', src.substring(0, 100)); } this.modalImage.src = src; this.modalImage.alt = caption; this.caption.textContent = caption; this.modal.setAttribute('aria-hidden', 'false'); this.modal.style.visibility = 'visible'; requestAnimationFrame(() => { this.modal.style.opacity = '1'; setTimeout(() => { this.modalImage.style.opacity = '1'; this.modalImage.style.transform = 'scale(1)'; this.caption.style.opacity = '1'; this.caption.style.transform = 'translateY(0)'; }, 50); }); document.body.style.overflow = 'hidden'; document.documentElement.style.overflow = 'hidden'; } close() { this.modalImage.style.opacity = '0'; this.modalImage.style.transform = 'scale(0.96)'; this.caption.style.opacity = '0'; this.caption.style.transform = 'translateY(10px)'; setTimeout(() => { this.modal.style.opacity = '0'; this.modal.style.visibility = 'hidden'; this.modal.setAttribute('aria-hidden', 'true'); document.body.style.overflow = ''; document.documentElement.style.overflow = ''; }, 400); } isOpen() { return this.modal.getAttribute('aria-hidden') === 'false'; } next() { if (this.images.length > 1) { this.currentIndex = (this.currentIndex + 1) % this.images.length; const nextImg = this.images[this.currentIndex]; this.open(nextImg.dataset.originalSrc || nextImg.src, nextImg.alt || nextImg.title || ''); } } prev() { if (this.images.length > 1) { this.currentIndex = (this.currentIndex - 1 + this.images.length) % this.images.length; const prevImg = this.images[this.currentIndex]; this.open(prevImg.dataset.originalSrc || prevImg.src, prevImg.alt || prevImg.title || ''); } } }
// Инициализация document.addEventListener('DOMContentLoaded', () => { setTimeout(() => { window.contentLightbox = new ContentLightbox(); }, 50); const style = document.createElement('style'); style.textContent = ` /* Базовые стили для всех изображений */ .content-image { max-width: 100%; height: auto; display: block; margin: 1.5rem auto; border-radius: 4px; transition: all 0.25s ease; } /* Стили для изображений, которые можно увеличивать */ .content-image.is-zoomable { cursor: zoom-in; } /* Для изображений, которые нельзя увеличивать */ .content-image.not-zoomable { cursor: default; } /* Тень только для zoomable изображений */ .content-image--shadow.is-zoomable { box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1); } /* Маленькие/не-zoomable изображения - без тени */ .content-image:not(.is-zoomable), .content-image.not-zoomable { box-shadow: none !important; } /* Эффект при наведении на zoomable изображения */ .content-image.is-zoomable:hover { box-shadow: 0 8px 24px rgba(0, 0, 0, 0.15); opacity: 0.95; } /* Отключаем hover эффекты для не-zoomable изображений */ .content-image.not-zoomable:hover { transform: none !important; opacity: 1 !important; box-shadow: none !important; } /* Подпись к изображениям */ .content-image__caption { text-align: center; font-size: 0.875rem; color: #666; margin-top: 0.5rem; line-height: 1.4; font-style: italic; } /* Обертка для изображений с подписью */ .content-image__wrapper { margin: 1.5rem 0; text-align: center; } /* Стили для lazy loading */ .content-image { opacity: 1; } .content-image[loading="lazy"] { opacity: 0; transition: opacity 0.3s ease; } .content-image[loading="lazy"].loaded { opacity: 1; } /* Стили для мобильных устройств */ @media (max-width: 768px) { .content-image { margin: 1rem auto; border-radius: 3px; } .content-image--shadow.is-zoomable { box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); } .content-image.is-zoomable:hover { box-shadow: 0 4px 16px rgba(0, 0, 0, 0.12); } .content-lightbox__close { top: 10px; right: 10px; width: 36px; height: 36px; font-size: 30px; line-height: 36px; } } /* Отключение hover на тач-устройствах */ @media (hover: none) { .content-image:hover { transform: none !important; } } `; document.head.appendChild(style); });
// Добавляем класс loaded для lazy images window.addEventListener('load', () => { const lazyImages = document.querySelectorAll('img[data-lightbox="true"][loading="lazy"]'); lazyImages.forEach(img => { if (img.complete) { img.classList.add('loaded'); } else { img.addEventListener('load', () => { img.classList.add('loaded'); }); } }); });
// Проверка при скролле для lazy images window.addEventListener('scroll', () => { setTimeout(() => { if (window.contentLightbox && window.contentLightbox.initialized) { const allImages = document.querySelectorAll('img[data-lightbox="true"]'); window.contentLightbox.checkLazyImages(allImages); } }, 500); });
// Проверка при изменении размера окна window.addEventListener('resize', () => { setTimeout(() => { if (window.contentLightbox) { const allImages = document.querySelectorAll('img[data-lightbox="true"]'); window.contentLightbox.finalCheck(allImages); } }, 300); });
// Дополнительная проверка для страниц, которые могли пропустить setTimeout(() => { if (window.contentLightbox && window.contentLightbox.initialized) { // Проверяем, есть ли вообще изображения на странице const anyImagesOnPage = document.querySelectorAll('img').length > 0; const lightboxImages = document.querySelectorAll('img[data-lightbox="true"]').length; if (anyImagesOnPage && lightboxImages === 0) { console.log('🔄 Перезапускаем lightbox для страницы без обработанных изображений'); window.contentLightbox.setup(); } } }, 5000); </script> Добавлено (2025-12-19, 12:40) --------------------------------------------- <!-- Lightbox для изображений в статьях --> <script> class ContentLightbox { constructor() { this.modal = null; this.modalImage = null; this.caption = null; this.images = []; this.currentIndex = 0; this.debug = true; this.initialized = false; this.init(); } init() { this.createModal(); if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', () => { setTimeout(() => this.setup(), 100); }); } else { setTimeout(() => this.setup(), 100); } } createModal() { // ... (код createModal остается без изменений) ... } setup() { if (this.initialized) return; this.initialized = true; if (this.debug) { console.log('=== DEBUG LIGHTBOX ==='); console.log('URL:', window.location.pathname); } // ВАЖНОЕ ИЗМЕНЕНИЕ: НЕ ИЩЕМ ПО data-lightbox="true" // Вместо этого находим ВСЕ изображения в основном контенте // 1. Находим основной контент const contentSelectors = [ 'main', 'article', '.content', '.post-content', '.article-content', '.entry-content', '.prose', '[role="main"]' ]; let contentArea = null; for (const selector of contentSelectors) { const element = document.querySelector(selector); if (element) { contentArea = element; break; } } // Если не нашли контент, используем body (но исключаем шапку/футер) if (!contentArea) { contentArea = document.body; } // 2. Находим ВСЕ изображения в контенте const allImages = contentArea.querySelectorAll('img'); if (this.debug) { console.log('Всего изображений на странице:', document.querySelectorAll('img').length); console.log('Изображений в контенте:', allImages.length); } // 3. Фильтруем изображения для lightbox const imagesForLightbox = Array.from(allImages).filter(img => { // Пропускаем явные иконки if (this.isIcon(img)) { if (this.debug) console.log('Пропущено (иконка):', img.src.substring(0, 80)); return false; } // Пропускаем изображения в шапке/футере/навигации const isInHeader = img.closest('header, .header, nav, .nav, .navbar, .menu, .navigation'); const isInFooter = img.closest('footer, .footer'); const isInSidebar = img.closest('aside, .sidebar, .aside'); if (isInHeader || isInFooter || isInSidebar) { if (this.debug) console.log('Пропущено (шапка/футер):', img.src.substring(0, 80)); return false; } return true; }); if (this.debug) { console.log('Изображений для обработки lightbox:', imagesForLightbox.length); } // 4. Обрабатываем каждое изображение imagesForLightbox.forEach(img => { // Всегда добавляем базовые классы для стилей if (!img.classList.contains('content-image')) { img.classList.add('content-image'); } // Добавляем атрибуты для lightbox (даже если в итоге не подойдет) if (!img.hasAttribute('data-lightbox')) { img.setAttribute('data-lightbox', 'pending'); // Вместо "true" } if (!img.hasAttribute('data-original-src') && img.src) { img.setAttribute('data-original-src', img.src); } // Проверяем, подходит ли изображение для увеличения this.checkImageForLightbox(img); }); // 5. Дополнительные проверки setTimeout(() => { this.checkAllImages(imagesForLightbox); }, 1000); setTimeout(() => { this.finalImageCheck(imagesForLightbox); }, 3000); } checkImageForLightbox(img) { // Ждем загрузки изображения if (!img.complete) { img.addEventListener('load', () => { setTimeout(() => this.processImage(img), 100); }); img.addEventListener('error', () => { if (this.debug) console.log('❌ Ошибка загрузки:', img.src); this.markAsNotZoomable(img); }); return; } // Если изображение уже загружено this.processImage(img); } processImage(img) { const naturalWidth = img.naturalWidth || 0; const naturalHeight = img.naturalHeight || 0; const displayedWidth = img.offsetWidth || img.width || 0; const displayedHeight = img.offsetHeight || img.height || 0; if (this.debug) { console.log('--- Проверка изображения ---'); console.log('Src:', img.src.substring(0, 100)); console.log('Natural:', naturalWidth, '×', naturalHeight); console.log('Displayed:', displayedWidth, '×', displayedHeight); } // Если изображение не загрузилось if (naturalWidth === 0 && displayedWidth === 0) { this.markAsNotZoomable(img); return false; } // Проверяем размеры для lightbox const MIN_WIDTH = 400; const MIN_HEIGHT = 150; const MIN_DISPLAYED_WIDTH = 300; const MIN_DISPLAYED_HEIGHT = 100; const naturalAspectRatio = naturalWidth / naturalHeight; const isNaturalHorizontal = naturalAspectRatio > 3; const isNaturalVeryHorizontal = naturalAspectRatio > 5; const isNaturalExtremelyHorizontal = naturalAspectRatio > 10; const displayedAspectRatio = displayedWidth / displayedHeight; const isDisplayedExtremelyHorizontal = displayedAspectRatio > 10; const compressionRatio = naturalWidth > 0 ? displayedWidth / naturalWidth : 1; const isCompressed = compressionRatio < 0.9; let isNaturalLargeEnough; if (isNaturalExtremelyHorizontal) { isNaturalLargeEnough = naturalWidth >= MIN_WIDTH * 1.2 && naturalHeight >= 50; } else if (isNaturalVeryHorizontal) { isNaturalLargeEnough = naturalWidth >= MIN_WIDTH * 1.5 && naturalHeight >= MIN_HEIGHT * 0.6; } else if (isNaturalHorizontal) { isNaturalLargeEnough = naturalWidth >= MIN_WIDTH && naturalHeight >= MIN_HEIGHT * 0.7; } else { isNaturalLargeEnough = naturalWidth >= MIN_WIDTH && naturalHeight >= MIN_HEIGHT; } let isDisplayedLargeEnough; if (isDisplayedExtremelyHorizontal) { isDisplayedLargeEnough = displayedWidth >= MIN_DISPLAYED_WIDTH * 1.5 && displayedHeight >= 40; } else if (isNaturalVeryHorizontal) { isDisplayedLargeEnough = displayedWidth >= MIN_DISPLAYED_WIDTH * 1.2 && displayedHeight >= MIN_DISPLAYED_HEIGHT * 0.8; } else if (isNaturalHorizontal) { isDisplayedLargeEnough = displayedWidth >= MIN_DISPLAYED_WIDTH && displayedHeight >= MIN_DISPLAYED_HEIGHT * 0.9; } else { isDisplayedLargeEnough = displayedWidth >= MIN_DISPLAYED_WIDTH && displayedHeight >= MIN_DISPLAYED_HEIGHT; } const isLargeEnough = isNaturalLargeEnough || (isCompressed && isDisplayedLargeEnough); if (!isLargeEnough) { if (this.debug) console.log('❌ Слишком маленькое для увеличения'); this.markAsNotZoomable(img); return false; } const currentPath = window.location.pathname.toLowerCase(); const isDocsPage = currentPath.includes('/manuals/') || currentPath.includes('/docs/') || currentPath.includes('/laboratory/'); if (!isDocsPage) { const isNotFullyVisible = !this.isFullyVisible(img, displayedWidth, displayedHeight); if (!isNotFullyVisible) { if (this.debug) console.log('❌ Полностью видно на странице'); this.markAsNotZoomable(img); return false; } } // Изображение подходит для lightbox if (!this.images.includes(img)) { this.images.push(img); } this.markAsZoomable(img); if (this.debug) { console.log('✅ Доступно для увеличения'); } return true; } markAsZoomable(img) { img.style.cursor = 'zoom-in'; img.classList.add('is-zoomable'); img.classList.remove('not-zoomable'); // Обновляем data-lightbox img.setAttribute('data-lightbox', 'true'); // Удаляем старые обработчики img.onclick = null; // Добавляем обработчик клика img.addEventListener('click', (e) => { e.preventDefault(); e.stopPropagation(); const filteredIndex = this.images.indexOf(img); if (filteredIndex !== -1) { this.currentIndex = filteredIndex; this.open(img.dataset.originalSrc || img.src, img.alt || img.title || ''); } }); // Эффекты при наведении img.addEventListener('mouseenter', () => { img.style.opacity = '0.92'; img.style.transform = 'translateY(-3px)'; }); img.addEventListener('mouseleave', () => { img.style.opacity = '1'; img.style.transform = 'translateY(0)'; }); } markAsNotZoomable(img) { img.style.cursor = 'default'; img.classList.add('not-zoomable'); img.classList.remove('is-zoomable'); img.onclick = null; // Убираем data-lightbox или ставим false img.setAttribute('data-lightbox', 'false'); } checkAllImages(images) { if (this.debug) { console.log('Проверка всех изображений через 1с...'); } images.forEach(img => { if (!img.classList.contains('is-zoomable') && !img.classList.contains('not-zoomable')) { this.checkImageForLightbox(img); } }); } finalImageCheck(images) { if (this.debug) { console.log('Финальная проверка через 3с...'); const zoomable = images.filter(img => img.classList.contains('is-zoomable')).length; const notZoomable = images.filter(img => img.classList.contains('not-zoomable')).length; const unprocessed = images.length - zoomable - notZoomable; console.log(`Итого: ${zoomable} можно увеличивать, ${notZoomable} нельзя, ${unprocessed} не обработано`); } // Обрабатываем все оставшиеся изображения images.forEach(img => { if (!img.classList.contains('is-zoomable') && !img.classList.contains('not-zoomable')) { this.markAsNotZoomable(img); // Если не обработано - считаем не zoomable } }); } isIcon(img) { // ... (код isIcon без изменений) ... } isFullyVisible(img, displayedWidth = null, displayedHeight = null) { // ... (код isFullyVisible без изменений) ... } open(src, caption) { // ... (код open без изменений) ... } close() { // ... (код close без изменений) ... } isOpen() { // ... (код isOpen без изменений) ... } next() { // ... (код next без изменений) ... } prev() { // ... (код prev без изменений) ... } }
// Инициализация document.addEventListener('DOMContentLoaded', () => { setTimeout(() => { window.contentLightbox = new ContentLightbox(); }, 100); const style = document.createElement('style'); style.textContent = ` /* Базовые стили для ВСЕХ изображений в контенте */ .content-image { max-width: 100%; height: auto; display: block; margin: 1.5rem auto; border-radius: 4px; transition: all 0.25s ease; } /* Стили ТОЛЬКО для изображений, которые можно увеличивать */ .content-image.is-zoomable { cursor: zoom-in; box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1); } .content-image.is-zoomable:hover { box-shadow: 0 8px 24px rgba(0, 0, 0, 0.15); opacity: 0.95; } /* Для изображений, которые нельзя увеличивать */ .content-image.not-zoomable { cursor: default; box-shadow: none; } .content-image.not-zoomable:hover { transform: none !important; opacity: 1 !important; box-shadow: none !important; } /* Подпись к изображениям */ .content-image__caption { text-align: center; font-size: 0.875rem; color: #666; margin-top: 0.5rem; line-height: 1.4; font-style: italic; } /* Обертка для изображений с подписью */ .content-image__wrapper { margin: 1.5rem 0; text-align: center; } /* Стили для lazy loading */ .content-image[loading="lazy"] { opacity: 0; transition: opacity 0.3s ease; } .content-image[loading="lazy"].loaded { opacity: 1; } /* Стили для мобильных устройств */ @media (max-width: 768px) { .content-image { margin: 1rem auto; border-radius: 3px; } .content-image.is-zoomable { box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); } .content-image.is-zoomable:hover { box-shadow: 0 4px 16px rgba(0, 0, 0, 0.12); } } /* Отключение hover на тач-устройствах */ @media (hover: none) { .content-image:hover { transform: none !important; } } `; document.head.appendChild(style); });
// Загрузка lazy images window.addEventListener('load', () => { const lazyImages = document.querySelectorAll('.content-image[loading="lazy"]'); lazyImages.forEach(img => { if (img.complete) { img.classList.add('loaded'); } else { img.addEventListener('load', () => { img.classList.add('loaded'); // Проверяем после загрузки if (window.contentLightbox) { setTimeout(() => window.contentLightbox.checkImageForLightbox(img), 100); } }); } }); });
// Проверка при скролле window.addEventListener('scroll', () => { setTimeout(() => { if (window.contentLightbox) { const images = document.querySelectorAll('.content-image:not(.is-zoomable):not(.not-zoomable)'); images.forEach(img => { window.contentLightbox.checkImageForLightbox(img); }); } }, 500); }); </script> Добавлено (2025-12-19, 12:49) --------------------------------------------- <!-- Lightbox для изображений в статьях --> <script> class ContentLightbox { constructor() { this.modal = null; this.modalImage = null; this.caption = null; this.images = []; this.currentIndex = 0; this.debug = true; this.initialized = false; this.init(); } init() { this.createModal(); if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', () => { setTimeout(() => this.setup(), 100); }); } else { setTimeout(() => this.setup(), 100); } } createModal() { this.modal = document.createElement('div'); this.modal.className = 'content-lightbox'; this.modal.setAttribute('aria-hidden', 'true'); this.modal.style.cssText = ` position: fixed; top: 0; left: 0; width: 100%; height: 100%; background-color: rgba(0, 0, 0, 0.45); display: flex; align-items: center; justify-content: center; z-index: 9999; opacity: 0; visibility: hidden; transition: opacity 0.3s ease, visibility 0.3s ease; backdrop-filter: blur(8px); `; const container = document.createElement('div'); container.className = 'content-lightbox__container'; container.style.cssText = ` position: relative; max-width: 95vw; max-height: 95vh; margin: 20px; `; const closeBtn = document.createElement('button'); closeBtn.className = 'content-lightbox__close'; closeBtn.innerHTML = '×'; closeBtn.setAttribute('aria-label', 'Закрыть'); closeBtn.style.cssText = ` position: absolute; top: -50px; right: 0; background: rgba(255, 255, 255, 0.15); border: none; color: white; font-size: 34px; cursor: pointer; width: 40px; height: 40px; border-radius: 50%; display: flex; align-items: center; justify-content: center; z-index: 10; transition: all 0.2s ease; backdrop-filter: blur(5px); padding: 0; margin: 0; line-height: 40px; `; closeBtn.addEventListener('mouseenter', () => { closeBtn.style.background = 'rgba(255, 255, 255, 0.25)'; closeBtn.style.transform = 'scale(1.1)'; }); closeBtn.addEventListener('mouseleave', () => { closeBtn.style.background = 'rgba(255, 255, 255, 0.15)'; closeBtn.style.transform = 'scale(1)'; }); closeBtn.addEventListener('click', (e) => { e.stopPropagation(); this.close(); }); this.modalImage = document.createElement('img'); this.modalImage.className = 'content-lightbox__image'; this.modalImage.style.cssText = ` display: block; max-width: 100%; max-height: 85vh; margin: 0 auto; border-radius: 4px; box-shadow: 0 25px 50px rgba(0, 0, 0, 0.5); cursor: default; transform: scale(0.96); opacity: 0; transition: transform 0.4s cubic-bezier(0.175, 0.885, 0.32, 1.275), opacity 0.4s ease; `; this.caption = document.createElement('div'); this.caption.className = 'content-lightbox__caption'; this.caption.style.cssText = ` text-align: center; color: rgba(255, 255, 255, 0.9); font-size: 15px; margin-top: 15px; padding: 8px 16px; background: rgba(255, 255, 255, 0.08); border-radius: 4px; backdrop-filter: blur(10px); opacity: 0; transform: translateY(10px); transition: opacity 0.4s ease, transform 0.4s ease; `; container.appendChild(closeBtn); container.appendChild(this.modalImage); container.appendChild(this.caption); this.modal.appendChild(container); document.body.appendChild(this.modal); this.modal.addEventListener('click', (e) => { if (e.target === this.modal) { this.close(); } }); document.addEventListener('keydown', (e) => { if (e.key === 'Escape' && this.isOpen()) { this.close(); } }); document.addEventListener('keydown', (e) => { if (!this.isOpen()) return; if (e.key === 'ArrowLeft') { e.preventDefault(); this.prev(); } else if (e.key === 'ArrowRight') { e.preventDefault(); this.next(); } }); } setup() { if (this.initialized) return; this.initialized = true; if (this.debug) { console.log('=== DEBUG LIGHTBOX ==='); console.log('URL:', window.location.pathname); } const contentSelectors = [ 'main', 'article', '.content', '.post-content', '.article-content', '.entry-content', '.prose', '[role="main"]' ]; let contentArea = null; for (const selector of contentSelectors) { const element = document.querySelector(selector); if (element) { contentArea = element; break; } } if (!contentArea) { contentArea = document.body; } const allImages = contentArea.querySelectorAll('img'); if (this.debug) { console.log('Всего изображений на странице:', document.querySelectorAll('img').length); console.log('Изображений в контенте:', allImages.length); } const imagesForLightbox = Array.from(allImages).filter(img => { if (this.isIcon(img)) { if (this.debug) console.log('Пропущено (иконка):', img.src.substring(0, 80)); return false; } const isInHeader = img.closest('header, .header, nav, .nav, .navbar, .menu, .navigation'); const isInFooter = img.closest('footer, .footer'); const isInSidebar = img.closest('aside, .sidebar, .aside'); if (isInHeader || isInFooter || isInSidebar) { if (this.debug) console.log('Пропущено (шапка/футер):', img.src.substring(0, 80)); return false; } return true; }); if (this.debug) { console.log('Изображений для обработки lightbox:', imagesForLightbox.length); } imagesForLightbox.forEach(img => { if (!img.classList.contains('content-image')) { img.classList.add('content-image'); } if (!img.hasAttribute('data-lightbox')) { img.setAttribute('data-lightbox', 'pending'); } if (!img.hasAttribute('data-original-src') && img.src) { img.setAttribute('data-original-src', img.src); } this.checkImageForLightbox(img); }); setTimeout(() => { this.checkAllImages(imagesForLightbox); }, 1000); setTimeout(() => { this.finalImageCheck(imagesForLightbox); }, 3000); } checkImageForLightbox(img) { if (!img.complete) { img.addEventListener('load', () => { setTimeout(() => this.processImage(img), 100); }); img.addEventListener('error', () => { if (this.debug) console.log('❌ Ошибка загрузки:', img.src); this.markAsNotZoomable(img); }); return; } this.processImage(img); } processImage(img) { const naturalWidth = img.naturalWidth || 0; const naturalHeight = img.naturalHeight || 0; const displayedWidth = img.offsetWidth || img.width || 0; const displayedHeight = img.offsetHeight || img.height || 0; if (this.debug) { console.log('--- Проверка изображения ---'); console.log('Src:', img.src.substring(0, 100)); console.log('Natural:', naturalWidth, '×', naturalHeight); console.log('Displayed:', displayedWidth, '×', displayedHeight); } if (naturalWidth === 0 && displayedWidth === 0) { this.markAsNotZoomable(img); return false; } const MIN_WIDTH = 400; const MIN_HEIGHT = 150; const MIN_DISPLAYED_WIDTH = 300; const MIN_DISPLAYED_HEIGHT = 100; const naturalAspectRatio = naturalWidth / naturalHeight; const isNaturalHorizontal = naturalAspectRatio > 3; const isNaturalVeryHorizontal = naturalAspectRatio > 5; const isNaturalExtremelyHorizontal = naturalAspectRatio > 10; const displayedAspectRatio = displayedWidth / displayedHeight; const isDisplayedExtremelyHorizontal = displayedAspectRatio > 10; const compressionRatio = naturalWidth > 0 ? displayedWidth / naturalWidth : 1; const isCompressed = compressionRatio < 0.9; let isNaturalLargeEnough; if (isNaturalExtremelyHorizontal) { isNaturalLargeEnough = naturalWidth >= MIN_WIDTH * 1.2 && naturalHeight >= 50; } else if (isNaturalVeryHorizontal) { isNaturalLargeEnough = naturalWidth >= MIN_WIDTH * 1.5 && naturalHeight >= MIN_HEIGHT * 0.6; } else if (isNaturalHorizontal) { isNaturalLargeEnough = naturalWidth >= MIN_WIDTH && naturalHeight >= MIN_HEIGHT * 0.7; } else { isNaturalLargeEnough = naturalWidth >= MIN_WIDTH && naturalHeight >= MIN_HEIGHT; } let isDisplayedLargeEnough; if (isDisplayedExtremelyHorizontal) { isDisplayedLargeEnough = displayedWidth >= MIN_DISPLAYED_WIDTH * 1.5 && displayedHeight >= 40; } else if (isNaturalVeryHorizontal) { isDisplayedLargeEnough = displayedWidth >= MIN_DISPLAYED_WIDTH * 1.2 && displayedHeight >= MIN_DISPLAYED_HEIGHT * 0.8; } else if (isNaturalHorizontal) { isDisplayedLargeEnough = displayedWidth >= MIN_DISPLAYED_WIDTH && displayedHeight >= MIN_DISPLAYED_HEIGHT * 0.9; } else { isDisplayedLargeEnough = displayedWidth >= MIN_DISPLAYED_WIDTH && displayedHeight >= MIN_DISPLAYED_HEIGHT; } const isLargeEnough = isNaturalLargeEnough || (isCompressed && isDisplayedLargeEnough); if (!isLargeEnough) { if (this.debug) console.log('❌ Слишком маленькое для увеличения'); this.markAsNotZoomable(img); return false; } const currentPath = window.location.pathname.toLowerCase(); const isDocsPage = currentPath.includes('/manuals/') || currentPath.includes('/docs/') || currentPath.includes('/laboratory/'); if (!isDocsPage) { const isNotFullyVisible = !this.isFullyVisible(img, displayedWidth, displayedHeight); if (!isNotFullyVisible) { if (this.debug) console.log('❌ Полностью видно на странице'); this.markAsNotZoomable(img); return false; } } if (!this.images.includes(img)) { this.images.push(img); } this.markAsZoomable(img); if (this.debug) { console.log('✅ Доступно для увеличения'); } return true; } markAsZoomable(img) { img.style.cursor = 'zoom-in'; img.classList.add('is-zoomable'); img.classList.remove('not-zoomable'); img.setAttribute('data-lightbox', 'true'); img.onclick = null; img.addEventListener('click', (e) => { e.preventDefault(); e.stopPropagation(); const filteredIndex = this.images.indexOf(img); if (filteredIndex !== -1) { this.currentIndex = filteredIndex; this.open(img.dataset.originalSrc || img.src, img.alt || img.title || ''); } }); img.addEventListener('mouseenter', () => { img.style.opacity = '0.92'; img.style.transform = 'translateY(-3px)'; }); img.addEventListener('mouseleave', () => { img.style.opacity = '1'; img.style.transform = 'translateY(0)'; }); } markAsNotZoomable(img) { img.style.cursor = 'default'; img.classList.add('not-zoomable'); img.classList.remove('is-zoomable'); img.onclick = null; img.setAttribute('data-lightbox', 'false'); } checkAllImages(images) { if (this.debug) { console.log('Проверка всех изображений через 1с...'); } images.forEach(img => { if (!img.classList.contains('is-zoomable') && !img.classList.contains('not-zoomable')) { this.checkImageForLightbox(img); } }); } finalImageCheck(images) { if (this.debug) { console.log('Финальная проверка через 3с...'); const zoomable = images.filter(img => img.classList.contains('is-zoomable')).length; const notZoomable = images.filter(img => img.classList.contains('not-zoomable')).length; const unprocessed = images.length - zoomable - notZoomable; console.log(`Итого: ${zoomable} можно увеличивать, ${notZoomable} нельзя, ${unprocessed} не обработано`); } images.forEach(img => { if (!img.classList.contains('is-zoomable') && !img.classList.contains('not-zoomable')) { this.markAsNotZoomable(img); } }); } isIcon(img) { const src = (img.src || '').toLowerCase(); const alt = (img.alt || '').toLowerCase(); const classes = (img.className || '').toLowerCase(); const iconKeywords = [ 'icon', 'logo', 'avatar', 'favicon', 'sprite', 'emoji', 'svg', 'ico', 'png-16', 'png-32', 'png-64', 'icon-', '-icon' ]; const naturalWidth = img.naturalWidth || img.offsetWidth || 0; const naturalHeight = img.naturalHeight || img.offsetHeight || 0; const isVerySmall = naturalWidth < 50 && naturalHeight < 50; const isSquareAndSmall = Math.abs(naturalWidth - naturalHeight) < 10 && naturalWidth < 100; const hasIconKeyword = iconKeywords.some(keyword => src.includes(keyword) || alt.includes(keyword) || classes.includes(keyword) ); return isVerySmall || (isSquareAndSmall && hasIconKeyword); } isFullyVisible(img, displayedWidth = null, displayedHeight = null) { const currentPath = window.location.pathname.toLowerCase(); const isDocsPage = currentPath.includes('/manuals/') || currentPath.includes('/docs/') || currentPath.includes('/laboratory/'); if (isDocsPage) { return false; } const imgWidth = displayedWidth || img.offsetWidth || img.naturalWidth; const container = img.parentElement; if (!container) return false; const containerWidth = container.clientWidth; return imgWidth < containerWidth * 0.90; } open(src, caption) { if (this.debug) { console.log('Открываем lightbox:', src.substring(0, 100)); } this.modalImage.src = src; this.modalImage.alt = caption; this.caption.textContent = caption; this.modal.setAttribute('aria-hidden', 'false'); this.modal.style.visibility = 'visible'; requestAnimationFrame(() => { this.modal.style.opacity = '1'; setTimeout(() => { this.modalImage.style.opacity = '1'; this.modalImage.style.transform = 'scale(1)'; this.caption.style.opacity = '1'; this.caption.style.transform = 'translateY(0)'; }, 50); }); document.body.style.overflow = 'hidden'; document.documentElement.style.overflow = 'hidden'; } close() { this.modalImage.style.opacity = '0'; this.modalImage.style.transform = 'scale(0.96)'; this.caption.style.opacity = '0'; this.caption.style.transform = 'translateY(10px)'; setTimeout(() => { this.modal.style.opacity = '0'; this.modal.style.visibility = 'hidden'; this.modal.setAttribute('aria-hidden', 'true'); document.body.style.overflow = ''; document.documentElement.style.overflow = ''; }, 400); } isOpen() { return this.modal.getAttribute('aria-hidden') === 'false'; } next() { if (this.images.length > 1) { this.currentIndex = (this.currentIndex + 1) % this.images.length; const nextImg = this.images[this.currentIndex]; this.open(nextImg.dataset.originalSrc || nextImg.src, nextImg.alt || nextImg.title || ''); } } prev() { if (this.images.length > 1) { this.currentIndex = (this.currentIndex - 1 + this.images.length) % this.images.length; const prevImg = this.images[this.currentIndex]; this.open(prevImg.dataset.originalSrc || prevImg.src, prevImg.alt || prevImg.title || ''); } } }
document.addEventListener('DOMContentLoaded', () => { setTimeout(() => { window.contentLightbox = new ContentLightbox(); }, 100); const style = document.createElement('style'); style.textContent = ` .content-image { max-width: 100%; height: auto; display: block; margin: 1.5rem auto; border-radius: 4px; transition: all 0.25s ease; } .content-image.is-zoomable { cursor: zoom-in; box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1); } .content-image.is-zoomable:hover { box-shadow: 0 8px 24px rgba(0, 0, 0, 0.15); opacity: 0.95; } .content-image.not-zoomable { cursor: default; box-shadow: none; } .content-image.not-zoomable:hover { transform: none !important; opacity: 1 !important; box-shadow: none !important; } .content-image__caption { text-align: center; font-size: 0.875rem; color: #666; margin-top: 0.5rem; line-height: 1.4; font-style: italic; } .content-image__wrapper { margin: 1.5rem 0; text-align: center; } .content-image[loading="lazy"] { opacity: 0; transition: opacity 0.3s ease; } .content-image[loading="lazy"].loaded { opacity: 1; } @media (max-width: 768px) { .content-image { margin: 1rem auto; border-radius: 3px; } .content-image.is-zoomable { box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); } .content-image.is-zoomable:hover { box-shadow: 0 4px 16px rgba(0, 0, 0, 0.12); } } @media (hover: none) { .content-image:hover { transform: none !important; } } `; document.head.appendChild(style); });
window.addEventListener('load', () => { const lazyImages = document.querySelectorAll('.content-image[loading="lazy"]'); lazyImages.forEach(img => { if (img.complete) { img.classList.add('loaded'); } else { img.addEventListener('load', () => { img.classList.add('loaded'); if (window.contentLightbox) { setTimeout(() => window.contentLightbox.checkImageForLightbox(img), 100); } }); } }); });
window.addEventListener('scroll', () => { setTimeout(() => { if (window.contentLightbox) { const images = document.querySelectorAll('.content-image:not(.is-zoomable):not(.not-zoomable)'); images.forEach(img => { window.contentLightbox.checkImageForLightbox(img); }); } }, 500); }); </script> Добавлено (2025-12-19, 13:00) --------------------------------------------- <!-- Lightbox для изображений в статьях --> <script> class ContentLightbox { constructor() { this.modal = null; this.modalImage = null; this.caption = null; this.images = []; this.currentIndex = 0; this.debug = true; this.initialized = false; this.init(); } init() { this.createModal(); if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', () => { setTimeout(() => this.setup(), 100); }); } else { setTimeout(() => this.setup(), 100); } } createModal() { this.modal = document.createElement('div'); this.modal.className = 'content-lightbox'; this.modal.setAttribute('aria-hidden', 'true'); this.modal.style.cssText = ` position: fixed; top: 0; left: 0; width: 100%; height: 100%; background-color: rgba(0, 0, 0, 0.45); display: flex; align-items: center; justify-content: center; z-index: 9999; opacity: 0; visibility: hidden; transition: opacity 0.3s ease, visibility 0.3s ease; backdrop-filter: blur(8px); `; const container = document.createElement('div'); container.className = 'content-lightbox__container'; container.style.cssText = ` position: relative; max-width: 95vw; max-height: 95vh; margin: 20px; `; const closeBtn = document.createElement('button'); closeBtn.className = 'content-lightbox__close'; closeBtn.innerHTML = '×'; closeBtn.setAttribute('aria-label', 'Закрыть'); closeBtn.style.cssText = ` position: absolute; top: -50px; right: 0; background: rgba(255, 255, 255, 0.15); border: none; color: white; font-size: 34px; cursor: pointer; width: 40px; height: 40px; border-radius: 50%; display: flex; align-items: center; justify-content: center; z-index: 10; transition: all 0.2s ease; backdrop-filter: blur(5px); padding: 0; margin: 0; line-height: 40px; `; closeBtn.addEventListener('mouseenter', () => { closeBtn.style.background = 'rgba(255, 255, 255, 0.25)'; closeBtn.style.transform = 'scale(1.1)'; }); closeBtn.addEventListener('mouseleave', () => { closeBtn.style.background = 'rgba(255, 255, 255, 0.15)'; closeBtn.style.transform = 'scale(1)'; }); closeBtn.addEventListener('click', (e) => { e.stopPropagation(); this.close(); }); this.modalImage = document.createElement('img'); this.modalImage.className = 'content-lightbox__image'; this.modalImage.style.cssText = ` display: block; max-width: 100%; max-height: 85vh; margin: 0 auto; border-radius: 4px; box-shadow: 0 25px 50px rgba(0, 0, 0, 0.5); cursor: default; transform: scale(0.96); opacity: 0; transition: transform 0.4s cubic-bezier(0.175, 0.885, 0.32, 1.275), opacity 0.4s ease; `; this.caption = document.createElement('div'); this.caption.className = 'content-lightbox__caption'; this.caption.style.cssText = ` text-align: center; color: rgba(255, 255, 255, 0.9); font-size: 15px; margin-top: 15px; padding: 8px 16px; background: rgba(255, 255, 255, 0.08); border-radius: 4px; backdrop-filter: blur(10px); opacity: 0; transform: translateY(10px); transition: opacity 0.4s ease, transform 0.4s ease; `; container.appendChild(closeBtn); container.appendChild(this.modalImage); container.appendChild(this.caption); this.modal.appendChild(container); document.body.appendChild(this.modal); this.modal.addEventListener('click', (e) => { if (e.target === this.modal) { this.close(); } }); document.addEventListener('keydown', (e) => { if (e.key === 'Escape' && this.isOpen()) { this.close(); } }); document.addEventListener('keydown', (e) => { if (!this.isOpen()) return; if (e.key === 'ArrowLeft') { e.preventDefault(); this.prev(); } else if (e.key === 'ArrowRight') { e.preventDefault(); this.next(); } }); } shouldEnableLightbox() { // Проверяем, нужно ли включать lightbox на этой странице const path = window.location.pathname.toLowerCase(); const host = window.location.hostname; // Главная страница (разные варианты) const isHomePage = ( path === '/' || path === '/index.html' || path === '' || path === '/hugo/' || path.endsWith('/index.html') || (path.split('/').filter(p => p).length <= 1) // Корневой путь ); // Страницы разделов (section pages) const isSectionPage = ( path.includes('/categories/') || path.includes('/tags/') || path.includes('/archive/') || path.includes('/blog/') && !path.includes('/blog/') && path.split('/').length <= 3 ); // Проверяем по структуре DOM (дополнительная проверка) const hasPostContent = document.querySelector('article, .post, .post-content, .article-content') !== null; const isListPage = document.querySelector('.posts-list, .articles-list, .blog-list, .post-list') !== null; if (this.debug) { console.log('=== ПРОВЕРКА СТРАНИЦЫ ==='); console.log('Путь:', path); console.log('Главная страница?', isHomePage); console.log('Страница раздела?', isSectionPage); console.log('Есть контент статьи?', hasPostContent); console.log('Страница списка?', isListPage); } // ИСКЛЮЧАЕМ: // 1. Главную страницу // 2. Страницы разделов (без конкретной статьи) // 3. Страницы списков if (isHomePage || isSectionPage || (isListPage && !hasPostContent)) { if (this.debug) console.log('❌ Lightbox отключен для этой страницы'); return false; } // ВКЛЮЧАЕМ: // 1. Страницы статей (имеют article или .post-content) // 2. Страницы документации (/manuals/, /docs/, /laboratory/) // 3. Страницы с конкретным контентом const isArticlePage = hasPostContent || path.includes('/manuals/') || path.includes('/docs/') || path.includes('/laboratory/') || path.includes('/posts/') || path.includes('/articles/'); if (this.debug) console.log('✅ Lightbox включен для этой страницы'); return isArticlePage; } setup() { if (this.initialized) return; this.initialized = true; // Проверяем, нужно ли включать lightbox на этой странице if (!this.shouldEnableLightbox()) { if (this.debug) { console.log('Lightbox отключен для этой страницы'); } return; // Не инициализируем lightbox } if (this.debug) { console.log('=== DEBUG LIGHTBOX ==='); console.log('URL:', window.location.pathname); } // Находим основной контент const contentSelectors = [ 'main', 'article', '.content', '.post-content', '.article-content', '.entry-content', '.prose', '[role="main"]' ]; let contentArea = null; for (const selector of contentSelectors) { const element = document.querySelector(selector); if (element) { contentArea = element; break; } } if (!contentArea) { contentArea = document.body; } // Находим ВСЕ изображения в контенте const allImages = contentArea.querySelectorAll('img'); if (this.debug) { console.log('Всего изображений на странице:', document.querySelectorAll('img').length); console.log('Изображений в контенте:', allImages.length); } // Фильтруем изображения для lightbox const imagesForLightbox = Array.from(allImages).filter(img => { // Пропускаем явные иконки if (this.isIcon(img)) { if (this.debug) console.log('Пропущено (иконка):', img.src.substring(0, 80)); return false; } // Пропускаем изображения в шапке/футере/навигации const isInHeader = img.closest('header, .header, nav, .nav, .navbar, .menu, .navigation'); const isInFooter = img.closest('footer, .footer'); const isInSidebar = img.closest('aside, .sidebar, .aside'); if (isInHeader || isInFooter || isInSidebar) { if (this.debug) console.log('Пропущено (шапка/футер):', img.src.substring(0, 80)); return false; } return true; }); if (this.debug) { console.log('Изображений для обработки lightbox:', imagesForLightbox.length); } // Обрабатываем каждое изображение imagesForLightbox.forEach(img => { // Всегда добавляем базовые классы для стилей if (!img.classList.contains('content-image')) { img.classList.add('content-image'); } // Добавляем атрибуты для lightbox if (!img.hasAttribute('data-lightbox')) { img.setAttribute('data-lightbox', 'pending'); } if (!img.hasAttribute('data-original-src') && img.src) { img.setAttribute('data-original-src', img.src); } // Проверяем, подходит ли изображение для увеличения this.checkImageForLightbox(img); }); // Дополнительные проверки setTimeout(() => { this.checkAllImages(imagesForLightbox); }, 1000); setTimeout(() => { this.finalImageCheck(imagesForLightbox); }, 3000); } checkImageForLightbox(img) { // Ждем загрузки изображения if (!img.complete) { img.addEventListener('load', () => { setTimeout(() => this.processImage(img), 100); }); img.addEventListener('error', () => { if (this.debug) console.log('❌ Ошибка загрузки:', img.src); this.markAsNotZoomable(img); }); return; } // Если изображение уже загружено this.processImage(img); } processImage(img) { const naturalWidth = img.naturalWidth || 0; const naturalHeight = img.naturalHeight || 0; const displayedWidth = img.offsetWidth || img.width || 0; const displayedHeight = img.offsetHeight || img.height || 0; if (this.debug) { console.log('--- Проверка изображения ---'); console.log('Src:', img.src.substring(0, 100)); console.log('Natural:', naturalWidth, '×', naturalHeight); console.log('Displayed:', displayedWidth, '×', displayedHeight); } // Если изображение не загрузилось if (naturalWidth === 0 && displayedWidth === 0) { this.markAsNotZoomable(img); return false; } // Проверяем размеры для lightbox const MIN_WIDTH = 400; const MIN_HEIGHT = 150; const MIN_DISPLAYED_WIDTH = 300; const MIN_DISPLAYED_HEIGHT = 100; const naturalAspectRatio = naturalWidth / naturalHeight; const isNaturalHorizontal = naturalAspectRatio > 3; const isNaturalVeryHorizontal = naturalAspectRatio > 5; const isNaturalExtremelyHorizontal = naturalAspectRatio > 10; const displayedAspectRatio = displayedWidth / displayedHeight; const isDisplayedExtremelyHorizontal = displayedAspectRatio > 10; const compressionRatio = naturalWidth > 0 ? displayedWidth / naturalWidth : 1; const isCompressed = compressionRatio < 0.9; let isNaturalLargeEnough; if (isNaturalExtremelyHorizontal) { isNaturalLargeEnough = naturalWidth >= MIN_WIDTH * 1.2 && naturalHeight >= 50; } else if (isNaturalVeryHorizontal) { isNaturalLargeEnough = naturalWidth >= MIN_WIDTH * 1.5 && naturalHeight >= MIN_HEIGHT * 0.6; } else if (isNaturalHorizontal) { isNaturalLargeEnough = naturalWidth >= MIN_WIDTH && naturalHeight >= MIN_HEIGHT * 0.7; } else { isNaturalLargeEnough = naturalWidth >= MIN_WIDTH && naturalHeight >= MIN_HEIGHT; } let isDisplayedLargeEnough; if (isDisplayedExtremelyHorizontal) { isDisplayedLargeEnough = displayedWidth >= MIN_DISPLAYED_WIDTH * 1.5 && displayedHeight >= 40; } else if (isNaturalVeryHorizontal) { isDisplayedLargeEnough = displayedWidth >= MIN_DISPLAYED_WIDTH * 1.2 && displayedHeight >= MIN_DISPLAYED_HEIGHT * 0.8; } else if (isNaturalHorizontal) { isDisplayedLargeEnough = displayedWidth >= MIN_DISPLAYED_WIDTH && displayedHeight >= MIN_DISPLAYED_HEIGHT * 0.9; } else { isDisplayedLargeEnough = displayedWidth >= MIN_DISPLAYED_WIDTH && displayedHeight >= MIN_DISPLAYED_HEIGHT; } const isLargeEnough = isNaturalLargeEnough || (isCompressed && isDisplayedLargeEnough); if (!isLargeEnough) { if (this.debug) console.log('❌ Слишком маленькое для увеличения'); this.markAsNotZoomable(img); return false; } const currentPath = window.location.pathname.toLowerCase(); const isDocsPage = currentPath.includes('/manuals/') || currentPath.includes('/docs/') || currentPath.includes('/laboratory/'); if (!isDocsPage) { const isNotFullyVisible = !this.isFullyVisible(img, displayedWidth, displayedHeight); if (!isNotFullyVisible) { if (this.debug) console.log('❌ Полностью видно на странице'); this.markAsNotZoomable(img); return false; } } // Изображение подходит для lightbox if (!this.images.includes(img)) { this.images.push(img); } this.markAsZoomable(img); if (this.debug) { console.log('✅ Доступно для увеличения'); } return true; } markAsZoomable(img) { img.style.cursor = 'zoom-in'; img.classList.add('is-zoomable'); img.classList.remove('not-zoomable'); // Обновляем data-lightbox img.setAttribute('data-lightbox', 'true'); // Удаляем старые обработчики img.onclick = null; // Добавляем обработчик клика img.addEventListener('click', (e) => { e.preventDefault(); e.stopPropagation(); const filteredIndex = this.images.indexOf(img); if (filteredIndex !== -1) { this.currentIndex = filteredIndex; this.open(img.dataset.originalSrc || img.src, img.alt || img.title || ''); } }); // Эффекты при наведении img.addEventListener('mouseenter', () => { img.style.opacity = '0.92'; img.style.transform = 'translateY(-3px)'; }); img.addEventListener('mouseleave', () => { img.style.opacity = '1'; img.style.transform = 'translateY(0)'; }); } markAsNotZoomable(img) { img.style.cursor = 'default'; img.classList.add('not-zoomable'); img.classList.remove('is-zoomable'); img.onclick = null; // Убираем data-lightbox img.setAttribute('data-lightbox', 'false'); } checkAllImages(images) { if (this.debug) { console.log('Проверка всех изображений через 1с...'); } images.forEach(img => { if (!img.classList.contains('is-zoomable') && !img.classList.contains('not-zoomable')) { this.checkImageForLightbox(img); } }); } finalImageCheck(images) { if (this.debug) { console.log('Финальная проверка через 3с...'); const zoomable = images.filter(img => img.classList.contains('is-zoomable')).length; const notZoomable = images.filter(img => img.classList.contains('not-zoomable')).length; const unprocessed = images.length - zoomable - notZoomable; console.log(`Итого: ${zoomable} можно увеличивать, ${notZoomable} нельзя, ${unprocessed} не обработано`); } // Обрабатываем все оставшиеся изображения images.forEach(img => { if (!img.classList.contains('is-zoomable') && !img.classList.contains('not-zoomable')) { this.markAsNotZoomable(img); } }); } isIcon(img) { const src = (img.src || '').toLowerCase(); const alt = (img.alt || '').toLowerCase(); const classes = (img.className || '').toLowerCase(); const iconKeywords = [ 'icon', 'logo', 'avatar', 'favicon', 'sprite', 'emoji', 'svg', 'ico', 'png-16', 'png-32', 'png-64', 'icon-', '-icon' ]; const naturalWidth = img.naturalWidth || img.offsetWidth || 0; const naturalHeight = img.naturalHeight || img.offsetHeight || 0; const isVerySmall = naturalWidth < 50 && naturalHeight < 50; const isSquareAndSmall = Math.abs(naturalWidth - naturalHeight) < 10 && naturalWidth < 100; const hasIconKeyword = iconKeywords.some(keyword => src.includes(keyword) || alt.includes(keyword) || classes.includes(keyword) ); return isVerySmall || (isSquareAndSmall && hasIconKeyword); } isFullyVisible(img, displayedWidth = null, displayedHeight = null) { const currentPath = window.location.pathname.toLowerCase(); const isDocsPage = currentPath.includes('/manuals/') || currentPath.includes('/docs/') || currentPath.includes('/laboratory/'); if (isDocsPage) { return false; } const imgWidth = displayedWidth || img.offsetWidth || img.naturalWidth; const container = img.parentElement; if (!container) return false; const containerWidth = container.clientWidth; return imgWidth < containerWidth * 0.90; } open(src, caption) { if (this.debug) { console.log('Открываем lightbox:', src.substring(0, 100)); } this.modalImage.src = src; this.modalImage.alt = caption; this.caption.textContent = caption; this.modal.setAttribute('aria-hidden', 'false'); this.modal.style.visibility = 'visible'; requestAnimationFrame(() => { this.modal.style.opacity = '1'; setTimeout(() => { this.modalImage.style.opacity = '1'; this.modalImage.style.transform = 'scale(1)'; this.caption.style.opacity = '1'; this.caption.style.transform = 'translateY(0)'; }, 50); }); document.body.style.overflow = 'hidden'; document.documentElement.style.overflow = 'hidden'; } close() { this.modalImage.style.opacity = '0'; this.modalImage.style.transform = 'scale(0.96)'; this.caption.style.opacity = '0'; this.caption.style.transform = 'translateY(10px)'; setTimeout(() => { this.modal.style.opacity = '0'; this.modal.style.visibility = 'hidden'; this.modal.setAttribute('aria-hidden', 'true'); document.body.style.overflow = ''; document.documentElement.style.overflow = ''; }, 400); } isOpen() { return this.modal.getAttribute('aria-hidden') === 'false'; } next() { if (this.images.length > 1) { this.currentIndex = (this.currentIndex + 1) % this.images.length; const nextImg = this.images[this.currentIndex]; this.open(nextImg.dataset.originalSrc || nextImg.src, nextImg.alt || nextImg.title || ''); } } prev() { if (this.images.length > 1) { this.currentIndex = (this.currentIndex - 1 + this.images.length) % this.images.length; const prevImg = this.images[this.currentIndex]; this.open(prevImg.dataset.originalSrc || prevImg.src, prevImg.alt || prevImg.title || ''); } } }
// Инициализация document.addEventListener('DOMContentLoaded', () => { setTimeout(() => { window.contentLightbox = new ContentLightbox(); }, 50); const style = document.createElement('style'); style.textContent = ` /* Базовые стили для ВСЕХ изображений в контенте */ .content-image { max-width: 100%; height: auto; display: block; margin: 1.5rem auto; border-radius: 4px; transition: all 0.25s ease; } /* Стили ТОЛЬКО для изображений, которые можно увеличивать */ .content-image.is-zoomable { cursor: zoom-in; box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1); } .content-image.is-zoomable:hover { box-shadow: 0 8px 24px rgba(0, 0, 0, 0.15); opacity: 0.95; } /* Для изображений, которые нельзя увеличивать */ .content-image.not-zoomable { cursor: default; box-shadow: none; } .content-image.not-zoomable:hover { transform: none !important; opacity: 1 !important; box-shadow: none !important; } /* Подпись к изображениям */ .content-image__caption { text-align: center; font-size: 0.875rem; color: #666; margin-top: 0.5rem; line-height: 1.4; font-style: italic; } /* Обертка для изображений с подписью */ .content-image__wrapper { margin: 1.5rem 0; text-align: center; } /* Стили для lazy loading */ .content-image[loading="lazy"] { opacity: 0; transition: opacity 0.3s ease; } .content-image[loading="lazy"].loaded { opacity: 1; } /* Стили для мобильных устройств */ @media (max-width: 768px) { .content-image { margin: 1rem auto; border-radius: 3px; } .content-image.is-zoomable { box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); } .content-image.is-zoomable:hover { box-shadow: 0 4px 16px rgba(0, 0, 0, 0.12); } } /* Отключение hover на тач-устройствах */ @media (hover: none) { .content-image:hover { transform: none !important; } } `; document.head.appendChild(style); });
// Загрузка lazy images window.addEventListener('load', () => { const lazyImages = document.querySelectorAll('.content-image[loading="lazy"]'); lazyImages.forEach(img => { if (img.complete) { img.classList.add('loaded'); } else { img.addEventListener('load', () => { img.classList.add('loaded'); // Проверяем после загрузки if (window.contentLightbox) { setTimeout(() => window.contentLightbox.checkImageForLightbox(img), 100); } }); } }); });
// Проверка при скролле window.addEventListener('scroll', () => { setTimeout(() => { if (window.contentLightbox) { const images = document.querySelectorAll('.content-image:not(.is-zoomable):not(.not-zoomable)'); images.forEach(img => { window.contentLightbox.checkImageForLightbox(img); }); } }, 500); }); </script> Добавлено (2025-12-19, 13:09) --------------------------------------------- <!-- Lightbox для изображений в статьях --> <script> class ContentLightbox { constructor() { this.modal = null; this.modalImage = null; this.caption = null; this.images = []; this.currentIndex = 0; this.debug = true; this.initialized = false; this.init(); } init() { this.createModal(); if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', () => { setTimeout(() => this.setup(), 100); }); } else { setTimeout(() => this.setup(), 100); } } createModal() { this.modal = document.createElement('div'); this.modal.className = 'content-lightbox'; this.modal.setAttribute('aria-hidden', 'true'); this.modal.style.cssText = ` position: fixed; top: 0; left: 0; width: 100%; height: 100%; background-color: rgba(0, 0, 0, 0.45); display: flex; align-items: center; justify-content: center; z-index: 9999; opacity: 0; visibility: hidden; transition: opacity 0.3s ease, visibility 0.3s ease; backdrop-filter: blur(8px); `; const container = document.createElement('div'); container.className = 'content-lightbox__container'; container.style.cssText = ` position: relative; max-width: 95vw; max-height: 95vh; margin: 20px; `; const closeBtn = document.createElement('button'); closeBtn.className = 'content-lightbox__close'; closeBtn.innerHTML = '×'; closeBtn.setAttribute('aria-label', 'Закрыть'); closeBtn.style.cssText = ` position: absolute; top: -50px; right: 0; background: rgba(255, 255, 255, 0.15); border: none; color: white; font-size: 34px; cursor: pointer; width: 40px; height: 40px; border-radius: 50%; display: flex; align-items: center; justify-content: center; z-index: 10; transition: all 0.2s ease; backdrop-filter: blur(5px); padding: 0; margin: 0; line-height: 40px; `; closeBtn.addEventListener('mouseenter', () => { closeBtn.style.background = 'rgba(255, 255, 255, 0.25)'; closeBtn.style.transform = 'scale(1.1)'; }); closeBtn.addEventListener('mouseleave', () => { closeBtn.style.background = 'rgba(255, 255, 255, 0.15)'; closeBtn.style.transform = 'scale(1)'; }); closeBtn.addEventListener('click', (e) => { e.stopPropagation(); this.close(); }); this.modalImage = document.createElement('img'); this.modalImage.className = 'content-lightbox__image'; this.modalImage.style.cssText = ` display: block; max-width: 100%; max-height: 85vh; margin: 0 auto; border-radius: 4px; box-shadow: 0 25px 50px rgba(0, 0, 0, 0.5); cursor: default; transform: scale(0.96); opacity: 0; transition: transform 0.4s cubic-bezier(0.175, 0.885, 0.32, 1.275), opacity 0.4s ease; `; this.caption = document.createElement('div'); this.caption.className = 'content-lightbox__caption'; this.caption.style.cssText = ` text-align: center; color: rgba(255, 255, 255, 0.9); font-size: 15px; margin-top: 15px; padding: 8px 16px; background: rgba(255, 255, 255, 0.08); border-radius: 4px; backdrop-filter: blur(10px); opacity: 0; transform: translateY(10px); transition: opacity 0.4s ease, transform 0.4s ease; `; container.appendChild(closeBtn); container.appendChild(this.modalImage); container.appendChild(this.caption); this.modal.appendChild(container); document.body.appendChild(this.modal); this.modal.addEventListener('click', (e) => { if (e.target === this.modal) { this.close(); } }); document.addEventListener('keydown', (e) => { if (e.key === 'Escape' && this.isOpen()) { this.close(); } }); document.addEventListener('keydown', (e) => { if (!this.isOpen()) return; if (e.key === 'ArrowLeft') { e.preventDefault(); this.prev(); } else if (e.key === 'ArrowRight') { e.preventDefault(); this.next(); } }); } setup() { if (this.initialized) return; this.initialized = true; // ПРОВЕРКА: Только для страниц в директории /hugo/docs/ const currentPath = window.location.pathname.toLowerCase(); const isDocsPage = currentPath.includes('/hugo/docs/'); if (this.debug) { console.log('=== DEBUG LIGHTBOX ==='); console.log('URL:', currentPath); console.log('В директории /hugo/docs/:', isDocsPage); } // Если не в нужной директории - ВЫХОДИМ if (!isDocsPage) { if (this.debug) console.log('❌ Не в директории /hugo/docs/, lightbox отключен'); return; } // Находим основной контент const contentSelectors = [ 'main', 'article', '.content', '.post-content', '.article-content', '.entry-content', '.prose', '[role="main"]' ]; let contentArea = null; for (const selector of contentSelectors) { const element = document.querySelector(selector); if (element) { contentArea = element; break; } } if (!contentArea) { contentArea = document.body; } // Находим ВСЕ изображения в контенте const allImages = contentArea.querySelectorAll('img'); if (this.debug) { console.log('Всего изображений в контенте:', allImages.length); } // Фильтруем изображения const imagesForProcessing = Array.from(allImages).filter(img => { // Пропускаем явные иконки if (this.isIcon(img)) { if (this.debug) console.log('Пропущено (иконка):', img.src.substring(0, 80)); return false; } // Пропускаем изображения в шапке/футере/навигации const isInHeader = img.closest('header, .header, nav, .nav, .navbar, .menu, .navigation'); const isInFooter = img.closest('footer, .footer'); const isInSidebar = img.closest('aside, .sidebar, .aside'); if (isInHeader || isInFooter || isInSidebar) { if (this.debug) console.log('Пропущено (шапка/футер):', img.src.substring(0, 80)); return false; } return true; }); if (this.debug) { console.log('Изображений для обработки:', imagesForProcessing.length); } // Обрабатываем каждое изображение imagesForProcessing.forEach(img => { // Всегда добавляем базовые классы для стилей if (!img.classList.contains('content-image')) { img.classList.add('content-image'); } // Добавляем атрибуты для lightbox if (!img.hasAttribute('data-lightbox')) { img.setAttribute('data-lightbox', 'pending'); } if (!img.hasAttribute('data-original-src') && img.src) { img.setAttribute('data-original-src', img.src); } // Проверяем изображение this.checkImageForLightbox(img); }); // Дополнительные проверки setTimeout(() => { this.checkAllImages(imagesForProcessing); }, 1000); setTimeout(() => { this.finalImageCheck(imagesForProcessing); }, 3000); } checkImageForLightbox(img) { if (!img.complete) { img.addEventListener('load', () => { setTimeout(() => this.processImage(img), 100); }); img.addEventListener('error', () => { if (this.debug) console.log('❌ Ошибка загрузки:', img.src); this.markAsNotZoomable(img); }); return; } this.processImage(img); } processImage(img) { const naturalWidth = img.naturalWidth || 0; const naturalHeight = img.naturalHeight || 0; const displayedWidth = img.offsetWidth || img.width || 0; const displayedHeight = img.offsetHeight || img.height || 0; if (this.debug) { console.log('--- Проверка изображения ---'); console.log('Src:', img.src.substring(0, 100)); console.log('Natural:', naturalWidth, '×', naturalHeight); console.log('Displayed:', displayedWidth, '×', displayedHeight); } if (naturalWidth === 0 && displayedWidth === 0) { this.markAsNotZoomable(img); return false; } const MIN_WIDTH = 400; const MIN_HEIGHT = 150; const MIN_DISPLAYED_WIDTH = 300; const MIN_DISPLAYED_HEIGHT = 100; const naturalAspectRatio = naturalWidth / naturalHeight; const isNaturalHorizontal = naturalAspectRatio > 3; const isNaturalVeryHorizontal = naturalAspectRatio > 5; const isNaturalExtremelyHorizontal = naturalAspectRatio > 10; const displayedAspectRatio = displayedWidth / displayedHeight; const isDisplayedExtremelyHorizontal = displayedAspectRatio > 10; const compressionRatio = naturalWidth > 0 ? displayedWidth / naturalWidth : 1; const isCompressed = compressionRatio < 0.9; let isNaturalLargeEnough; if (isNaturalExtremelyHorizontal) { isNaturalLargeEnough = naturalWidth >= MIN_WIDTH * 1.2 && naturalHeight >= 50; } else if (isNaturalVeryHorizontal) { isNaturalLargeEnough = naturalWidth >= MIN_WIDTH * 1.5 && naturalHeight >= MIN_HEIGHT * 0.6; } else if (isNaturalHorizontal) { isNaturalLargeEnough = naturalWidth >= MIN_WIDTH && naturalHeight >= MIN_HEIGHT * 0.7; } else { isNaturalLargeEnough = naturalWidth >= MIN_WIDTH && naturalHeight >= MIN_HEIGHT; } let isDisplayedLargeEnough; if (isDisplayedExtremelyHorizontal) { isDisplayedLargeEnough = displayedWidth >= MIN_DISPLAYED_WIDTH * 1.5 && displayedHeight >= 40; } else if (isNaturalVeryHorizontal) { isDisplayedLargeEnough = displayedWidth >= MIN_DISPLAYED_WIDTH * 1.2 && displayedHeight >= MIN_DISPLAYED_HEIGHT * 0.8; } else if (isNaturalHorizontal) { isDisplayedLargeEnough = displayedWidth >= MIN_DISPLAYED_WIDTH && displayedHeight >= MIN_DISPLAYED_HEIGHT * 0.9; } else { isDisplayedLargeEnough = displayedWidth >= MIN_DISPLAYED_WIDTH && displayedHeight >= MIN_DISPLAYED_HEIGHT; } const isLargeEnough = isNaturalLargeEnough || (isCompressed && isDisplayedLargeEnough); if (!isLargeEnough) { if (this.debug) console.log('❌ Слишком маленькое для увеличения'); this.markAsNotZoomable(img); return false; } // Для документации всегда разрешаем, если достаточно большое const isNotFullyVisible = !this.isFullyVisible(img, displayedWidth, displayedHeight); if (!isNotFullyVisible) { if (this.debug) console.log('❌ Полностью видно на странице'); this.markAsNotZoomable(img); return false; } // Изображение подходит для lightbox if (!this.images.includes(img)) { this.images.push(img); } this.markAsZoomable(img); if (this.debug) { console.log('✅ Доступно для увеличения'); } return true; } markAsZoomable(img) { img.style.cursor = 'zoom-in'; img.classList.add('is-zoomable'); img.classList.remove('not-zoomable'); img.setAttribute('data-lightbox', 'true'); img.onclick = null; img.addEventListener('click', (e) => { e.preventDefault(); e.stopPropagation(); const filteredIndex = this.images.indexOf(img); if (filteredIndex !== -1) { this.currentIndex = filteredIndex; this.open(img.dataset.originalSrc || img.src, img.alt || img.title || ''); } }); img.addEventListener('mouseenter', () => { img.style.opacity = '0.92'; img.style.transform = 'translateY(-3px)'; }); img.addEventListener('mouseleave', () => { img.style.opacity = '1'; img.style.transform = 'translateY(0)'; }); } markAsNotZoomable(img) { img.style.cursor = 'default'; img.classList.add('not-zoomable'); img.classList.remove('is-zoomable'); img.onclick = null; img.setAttribute('data-lightbox', 'false'); } checkAllImages(images) { if (this.debug) { console.log('Проверка всех изображений через 1с...'); } images.forEach(img => { if (!img.classList.contains('is-zoomable') && !img.classList.contains('not-zoomable')) { this.checkImageForLightbox(img); } }); } finalImageCheck(images) { if (this.debug) { const zoomable = images.filter(img => img.classList.contains('is-zoomable')).length; const notZoomable = images.filter(img => img.classList.contains('not-zoomable')).length; const unprocessed = images.length - zoomable - notZoomable; console.log(`Итого: ${zoomable} можно увеличивать, ${notZoomable} нельзя, ${unprocessed} не обработано`); } images.forEach(img => { if (!img.classList.contains('is-zoomable') && !img.classList.contains('not-zoomable')) { this.markAsNotZoomable(img); } }); } isIcon(img) { const src = (img.src || '').toLowerCase(); const alt = (img.alt || '').toLowerCase(); const classes = (img.className || '').toLowerCase(); const iconKeywords = [ 'icon', 'logo', 'avatar', 'favicon', 'sprite', 'emoji', 'svg', 'ico', 'png-16', 'png-32', 'png-64', 'icon-', '-icon' ]; const naturalWidth = img.naturalWidth || img.offsetWidth || 0; const naturalHeight = img.naturalHeight || img.offsetHeight || 0; const isVerySmall = naturalWidth < 50 && naturalHeight < 50; const isSquareAndSmall = Math.abs(naturalWidth - naturalHeight) < 10 && naturalWidth < 100; const hasIconKeyword = iconKeywords.some(keyword => src.includes(keyword) || alt.includes(keyword) || classes.includes(keyword) ); return isVerySmall || (isSquareAndSmall && hasIconKeyword); } isFullyVisible(img, displayedWidth = null, displayedHeight = null) { const imgWidth = displayedWidth || img.offsetWidth || img.naturalWidth; const container = img.parentElement; if (!container) return false; const containerWidth = container.clientWidth; return imgWidth < containerWidth * 0.90; } open(src, caption) { if (this.debug) { console.log('Открываем lightbox:', src.substring(0, 100)); } this.modalImage.src = src; this.modalImage.alt = caption; this.caption.textContent = caption; this.modal.setAttribute('aria-hidden', 'false'); this.modal.style.visibility = 'visible'; requestAnimationFrame(() => { this.modal.style.opacity = '1'; setTimeout(() => { this.modalImage.style.opacity = '1'; this.modalImage.style.transform = 'scale(1)'; this.caption.style.opacity = '1'; this.caption.style.transform = 'translateY(0)'; }, 50); }); document.body.style.overflow = 'hidden'; document.documentElement.style.overflow = 'hidden'; } close() { this.modalImage.style.opacity = '0'; this.modalImage.style.transform = 'scale(0.96)'; this.caption.style.opacity = '0'; this.caption.style.transform = 'translateY(10px)'; setTimeout(() => { this.modal.style.opacity = '0'; this.modal.style.visibility = 'hidden'; this.modal.setAttribute('aria-hidden', 'true'); document.body.style.overflow = ''; document.documentElement.style.overflow = ''; }, 400); } isOpen() { return this.modal.getAttribute('aria-hidden') === 'false'; } next() { if (this.images.length > 1) { this.currentIndex = (this.currentIndex + 1) % this.images.length; const nextImg = this.images[this.currentIndex]; this.open(nextImg.dataset.originalSrc || nextImg.src, nextImg.alt || nextImg.title || ''); } } prev() { if (this.images.length > 1) { this.currentIndex = (this.currentIndex - 1 + this.images.length) % this.images.length; const prevImg = this.images[this.currentIndex]; this.open(prevImg.dataset.originalSrc || prevImg.src, prevImg.alt || prevImg.title || ''); } } }
// Инициализация document.addEventListener('DOMContentLoaded', () => { setTimeout(() => { window.contentLightbox = new ContentLightbox(); }, 100); const style = document.createElement('style'); style.textContent = ` /* Базовые стили для изображений в контенте (только в /hugo/docs/) */ body:has([href*="/hugo/docs/"]) .content-image, .content-image { max-width: 100%; height: auto; display: block; margin: 1.5rem auto; border-radius: 4px; transition: all 0.25s ease; } /* Стили ТОЛЬКО для изображений, которые можно увеличивать */ .content-image.is-zoomable { cursor: zoom-in; box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1); } .content-image.is-zoomable:hover { box-shadow: 0 8px 24px rgba(0, 0, 0, 0.15); opacity: 0.95; } /* Для изображений, которые нельзя увеличивать */ .content-image.not-zoomable { cursor: default; box-shadow: none; } .content-image.not-zoomable:hover { transform: none !important; opacity: 1 !important; box-shadow: none !important; } /* Подпись к изображениям */ .content-image__caption { text-align: center; font-size: 0.875rem; color: #666; margin-top: 0.5rem; line-height: 1.4; font-style: italic; } /* Обертка для изображений с подписью */ .content-image__wrapper { margin: 1.5rem 0; text-align: center; } /* Стили для lazy loading */ .content-image[loading="lazy"] { opacity: 0; transition: opacity 0.3s ease; } .content-image[loading="lazy"].loaded { opacity: 1; } /* Стили для мобильных устройств */ @media (max-width: 768px) { .content-image { margin: 1rem auto; border-radius: 3px; } .content-image.is-zoomable { box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); } .content-image.is-zoomable:hover { box-shadow: 0 4px 16px rgba(0, 0, 0, 0.12); } } /* Отключение hover на тач-устройствах */ @media (hover: none) { .content-image:hover { transform: none !important; } } `; document.head.appendChild(style); });
// Загрузка lazy images window.addEventListener('load', () => { const lazyImages = document.querySelectorAll('.content-image[loading="lazy"]'); lazyImages.forEach(img => { if (img.complete) { img.classList.add('loaded'); } else { img.addEventListener('load', () => { img.classList.add('loaded'); if (window.contentLightbox) { setTimeout(() => window.contentLightbox.checkImageForLightbox(img), 100); } }); } }); });
// Проверка при скролле window.addEventListener('scroll', () => { setTimeout(() => { if (window.contentLightbox) { const images = document.querySelectorAll('.content-image:not(.is-zoomable):not(.not-zoomable)'); images.forEach(img => { window.contentLightbox.checkImageForLightbox(img); }); } }, 500); }); </script>
|