|
/** |
|
* Toggle a class between child elements. Make a simple gallery fader! |
|
* |
|
* @param {Element} parent The wrapper element |
|
* @param {string} activeClass Class to add to active child |
|
* @param {number} timeout Milliseconds to wait between children |
|
* @returns {() => void} Stop function that ends the fader |
|
* |
|
* @fires `beforeNext` Event fires before switching to new child. Bubbles. Cancelable with `preventDefault()`. |
|
* - `e.target` => Current child |
|
* - `e.relatedTarget` => Next child |
|
* |
|
* @fires `afterNext` Event fires after switching to new child. Bubbles. Not cancelable. |
|
* - `e.target` => Current child |
|
* - `e.relatedTarget` => Last child |
|
* |
|
* @example |
|
* |
|
* ```html |
|
* <div class="gallery"> |
|
* <img src="..." class="active"> |
|
* <img src="..."> |
|
* <img src="..."> |
|
* </div> |
|
* ``` |
|
* |
|
* # JS |
|
* ```js |
|
* const gallery = document.querySelector( '.gallery' ); |
|
* if ( gallery ) { |
|
* const stop = fader( gallery, 'active', 3000 ); |
|
* |
|
* // Do things after change |
|
* gallery.addEventListener( 'afterNext', e => console.log( 'Changed from %o to %o', e.relatedTarget, e.target ) ); |
|
* |
|
* // Pause on hover |
|
* gallery.addEventListener( 'beforeNext', e => e.target.matches( ':hover' ) && e.preventDefault() ); |
|
* |
|
* // Stop the fader after 10 minutes |
|
* setTimeout( stop, 10 * 60 * 1000 ); |
|
* } |
|
* ``` |
|
* |
|
* # CSS |
|
* ```css |
|
* .gallery { |
|
* position: relative; |
|
* width: 100px; |
|
* height: 100px; |
|
* --fade-speed: 0.5s; |
|
* } |
|
* |
|
* .gallery > * { |
|
* position: absolute; |
|
* width: 100%; |
|
* height: 100%; |
|
* object-fit: cover; |
|
* opacity: 1; |
|
* transition: |
|
* opacity var(--fade-speed) ease-in-out, |
|
* visibility var(--fade-speed) step-start; |
|
* z-index: 2; |
|
* } |
|
* |
|
* .gallery > *:not( .active ) { |
|
* visibility: hidden; |
|
* opacity: 0; |
|
* transition: |
|
* opacity var(--fade-speed) step-end, |
|
* visibility var(--fade-speed) step-end; |
|
* z-index: 1; |
|
* } |
|
* ``` |
|
*/ |
|
function fader( parent, activeClass = 'active', timeout = 5000 ) { |
|
|
|
if ( !( parent instanceof Element ) ) { |
|
console.log( 'Fader could not be started.' ); |
|
return; |
|
} |
|
|
|
/** |
|
* Create a new event |
|
* |
|
* @param {string} type |
|
* @param {boolean} cancelable |
|
*/ |
|
const event = ( type, cancelable = false ) => new Event( type, { bubbles: true, cancelable } ); |
|
|
|
/** |
|
* Dispatch an event to an element |
|
* |
|
* @param {Element} target Target element |
|
* @param {Event} event Event to dispatch |
|
* @param {Element} relatedTarget Related target element |
|
*/ |
|
const dispatch = ( target, event, relatedTarget = null ) => ( relatedTarget && ( event.relatedTarget = relatedTarget ), target.dispatchEvent( event ) ); |
|
|
|
const tick = ( ( index = 0 ) => () => { |
|
const last = parent.children.item( ( index + 0 ) % parent.children.length ); |
|
const next = parent.children.item( ( index + 1 ) % parent.children.length ); |
|
|
|
if ( dispatch( last, event( 'beforeNext', true ), next ) ) { |
|
last.classList.remove( activeClass ); |
|
next.classList.add( activeClass ); |
|
|
|
index++; |
|
dispatch( next, event( 'afterNext' ), last ); |
|
} |
|
|
|
tock(); |
|
} )(); |
|
|
|
let timeoutId = null; |
|
function tock() { |
|
timeoutId = setTimeout( () => requestAnimationFrame( tick ), timeout ); |
|
} |
|
|
|
tock(); |
|
|
|
return () => clearTimeout( timeoutId ); |
|
} |