diff --git a/packages/lightbox-gallery/index.js b/packages/lightbox-gallery/index.js index 1755b56..f40408a 100644 --- a/packages/lightbox-gallery/index.js +++ b/packages/lightbox-gallery/index.js @@ -2,403 +2,440 @@ import { getScrollbarWidth } from '@advdominion/get-scrollbar-width'; -const getYTPlayer = () => { - const script = document.createElement('script'); - script.src = 'https://www.youtube.com/player_api'; - script.async = true; - document.body.append(script); - }, - getVKPlayer = () => { - const script = document.createElement('script'); - script.src = 'https://vk.com/js/api/videoplayer.js'; - script.async = true; - document.body.append(script); - }, - playVideo = (slide) => { - const youtube = slide.querySelector('.advdominion-lg__video_youtube'), - vk = slide.querySelector('.advdominion-lg__video_vk'), - rt = slide.querySelector('.advdominion-lg__video_rt'); +const ESCAPE_MAP = { + '&': '&', + '<': '<', + '>': '>', + '"': '"', + "'": ''', +}; - 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(); - } - }, +/* + Экранирует 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); - slide.VKPlayer.on('inited', () => { - if (slide.classList.contains('swiper-slide-active')) { - slide.VKPlayer.play(); - } - }); - } else { - GalleryVKPlayer.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); // eslint-disable-line new-cap + slide.VKPlayer.on('inited', () => { + if (slide.classList.contains('swiper-slide-active')) { + slide.VKPlayer.play(); + } + }); } else { - getVKPlayer(); GalleryVKPlayer.addListener(() => { playVideo(slide); }); } - } else if (rt) { - const videoId = /embed\/(\w+)/.exec(rt.getAttribute('src'))?.[1], - 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); + } 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: {}, + }), + '*', + ); } - }, 100); - slide.checkInterval = checkInterval; - } - }, - pauseVideo = (slide) => { - const youtube = slide.querySelector('.advdominion-lg__video_youtube'), - vk = slide.querySelector('.advdominion-lg__video_vk'), - 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: {}, - }), - '*', - ); - } - }, - setVideoSize = (gallery) => { - const items = gallery.querySelectorAll('.advdominion-lg__video-wrapper'); - items[0].style.cssText = ''; - let height = (items[0].getBoundingClientRect().width * 9) / 16, - width = ''; - if (height > items[0].getBoundingClientRect().height) { - height = items[0].getBoundingClientRect().height; - width = `${(height * 16) / 9}px`; - } - for (const item of items) { - item.style.cssText = ` - height: ${height}px; - width: ${width}; - `; - } - }, - 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); + } + } 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(); - delete window.GalleryRTPlayer; + gallery.remove(); + // Сбрасываем накопленные id готовых RuTube-плееров, чтобы следующее открытие начинало с чистого состояния. + delete window.GalleryRTPlayer; - document.body.style.cssText = ` - overflow: ''; - margin-right: ''; - `; - }, - show = async (duration = 500, easing = 'linear') => { - const gallery = document.body.querySelector('.advdominion-lg'); + document.body.style.overflow = ''; + document.body.style.marginRight = ''; +}; - document.body.style.cssText = ` - overflow: hidden; - margin-right: ${getScrollbarWidth()}px - `; +const show = async (duration = 500, easing = 'linear') => { + const gallery = document.body.querySelector('.advdominion-lg'); - await gallery.animate([{ opacity: 0 }, { opacity: 1 }], { - duration, - easing, - }).finished; - }, - init = async (items = [], options = {}, index = 0) => { - const [{ default: Swiper }, { Navigation, Pagination }] = await Promise.all([ - import('swiper'), - import('swiper/modules'), - ]), - isVideoInGallery = { - yt: false, - vk: false, - rt: false, - }; - items = items.map(({ url: item, caption }) => { - if (/youtube\.com\/watch\?v=/.test(item)) { - isVideoInGallery.yt = true; - return ` + 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 ` -