Skip to content

Instantly share code, notes, and snippets.

@LachlanArthur
Created June 19, 2019 08:29
Show Gist options
  • Save LachlanArthur/9715cb7e919b3a27736d5fab6cf4675e to your computer and use it in GitHub Desktop.
Save LachlanArthur/9715cb7e919b3a27736d5fab6cf4675e to your computer and use it in GitHub Desktop.
Fader.js - Toggle a class between child elements. Make a simple gallery fader!
/**
* 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 );
}

Example

HTML

<div class="gallery">
  <img src="..." class="active">
  <img src="...">
  <img src="...">
</div>

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

.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;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment