slider-pro-js
Version:
Modular JavaScript slider that is dependency-free, rich-featured, flexible and easy to use
519 lines (410 loc) • 25.3 kB
JavaScript
import CustomEventTarget from '../../helpers/custom-event-target.js';
import { resolveUnit } from '../../helpers/util.js';
import Thumbnail from './thumbnail.js';
class Thumbnails extends CustomEventTarget {
// The namespace to be used when adding event listeners
namespace = 'thumbnails';
// Reference to the base slider instance
slider;
// Stores the current settings of the slider
settings;
// Reference to the thumbnail scroller
thumbnailsEl = null;
// Reference to the container of the thumbnail scroller
thumbnailsContainerEl = null;
// List of Thumbnail objects
thumbnails = null;
// Index of the selected thumbnail
selectedThumbnailIndex = 0;
// Total size (width or height, depending on the orientation) of the thumbnails
thumbnailsSize = 0;
// Size of the thumbnail's container
thumbnailsContainerSize = 0;
// The position of the thumbnail scroller inside its container
thumbnailsPosition = 0;
// Orientation of the thumbnails
thumbnailsOrientation = null;
// Indicates the 'left' or 'top' position based on the orientation of the thumbnails
thumbnailsPositionProperty = null;
// Indicates if there are thumbnails in the slider
isThumbnailScroller = false;
// Stores references the event handlers in pairs containing the event identifier and the event handler
// in order to be able to retrieve them when they need to be removed
eventHandlerReferences = {};
// Default add-on settings
defaults = {
// Sets the width of the thumbnail
thumbnailWidth: 100,
// Sets the height of the thumbnail
thumbnailHeight: 80,
// Sets the position of the thumbnail scroller (top, bottom, right, left)
thumbnailsPosition: 'bottom',
// Indicates if a pointer will be displayed for the selected thumbnail
thumbnailPointer: false
};
constructor ( slider ) {
super();
this.slider = slider;
this.slider.thumbnails = this;
this.init();
}
init() {
this.thumbnails = [];
this.slider.addEventListener( 'beforeInit.' + this.namespace, () => {
Array.from( document.querySelector( this.slider.selector ).getElementsByClassName( 'sp-slide' ) ).forEach( ( slideEl, index ) => {
slideEl.setAttribute( 'data-original-index', index );
});
});
this.slider.addEventListener( 'update.' + this.namespace, this.updateHandler.bind( this ) );
this.slider.addEventListener( 'resize.' + this.namespace, this.resizeHandler.bind( this ) );
this.slider.addEventListener( 'gotoSlide.' + this.namespace, ( event ) => {
this.gotoThumbnail( event.detail.index );
});
}
// Called when the slider is updated
updateHandler() {
this.settings = { ...this.defaults, ...this.slider.settings };
if ( this.slider.sliderEl.getElementsByClassName( 'sp-thumbnail' ).length === 0 && this.thumbnails.length === 0 ) {
this.isThumbnailScroller = false;
return;
}
this.isThumbnailScroller = true;
// Create the container of the thumbnail scroller, if it wasn't created yet
if ( this.thumbnailsContainerEl === null ) {
this.thumbnailsContainerEl = document.createElement( 'div' );
this.thumbnailsContainerEl.classList.add( 'sp-thumbnails-container' );
this.slider.slidesContainerEl.after( this.thumbnailsContainerEl );
}
// If the thumbnails' main container doesn't exist, create it, and get a reference to it
if ( this.thumbnailsEl === null ) {
if ( this.slider.sliderEl.getElementsByClassName( 'sp-thumbnails' ).length !== 0 ) {
this.thumbnailsEl = this.slider.sliderEl.getElementsByClassName( 'sp-thumbnails' )[0];
this.thumbnailsContainerEl.appendChild( this.thumbnailsEl );
// Shuffle/randomize the thumbnails
if ( this.settings.shuffle === true ) {
let thumbnails = Array.from( this.thumbnailsEl.getElementsByClassName( 'sp-thumbnail' ) ),
shuffledThumbnails = [];
// Reposition the thumbnails based on the order of the indexes in the
// 'shuffledIndexes' array from the slider
Array.from( this.slider.sliderEl.getElementsByClassName( 'sp-slide' ) ).forEach( ( slideEl ) => {
let thumbnailEl = thumbnails[ parseInt( slideEl.getAttribute( 'data-original-index' ), 10 ) ];
if ( thumbnailEl.parentElement.tagName === 'A' ) {
thumbnailEl = thumbnailEl.parentElement;
}
shuffledThumbnails.push( thumbnailEl );
});
// Append the sorted thumbnails to the thumbnail scroller
this.thumbnailsEl.replaceChildren( ...shuffledThumbnails ) ;
}
} else {
this.thumbnailsEl = document.createElement( 'div' );
this.thumbnailsEl.classList.add( 'sp-thumbnails' );
this.thumbnailsContainerEl.appendChild( this.thumbnailsEl );
}
}
// Check if there are thumbnails inside the slides and move them in the thumbnails container
Array.from( this.slider.slidesEl.getElementsByClassName( 'sp-thumbnail' ) ).forEach( function( thumbnailEl, index ) {
const lastThumbnailIndex = this.thumbnailsEl.getElementsByClassName( 'sp-thumbnail' ).length - 1;
if ( thumbnailEl.parentElement.tagName === 'A' ) {
thumbnailEl = thumbnailEl.parentElement;
}
// If the index of the slide that contains the thumbnail is greater than the total number
// of thumbnails from the thumbnails container, position the thumbnail at the end.
// Otherwise, add the thumbnails at the corresponding position.
if ( index > lastThumbnailIndex ) {
this.thumbnailsEl.appendChild( thumbnailEl );
} else {
this.thumbnailsEl.insertBefore( thumbnailEl, this.thumbnailsEl.getElementsByClassName( 'sp-thumbnail' )[ index ] );
}
});
// Loop through the Thumbnail objects and if a corresponding element is not found in the DOM,
// it means that the thumbnail might have been removed. In this case, destroy that Thumbnail instance.
for ( let i = this.thumbnails.length - 1; i >= 0; i-- ) {
if ( this.thumbnailsEl.querySelector( '.sp-thumbnail[data-index="' + i + '"]' ) === null ) {
const thumbnail = this.thumbnails[ i ];
thumbnail.destroy();
this.thumbnails.splice( i, 1 );
}
}
// Loop through the thumbnails and if there is any uninitialized thumbnail,
// initialize it, else update the thumbnail's index.
Array.from( this.thumbnailsEl.getElementsByClassName( 'sp-thumbnail' ) ).forEach( ( thumbnailEl, index ) => {
if ( thumbnailEl.getAttribute( 'data-init' ) === null ) {
this._createThumbnail( thumbnailEl, index );
} else {
this.thumbnails[ index ].setIndex( index );
}
});
// Remove the previous class that corresponds to the position of the thumbnail scroller
this.thumbnailsContainerEl.classList.remove( 'sp-top-thumbnails', 'sp-bottom-thumbnails', 'sp-left-thumbnails', 'sp-right-thumbnails' );
// Check the position of the thumbnail scroller and assign it the appropriate class and styling
if ( this.settings.thumbnailsPosition === 'top' ) {
this.thumbnailsContainerEl.classList.add( 'sp-top-thumbnails' );
this.thumbnailsOrientation = 'horizontal';
} else if ( this.settings.thumbnailsPosition === 'bottom' ) {
this.thumbnailsContainerEl.classList.add( 'sp-bottom-thumbnails' );
this.thumbnailsOrientation = 'horizontal';
} else if ( this.settings.thumbnailsPosition === 'left' ) {
this.thumbnailsContainerEl.classList.add( 'sp-left-thumbnails' );
this.thumbnailsOrientation = 'vertical';
} else if ( this.settings.thumbnailsPosition === 'right' ) {
this.thumbnailsContainerEl.classList.add( 'sp-right-thumbnails' );
this.thumbnailsOrientation = 'vertical';
}
// Check if the pointer needs to be created
if ( this.settings.thumbnailPointer === true ) {
this.thumbnailsContainerEl.classList.add( 'sp-has-pointer' );
} else {
this.thumbnailsContainerEl.classList.remove( 'sp-has-pointer' );
}
// Mark the thumbnail that corresponds to the selected slide
this.selectedThumbnailIndex = this.slider.selectedSlideIndex;
this.thumbnailsEl.getElementsByClassName( 'sp-thumbnail-container' )[ this.selectedThumbnailIndex ].classList.add( 'sp-selected-thumbnail' );
// Calculate the total size of the thumbnails
this.thumbnailsSize = 0;
this.thumbnails.forEach( ( thumbnail ) => {
thumbnail.setSize( this.settings.thumbnailWidth, this.settings.thumbnailHeight );
this.thumbnailsSize += this.thumbnailsOrientation === 'horizontal' ? thumbnail.getSize().width : thumbnail.getSize().height;
});
// Set the size of the thumbnails
if ( this.thumbnailsOrientation === 'horizontal' ) {
this.thumbnailsEl.style.width = resolveUnit( this.thumbnailsSize );
this.thumbnailsEl.style.height = resolveUnit( this.settings.thumbnailHeight );
this.thumbnailsContainerEl.style.removeProperty( 'height' );
this.thumbnailsPositionProperty = 'left';
} else {
this.thumbnailsEl.style.width = resolveUnit( this.settings.thumbnailWidth );
this.thumbnailsEl.style.height = resolveUnit( this.thumbnailsSize );
this.thumbnailsContainerEl.style.removeProperty( 'width' );
this.thumbnailsPositionProperty = 'top';
}
// Fire the 'thumbnailsUpdate' event
this.dispatchEvent( 'thumbnailsUpdate' );
}
// Create an individual thumbnail
_createThumbnail( thumbnailEl, index ) {
const thumbnail = new Thumbnail( thumbnailEl, this.thumbnailsEl, index );
// When the thumbnail is clicked, navigate to the corresponding slide
const thumbnailClickHandler = ( event ) => {
this.slider.gotoSlide( event.detail.index );
};
thumbnail.addEventListener( 'thumbnailClick', thumbnailClickHandler );
this.eventHandlerReferences[ 'thumbnailClick' + index ] = thumbnailClickHandler;
// Add the thumbnail at the specified index
this.thumbnails.splice( index, 0, thumbnail );
}
// Called when the slider is resized.
// Resets the size and position of the thumbnail scroller container.
resizeHandler() {
if ( this.isThumbnailScroller === false ) {
return;
}
let newThumbnailsPosition;
if ( this.thumbnailsOrientation === 'horizontal' ) {
this.thumbnailsContainerSize = Math.min( this.slider.slidesMaskEl.clientWidth, this.thumbnailsSize );
this.thumbnailsContainerEl.style.width = resolveUnit( this.thumbnailsContainerSize );
// Reduce the slide mask's height, to make room for the thumbnails
if ( this.settings.forceSize === 'fullWindow' ) {
this.slider.slidesMaskEl.style.height = resolveUnit( this.slider.slidesMaskEl.clientHeight - this.thumbnailsContainerEl.offsetHeight );
// Resize the slides
this.slider.slideHeight = this.slider.slidesMaskEl.clientHeight;
this.slider.resizeSlides();
// Re-arrange the slides
this.slider.resetSlidesPosition();
}
} else if ( this.thumbnailsOrientation === 'vertical' ) {
// Check if the width of the slide mask plus the width of the thumbnail scroller is greater than
// the width of the slider's container and if that's the case, reduce the slides container width
// in order to make the entire slider fit inside the slider's container.
if ( this.slider.slidesMaskEl.clientWidth + this.thumbnailsContainerEl.offsetWidth > this.slider.sliderEl.parentElement.clientWidth ) {
// Reduce the slider's width, to make room for the thumbnails
if ( this.settings.forceSize === 'fullWidth' || this.settings.forceSize === 'fullWindow' ) {
this.slider.sliderEl.style.maxWidth = resolveUnit( window.innerWidth - this.thumbnailsContainerEl.offsetWidth );
} else {
this.slider.sliderEl.style.maxWidth = resolveUnit( this.slider.sliderEl.parentElement.clientWidth - this.thumbnailsContainerEl.offsetWidth );
}
this.slider.slidesMaskEl.style.width = resolveUnit( window.getComputedStyle( this.slider.sliderEl ).width );
// If the slides are vertically oriented, update the width and height (to maintain the aspect ratio)
// of the slides.
if ( this.settings.orientation === 'vertical' ) {
this.slider.slideWidth = this.slider.sliderEl.clientWidth;
this.slider.resizeSlides();
}
// Re-arrange the slides
this.slider.resetSlidesPosition();
}
this.thumbnailsContainerSize = Math.min( this.slider.slidesMaskEl.clientHeight, this.thumbnailsSize );
this.thumbnailsContainerEl.style.height = resolveUnit( this.thumbnailsContainerSize );
}
// If the total size of the thumbnails is smaller than the thumbnail scroller' container (which has
// the same size as the slides container), it means that all the thumbnails will be visible, so set
// the position of the thumbnail scroller to 0.
//
// If that's not the case, the thumbnail scroller will be positioned based on which thumbnail is selected.
if ( this.thumbnailsSize <= this.thumbnailsContainerSize || this.thumbnailsEl.getElementsByClassName( 'sp-selected-thumbnail' ).length === 0 ) {
newThumbnailsPosition = 0;
} else {
newThumbnailsPosition = Math.max( - this.thumbnails[ this.selectedThumbnailIndex ].getPosition()[ this.thumbnailsPositionProperty ], this.thumbnailsContainerSize - this.thumbnailsSize );
}
// Add a padding to the slider, based on the thumbnail scroller's orientation, to make room
// for the thumbnails.
const thumbnailsComputedStyle = window.getComputedStyle( this.thumbnailsContainerEl );
const thumbnailsWidth = this.thumbnailsContainerEl.offsetWidth + parseInt( thumbnailsComputedStyle.marginLeft ) + parseInt( thumbnailsComputedStyle.marginRight );
const thumbnailsHeight = this.thumbnailsContainerEl.offsetHeight + parseInt( thumbnailsComputedStyle.marginTop ) + parseInt( thumbnailsComputedStyle.marginBottom );
if ( this.settings.thumbnailsPosition === 'top' ) {
this.slider.sliderEl.style.paddingTop = resolveUnit( thumbnailsHeight );
this.slider.sliderEl.style.removeProperty( 'padding-left' );
this.slider.sliderEl.style.removeProperty( 'padding-right' );
} else if ( this.settings.thumbnailsPosition === 'bottom' ) {
this.slider.sliderEl.style.removeProperty( 'padding-top' );
this.slider.sliderEl.style.removeProperty( 'padding-left' );
this.slider.sliderEl.style.removeProperty( 'padding-right' );
} else if ( this.settings.thumbnailsPosition === 'left' ) {
this.slider.sliderEl.style.removeProperty( 'padding-top' );
this.slider.sliderEl.style.paddingLeft = resolveUnit( thumbnailsWidth );
this.slider.sliderEl.style.removeProperty( 'padding-right' );
} else if ( this.settings.thumbnailsPosition === 'right' ) {
this.slider.sliderEl.style.removeProperty( 'padding-top' );
this.slider.sliderEl.style.removeProperty( 'padding-left' );
this.slider.sliderEl.style.paddingRight = resolveUnit( thumbnailsWidth );
}
this.moveTo( newThumbnailsPosition, true );
}
// Selects the thumbnail at the indicated index and moves the thumbnail scroller
// accordingly.
gotoThumbnail( index ) {
if ( this.isThumbnailScroller === false || typeof this.thumbnails[ index ] === 'undefined' ) {
return;
}
let previousIndex = this.selectedThumbnailIndex,
newThumbnailsPosition = this.thumbnailsPosition;
this.selectedThumbnailIndex = index;
// Set the 'selected' class to the appropriate thumbnail
this.thumbnailsEl.getElementsByClassName( 'sp-selected-thumbnail' )[0].classList.remove( 'sp-selected-thumbnail' );
this.thumbnailsEl.getElementsByClassName( 'sp-thumbnail-container' )[ this.selectedThumbnailIndex ].classList.add( 'sp-selected-thumbnail' );
// Calculate the new position that the thumbnail scroller needs to go to.
//
// If the selected thumbnail has a higher index than the previous one, make sure that the thumbnail
// that comes after the selected thumbnail will be visible, if the selected thumbnail is not the
// last thumbnail in the list.
//
// If the selected thumbnail has a lower index than the previous one, make sure that the thumbnail
// that's before the selected thumbnail will be visible, if the selected thumbnail is not the
// first thumbnail in the list.
if ( this.settings.rightToLeft === true && this.thumbnailsOrientation === 'horizontal' ) {
if ( this.selectedThumbnailIndex >= previousIndex ) {
const rtlNextThumbnailIndex = this.selectedThumbnailIndex === this.thumbnails.length - 1 ? this.selectedThumbnailIndex : this.selectedThumbnailIndex + 1,
rtlNextThumbnail = this.thumbnails[ rtlNextThumbnailIndex ];
if ( rtlNextThumbnail.getPosition().left < - this.thumbnailsPosition ) {
newThumbnailsPosition = - rtlNextThumbnail.getPosition().left;
}
} else if ( this.selectedThumbnailIndex < previousIndex ) {
const rtlPreviousThumbnailIndex = this.selectedThumbnailIndex === 0 ? this.selectedThumbnailIndex : this.selectedThumbnailIndex - 1,
rtlPreviousThumbnail = this.thumbnails[ rtlPreviousThumbnailIndex ],
rtlThumbnailsRightPosition = - this.thumbnailsPosition + this.thumbnailsContainerSize;
if ( rtlPreviousThumbnail.getPosition().right > rtlThumbnailsRightPosition ) {
newThumbnailsPosition = this.thumbnailsPosition - ( rtlPreviousThumbnail.getPosition().right - rtlThumbnailsRightPosition );
}
}
} else {
if ( this.selectedThumbnailIndex >= previousIndex ) {
const nextThumbnailIndex = this.selectedThumbnailIndex === this.thumbnails.length - 1 ? this.selectedThumbnailIndex : this.selectedThumbnailIndex + 1,
nextThumbnail = this.thumbnails[ nextThumbnailIndex ],
nextThumbnailPosition = this.thumbnailsOrientation === 'horizontal' ? nextThumbnail.getPosition().right : nextThumbnail.getPosition().bottom,
thumbnailsRightPosition = - this.thumbnailsPosition + this.thumbnailsContainerSize;
if ( nextThumbnailPosition > thumbnailsRightPosition ) {
newThumbnailsPosition = this.thumbnailsPosition - ( nextThumbnailPosition - thumbnailsRightPosition );
}
} else if ( this.selectedThumbnailIndex < previousIndex ) {
const previousThumbnailIndex = this.selectedThumbnailIndex === 0 ? this.selectedThumbnailIndex : this.selectedThumbnailIndex - 1,
previousThumbnail = this.thumbnails[ previousThumbnailIndex ],
previousThumbnailPosition = this.thumbnailsOrientation === 'horizontal' ? previousThumbnail.getPosition().left : previousThumbnail.getPosition().top;
if ( previousThumbnailPosition < - this.thumbnailsPosition ) {
newThumbnailsPosition = - previousThumbnailPosition;
}
}
}
// Move the thumbnail scroller to the calculated position
this.moveTo( newThumbnailsPosition );
// Fire the 'gotoThumbnail' event
this.dispatchEvent( 'gotoThumbnail' );
}
// Move the thumbnail scroller to the indicated position
moveTo( position, instant, callback ) {
const css = {};
// Return if the position hasn't changed
if ( position === this.thumbnailsPosition ) {
return;
}
this.thumbnailsPosition = position;
// Use CSS transitions if they are supported. If not, use JavaScript animation
let transition,
left = this.thumbnailsOrientation === 'horizontal' ? position : 0,
top = this.thumbnailsOrientation === 'horizontal' ? 0 : position;
css[ 'transform' ] = 'translate3d(' + left + 'px, ' + top + 'px, 0)';
if ( typeof instant !== 'undefined' && instant === true ) {
transition = '';
} else {
this.thumbnailsEl.classList.add( 'sp-animated' );
transition = 'transform ' + 700 / 1000 + 's';
const transitionEndHandler = ( event ) => {
if ( event.target !== event.currentTarget ) {
return;
}
this.thumbnailsEl.removeEventListener( 'transitionend', transitionEndHandler );
this.thumbnailsEl.classList.remove( 'sp-animated' );
if ( typeof callback === 'function' ) {
callback();
}
// Fire the 'thumbnailsMoveComplete' event
this.dispatchEvent( 'thumbnailsMoveComplete' );
};
this.thumbnailsEl.addEventListener( 'transitionend', transitionEndHandler );
this.eventHandlerReferences[ 'thumbnailsTransitionEnd' ] = transitionEndHandler;
}
css[ 'transition' ] = transition;
for ( let property in css ) {
this.thumbnailsEl.style[ property ] = css[ property ];
}
}
// Stop the movement of the thumbnail scroller
stopMovement() {
const matrixString = window.getComputedStyle( this.thumbnailsEl ).transform,
matrixType = matrixString.indexOf( 'matrix3d' ) !== -1 ? 'matrix3d' : 'matrix',
matrixArray = matrixString.replace( matrixType, '' ).match( /-?[0-9.]+/g ),
left = matrixType === 'matrix3d' ? parseInt( matrixArray[ 12 ], 10 ) : parseInt( matrixArray[ 4 ], 10 ),
top = matrixType === 'matrix3d' ? parseInt( matrixArray[ 13 ], 10 ) : parseInt( matrixArray[ 5 ], 10 );
this.thumbnailsEl.style.transform = 'translate3d(' + left + 'px, ' + top + 'px, 0)';
this.thumbnailsEl.style.removeProperty( 'transition' );
this.thumbnailsEl.removeEventListener( 'transitionend', this.eventHandlerReferences['thumbnailsTransitionEnd'] );
this.thumbnailsPosition = this.thumbnailsOrientation === 'horizontal' ? parseInt( matrixArray[ 4 ] , 10 ) : parseInt( matrixArray[ 5 ] , 10 );
this.thumbnailsEl.classList.remove( 'sp-animated' );
}
// Destroy the module
destroy() {
// Remove event listeners
this.slider.removeEventListener( 'update.' + this.namespace );
if ( this.isThumbnailScroller === false ) {
return;
}
this.slider.removeEventListener( 'resize.' + this.namespace );
this.slider.removeEventListener( 'gotoSlide.' + this.namespace );
// Destroy the individual thumbnails
Array.from( this.thumbnailsEl.getElementsByClassName( 'sp-thumbnail' ) ).forEach( ( thumbnailEl ) => {
const index = parseInt( thumbnailEl.getAttribute( 'data-index' ), 10 ),
thumbnail = this.thumbnails[ index ];
thumbnail.removeEventListener( 'thumbnailClick', this.eventHandlerReferences[ 'thumbnailClick' + index ] );
thumbnail.destroy();
});
this.thumbnails.length = 0;
// Add the thumbnail scroller directly in the slider and
// remove the thumbnail scroller container
this.slider.sliderEl.appendChild( this.thumbnailsEl );
this.thumbnailsContainerEl.remove();
// Remove any created padding
this.slider.sliderEl.style.removeProperty( 'paddin-top' );
this.slider.sliderEl.style.removeProperty( 'padding-left' );
this.slider.sliderEl.style.removeProperty( 'padding-right' );
}
}
export default Thumbnails;