From 4a418c8c2da58babd7b35725a8a7eef04ef901e0 Mon Sep 17 00:00:00 2001 From: Artem Kozynets Date: Thu, 7 Nov 2024 17:03:58 +0100 Subject: [PATCH] feat: add video --- blocks/video/video.css | 71 ++++++++++++++++++++ blocks/video/video.js | 145 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 216 insertions(+) create mode 100644 blocks/video/video.css create mode 100644 blocks/video/video.js diff --git a/blocks/video/video.css b/blocks/video/video.css new file mode 100644 index 0000000..09ee73c --- /dev/null +++ b/blocks/video/video.css @@ -0,0 +1,71 @@ +.video { + text-align: center; + max-width: 900px; + margin: 24px auto; +} + +.video[data-embed-loaded='false']:not(.placeholder) { + /* reserve an approximate space to avoid extensive layout shifts */ + aspect-ratio: 16 / 9; +} + +.video > div { + display: flex; + justify-content: center; +} + +.video video { + max-width: 100%; +} + +.video .video-placeholder { + width: 100%; + aspect-ratio: 16 / 9; + position: relative; +} + +.video .video-placeholder > * { + display: flex; + align-items: center; + justify-content: center; + position: absolute; + inset: 0; +} + +.video[data-embed-loaded='true'] .video-placeholder, +.video[data-embed-loaded='false'] .video-placeholder + * { + visibility: hidden; + height: 0; + width: 0; +} + +.video .video-placeholder picture img { + width: 100%; + height: 100%; + object-fit: cover; +} + +.video .video-placeholder-play button { + position: relative; + display: block; + width: 44px; + height: 44px; + border-radius: 50%; + outline: 2px solid; + padding: 0; +} + +.video .video-placeholder-play button::before { + content: ''; + display: block; + box-sizing: border-box; + position: absolute; + width: 0; + height: 24px; + border-top: 12px solid transparent; + border-bottom: 12px solid transparent; + border-left: 18px solid; + top: 50%; + left: calc(50% + 2px); + transform: translate(-50%, -50%); +} diff --git a/blocks/video/video.js b/blocks/video/video.js new file mode 100644 index 0000000..5cf24ec --- /dev/null +++ b/blocks/video/video.js @@ -0,0 +1,145 @@ +/* + * Video Block + * Show a video referenced by a link + * https://www.hlx.live/developer/block-collection/video + */ + +const prefersReducedMotion = window.matchMedia('(prefers-reduced-motion: reduce)'); + +function embedYoutube(url, autoplay, background) { + const usp = new URLSearchParams(url.search); + let suffix = ''; + if (background || autoplay) { + const suffixParams = { + autoplay: autoplay ? '1' : '0', + mute: background ? '1' : '0', + controls: background ? '0' : '1', + disablekb: background ? '1' : '0', + loop: background ? '1' : '0', + playsinline: background ? '1' : '0', + }; + suffix = `&${Object.entries(suffixParams).map(([k, v]) => `${k}=${encodeURIComponent(v)}`).join('&')}`; + } + let vid = usp.get('v') ? encodeURIComponent(usp.get('v')) : ''; + const embed = url.pathname; + if (url.origin.includes('youtu.be')) { + [, vid] = url.pathname.split('/'); + } + + const temp = document.createElement('div'); + temp.innerHTML = `
+ +
`; + return temp.children.item(0); +} + +function embedVimeo(url, autoplay, background) { + const [, video] = url.pathname.split('/'); + let suffix = ''; + if (background || autoplay) { + const suffixParams = { + autoplay: autoplay ? '1' : '0', + background: background ? '1' : '0', + }; + suffix = `?${Object.entries(suffixParams).map(([k, v]) => `${k}=${encodeURIComponent(v)}`).join('&')}`; + } + const temp = document.createElement('div'); + temp.innerHTML = `
+ +
`; + return temp.children.item(0); +} + +function getVideoElement(source, autoplay, background) { + const video = document.createElement('video'); + video.setAttribute('controls', ''); + if (autoplay) video.setAttribute('autoplay', ''); + if (background) { + video.setAttribute('loop', ''); + video.setAttribute('playsinline', ''); + video.removeAttribute('controls'); + video.addEventListener('canplay', () => { + video.muted = true; + if (autoplay) video.play(); + }); + } + + const sourceEl = document.createElement('source'); + sourceEl.setAttribute('src', source); + sourceEl.setAttribute('type', `video/${source.split('.').pop()}`); + video.append(sourceEl); + + return video; +} + +const loadVideoEmbed = (block, link, autoplay, background) => { + if (block.dataset.embedLoaded === 'true') { + return; + } + const url = new URL(link); + + const isYoutube = link.includes('youtube') || link.includes('youtu.be'); + const isVimeo = link.includes('vimeo'); + + if (isYoutube) { + const embedWrapper = embedYoutube(url, autoplay, background); + block.append(embedWrapper); + embedWrapper.querySelector('iframe').addEventListener('load', () => { + block.dataset.embedLoaded = true; + }); + } else if (isVimeo) { + const embedWrapper = embedVimeo(url, autoplay, background); + block.append(embedWrapper); + embedWrapper.querySelector('iframe').addEventListener('load', () => { + block.dataset.embedLoaded = true; + }); + } else { + const videoEl = getVideoElement(link, autoplay, background); + block.append(videoEl); + videoEl.addEventListener('canplay', () => { + block.dataset.embedLoaded = true; + }); + } +}; + +export default async function decorate(block) { + const placeholder = block.querySelector('picture'); + const link = block.querySelector('a').href; + block.textContent = ''; + block.dataset.embedLoaded = false; + + const autoplay = block.classList.contains('autoplay'); + if (placeholder) { + block.classList.add('placeholder'); + const wrapper = document.createElement('div'); + wrapper.className = 'video-placeholder'; + wrapper.append(placeholder); + + if (!autoplay) { + wrapper.insertAdjacentHTML( + 'beforeend', + '
', + ); + wrapper.addEventListener('click', () => { + wrapper.remove(); + loadVideoEmbed(block, link, true, false); + }); + } + block.append(wrapper); + } + + if (!placeholder || autoplay) { + const observer = new IntersectionObserver((entries) => { + if (entries.some((e) => e.isIntersecting)) { + observer.disconnect(); + const playOnLoad = autoplay && !prefersReducedMotion.matches; + loadVideoEmbed(block, link, playOnLoad, autoplay); + } + }); + observer.observe(block); + } +}