1// !IMPORTANT: This JS file is loaded as module in browser 2 3// TODO: use --mobile-bp instead 4const MOBILE_BREAKPOINT = 768; 5 6document.addEventListener('DOMContentLoaded', () => { 7 document.querySelectorAll('.nblock-video').forEach((block) => { 8 window.lozad 9 ? window 10 .lozad(block, { load: () => Main.initBlock(block) }) 11 .observe() 12 : Main.initBlock(block); 13 }); 14}); 15 16class Main { 17 static initBlock(block) { 18 if (block._initialised) { 19 return; 20 } 21 block._initialised = true; 22 block.classList.remove('nblock-video--playing'); 23 24 block._jsData = Utils.normalizeJsData(block.dataset['js_data']); 25 26 Main.setPlayer(block); 27 Main.onResizeInit(block); 28 29 if (!block._activeData) { 30 console.error('Video block: video URL not set'); // eslint-disable-line no-console 31 return; 32 } 33 34 Main.rotatePlayButtonOnScroll(block); 35 Main.playButtonInit(block); 36 37 // For autoplayLoop file videos, hide the cover immediately. 38 if ( 39 block._activeData.autoplayLoop && 40 block._activeData.provider === 'file' 41 ) { 42 const overlay = block.querySelector('.nblock-video__cover'); 43 if (overlay) overlay.classList.add('nblock-video__cover--hidden'); 44 block.classList.add('nblock-video--playing'); 45 } 46 } 47 48 static setPlayer(block) { 49 const data = block._jsData; 50 51 const deviceVideoData = Utils.getDeviceVideoData(data); 52 if (!deviceVideoData.embed_url) { 53 return; 54 } 55 56 const muted = !!data.muted; 57 const autoplayLoop = !!data.autoplayLoop; 58 const finalEmbedUrl = VideoBlock.addUrlParams( 59 deviceVideoData.embed_url, 60 deviceVideoData.provider, 61 { muted }, 62 ); 63 64 block._activeData = { 65 provider: deviceVideoData.provider, 66 embed_url: finalEmbedUrl, 67 muted, 68 autoplayLoop, 69 }; 70 71 const wrapper = block.querySelector('.nblock-video__player'); 72 73 const player = 74 Utils._getValidPlayer({ wrapper, videoData: deviceVideoData }) || 75 Utils._createPlayer({ 76 wrapper, 77 videoData: deviceVideoData, 78 finalEmbedUrl, 79 muted, 80 autoplayLoop, 81 }); 82 83 if (deviceVideoData.provider === 'vimeo') { 84 Iframer.enforceVimeoIframeVolume({ iframe: player, muted }); 85 } 86 } 87 88 static onResizeInit(block) { 89 block._isMobileSaved = Utils.isMobile(); 90 window.addEventListener('resize', () => { 91 if (Utils.isMobile() !== block._isMobileSaved) { 92 block._isMobileSaved = Utils.isMobile(); 93 Main.setPlayer(block); 94 } 95 }); 96 } 97 98 static playButtonInit(block) { 99 const overlay = block.querySelector('.nblock-video__cover'); 100 const overlayInteractiveSelector = [ 101 '.nblock-video__content a[href]', 102 '.nblock-video__content button', 103 '.nblock-video__content input', 104 '.nblock-video__content textarea', 105 '.nblock-video__content select', 106 '.nblock-video__content [tabindex]:not([tabindex="-1"])', 107 '.nblock-video__content [role="button"]', 108 ].join(', '); 109 110 overlay.addEventListener('click', (event) => { 111 if (event.target.closest(overlayInteractiveSelector)) { 112 return; 113 } 114 115 event.preventDefault(); 116 Main.onPlayButtonClick(block); 117 }); 118 } 119 120 static rotatePlayButtonOnScroll(block) { 121 const playEl = block.querySelector('.js-vimeo-content-play-text'); 122 if (!playEl) { 123 return; 124 } 125 126 const rotate = () => { 127 const rotation = window.pageYOffset || window.scrollY || 0; 128 playEl.style.transform = `rotate(${rotation}deg)`; 129 }; 130 131 rotate(); 132 document.addEventListener('scroll', rotate); 133 } 134 135 static onPlayButtonClick(block) { 136 if (block._isPlaying) { 137 return; 138 } 139 140 const playerWrapper = block.querySelector('.nblock-video__player'); 141 const targetContainer = playerWrapper || block; 142 const activePlayer = targetContainer.querySelector('iframe, video'); 143 144 if (!activePlayer) { 145 console.warn('[nblock-video] Missing pre-rendered player element'); // eslint-disable-line no-console 146 return; 147 } 148 block._isPlaying = true; 149 150 if (typeof activePlayer.play === 'function') { 151 activePlayer.play(); 152 } else { 153 const fallbackVariant = Utils.getDeviceVideoData(block._jsData); 154 const active = block._activeData || { 155 provider: fallbackVariant.provider, 156 embed_url: fallbackVariant.embed_url, 157 muted: Boolean(block._jsData?.muted), 158 }; 159 const provider = active.provider; 160 161 Iframer.startIframePlayback({ 162 iframe: activePlayer, 163 provider, 164 muted: Boolean(active.muted), 165 }); 166 } 167
168 const overlay = block.querySelector('.nblock-video__cover'); 169 if (overlay) { 170 overlay.classList.add('nblock-video__cover--hidden'); 171 } 172 block.classList.add('nblock-video--playing'); 173 const playButton = block.querySelector('.nblock-video__play'); 174 if (playButton) { 175 playButton.setAttribute('aria-hidden', 'true'); 176 } 177 } 178} 179 180class Utils { 181 static _getValidPlayer({ wrapper, videoData }) { 182 let player = wrapper.querySelector('iframe, video'); 183 184 const isFile = videoData.provider === 'file'; 185 186 return player && 187 ((isFile && player.tagName === 'VIDEO') || 188 (!isFile && player.tagName === 'IFRAME')) 189 ? player 190 : null; 191 } 192 193 static _createPlayer({ 194 wrapper, 195 videoData, 196 finalEmbedUrl, 197 muted, 198 autoplayLoop, 199 }) { 200 const playerHtml = VideoBlock.getPlayerHtml({ 201 provider: videoData.provider, 202 embedUrl: finalEmbedUrl, 203 attr: { muted, autoplayLoop }, 204 }); 205 206 const player = Utils._createEl(playerHtml); 207 208 wrapper.innerHTML = ''; // Clear existing content 209 wrapper.appendChild(player); 210 211 return player; 212 } 213 214 static getDeviceVideoData(data) { 215 const useMobile = 216 Utils.isMobile() && data.mobile && data.mobile.embed_url; 217 const preferred = useMobile ? data.mobile : data.desktop; 218 219 if (preferred && preferred.embed_url) { 220 return preferred; 221 } 222 if (data.mobile && data.mobile.embed_url) { 223 return data.mobile; 224 } 225 if (data.desktop && data.desktop.embed_url) { 226 return data.desktop; 227 } 228 229 return { provider: '', embed_url: '' }; 230 } 231 232 static isMobile() { 233 return window.innerWidth <= MOBILE_BREAKPOINT; 234 } 235 236 static normalizeJsData(raw) { 237 let parsed = {}; 238 try { 239 parsed = raw ? JSON.parse(raw) : {}; 240 } catch (error) { 241 parsed = {}; 242 } 243 244 const desktop = parsed.desktop || {}; 245 const mobile = parsed.mobile || {}; 246 247 return { 248 desktop: { 249 provider: desktop.provider || '', 250 embed_url: desktop.embed_url || '', 251 }, 252 mobile: { 253 provider: mobile.provider || '', 254 embed_url: mobile.embed_url || '', 255 }, 256 muted: parsed.muted, 257 autoplayLoop: parsed.autoplayLoop, 258 }; 259 } 260 261 static _createEl(html) { 262 const temp = document.createElement('div'); 263 temp.innerHTML = html; 264 return temp.firstElementChild; 265 } 266} 267 268export default class VideoBlock { 269 static addUrlParams(embedUrl, provider, options = {}) { 270 if (!embedUrl) { 271 return ''; 272 } 273 274 let params = {}; 275 276 if (provider === 'youtube') { 277 params = { 278 mute: options.muted ? '1' : '0', 279 rel: '0', 280 modestbranding: '1', 281 enablejsapi: '1', 282 }; 283 } 284 285 if (provider === 'vimeo') { 286 params = { 287 playsinline: '1', 288 muted: options.muted ? '1' : '0', 289 title: '0', 290 byline: '0', 291 portrait: '0', 292 api: '1', 293 background: '0', 294 }; 295 } 296 297 const url = new URL(embedUrl); 298 for (const [k, v] of Object.entries(params)) url.searchParams.set(k, v); 299 300 return url.href; 301 } 302 303 static getPlayerHtml({ provider, embedUrl, attr }) { 304 if (provider === 'file') { 305 const autoplayLoop = attr.autoplayLoop; 306 const muted = attr.muted; 307 return ` 308 <video 309 class="nblock-video__video" 310 src="${embedUrl}" 311 ${autoplayLoop ? '' : 'controls'} 312 playsinline 313 preload="${autoplayLoop ? 'auto' : 'metadata'}" 314 ${muted || autoplayLoop ? 'muted' : ''} 315 ${autoplayLoop ? 'autoplay loop' : ''} 316 ></video> 317 `; 318 } 319 320 const permissions = 321 provider === 'youtube' 322 ? 'accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share' 323 : 'autoplay; fullscreen; picture-in-picture'; 324 325 let title = 'Video player'; 326 if (provider === 'youtube') title = 'YouTube video player'; 327 if (provider === 'vimeo') title = 'Vimeo video player'; 328 329 return ` 330 <iframe 331 class="nblock-video__iframe" 332 src="${embedUrl}" 333 title="${title}" 334 loading="lazy" 335 referrerpolicy="strict-origin-when-cross-origin" 336 allow="${permissions}" 337 ></iframe> 338 `; 339 } 340} 341 342class Iframer { 343 static startIframePlayback({ iframe, provider }) { 344 if (!iframe || !iframe.contentWindow) { 345 return; 346 } 347 348 try { 349 if (provider === 'youtube') { 350 iframe.contentWindow.postMessage( 351 JSON.stringify({ 352 event: 'command', 353 func: 'playVideo', 354 args: [], 355 }), 356 '*', 357 ); 358 return; 359 } 360 361 if (provider === 'vimeo') { 362 iframe.contentWindow.postMessage( 363 JSON.stringify({ method: 'play' }), 364 '*', 365 ); 366 return; 367 } 368 } catch (error) { 369 console.warn( 370 '[nblock-video] Unable to trigger iframe playback via API', 371 error, 372 ); // eslint-disable-line no-console 373 } 374 } 375 376 // Vimeo renders muted by default, so we enforce it 377 static enforceVimeoIframeVolume({ iframe, muted }) { 378 const sendVolume = () => { 379 if (!iframe.contentWindow) { 380 return; 381 } 382 383 try { 384 iframe.contentWindow.postMessage( 385 JSON.stringify({ 386 method: 'setVolume', 387 value: muted ? 0 : 1, 388 }), 389 '*', 390 ); 391 } catch (error) { 392 console.warn( 393 '[nblock-video] Unable to set Vimeo volume', 394 error, 395 ); // eslint-disable-line no-console 396 } 397 }; 398 399 if (iframe._volumeEnforcer) { 400 iframe.removeEventListener('load', iframe._volumeEnforcer); 401 } 402 403 iframe._volumeEnforcer = () => sendVolume(); 404 iframe.addEventListener('load', iframe._volumeEnforcer); 405 } 406}
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.