@@ -2,22 +2,38 @@
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 ) ;
} ,
getVKPlayer = ( ) => {
} ;
const 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 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 ) {
@@ -54,7 +70,7 @@ const getYTPlayer = () => {
}
} else if ( window . VK ? . VideoPlayer ) {
if ( GalleryVKPlayer . state ) {
slide . VKPlayer = VK . VideoPlayer ( vk ) ;
slide . VKPlayer = VK . VideoPlayer ( vk ) ; // eslint-disable-line new-cap
slide . VKPlayer . on ( 'inited' , ( ) => {
if ( slide . classList . contains ( 'swiper-slide-active' ) ) {
slide . VKPlayer . play ( ) ;
@@ -72,8 +88,13 @@ const getYTPlayer = () => {
} ) ;
}
} else if ( rt ) {
const videoId = /embed\/(\w+)/ . exec ( rt . getAttribute ( 'src' ) ) ? . [ 1 ] ,
checkInterval = setInterval ( ( ) => {
/*
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 ) ;
@@ -93,11 +114,12 @@ const getYTPlayer = () => {
} , 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 ' ) ;
} ;
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 ( ) ;
@@ -112,24 +134,9 @@ const getYTPlayer = () => {
'*' ,
) ;
}
} ,
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 hide = async ( duration = 500 , easing = 'linear' ) => {
const gallery = document . body . querySelector ( '.advdominion-lg' ) ;
await gallery . animate ( [ { opacity : 1 } , { opacity : 0 } ] , {
@@ -145,41 +152,47 @@ const getYTPlayer = () => {
}
gallery . remove ( ) ;
// Сбрасываем накопленные id готовых RuTube-плееров, чтобы следующее открытие начинало с чистого состояния.
delete window . GalleryRTPlayer ;
document . body . style . cssText = `
overflow: '';
margin-right: '';
` ;
} ,
show = async ( duration = 500 , easing = 'linear' ) => {
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 . cssText = `
overflow: hidden;
margin-right: ${ getScrollbarWidth ( ) } px
` ;
document . body . style . overflow = 'hidden' ;
document . body . style . marginRight = ` ${ getScrollbarWidth ( ) } px ` ;
await gallery . animate ( [ { opacity : 0 } , { opacity : 1 } ] , {
duration ,
easing ,
} ) . finished ;
} ,
init = async ( items = [ ] , options = { } , index = 0 ) => {
} ;
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' ) ,
] ) ,
isVideoInGallery = {
] ) ;
const isVideoInGallery = {
yt : false ,
vk : false ,
rt : false ,
} ;
items = items . map ( ( { url : item , caption } ) => {
const slides = items . map ( ( { url : item , caption } ) => {
if ( /youtube\.com\/watch\?v=/ . test ( item ) ) {
isVideoInGallery . yt = true ;
return `
<div class="advdominion-lg__item advdominion-lg__item_video swiper-slide">
<div class="advdominion-lg__video-area">
<div class="advdominion-lg__video-wrapper">
<div
class="advdominion-lg__video advdominion-lg__video_youtube"
@@ -187,7 +200,8 @@ const getYTPlayer = () => {
Загрузка YouTube-плеера...
</div>
</div>
${ caption ? ` <div class="advdominion-lg__caption"> ${ caption } </div> ` : '' }
</div>
${ caption ? ` <div class="advdominion-lg__caption"> ${ escapeHtml ( caption ) } </div> ` : '' }
</div>
` ;
} else if ( /(?:vkvideo\.ru|vk\.com)\/video/ . test ( item ) ) {
@@ -196,17 +210,20 @@ const getYTPlayer = () => {
if ( videoId === null ) {
return `
<div class="advdominion-lg__item advdominion-lg__item_video swiper-slide">
<div class="advdominion-lg__video-area">
<div class="advdominion-lg__video-wrapper">
<div class="advdominion-lg__video advdominion-lg__video_vk">
Ошибка при загрузке VK-плеера
</div>
</div>
${ caption ? ` <div class="advdominion-lg__caption"> ${ caption } </div> ` : '' }
</div>
${ caption ? ` <div class="advdominion-lg__caption"> ${ escapeHtml ( caption ) } </div> ` : '' }
</div>
` ;
}
return `
<div class="advdominion-lg__item advdominion-lg__item_video swiper-slide">
<div class="advdominion-lg__video-area">
<div class="advdominion-lg__video-wrapper">
<iframe
class="advdominion-lg__video advdominion-lg__video_vk"
@@ -214,7 +231,8 @@ const getYTPlayer = () => {
allow="autoplay; encrypted-media; fullscreen; picture-in-picture; screen-wake-lock;">
</iframe>
</div>
${ caption ? ` <div class="advdominion-lg__caption"> ${ caption } </div> ` : '' }
</div>
${ caption ? ` <div class="advdominion-lg__caption"> ${ escapeHtml ( caption ) } </div> ` : '' }
</div>
` ;
} else if ( /rutube\.ru/ . test ( item ) ) {
@@ -223,17 +241,20 @@ const getYTPlayer = () => {
if ( videoId === null ) {
return `
<div class="advdominion-lg__item advdominion-lg__item_video swiper-slide">
<div class="advdominion-lg__video-area">
<div class="advdominion-lg__video-wrapper">
<div class="advdominion-lg__video advdominion-lg__video_rt">
Ошибка при загрузке RuTube-плеера
</div>
</div>
${ caption ? ` <div class="advdominion-lg__caption"> ${ caption } </div> ` : '' }
</div>
${ caption ? ` <div class="advdominion-lg__caption"> ${ escapeHtml ( caption ) } </div> ` : '' }
</div>
` ;
}
return `
<div class="advdominion-lg__item advdominion-lg__item_video swiper-slide">
<div class="advdominion-lg__video-area">
<div class="advdominion-lg__video-wrapper">
<iframe
class="advdominion-lg__video advdominion-lg__video_rt"
@@ -241,18 +262,24 @@ const getYTPlayer = () => {
allow="autoplay; encrypted-media; fullscreen; picture-in-picture; screen-wake-lock;">
</iframe>
</div>
${ caption ? ` <div class="advdominion-lg__caption"> ${ caption } </div> ` : '' }
</div>
${ caption ? ` <div class="advdominion-lg__caption"> ${ escapeHtml ( caption ) } </div> ` : '' }
</div>
` ;
}
return `
<div class="advdominion-lg__item swiper-slide">
<img class="advdominion-lg__image" src=" ${ item } " alt="" loading="lazy">
${ caption ? ` <div class="advdominion-lg__caption"> ${ caption } </div> ` : '' }
${ caption ? ` <div class="advdominion-lg__caption"> ${ escapeHtml ( caption ) } </div> ` : '' }
</div>
` ;
} ) ;
/*
GalleryYTPlayer и GalleryVKPlayer — простые накопители колбэков до готовности внешнего API.
Пока плеер не загружен, playVideo регистрирует себя через addListener и будет вызван снова,
когда init станет true.
*/
if ( isVideoInGallery . yt && ! window . GalleryYTPlayer ) {
window . GalleryYTPlayer = {
state : false ,
@@ -271,10 +298,21 @@ const getYTPlayer = () => {
} ,
} ;
/*
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 = {
@@ -294,6 +332,7 @@ const getYTPlayer = () => {
} ,
} ;
// У VK нет события готовности API, поэтому проверяем его появление опросом.
const checkInterval = setInterval ( ( ) => {
if ( window . VK ? . VideoPlayer ) {
clearInterval ( checkInterval ) ;
@@ -304,14 +343,18 @@ const getYTPlayer = () => {
if ( isVideoInGallery . rt ) {
window . GalleryRTPlayer = new Set ( ) ;
window . addEventListener ( 'message' , ( event ) => {
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 (
@@ -323,7 +366,7 @@ const getYTPlayer = () => {
</button>
<div class="advdominion-lg__container swiper">
<div class="advdominion-lg__wrapper swiper-wrapper">
${ items . join ( '' ) }
${ slides . join ( '' ) }
</div>
${ options . pagination || '' }
${ options . navigation || '' }
@@ -355,10 +398,6 @@ const getYTPlayer = () => {
if ( this . slides [ this . activeIndex ] . classList . contains ( 'advdominion-lg__item_video' ) ) {
playVideo ( this . slides [ this . activeIndex ] ) ;
}
if ( isVideoInGallery . yt || isVideoInGallery . vk || isVideoInGallery . rt ) {
setVideoSize ( gallery ) ;
}
} ) ;
swiper . on ( 'slideChange' , function init ( ) {
@@ -375,30 +414,28 @@ const getYTPlayer = () => {
} , 0 ) ;
} ) ;
swiper . on ( 'resize' , ( ) => {
if ( isVideoInGallery . yt || isVideoInGallery . vk || isVideoInGallery . rt ) {
setVideoSize ( gallery ) ;
}
} ) ;
swiper . init ( ) ;
const closeHandler = ( ) => {
hide ( options ? . animation ? . hide ? . duration , options ? . animation ? . hide ? . easing ) ;
} ,
keydownHandler = ( e ) => {
controller . abort ( ) ;
} ;
const keydownHandler = ( e ) => {
if ( e . key === 'Escape' ) {
closeHandler ( ) ;
document . body . removeEventListener ( 'keydown' , keydownHandler ) ;
}
} ;
gallery . querySelector ( '.advdominion-lg__close' ) . addEventListener ( 'click' , closeHandler ) ;
document . body . addEventListener ( 'keydown' , keydownHandler ) ;
document . body . addEventListener ( 'keydown' , keydownHandler , { signal : controller . signal } ) ;
} ;
export const gallery = ( container , options ) => {
const items = [ ] ;
/*
Подпись берётся из элемента, на который указывает data-gallery-caption у ссылки,
поэтому одна и та же подпись не дублируется в разметке галереи.
*/
if ( options . single ) {
const element = container . children [ 0 ] ;
items . push ( {