UNPKG

flickity

Version:

Touch, responsive, flickable carousels

385 lines (329 loc) 11.4 kB
// 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: true, dragThreshold: 3, }); // ----- create ----- // Flickity.createMethods.push('_createDrag'); // -------------------------- drag prototype -------------------------- // var proto = Flickity.prototype; utils.extend( proto, Unidragger.prototype ); // -------------------------- -------------------------- // var isTouch = 'createTouch' in document; var isTouchmoveScrollCanceled = false; proto._createDrag = function() { this.on( 'activate', this.bindDrag ); this.on( 'uiChange', this._uiChangeDrag ); this.on( 'childUIPointerDown', this._childUIPointerDownDrag ); this.on( 'deactivate', this.unbindDrag ); // 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.bindDrag = function() { if ( !this.options.draggable || this.isDragBound ) { return; } this.element.classList.add('is-draggable'); this.handles = [ this.viewport ]; this.bindHandles(); this.isDragBound = true; }; proto.unbindDrag = function() { if ( !this.isDragBound ) { return; } this.element.classList.remove('is-draggable'); this.unbindHandles(); delete this.isDragBound; }; proto._uiChangeDrag = function() { delete this.isFreeScrolling; }; proto._childUIPointerDownDrag = function( event ) { event.preventDefault(); this.pointerDownFocus( event ); }; // -------------------------- pointer events -------------------------- // // nodes that have text fields var cursorNodes = { TEXTAREA: true, INPUT: true, OPTION: true, }; // input types that do not have text fields var clickTypes = { radio: true, checkbox: true, button: true, submit: true, image: true, file: true, }; proto.pointerDown = function( event, pointer ) { // dismiss inputs with text fields. #403, #404 var isCursorInput = cursorNodes[ event.target.nodeName ] && !clickTypes[ event.target.type ]; if ( isCursorInput ) { // reset pointerDown logic this.isPointerDown = false; delete this.pointerIdentifier; return; } this._dragPointerDown( event, pointer ); // kludge to blur focused inputs in dragger var focused = document.activeElement; if ( focused && focused.blur && focused != this.element && // do not blur body for IE9 & 10, #117 focused != document.body ) { focused.blur(); } this.pointerDownFocus( event ); // stop if it was moving this.dragX = this.x; this.viewport.classList.add('is-pointer-down'); // bind move and end events this._bindPostStartEvents( event ); // track scrolling this.pointerDownScroll = getScrollPosition(); window.addEventListener( 'scroll', this ); this.dispatchEvent( 'pointerDown', event, [ pointer ] ); }; var touchStartEvents = { touchstart: true, MSPointerDown: true }; var focusNodes = { INPUT: true, SELECT: true }; proto.pointerDownFocus = function( event ) { // focus element, if not touch, and its not an input or select if ( !this.options.accessibility || touchStartEvents[ event.type ] || focusNodes[ event.target.nodeName ] ) { return; } var prevScrollY = window.pageYOffset; this.element.focus(); // hack to fix scroll jump after focus, #76 if ( window.pageYOffset != prevScrollY ) { window.scrollTo( window.pageXOffset, prevScrollY ); } }; proto.canPreventDefaultOnPointerDown = function( event ) { // prevent default, unless touchstart or <select> var isTouchstart = event.type == 'touchstart'; var targetNodeName = event.target.nodeName; return !isTouchstart && targetNodeName != 'SELECT'; }; // ----- 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 ) { 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 ) { event.preventDefault(); this.previousDragX = this.dragX; // reverse if right-to-left var direction = this.options.rightToLeft ? -1 : 1; 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.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; }));