ga-basic-slider
Version:
A vanilla javascript slider plug-in that's made to be styleable
527 lines (440 loc) • 17.7 kB
JavaScript
/*!
* gaBasicSlider v1.0.1
* Licensed under the MIT license - http://opensource.org/licenses/MIT
*
* Copyright (c) 2018 Greg Arutunyan
*/
;(function(){
if(window.gaBasicSlider)
throw new Error("gaBasicSlider alredy exists");
// Constructor
window.gaBasicSlider = function(params) {
// Slider defaults
var defaults = {
slider: null,
btnNext: null,
btnPrevious: null,
indicators: null,
animate: true,
animationDelay: 6000, // milliseconds
animationDuration: 500 // milliseconds
}
// Private instance variables
var instance = {
params : extend(defaults, params),
currentIndex : 0,
// animation variables
interval : null,
timeout : null, // used with delayAnimation
// touch variables
touchXStart : null,
touchXCurrent : null,
touchYStart : null,
touchYCurrent : null
};
// Public methods
this.start = function() {
instance.params.animate = true;
startAnimation(instance);
}
this.stop = function() {
instance.params.animate = false;
stopAnimation(instance);
}
// Start the slider up
if(instance.params.slider && instance.params.slider.children.length > 1)
init(instance);
}
// Private
function init(instance) {
// setup the slider CSS
setup(instance);
// add slider event handlers
addSliderEventHandlers(instance);
// add indicators
addIndicators(instance);
// add next and previous buttons
addNextPreviousBtns(instance);
// update active states
updateActiveStates(instance);
// start animation
startAnimation(instance);
}
function setup(instance){
var slider = instance.params.slider;
// Prepare the parent
slider.className += ' gabs-initiated';
slider.style.position = 'relative';
slider.style.overflow = 'hidden';
slider.style.padding = '0';
// Children base css
for(var i = 0; i < slider.children.length; i++) {
slider.children[i].style.width = '100%';
slider.children[i].style.position = 'absolute';
slider.children[i].style.top = '0';
slider.children[i].style.left = '0';
slider.children[i].style[js.boxSizing] = 'border-box';
slider.children[i].style.willChange = css.transform;
if(i == 0) {
// prevent slider from collapsing
slider.children[0].style.position = 'relative';
// update z-index for animation
slider.children[0].style.zIndex = '2';
} else {
// update z-index for animation
slider.children[i].style.zIndex = '1';
}
}
// dispatch initiated event
var event = new Event('initiated');
slider.dispatchEvent(event);
}
function addSliderEventHandlers(instance) {
// Mouse events
instance.params.slider.addEventListener('mouseover', function(e){
stopAnimation(instance);
});
instance.params.slider.addEventListener('mouseout', function(e){
startAnimation(instance);
});
// Touch events
instance.params.slider.addEventListener('touchstart', function(e){
EventTouchStart(e, instance);
});
instance.params.slider.addEventListener('touchmove', function(e){
EventTouchMove(e, instance);
});
instance.params.slider.addEventListener('touchend', function(e){
EventTouchEnd(e, instance);
});
}
function addIndicators(instance) {
// if an indicators element isn't present return
if (!instance.params.indicators) return;
function indicatorInit(index) {
// add click handler to the indicators
this.addEventListener('click', function(e){
e.preventDefault();
slideToIndex(instance, {
toIndex : index,
moveBackward : instance.currentIndex > index
});
delayAnimation(instance);
});
// update indicator css
this.style.cursor = 'pointer';
}
// create indicators if none are present
if(!instance.params.indicators.children.length) {
for (var i = 0; i < instance.params.slider.children.length; i++) {
// create the indicator html
instance.params.indicators.insertAdjacentHTML('beforeend', '<span class="gabs-indicator">•</span>');
}
}
// add click handler and css to the indicators
for(var i = 0; i < instance.params.indicators.children.length; i++) {
indicatorInit.call(instance.params.indicators.children[i], i)
}
}
function addNextPreviousBtns(instance){
// Add click event to the next button
if(instance.params.btnNext) {
instance.params.btnNext.addEventListener('click', function(e){
slideToIndex(instance, {toIndex : getNextIndex(instance)});
delayAnimation(instance);
});
}
// Add click event to the previous button
if(instance.params.btnPrevious) {
instance.params.btnPrevious.addEventListener('click', function(e){
slideToIndex(instance, {
toIndex : getPreviousIndex(instance),
moveBackward : true
});
delayAnimation(instance);
});
}
}
function updateActiveStates(instance) {
// update current indicator
if(instance.params.indicators) {
// Remove the gabs-active class from all the children elements
for(var i = 0; i < instance.params.indicators.children.length; i++){
instance.params.indicators.children[i].className =
instance.params.indicators.children[i].className.replace(' gabs-active', '');
}
// Add the gabs-active class to the active child element
instance.params.indicators.children[instance.currentIndex].className += ' gabs-active';
}
}
function startAnimation(instance) {
if(instance.params.animate) {
stopAnimation(instance);
instance.interval = setInterval(function(){
slideToIndex(instance, {toIndex : getNextIndex(instance)});
}, instance.params.animationDelay)
// start animation event
var event = new Event('start');
instance.params.slider.dispatchEvent(event);
}
}
function stopAnimation(instance) {
if(instance.interval || instance.timeout) {
clearInterval(instance.interval);
clearInterval(instance.timeout);
instance.interval = null;
instance.timeout = null;
// stop animation event
var event = new Event('stop');
instance.params.slider.dispatchEvent(event);
}
}
function getPreviousIndex(instance) {
var index = instance.currentIndex - 1;
return (index < 0) ? instance.params.slider.children.length - 1 : index;
}
function getNextIndex(instance) {
var index = instance.currentIndex + 1;
return (index == instance.params.slider.children.length) ? 0 : index;
}
/**
* Slide to the specified index
*
* @param {Object} instance
* @param {Object} args
* @param {Integer} args.toIndex
* @param {Boolean} args.moveBackward - True to move backward
*/
function slideToIndex(instance, args) {
// Determine which direction to slide left, right or stay in place
var xPos = null;
if (args.toIndex == instance.currentIndex) {
xPos = '0%';
} else {
xPos = args.moveBackward ? '100%' : '-100%';
}
// Animate
doAnimation(instance, {
toIndex : args.toIndex,
xPos : xPos,
useTransition: true,
moveBackward: args.moveBackward
})
// Update the current index
instance.currentIndex = args.toIndex;
updateActiveStates(instance);
}
/**
* Animation
*
* @param {Object} instance
* @param {Object} args
* @param {String} args.toIndex
* @param {String} args.xPos
* @param {Boolean} args.useTransition - When true we transition to xPos instead of just moving to xPos
* @param {Boolean} args.moveBackward - True to move backward
*/
function doAnimation(instance, args) {
var currentIndex = instance.currentIndex;
// offsetPercent is the amount toIndex will move in the background
// as currentIndex moves in the forground (creates parallax feel)
var offsetPercent = 5;
// Preparing slides
for(var i = 0; i < instance.params.slider.children.length; i++) {
// remove transition
instance.params.slider.children[i].style[js.transition] = 'none';
if(currentIndex != args.toIndex) {
// update z index
if(i == args.toIndex) {
instance.params.slider.children[args.toIndex].style.zIndex = '2';
} else if (i == currentIndex) {
instance.params.slider.children[currentIndex].style.zIndex = '3';
} else {
instance.params.slider.children[i].style.zIndex = '1';
}
// setup parallax
if(i != currentIndex) {
// get current transform value
var currentTransform = instance.params.slider.children[i].style[js.transform];
if (currentTransform) currentTransform = currentTransform.match(/\d+/)[0];
// isParallaxing will be true for the toIndex slide that is being slightly animated in the
// background while the currentIndex slide is being touch dragged or animated in the foreground
var isParallaxing = !(!currentTransform || currentTransform == 100 || currentTransform == offsetPercent);
// position off screen slides so they are ready for parallax animation
if(args.moveBackward) {
if(!isParallaxing)
instance.params.slider.children[i].style[js.transform] = 'translateX(-' + offsetPercent + '%)';
} else {
if(!isParallaxing)
instance.params.slider.children[i].style[js.transform] = 'translateX(' + offsetPercent + '%)';
}
}
}
}
function slide(){
// add transition style
if (args.useTransition) {
// currentIndex
instance.params.slider.children[currentIndex].style[js.transition] = css.transform + ' ' + instance.params.animationDuration + 'ms';
// toIndex
instance.params.slider.children[args.toIndex].style[js.transition] = css.transform + ' ' + instance.params.animationDuration + 'ms';
}
// move currentIndex to args.xPos
instance.params.slider.children[currentIndex].style[js.transform] = 'translateX(' + args.xPos + ')';
// do parallax
if (args.useTransition) {
// animate toIndex in
instance.params.slider.children[args.toIndex].style[js.transform] = 'translateX(0%)';
}
else {
// find pixel value of offsetPercent relative to window width
var offset = window.innerWidth * (offsetPercent / 100);
// adjust sign to move backward if needed
if(args.moveBackward) { offset *= -1; }
// update offset based on xPos
offset += parseInt(args.xPos,10) * (offsetPercent / 100);
// move toIndex to offset value
instance.params.slider.children[args.toIndex].style[js.transform] = 'translateX(' + offset + 'px)';
}
}
// using timeout to work around transition styles not taking effect
if(args.useTransition) {
setTimeout(slide, 1);
} else {
slide();
}
}
function delayAnimation(instance) {
stopAnimation(instance);
instance.timeout = setTimeout(function(){
startAnimation(instance);
}, instance.params.animationDelay);
}
// Touch events
function EventTouchStart(e, instance) {
stopAnimation(instance);
if (e.touches.length > 1) {
gestureAbortSwipe(instance);
} else if (e.touches.length == 1) {
instance.touchXStart = e.targetTouches[0].pageX;
instance.touchXCurrent = instance.touchXStart;
instance.touchYStart = e.targetTouches[0].pageY;
instance.touchYCurrent = instance.touchYStart;
}
}
function gestureAbortSwipe(instance) {
var dx = instance.touchXCurrent - instance.touchXStart;
// set touchBounceBackThreshold
var touchBounceBackThreshold = window.innerWidth * .2;
if(touchBounceBackThreshold > 110) touchBounceBackThreshold = 110;
if(dx > touchBounceBackThreshold) {
slideToIndex(instance, {
toIndex : getPreviousIndex(instance),
moveBackward : true
});
} else if (dx < (touchBounceBackThreshold * -1)) {
slideToIndex(instance, {toIndex : getNextIndex(instance)});
} else {
slideToIndex(instance, {toIndex : instance.currentIndex});
}
}
function EventTouchMove(e, instance) {
if (e.touches.length > 1) {
gestureAbortSwipe(instance);
} else if (e.touches.length == 1) {
instance.touchXCurrent = e.targetTouches[0].pageX;
instance.touchYCurrent = e.targetTouches[0].pageY;
if (isTouchDirectionX(instance)) {
e.preventDefault();
gestureContinueSwipe(instance);
} else {
gestureAbortSwipe(instance);
}
}
}
function gestureContinueSwipe(instance) {
var xPos = instance.touchXCurrent - instance.touchXStart;
var moveBackward = xPos > 0;
// Determine toIndex
var toIndex = null;
if(moveBackward) {
// going backwards
toIndex = instance.currentIndex - 1;
if (instance.currentIndex == 0) {
toIndex = instance.params.slider.children.length - 1;
}
} else {
// going forwards
toIndex = instance.currentIndex + 1;
if (instance.currentIndex == instance.params.slider.children.length - 1) {
toIndex = 0
}
}
// Animate
doAnimation(instance, {
toIndex : toIndex,
xPos : xPos + 'px',
useTransition: false,
moveBackward: moveBackward
})
}
function EventTouchEnd(e, instance) {
gestureAbortSwipe(instance);
startAnimation(instance);
}
/**
* Detect touch direction
*
* @return {Boolean} Return true if mostly in the x-direction
*/
function isTouchDirectionX(instance) {
var dx = instance.touchXCurrent - instance.touchXStart;
var dy = instance.touchYCurrent - instance.touchYStart;
if (Math.abs(dy) > Math.abs(dx)) {
return false;
} else {
return true;
}
}
// Utility
function extend(o, p) {
var o = o || {};
var p = p || {};
for(prop in p) {
o[prop] = p[prop];
}
return o;
}
// feature detection
var tempElem = document.createElement('div');
function checkProps(props){
for ( var i in props ) if (tempElem.style[props[i]] !== undefined) return props[i];
return false;
}
var js = {
transition : (function(){
var props = ['transition', 'webkitTransition', 'MozTransition', 'OTransition', 'msTransition'];
return checkProps(props);
})(),
transform : (function(){
var props = ['transform', 'webkitTransform', 'MozTransform', 'OTransform', 'msTransform'];
return checkProps(props);
})(),
boxSizing : (function(){
var props = ['boxSizing', 'webkitBoxSizing', 'MozBoxSizing'];
return checkProps(props);
})()
}
// return css property name from js style name
var css = {
transform : (function(){
var props = {'transform' : 'transform',
'webkitTransform' : '-webkit-transform',
'MozTransform' : '-moz-transform',
'OTransform' : '-o-transform',
'msTransform' : '-ms-transform'};
if (js.transform) return props[js.transform]
return false;
})()
}
})();