UNPKG

flickity

Version:

Touch, responsive, flickable carousels

293 lines (247 loc) 9.07 kB
// 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; } ) );