From 402d660c723cd0af036415586fd89c99a6ae290c Mon Sep 17 00:00:00 2001 From: fkakatie Date: Fri, 26 Jun 2026 16:31:53 -0600 Subject: [PATCH 1/3] feat: play icon --- icons/play.svg | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 icons/play.svg diff --git a/icons/play.svg b/icons/play.svg new file mode 100644 index 0000000..e086544 --- /dev/null +++ b/icons/play.svg @@ -0,0 +1,4 @@ + + + + From f73f47b5b44177e85550f78530cecfa111a750f0 Mon Sep 17 00:00:00 2001 From: fkakatie Date: Fri, 26 Jun 2026 16:32:52 -0600 Subject: [PATCH 2/3] feat: video block --- blocks/video/video.css | 38 ++++++++++++++ blocks/video/video.js | 115 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 153 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..6e34135 --- /dev/null +++ b/blocks/video/video.css @@ -0,0 +1,38 @@ +.video { + display: block; + position: relative; + margin: 0 auto; + aspect-ratio: 16 / 9; +} + +.video iframe { + height: 100%; + width: 100%; + border: none; + border-radius: 0; +} + +.video .placeholder { + position: absolute; + inset: 0; + margin: 0; + cursor: pointer; +} + +.video .placeholder img { + height: 100%; + width: 100%; + object-fit: cover; +} + +.video .placeholder .icon { + position: absolute; + top: 50%; + left: 50%; + height: 80px; + width: 80px; + color: var(--color-white); + filter: drop-shadow(0 1px 5px rgb(0 0 0 / 60%)); + transform: translate(-50%, -50%); + pointer-events: none; +} diff --git a/blocks/video/video.js b/blocks/video/video.js new file mode 100644 index 0000000..07dab7c --- /dev/null +++ b/blocks/video/video.js @@ -0,0 +1,115 @@ +import { decorateIcons } from '../../scripts/aem.js'; + +/** + * Returns the video provider name for a supported URL. + * @param {string} url - the video page URL to inspect + * @returns {string|null} provider name (e.g. 'youtube') + */ +function detectType(url) { + const { hostname } = new URL(url); + if (hostname === 'youtu.be' || hostname.endsWith('youtube.com')) return 'youtube'; + return null; +} + +/** + * Builds a YouTube iframe. + * @param {string} url - the video page or short URL to parse + * @returns {HTMLIFrameElement|null} configured embed iframe + */ +function createYouTubeEmbed(url) { + const { hostname, pathname, searchParams } = new URL(url); + const id = hostname === 'youtu.be' ? pathname.slice(1) : searchParams.get('v'); + if (!id) return null; + + const iframe = document.createElement('iframe'); + iframe.src = `https://www.youtube.com/embed/${id}`; + iframe.setAttribute('allowfullscreen', ''); + iframe.setAttribute('allow', 'autoplay; encrypted-media; picture-in-picture'); + iframe.title = 'YouTube video'; // TODO: localization + return iframe; +} + +/** + * Builds an embed iframe for the given provider type and URL. + * @param {string} type - provider name returned by detectType + * @param {string} url - the video URL to embed + * @returns {HTMLIFrameElement|null} embed iframe + */ +function createEmbed(type, url) { + if (type === 'youtube') return createYouTubeEmbed(url); + return null; +} + +/** + * Builds a thumbnail overlay. + * @param {HTMLElement} block - block element containing the authored thumbnail image + * @param {HTMLIFrameElement} embed - video iframe + * @returns {HTMLElement|null} the placeholder figure + */ +function createPlaceholder(block, embed) { + const image = block.querySelector('picture, img'); + if (!image) return null; + + const figure = document.createElement('figure'); + figure.classList.add('placeholder'); + figure.setAttribute('role', 'button'); + figure.setAttribute('tabindex', '0'); + figure.append(image); + + const icon = document.createElement('span'); + icon.className = 'icon icon-play'; + figure.append(icon); + decorateIcons(figure); + + function play() { + const src = new URL(embed.src); + src.searchParams.set('autoplay', 1); + embed.src = src.href; + figure.remove(); + } + + figure.addEventListener('click', play); + figure.addEventListener('keydown', (e) => { + if (e.key === 'Enter' || e.key === ' ') { + e.preventDefault(); + play(); + } + }); + + return figure; +} + +/** + * Removes the block and its section wrapper from the DOM. + * @param {HTMLElement} block - the block element to remove + */ +function removeBlock(block) { + const wrapper = block.closest('.video-wrapper'); + if (wrapper) wrapper.remove(); +} + +export default function decorate(block) { + const link = block.querySelector('a[href]'); + if (!link) { removeBlock(block); return; } + + const type = detectType(link.href); + if (!type) { removeBlock(block); return; } + block.dataset.source = type; + + const embed = createEmbed(type, link.href); + if (!embed) { removeBlock(block); return; } + + const placeholder = createPlaceholder(block, embed); + + block.textContent = ''; + if (placeholder) block.append(placeholder); + + const observer = new IntersectionObserver((entries) => { + entries.forEach((entry) => { + if (!entry.isIntersecting) return; + block.append(embed); + observer.disconnect(); + }); + }, { rootMargin: '0px' }); + observer.observe(block); +} From 60cbff52c09093285009377f3107275666843a78 Mon Sep 17 00:00:00 2001 From: fkakatie Date: Fri, 26 Jun 2026 16:35:09 -0600 Subject: [PATCH 3/3] fix: guard on play before embed --- blocks/video/video.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/blocks/video/video.js b/blocks/video/video.js index 07dab7c..e2c331d 100644 --- a/blocks/video/video.js +++ b/blocks/video/video.js @@ -43,7 +43,7 @@ function createEmbed(type, url) { /** * Builds a thumbnail overlay. * @param {HTMLElement} block - block element containing the authored thumbnail image - * @param {HTMLIFrameElement} embed - video iframe + * @param {HTMLIFrameElement} embed - video iframe to activate on play * @returns {HTMLElement|null} the placeholder figure */ function createPlaceholder(block, embed) { @@ -65,6 +65,7 @@ function createPlaceholder(block, embed) { const src = new URL(embed.src); src.searchParams.set('autoplay', 1); embed.src = src.href; + if (!embed.isConnected) block.append(embed); figure.remove(); }