vue-ssr-carousel
Version:
A performance focused Vue carousel designed for SSR/SSG environments.
261 lines (206 loc) • 9.18 kB
text/coffeescript
###
Code related to handling dragging of the track
###
passive = passive: true
notPassive = passive: false
export default
props:
# Boundary drag dampening modifier. Increase to allow greater travel outside
# the boundaries.
boundaryDampening:
type: Number
default: 0.6
# The percentage of a pageWidth that was dragged before we advance to
# another page on slide
dragAdvanceRatio:
type: Number
default: .33
# The ratio of X:Y mouse travel. Decrease this number to allow for greater
# y dragging before the drag is cancelled.
verticalDragTreshold:
type: Number
default: 1
# Turn off draggability
noDrag: Boolean
data: ->
pressing: false # The user pressing pointer down
dragging: false # The user has translated while pointer was down
isTouchDrag: false # Is the browser firing touch events
startPointer: null # Where was the mouse when the drag started
lastPointer: null # Where was the mouse on the last move event
dragVelocity: null # The px/tick while dragging, negative is rightward
dragDirectionRatio: null # The ratio of horizontal vs vertical dragging
# Cleanup listeners
beforeDestroy: ->
window.removeEventListener 'mousemove', @onPointerMove, passive
window.removeEventListener 'mouseup', @onPointerUp, passive
window.removeEventListener 'touchmove', @onPointerMove, passive
window.removeEventListener 'touchend', @onPointerUp, passive
window.removeEventListener 'touchmove', @onWinMove, notPassive
window.removeEventListener 'contextmenu', @onPointerUp, passive
window.removeEventListener 'touchmove', @stopEvent, notPassive
computed:
# The current slide or page index. It rounds differently depending on the
# direction of the velocity. So that it eases to a stop in the direction
# the user was dragging.
dragIndex: -> switch
# If there is very little velocity, go to the closet page
when Math.abs(@dragVelocity) <= 2 then Math.round @fractionalIndex
# User was moving forward
when @dragVelocity < 0 then Math.ceil @fractionalIndex
# User was moving backward
else Math.floor @fractionalIndex
# Determine the current index given the currentX as a fraction. For
# instance, when dragging forward, it will be like 0.1 and when you've
# dragged almost a full page, forward it would be 0.9. This got
# complicated because the final page may not have a full compliment of
# slides like if we have 2 per page and 3 slides. When you have tweened
# to the 2nd page, the fractionalIndex should be 2 even though you
# haven't traveled the same width as it took to get from 1 to 2.
fractionalIndex: ->
return 0 unless @trackWidth
if @isVariableWidth then return @fractionalIndexFromMeasurements
# Work in positive numbers
x = @currentX * -1
# Figure out what set we're in, like if, through looping, we've gone
# through all the pages multiple times.
setIndex = Math.floor x / @trackWidth
# Figure out the index of last page of the set that has been fully
# scrolled into. Not using modulo for this because I got rounding errors.
widthDivisor = if @paginateBySlide then @slideWidth else @pageWidth
pageIndex = Math.floor (x - setIndex * @trackWidth) / widthDivisor
# Figure out the progress into the current page
distanceIntoPage = x - setIndex * @trackWidth - pageIndex * widthDivisor
# Determine if we're on the last page. If we're not looping, an extra
# "page" of slides is treated as part of the last page because of how we
# end with the slides flush with the right edge.
slidesPerPage = @currentSlidesPerPage
remainingSlides = switch
when @shouldLoop then @slidesCount - pageIndex * slidesPerPage
else @slidesCount - (pageIndex + 1) * slidesPerPage
isLastPage = remainingSlides <= slidesPerPage
# Make a percentage of travel into the page
pageWidth = if isLastPage then @lastPageWidth else widthDivisor
pageProgressPercent = distanceIntoPage / pageWidth
# Return the final value by adding all the passed index values
return pageProgressPercent + setIndex * @pages + pageIndex
fractionalIndexFromMeasurements: ->
# Work in positive numbers
x = @currentX * -1
slideIdx = @measuredSlidesWidths.findIndex((measuredSlide) => measuredSlide.targetXScroll > x) - 1
percentage = (x - @measuredSlidesWidths[slideIdx].targetXScroll) / @measuredSlidesWidths[slideIdx].width
return slideIdx + percentage
# Determine if the user is dragging vertically
isVerticalDrag: ->
return unless @dragDirectionRatio
@dragDirectionRatio < @verticalDragTreshold
# If we're horiztonally swiping on a touch device, prevent vertical scroll
preventVerticalScroll: ->
@pressing and @isTouchDrag and not @isVerticalDrag
watch:
# Watch for mouse move changes when the user starts dragging
pressing: ->
# Determine the type of event
[ moveEvent, upEvent ] = if @isTouchDrag
then [ 'touchmove', 'touchend' ]
else [ 'mousemove', 'mouseup' ]
# Pointer is down, start watching for drags
if @pressing
window.addEventListener moveEvent, @onPointerMove, passive
window.addEventListener upEvent, @onPointerUp, passive
window.addEventListener 'contextmenu', @onPointerUp, passive
@dragVelocity = 0 # Reset any previous velocity
@preventContentDrag()
@stopTweening()
# The pointer is up, so tween to final position
else
# Tween so the track is in bounds if it was out
if @isOutOfBounds and not @shouldLoop
if @currentX >= 0 then @gotoStart()
else @gotoEnd()
# If rendering variable width slides, don't come to a rest at an index
else if @isVariableWidth then @goto @dragIndex
# If user was vertically dragging, reset the index
else if @isVerticalDrag then @goto @index
# Handle normal swiping
else @goto @dragIndex
# Cleanup vars and listeners
window.removeEventListener moveEvent, @onPointerMove, passive
window.removeEventListener upEvent, @onPointerUp, passive
window.removeEventListener 'contextmenu', @onPointerUp, passive
@dragging = false
@startPointer = @lastPointer = @dragDirectionRatio = null
# Fire events
if @pressing
then @$emit 'press'
else @$emit 'release'
# Fire events related to dragging
dragging: ->
if @dragging
then @$emit 'drag:start'
else @$emit 'drag:end'
# If the user is dragging vertically, end the drag based on the assumption
# that the user is attempting to scroll the page via touch rather than
# pan the carousel.
isVerticalDrag: ->
return unless @isVerticalDrag and @isTouchDrag
@pressing = false
# Stop vertical scrolling by listening for touchmove events on the body
# and cancel them. Need to explicitly set pasive because some mobile
# browsers set to true by default.
preventVerticalScroll: (shouldPrevent) ->
if shouldPrevent
then window.addEventListener 'touchmove', @stopEvent, notPassive
else window.removeEventListener 'touchmove', @stopEvent, notPassive
methods:
# Cancel an Event
stopEvent: (e) ->
if e.cancelable
then e.preventDefault()
# Keep track of whether user is dragging
onPointerDown: (pointerEvent) ->
@isTouchDrag = TouchEvent? and pointerEvent instanceof TouchEvent
@startPointer = @lastPointer = @getPointerCoords pointerEvent
@pressing = true
@usingKeyboard = false
# Keep track of release of press
onPointerUp: -> @pressing = false
# Keep x values up to date while dragging
onPointerMove: (pointerEvent) ->
# Mark the carousel as dragging, which is used to disable clicks
@dragging = true unless @dragging
# Calculated how much drag has happened since the list move
pointer = @getPointerCoords pointerEvent
@dragVelocity = pointer.x - @lastPointer.x
@targetX += @dragVelocity
@lastPointer = pointer
# Caculate the drag direction ratio
@dragDirectionRatio = Math.abs(
(pointer.x - @startPointer.x) /
(pointer.y - @startPointer.y)
)
# Update the track position
@currentX = @applyBoundaryDampening @targetX
# Helper to get the x position of either a touch or mouse event
getPointerCoords: (pointerEvent) ->
x: pointerEvent.touches?[0]?.pageX || pointerEvent.pageX
y: pointerEvent.touches?[0]?.pageY || pointerEvent.pageY
# Prevent dragging from exceeding the min/max edges
applyBoundaryDampening: (x) -> switch
when @shouldLoop then x # Don't apply dampening
when x > 0 then Math.pow x, @boundaryDampening
when x < @endX then @endX - Math.pow @endX - x, @boundaryDampening
else @applyXBoundaries x
# Constraint the x value to the min and max values
applyXBoundaries: (x) ->
if @shouldLoop then x # Don't apply boundaries
else Math.max @endX, Math.min 0, x
# Prevent the anchors and images from being draggable (like via their
# ghost outlines). Using this approach because the draggable html attribute
# didn't work in FF. This only needs to be run once.
preventContentDrag: ->
return if @contentDragPrevented
@$refs.track.$el.querySelectorAll 'a, img'
.forEach (el) -> el.addEventListener 'dragstart', (e) ->
e.preventDefault()
@contentDragPrevented = true