1- /**
2- * Video Library - Custom video functionality for lazy loading, play/pause controls, and accessibility
3- * Features: lazy loading, play/pause buttons, scroll triggers, reduced motion support
4- *
5- * Performance Optimizations for CDN Usage:
6- * - Early exit if no videos present (no DOM manipulation or event listeners)
7- * - Minimal object creation on pages without videos
8- * - Conditional resize listener only for desktop-only videos
9- * - Zero CSS class dependencies - uses only data attributes
10- *
11- * Safe for loading on every page via CDN - will not impact performance on pages without videos.
12- */
13-
141class VideoLibrary {
152 constructor ( options = { } ) {
163 this . options = {
@@ -52,7 +39,7 @@ class VideoLibrary {
5239 // Initialize video functionality
5340 this . setupLazyLoading ( ) ;
5441 this . setupVideoControls ( ) ;
55-
42+ this . setupHoverPlay ( ) ;
5643 // Only add resize listener if desktop-only videos are present
5744 const desktopOnlyVideos = document . querySelectorAll (
5845 'video[data-video-desktop-only="true"]'
@@ -186,6 +173,12 @@ class VideoLibrary {
186173 videos . forEach ( ( video ) => {
187174 const scrollInPlay =
188175 video . getAttribute ( "data-video-scroll-in-play" ) === "true" ;
176+ const hoverPlay = video . getAttribute ( "data-video-hover" ) === "true" ;
177+
178+ // If it's a hover-play video, do nothing here. Its logic is handled in setupHoverPlay.
179+ if ( hoverPlay ) {
180+ return ;
181+ }
189182
190183 if ( this . prefersReducedMotion ) {
191184 video . pause ( ) ;
@@ -213,9 +206,6 @@ class VideoLibrary {
213206 source . src = source . getAttribute ( "data-src" ) ;
214207 video . load ( ) ;
215208
216- // Hide the corresponding picture element when video starts loading
217- this . hidePictureElement ( video ) ;
218-
219209 video . addEventListener ( "canplaythrough" , function onCanPlayThrough ( ) {
220210 video . removeEventListener ( "canplaythrough" , onCanPlayThrough ) ;
221211 resolve ( ) ;
@@ -232,21 +222,37 @@ class VideoLibrary {
232222 }
233223
234224 /**
235- * Hide the corresponding picture element for a video
225+ * Show the corresponding picture element for a video (with opacity transition)
226+ * @param {HTMLVideoElement } video - The video element
227+ */
228+ showPictureElement ( video ) {
229+ const videoId = video . getAttribute ( "data-video" ) ;
230+ if ( ! videoId ) return ;
231+
232+ const pictureElement = this . findPictureElement ( video , videoId ) ;
233+
234+ if ( pictureElement ) {
235+ pictureElement . style . opacity = "1" ;
236+ if ( this . options . debug ) {
237+ console . log ( `Faded in picture element for video: ${ videoId } ` ) ;
238+ }
239+ }
240+ }
241+
242+ /**
243+ * Hide the corresponding picture element for a video (with opacity transition)
236244 * @param {HTMLVideoElement } video - The video element
237245 */
238246 hidePictureElement ( video ) {
239247 const videoId = video . getAttribute ( "data-video" ) ;
240248 if ( ! videoId ) return ;
241249
242- // Find the picture element with matching data-video-picture attribute
243- // Use the most performant approach: check previous siblings first, then fallback to document query
244250 const pictureElement = this . findPictureElement ( video , videoId ) ;
245251
246252 if ( pictureElement ) {
247- pictureElement . style . display = "none " ;
253+ pictureElement . style . opacity = "0 " ;
248254 if ( this . options . debug ) {
249- console . log ( `Hidden picture element for video: ${ videoId } ` ) ;
255+ console . log ( `Faded out picture element for video: ${ videoId } ` ) ;
250256 }
251257 }
252258 }
@@ -296,13 +302,73 @@ class VideoLibrary {
296302 } ) ;
297303 }
298304
305+ /**
306+ * Setup hover-to-play functionality for videos with data-video-hover="true"
307+ */
308+ setupHoverPlay ( ) {
309+ const hoverVideos = document . querySelectorAll (
310+ 'video[data-video-hover="true"]'
311+ ) ;
312+
313+ hoverVideos . forEach ( ( video ) => {
314+ const videoId = video . getAttribute ( "data-video" ) ;
315+ if ( ! videoId ) return ;
316+
317+ const trigger = video . closest ( `[data-video-trigger="${ videoId } "]` ) ;
318+
319+ if ( trigger ) {
320+ // ✅ ADJUSTED: Target the individual play and pause buttons directly
321+ // This leaves the wrapper element untouched.
322+ const videoContainer = video . parentElement ;
323+ if ( videoContainer ) {
324+ const playButton = videoContainer . querySelector (
325+ `[data-video-playback="play"][data-video="${ videoId } "]`
326+ ) ;
327+ const pauseButton = videoContainer . querySelector (
328+ `[data-video-playback="pause"][data-video="${ videoId } "]`
329+ ) ;
330+
331+ // Make the buttons inaccessible since hover is the primary interaction.
332+ if ( playButton ) {
333+ playButton . setAttribute ( "aria-hidden" , "true" ) ;
334+ playButton . setAttribute ( "tabindex" , "-1" ) ;
335+ }
336+ if ( pauseButton ) {
337+ pauseButton . setAttribute ( "aria-hidden" , "true" ) ;
338+ pauseButton . setAttribute ( "tabindex" , "-1" ) ;
339+ }
340+ }
341+
342+ // On mouse enter, explicitly hide poster, then lazy load and play
343+ trigger . addEventListener ( "mouseenter" , async ( ) => {
344+ if ( this . prefersReducedMotion ) return ;
345+ try {
346+ this . hidePictureElement ( video ) ; // This ensures the poster is hidden on EVERY hover
347+ await this . lazyLoadVideo ( video ) ;
348+ video . currentTime = 0 ;
349+ video . play ( ) ;
350+ } catch ( error ) {
351+ console . error ( `Error playing hover video ${ videoId } :` , error ) ;
352+ }
353+ } ) ;
354+
355+ // On mouse leave, pause the video AND show the poster
356+ trigger . addEventListener ( "mouseleave" , ( ) => {
357+ video . pause ( ) ;
358+ this . showPictureElement ( video ) ;
359+ } ) ;
360+ }
361+ } ) ;
362+ }
299363 /**
300364 * Setup autoplay for videos that should play immediately when loaded
301365 * @param {HTMLVideoElement } video - The video element
302366 */
303367 setupAutoplay ( video ) {
304368 video . addEventListener ( "canplaythrough" , ( ) => {
305369 if ( ! this . prefersReducedMotion ) {
370+ // ✅ FIX: Hide the poster before playing for standard autoplay videos
371+ this . hidePictureElement ( video ) ;
306372 video . play ( ) . catch ( console . error ) ;
307373 }
308374 } ) ;
@@ -323,19 +389,25 @@ class VideoLibrary {
323389
324390 // If reduced motion is preferred, don't play the video
325391 if ( ! this . prefersReducedMotion ) {
392+ // ✅ FIX: Hide the poster before playing for scroll-in-play videos
393+ this . hidePictureElement ( video ) ;
326394 video . currentTime = 0 ;
327395 video . play ( ) ;
328396 }
329397 } catch ( error ) {
330398 console . error ( error ) ;
331399 }
332- } else {
333- // Pause on scroll out for scroll-in-play videos
400+ }
401+ // On scroll out, pause the video and show the poster
402+ else {
334403 video . pause ( ) ;
404+ this . showPictureElement ( video ) ;
335405 }
336406 } ) ;
337407 } ,
338- { threshold : this . options . scrollTriggerThreshold }
408+ {
409+ threshold : this . options . scrollTriggerThreshold ,
410+ }
339411 ) ;
340412
341413 observer . observe ( video ) ;
@@ -411,6 +483,7 @@ class VideoLibrary {
411483 event . stopPropagation ( ) ;
412484 try {
413485 await this . lazyLoadVideo ( video ) ;
486+ this . hidePictureElement ( video ) ; // Hide poster on manual play
414487 video . play ( ) ;
415488 toggleButtonVisibility ( true ) ;
416489 } catch ( error ) {
@@ -444,6 +517,7 @@ class VideoLibrary {
444517 try {
445518 await this . lazyLoadVideo ( video ) ;
446519 if ( ! this . prefersReducedMotion ) {
520+ this . hidePictureElement ( video ) ;
447521 video . currentTime = 0 ;
448522 video . play ( ) ;
449523 }
0 commit comments