/* globals GalleryYTPlayer, YT, GalleryVKPlayer, VK */ import { getScrollbarWidth } from '@advdominion/get-scrollbar-width'; const ESCAPE_MAP = { '&': '&', '<': '<', '>': '>', '"': '"', "'": ''', }; /* Экранирует HTML в значениях (сейчас — подписи), которые попадают в разметку через insertAdjacentHTML. Без этого подпись с тегами выполнится как разметка. Готового санитайзера с нужной поддержкой браузеров нет. */ const escapeHtml = (value) => String(value).replaceAll(/[&<>"']/g, (char) => ESCAPE_MAP[char]); const getYTPlayer = () => { const script = document.createElement('script'); script.src = 'https://www.youtube.com/player_api'; script.async = true; document.body.append(script); }; const getVKPlayer = () => { const script = document.createElement('script'); script.src = 'https://vk.com/js/api/videoplayer.js'; script.async = true; document.body.append(script); }; const playVideo = (slide) => { const youtube = slide.querySelector('.advdominion-lg__video_youtube'); const vk = slide.querySelector('.advdominion-lg__video_vk'); const rt = slide.querySelector('.advdominion-lg__video_rt'); if (youtube) { if (slide.YTPlayer) { if (slide.classList.contains('swiper-slide-active')) { slide.YTPlayer.playVideo(); } } else if (window.YT) { if (GalleryYTPlayer.state) { slide.YTPlayer = new YT.Player(youtube, { videoId: youtube.dataset.id, events: { onReady(event) { if (slide.classList.contains('swiper-slide-active')) { event.target.playVideo(); } }, }, }); } else { GalleryYTPlayer.addListener(() => { playVideo(slide); }); } } else { getYTPlayer(); GalleryYTPlayer.addListener(() => { playVideo(slide); }); } } else if (vk) { if (slide.VKPlayer) { if (slide.classList.contains('swiper-slide-active')) { slide.VKPlayer.play(); } } else if (window.VK?.VideoPlayer) { if (GalleryVKPlayer.state) { slide.VKPlayer = VK.VideoPlayer(vk); // oxlint-disable-line new-cap slide.VKPlayer.on('inited', () => { if (slide.classList.contains('swiper-slide-active')) { slide.VKPlayer.play(); } }); } else { GalleryVKPlayer.addListener(() => { playVideo(slide); }); } } else { getVKPlayer(); GalleryVKPlayer.addListener(() => { playVideo(slide); }); } } else if (rt) { /* RuTube не отдаёт объект плеера вроде YT.Player или VK.VideoPlayer — о готовности он сообщает через postMessage, а id готовых видео копятся в window.GalleryRTPlayer (см. слушатель ниже). Поэтому ждём появления своего id в этом наборе и только тогда отправляем команду. */ const videoId = /embed\/(\w+)/.exec(rt.getAttribute('src'))?.[1]; const checkInterval = setInterval(() => { if (window.GalleryRTPlayer) { if (window.GalleryRTPlayer.has(videoId)) { clearInterval(checkInterval); if (slide.classList.contains('swiper-slide-active')) { rt.contentWindow.postMessage( JSON.stringify({ type: 'player:play', data: {}, }), '*', ); } } } else { clearInterval(checkInterval); } }, 100); slide.checkInterval = checkInterval; } }; const pauseVideo = (slide) => { const youtube = slide.querySelector('.advdominion-lg__video_youtube'); const vk = slide.querySelector('.advdominion-lg__video_vk'); const rt = slide.querySelector('.advdominion-lg__video_rt'); if (youtube && slide.YTPlayer) { slide.YTPlayer.pauseVideo(); } else if (vk && slide.VKPlayer) { slide.VKPlayer.pause(); } else if (rt) { rt.contentWindow.postMessage( JSON.stringify({ type: 'player:pause', data: {}, }), '*', ); } }; const hide = async (duration = 500, easing = 'linear') => { const gallery = document.body.querySelector('.advdominion-lg'); await gallery.animate([{ opacity: 1 }, { opacity: 0 }], { duration, easing, }).finished; // Очищаем интервалы проверки для видео for (const slide of gallery.querySelectorAll('.advdominion-lg__item_video')) { if (slide.checkInterval) { clearInterval(slide.checkInterval); } } gallery.remove(); // Сбрасываем накопленные id готовых RuTube-плееров, чтобы следующее открытие начинало с чистого состояния. delete window.GalleryRTPlayer; document.body.style.overflow = ''; document.body.style.marginRight = ''; }; const show = async (duration = 500, easing = 'linear') => { const gallery = document.body.querySelector('.advdominion-lg'); document.body.style.overflow = 'hidden'; document.body.style.marginRight = `${getScrollbarWidth()}px`; await gallery.animate([{ opacity: 0 }, { opacity: 1 }], { duration, easing, }).finished; }; const init = async (items = [], options = {}, index = 0) => { // Вторая галерея поверх первой сломала бы закрытие: show/hide ищут только первый элемент .advdominion-lg. if (document.body.querySelector('.advdominion-lg')) { return; } // Один отменяемый контроллер на время жизни галереи: при закрытии снимает все её слушатели. const controller = new AbortController(); const [{ default: Swiper }, { Navigation, Pagination }] = await Promise.all([ import('swiper'), import('swiper/modules'), ]); const isVideoInGallery = { yt: false, vk: false, rt: false, }; const slides = items.map(({ url: item, caption }) => { if (/youtube\.com\/watch\?v=/.test(item)) { isVideoInGallery.yt = true; return `
`; } else if (/(?:vkvideo\.ru|vk\.com)\/video/.test(item)) { isVideoInGallery.vk = true; const videoId = /(?:vkvideo\.ru|vk\.com)\/video(-?\d+)_(\d+)/.exec(item); if (videoId === null) { return ` `; } return ` `; } else if (/rutube\.ru/.test(item)) { isVideoInGallery.rt = true; const videoId = /video\/(\w+)/.exec(item); if (videoId === null) { return ` `; } return ` `; } return ` `; }); /* GalleryYTPlayer и GalleryVKPlayer — простые накопители колбэков до готовности внешнего API. Пока плеер не загружен, playVideo регистрирует себя через addListener и будет вызван снова, когда init станет true. */ if (isVideoInGallery.yt && !window.GalleryYTPlayer) { window.GalleryYTPlayer = { state: false, get init() { return this.state; }, set init(val) { this.state = val; for (const i of this.callback) { i(val); } }, callback: [], addListener(callback) { this.callback.push(callback); }, }; /* YouTube вызывает onYouTubeIframeAPIReady только один раз, поэтому нельзя просто перезаписать его: если API уже загружен или колбэк занят другим кодом, он больше не сработает. Поэтому сохраняем прежний обработчик и явно отмечаем готовность, когда API уже присутствует. */ if (window.YT?.Player) { window.GalleryYTPlayer.init = true; } else { const prevOnYouTubeIframeAPIReady = window.onYouTubeIframeAPIReady; window.onYouTubeIframeAPIReady = () => { prevOnYouTubeIframeAPIReady?.(); window.GalleryYTPlayer.init = true; }; } } if (isVideoInGallery.vk && !window.GalleryVKPlayer) { window.GalleryVKPlayer = { state: false, get init() { return this.state; }, set init(val) { this.state = val; for (const i of this.callback) { i(val); } }, callback: [], addListener(callback) { this.callback.push(callback); }, }; // У VK нет события готовности API, поэтому проверяем его появление опросом. const checkInterval = setInterval(() => { if (window.VK?.VideoPlayer) { clearInterval(checkInterval); window.GalleryVKPlayer.init = true; } }, 100); } if (isVideoInGallery.rt) { window.GalleryRTPlayer = new Set(); window.addEventListener( 'message', (event) => { if (event.origin.includes('rutube.ru') && typeof event.data === 'string') { const message = JSON.parse(event.data); if (message.type === 'player:ready') { window.GalleryRTPlayer.add(message.data.videoId); } } }, { signal: controller.signal }, ); } document.body.insertAdjacentHTML( 'beforeend', `