394 lines
11 KiB
JavaScript
394 lines
11 KiB
JavaScript
|
// drag
|
||
|
( function( window, factory ) {
|
||
|
// universal module definition
|
||
|
/* jshint strict: false */
|
||
|
if ( typeof define == 'function' && define.amd ) {
|
||
|
// AMD
|
||
|
define( [
|
||
|
'./flickity',
|
||
|
'unidragger/unidragger',
|
||
|
'fizzy-ui-utils/utils'
|
||
|
], function( Flickity, Unidragger, utils ) {
|
||
|
return factory( window, Flickity, Unidragger, utils );
|
||
|
});
|
||
|
} else if ( typeof module == 'object' && module.exports ) {
|
||
|
// CommonJS
|
||
|
module.exports = factory(
|
||
|
window,
|
||
|
require('./flickity'),
|
||
|
require('unidragger'),
|
||
|
require('fizzy-ui-utils')
|
||
|
);
|
||
|
} else {
|
||
|
// browser global
|
||
|
window.Flickity = factory(
|
||
|
window,
|
||
|
window.Flickity,
|
||
|
window.Unidragger,
|
||
|
window.fizzyUIUtils
|
||
|
);
|
||
|
}
|
||
|
|
||
|
}( window, function factory( window, Flickity, Unidragger, utils ) {
|
||
|
|
||
|
'use strict';
|
||
|
|
||
|
// ----- defaults ----- //
|
||
|
|
||
|
utils.extend( Flickity.defaults, {
|
||
|
draggable: '>1',
|
||
|
dragThreshold: 3,
|
||
|
});
|
||
|
|
||
|
// ----- create ----- //
|
||
|
|
||
|
Flickity.createMethods.push('_createDrag');
|
||
|
|
||
|
// -------------------------- drag prototype -------------------------- //
|
||
|
|
||
|
var proto = Flickity.prototype;
|
||
|
utils.extend( proto, Unidragger.prototype );
|
||
|
proto._touchActionValue = 'pan-y';
|
||
|
|
||
|
// -------------------------- -------------------------- //
|
||
|
|
||
|
var isTouch = 'createTouch' in document;
|
||
|
var isTouchmoveScrollCanceled = false;
|
||
|
|
||
|
proto._createDrag = function() {
|
||
|
this.on( 'activate', this.onActivateDrag );
|
||
|
this.on( 'uiChange', this._uiChangeDrag );
|
||
|
this.on( 'deactivate', this.onDeactivateDrag );
|
||
|
this.on( 'cellChange', this.updateDraggable );
|
||
|
// TODO updateDraggable on resize? if groupCells & slides change
|
||
|
// HACK - add seemingly innocuous handler to fix iOS 10 scroll behavior
|
||
|
// #457, RubaXa/Sortable#973
|
||
|
if ( isTouch && !isTouchmoveScrollCanceled ) {
|
||
|
window.addEventListener( 'touchmove', function() {});
|
||
|
isTouchmoveScrollCanceled = true;
|
||
|
}
|
||
|
};
|
||
|
|
||
|
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;
|
||
|
}
|
||
|
if ( this.isDraggable ) {
|
||
|
this.element.classList.add('is-draggable');
|
||
|
} else {
|
||
|
this.element.classList.remove('is-draggable');
|
||
|
}
|
||
|
};
|
||
|
|
||
|
// backwards compatibility
|
||
|
proto.bindDrag = function() {
|
||
|
this.options.draggable = true;
|
||
|
this.updateDraggable();
|
||
|
};
|
||
|
|
||
|
proto.unbindDrag = function() {
|
||
|
this.options.draggable = false;
|
||
|
this.updateDraggable();
|
||
|
};
|
||
|
|
||
|
proto._uiChangeDrag = function() {
|
||
|
delete this.isFreeScrolling;
|
||
|
};
|
||
|
|
||
|
// -------------------------- pointer events -------------------------- //
|
||
|
|
||
|
proto.pointerDown = function( event, pointer ) {
|
||
|
if ( !this.isDraggable ) {
|
||
|
this._pointerDownDefault( event, pointer );
|
||
|
return;
|
||
|
}
|
||
|
var isOkay = this.okayPointerDown( event );
|
||
|
if ( !isOkay ) {
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
this._pointerDownPreventDefault( event );
|
||
|
this.pointerDownFocus( event );
|
||
|
// blur
|
||
|
if ( document.activeElement != this.element ) {
|
||
|
// do not blur if already focused
|
||
|
this.pointerDownBlur();
|
||
|
}
|
||
|
|
||
|
// 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._pointerDownDefault( event, pointer );
|
||
|
};
|
||
|
|
||
|
// default pointerDown logic, used for staticClick
|
||
|
proto._pointerDownDefault = function( event, pointer ) {
|
||
|
// track start event position
|
||
|
// Safari 9 overrides pageX and pageY. These values needs to be copied. #779
|
||
|
this.pointerDownPointer = {
|
||
|
pageX: pointer.pageX,
|
||
|
pageY: pointer.pageY,
|
||
|
};
|
||
|
// bind move and end events
|
||
|
this._bindPostStartEvents( event );
|
||
|
this.dispatchEvent( 'pointerDown', event, [ pointer ] );
|
||
|
};
|
||
|
|
||
|
var focusNodes = {
|
||
|
INPUT: true,
|
||
|
TEXTAREA: true,
|
||
|
SELECT: true,
|
||
|
};
|
||
|
|
||
|
proto.pointerDownFocus = function( event ) {
|
||
|
var isFocusNode = focusNodes[ event.target.nodeName ];
|
||
|
if ( !isFocusNode ) {
|
||
|
this.focus();
|
||
|
}
|
||
|
};
|
||
|
|
||
|
proto._pointerDownPreventDefault = function( event ) {
|
||
|
var isTouchStart = event.type == 'touchstart';
|
||
|
var isTouchPointer = event.pointerType == 'touch';
|
||
|
var isFocusNode = focusNodes[ event.target.nodeName ];
|
||
|
if ( !isTouchStart && !isTouchPointer && !isFocusNode ) {
|
||
|
event.preventDefault();
|
||
|
}
|
||
|
};
|
||
|
|
||
|
// ----- move ----- //
|
||
|
|
||
|
proto.hasDragStarted = function( moveVector ) {
|
||
|
return Math.abs( moveVector.x ) > this.options.dragThreshold;
|
||
|
};
|
||
|
|
||
|
// ----- up ----- //
|
||
|
|
||
|
proto.pointerUp = function( event, pointer ) {
|
||
|
delete this.isTouchScrolling;
|
||
|
this.viewport.classList.remove('is-pointer-down');
|
||
|
this.dispatchEvent( 'pointerUp', event, [ pointer ] );
|
||
|
this._dragPointerUp( event, pointer );
|
||
|
};
|
||
|
|
||
|
proto.pointerDone = function() {
|
||
|
window.removeEventListener( 'scroll', this );
|
||
|
delete this.pointerDownScroll;
|
||
|
};
|
||
|
|
||
|
// -------------------------- dragging -------------------------- //
|
||
|
|
||
|
proto.dragStart = function( event, pointer ) {
|
||
|
if ( !this.isDraggable ) {
|
||
|
return;
|
||
|
}
|
||
|
this.dragStartPosition = this.x;
|
||
|
this.startAnimation();
|
||
|
window.removeEventListener( 'scroll', this );
|
||
|
this.dispatchEvent( 'dragStart', event, [ pointer ] );
|
||
|
};
|
||
|
|
||
|
proto.pointerMove = function( event, pointer ) {
|
||
|
var moveVector = this._dragPointerMove( event, pointer );
|
||
|
this.dispatchEvent( 'pointerMove', event, [ pointer, moveVector ] );
|
||
|
this._dragMove( event, pointer, moveVector );
|
||
|
};
|
||
|
|
||
|
proto.dragMove = function( event, pointer, moveVector ) {
|
||
|
if ( !this.isDraggable ) {
|
||
|
return;
|
||
|
}
|
||
|
event.preventDefault();
|
||
|
|
||
|
this.previousDragX = this.dragX;
|
||
|
// reverse if right-to-left
|
||
|
var direction = this.options.rightToLeft ? -1 : 1;
|
||
|
if ( this.options.wrapAround ) {
|
||
|
// wrap around move. #589
|
||
|
moveVector.x = moveVector.x % this.slideableWidth;
|
||
|
}
|
||
|
var dragX = this.dragStartPosition + moveVector.x * direction;
|
||
|
|
||
|
if ( !this.options.wrapAround && this.slides.length ) {
|
||
|
// slow drag
|
||
|
var originBound = Math.max( -this.slides[0].target, this.dragStartPosition );
|
||
|
dragX = dragX > originBound ? ( dragX + originBound ) * 0.5 : dragX;
|
||
|
var endBound = Math.min( -this.getLastSlide().target, this.dragStartPosition );
|
||
|
dragX = dragX < endBound ? ( dragX + endBound ) * 0.5 : dragX;
|
||
|
}
|
||
|
|
||
|
this.dragX = dragX;
|
||
|
|
||
|
this.dragMoveTime = new Date();
|
||
|
this.dispatchEvent( 'dragMove', event, [ pointer, moveVector ] );
|
||
|
};
|
||
|
|
||
|
proto.dragEnd = function( event, pointer ) {
|
||
|
if ( !this.isDraggable ) {
|
||
|
return;
|
||
|
}
|
||
|
if ( this.options.freeScroll ) {
|
||
|
this.isFreeScrolling = true;
|
||
|
}
|
||
|
// set selectedIndex based on where flick will end up
|
||
|
var index = this.dragEndRestingSelect();
|
||
|
|
||
|
if ( this.options.freeScroll && !this.options.wrapAround ) {
|
||
|
// 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
|
||
|
var restingX = this.getRestingPosition();
|
||
|
this.isFreeScrolling = -restingX > this.slides[0].target &&
|
||
|
-restingX < this.getLastSlide().target;
|
||
|
} else if ( !this.options.freeScroll && index == this.selectedIndex ) {
|
||
|
// boost selection if selected index has not changed
|
||
|
index += this.dragEndBoostSelect();
|
||
|
}
|
||
|
delete this.previousDragX;
|
||
|
// apply selection
|
||
|
// TODO refactor this, selecting here feels weird
|
||
|
// HACK, set flag so dragging stays in correct direction
|
||
|
this.isDragSelect = this.options.wrapAround;
|
||
|
this.select( index );
|
||
|
delete this.isDragSelect;
|
||
|
this.dispatchEvent( 'dragEnd', event, [ pointer ] );
|
||
|
};
|
||
|
|
||
|
proto.dragEndRestingSelect = function() {
|
||
|
var restingX = this.getRestingPosition();
|
||
|
// how far away from selected slide
|
||
|
var distance = Math.abs( this.getSlideDistance( -restingX, this.selectedIndex ) );
|
||
|
// get closet resting going up and going down
|
||
|
var positiveResting = this._getClosestResting( restingX, distance, 1 );
|
||
|
var negativeResting = this._getClosestResting( restingX, distance, -1 );
|
||
|
// use closer resting for wrap-around
|
||
|
var index = positiveResting.distance < negativeResting.distance ?
|
||
|
positiveResting.index : negativeResting.index;
|
||
|
return 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 ) {
|
||
|
var index = this.selectedIndex;
|
||
|
var minDistance = Infinity;
|
||
|
var condition = this.options.contain && !this.options.wrapAround ?
|
||
|
// if contain, keep going if distance is equal to minDistance
|
||
|
function( d, md ) { return d <= md; } : function( d, md ) { return d < md; };
|
||
|
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
|
||
|
* @param {Integer} index - slide index
|
||
|
*/
|
||
|
proto.getSlideDistance = function( x, index ) {
|
||
|
var len = this.slides.length;
|
||
|
// wrap around if at least 2 slides
|
||
|
var isWrapAround = this.options.wrapAround && len > 1;
|
||
|
var slideIndex = isWrapAround ? utils.modulo( index, len ) : index;
|
||
|
var slide = this.slides[ slideIndex ];
|
||
|
if ( !slide ) {
|
||
|
return null;
|
||
|
}
|
||
|
// add distance for wrap-around slides
|
||
|
var 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;
|
||
|
}
|
||
|
|
||
|
var distance = this.getSlideDistance( -this.dragX, this.selectedIndex );
|
||
|
var 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;
|
||
|
};
|
||
|
|
||
|
// ----- staticClick ----- //
|
||
|
|
||
|
proto.staticClick = function( event, pointer ) {
|
||
|
// get clickedCell, if cell was clicked
|
||
|
var clickedCell = this.getParentCell( event.target );
|
||
|
var cellElem = clickedCell && clickedCell.element;
|
||
|
var cellIndex = clickedCell && this.cells.indexOf( clickedCell );
|
||
|
this.dispatchEvent( 'staticClick', event, [ pointer, cellElem, cellIndex ] );
|
||
|
};
|
||
|
|
||
|
// ----- scroll ----- //
|
||
|
|
||
|
proto.onscroll = function() {
|
||
|
var scroll = getScrollPosition();
|
||
|
var scrollMoveX = this.pointerDownScroll.x - scroll.x;
|
||
|
var 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;
|
||
|
|
||
|
}));
|