flickity
Version:
Touch, responsive, flickable carousels
293 lines (247 loc) • 9.07 kB
JavaScript
// drag
( function( window, factory ) {
// universal module definition
if ( typeof module == 'object' && module.exports ) {
// CommonJS
module.exports = factory(
window,
require('./core'),
require('unidragger'),
require('fizzy-ui-utils'),
);
} else {
// browser global
window.Flickity = factory(
window,
window.Flickity,
window.Unidragger,
window.fizzyUIUtils,
);
}
}( typeof window != 'undefined' ? window : this,
function factory( window, Flickity, Unidragger, utils ) {
// ----- defaults ----- //
Object.assign( Flickity.defaults, {
draggable: '>1',
dragThreshold: 3,
} );
// -------------------------- drag prototype -------------------------- //
let proto = Flickity.prototype;
Object.assign( proto, Unidragger.prototype ); // inherit Unidragger
proto.touchActionValue = '';
// -------------------------- -------------------------- //
Flickity.create.drag = function() {
this.on( 'activate', this.onActivateDrag );
this.on( 'uiChange', this._uiChangeDrag );
this.on( 'deactivate', this.onDeactivateDrag );
this.on( 'cellChange', this.updateDraggable );
this.on( 'pointerDown', this.handlePointerDown );
this.on( 'pointerUp', this.handlePointerUp );
this.on( 'pointerDown', this.handlePointerDone );
this.on( 'dragStart', this.handleDragStart );
this.on( 'dragMove', this.handleDragMove );
this.on( 'dragEnd', this.handleDragEnd );
this.on( 'staticClick', this.handleStaticClick );
// TODO updateDraggable on resize? if groupCells & slides change
};
proto.onActivateDrag = function() {
this.handles = [ this.viewport ];
this.bindHandles();
this.updateDraggable();
};
proto.onDeactivateDrag = function() {
this.unbindHandles();
this.element.classList.remove('is-draggable');
};
proto.updateDraggable = function() {
// disable dragging if less than 2 slides. #278
if ( this.options.draggable === '>1' ) {
this.isDraggable = this.slides.length > 1;
} else {
this.isDraggable = this.options.draggable;
}
this.element.classList.toggle( 'is-draggable', this.isDraggable );
};
proto._uiChangeDrag = function() {
delete this.isFreeScrolling;
};
// -------------------------- pointer events -------------------------- //
proto.handlePointerDown = function( event ) {
if ( !this.isDraggable ) {
// proceed for staticClick
this.bindActivePointerEvents( event );
return;
}
let isTouchStart = event.type === 'touchstart';
let isTouchPointer = event.pointerType === 'touch';
let isFocusNode = event.target.matches('input, textarea, select');
if ( !isTouchStart && !isTouchPointer && !isFocusNode ) event.preventDefault();
if ( !isFocusNode ) this.focus();
// blur
if ( document.activeElement !== this.element ) document.activeElement.blur();
// stop if it was moving
this.dragX = this.x;
this.viewport.classList.add('is-pointer-down');
// track scrolling
this.pointerDownScroll = getScrollPosition();
window.addEventListener( 'scroll', this );
this.bindActivePointerEvents( event );
};
// ----- move ----- //
proto.hasDragStarted = function( moveVector ) {
return Math.abs( moveVector.x ) > this.options.dragThreshold;
};
// ----- up ----- //
proto.handlePointerUp = function() {
delete this.isTouchScrolling;
this.viewport.classList.remove('is-pointer-down');
};
proto.handlePointerDone = function() {
window.removeEventListener( 'scroll', this );
delete this.pointerDownScroll;
};
// -------------------------- dragging -------------------------- //
proto.handleDragStart = function() {
if ( !this.isDraggable ) return;
this.dragStartPosition = this.x;
this.startAnimation();
window.removeEventListener( 'scroll', this );
};
proto.handleDragMove = function( event, pointer, moveVector ) {
if ( !this.isDraggable ) return;
event.preventDefault();
this.previousDragX = this.dragX;
// reverse if right-to-left
let direction = this.options.rightToLeft ? -1 : 1;
// wrap around move. #589
if ( this.isWrapping ) moveVector.x %= this.slideableWidth;
let dragX = this.dragStartPosition + moveVector.x * direction;
if ( !this.isWrapping ) {
// slow drag
let originBound = Math.max( -this.slides[0].target, this.dragStartPosition );
dragX = dragX > originBound ? ( dragX + originBound ) * 0.5 : dragX;
let endBound = Math.min( -this.getLastSlide().target, this.dragStartPosition );
dragX = dragX < endBound ? ( dragX + endBound ) * 0.5 : dragX;
}
this.dragX = dragX;
this.dragMoveTime = new Date();
};
proto.handleDragEnd = function() {
if ( !this.isDraggable ) return;
let { freeScroll } = this.options;
if ( freeScroll ) this.isFreeScrolling = true;
// set selectedIndex based on where flick will end up
let index = this.dragEndRestingSelect();
if ( freeScroll && !this.isWrapping ) {
// if free-scroll & not wrap around
// do not free-scroll if going outside of bounding slides
// so bounding slides can attract slider, and keep it in bounds
let restingX = this.getRestingPosition();
this.isFreeScrolling = -restingX > this.slides[0].target &&
-restingX < this.getLastSlide().target;
} else if ( !freeScroll && index === this.selectedIndex ) {
// boost selection if selected index has not changed
index += this.dragEndBoostSelect();
}
delete this.previousDragX;
// apply selection
// HACK, set flag so dragging stays in correct direction
this.isDragSelect = this.isWrapping;
this.select( index );
delete this.isDragSelect;
};
proto.dragEndRestingSelect = function() {
let restingX = this.getRestingPosition();
// how far away from selected slide
let distance = Math.abs( this.getSlideDistance( -restingX, this.selectedIndex ) );
// get closet resting going up and going down
let positiveResting = this._getClosestResting( restingX, distance, 1 );
let negativeResting = this._getClosestResting( restingX, distance, -1 );
// use closer resting for wrap-around
return positiveResting.distance < negativeResting.distance ?
positiveResting.index : negativeResting.index;
};
/**
* given resting X and distance to selected cell
* get the distance and index of the closest cell
* @param {Number} restingX - estimated post-flick resting position
* @param {Number} distance - distance to selected cell
* @param {Integer} increment - +1 or -1, going up or down
* @returns {Object} - { distance: {Number}, index: {Integer} }
*/
proto._getClosestResting = function( restingX, distance, increment ) {
let index = this.selectedIndex;
let minDistance = Infinity;
let condition = this.options.contain && !this.isWrapping ?
// if containing, keep going if distance is equal to minDistance
( dist, minDist ) => dist <= minDist :
( dist, minDist ) => dist < minDist;
while ( condition( distance, minDistance ) ) {
// measure distance to next cell
index += increment;
minDistance = distance;
distance = this.getSlideDistance( -restingX, index );
if ( distance === null ) break;
distance = Math.abs( distance );
}
return {
distance: minDistance,
// selected was previous index
index: index - increment,
};
};
/**
* measure distance between x and a slide target
* @param {Number} x - horizontal position
* @param {Integer} index - slide index
* @returns {Number} - slide distance
*/
proto.getSlideDistance = function( x, index ) {
let len = this.slides.length;
// wrap around if at least 2 slides
let isWrapAround = this.options.wrapAround && len > 1;
let slideIndex = isWrapAround ? utils.modulo( index, len ) : index;
let slide = this.slides[ slideIndex ];
if ( !slide ) return null;
// add distance for wrap-around slides
let wrap = isWrapAround ? this.slideableWidth * Math.floor( index/len ) : 0;
return x - ( slide.target + wrap );
};
proto.dragEndBoostSelect = function() {
// do not boost if no previousDragX or dragMoveTime
if ( this.previousDragX === undefined || !this.dragMoveTime ||
// or if drag was held for 100 ms
new Date() - this.dragMoveTime > 100 ) {
return 0;
}
let distance = this.getSlideDistance( -this.dragX, this.selectedIndex );
let delta = this.previousDragX - this.dragX;
if ( distance > 0 && delta > 0 ) {
// boost to next if moving towards the right, and positive velocity
return 1;
} else if ( distance < 0 && delta < 0 ) {
// boost to previous if moving towards the left, and negative velocity
return -1;
}
return 0;
};
// ----- scroll ----- //
proto.onscroll = function() {
let scroll = getScrollPosition();
let scrollMoveX = this.pointerDownScroll.x - scroll.x;
let scrollMoveY = this.pointerDownScroll.y - scroll.y;
// cancel click/tap if scroll is too much
if ( Math.abs( scrollMoveX ) > 3 || Math.abs( scrollMoveY ) > 3 ) {
this.pointerDone();
}
};
// ----- utils ----- //
function getScrollPosition() {
return {
x: window.pageXOffset,
y: window.pageYOffset,
};
}
// ----- ----- //
return Flickity;
} ) );