Skip to content

Instantly share code, notes, and snippets.

@jorgeortega
Last active March 20, 2018 15:21
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save jorgeortega/56d8159ec9b0afaebcd0bb05403e8832 to your computer and use it in GitHub Desktop.
Save jorgeortega/56d8159ec9b0afaebcd0bb05403e8832 to your computer and use it in GitHub Desktop.
Utility class to handle events of a scroller wheel
import throttle from 'lodash/throttle'
export class Wheel {
constructor ({ id, wheelEndCallback }) {
this.initialPointerY = -1
this.pointerY = 0
this.currentItemIndex = -1
this.elementHeight = -1
this.childrenHeight = -1
this.isDragging = false
this.id = id
this.wheelEndCallback = wheelEndCallback
this.theta = -1
this.rotation = -1
}
// Gets the initial element position
// at touchstart or mousedown
start (e) {
const event = e || window.event
this.isDragging = true
if (event) {
// touch event
if (event.type === 'touchstart' && event.changedTouches && event.changedTouches.length) {
this.initialPointerY = event.changedTouches[0].clientY
// mouse event
} else if (event.type === 'mousedown' && event.y) {
this.initialPointerY = event.y
}
}
}
// Touchmove or mousemove event
// Calculates the position of the pointer as it moves
move (e) {
const event = e || window.event
if (event) {
e.preventDefault()
e.stopPropagation()
if (this.isDragging) {
this.handleMoveEvent(event)
} else {
if (event.type === 'wheel') {
this.handleMouseWheelEvent(event.deltaY)
}
}
}
return false
}
handleMoveEvent (event) {
if (event.type === 'touchmove' && event.changedTouches && event.changedTouches.length) {
this.pointerY = event.changedTouches[0].clientY
} else if (event.type === 'mousemove') {
this.pointerY = event.clientY
}
const pathLength = this.initialPointerY - this.pointerY
let wheelDirection = 0
// if movement is small
if (Math.abs(pathLength) < 10) {
wheelDirection = 0
} else {
// checks if it should spin up or down
wheelDirection = this.initialPointerY - this.pointerY > 0 ? -1 : 1
}
this.moveCarouselSteps(wheelDirection)
this.initialPointerY = this.pointerY
}
handleMouseWheelEvent (deltaY) {
if (deltaY > 0) {
this.moveCarouselSteps(1)
} else {
this.moveCarouselSteps(-1)
}
}
moveCarouselSteps (n) {
if (Math.abs(n) > this.childrenLength - 1) {
throw new Error('Number of steps should be less than the number of wheel items')
}
const lastItemIndex = this.childrenLength - 1
if (n > 0) {
// if it goes before last item
if (this.currentItemIndex + n <= lastItemIndex) {
this.currentItemIndex += n
// if it goes beyond the last item, it should jump to the beginning
// and continue counting
// number of steps n, minus the last one
} else if (this.currentItemIndex + n > lastItemIndex) {
this.currentItemIndex = n - 1
}
} else {
// n is negative in this case
// you still have to add +
// if it's still greater or equal zero
if (this.currentItemIndex + n >= 0) {
this.currentItemIndex = this.currentItemIndex + n
// if it's less than zero, it should jump to last position and
// and keep counting backwards
// n plus the first one
} else if (this.currentItemIndex + n < 0) {
this.currentItemIndex = lastItemIndex + (n + 1)
}
}
// 1 -> forward, -1 -> backwards
this.rotation += this.theta * n * -1
this.rotateCarousel()
// callback to send the index to the parent component
this.wheelEndCallback({
currentItemIndex: this.currentItemIndex,
id: this.id
})
}
setWheel (index) {
this.currentItemIndex = index
this.rotation = this.theta * index * -1
this.rotateCarousel()
}
// touchend or mouseup event
end () {
if (this.isDragging) {
// end dragging
this.isDragging = false
// make last position the next initial point
this.initialPointerY = this.pointerY
}
}
selectItem (wheelIndex) {
this.removeClass('selected')
this.element.children[wheelIndex].classList.add('selected')
}
// remove className from all elements with that class
removeClass (className) {
const matchedElements = this.element.getElementsByClassName(className)
for (const match of matchedElements) {
match.classList.remove(className)
}
}
// calculates radius and angle of each item in element to make it a 3D carousel
create3DCarousel () {
let panel, angle, i
this.theta = 360 / this.childrenLength
// do some trig to figure out how big the carousel is in 3D space
this.radius = Math.round((this.elementHeight / 2) / Math.tan(Math.PI / this.childrenLength))
for (i = 0; i < this.childrenLength; i++) {
panel = this.element.children[i]
angle = this.theta * i
// rotate panel, then push it out in 3D space
panel.style['transform'] = 'rotateX(' + angle + 'deg) translateZ(' + this.radius + 'px)'
}
// adjust rotation so panels are always flat
this.rotation = Math.round(this.rotation / this.theta) * this.theta
this.rotateCarousel()
}
// rotates the whole element
rotateCarousel () {
// push the carousel back in 3D space and rotate it
this.element.style['transform'] = 'translateZ(-' + this.radius + 'px) ' + 'rotateX(' + this.rotation + 'deg)'
if (this.currentItemIndex > 0) {
this.selectItem(this.currentItemIndex)
}
}
// get the element and register all the events necessary for dragging
register (element) {
if (element) {
this.element = element
this.childrenLength = this.element.children.length
this.childrenHeight = this.element.firstElementChild.offsetHeight
this.elementHeight = this.element.offsetHeight
this.create3DCarousel()
element.addEventListener('mousedown', this.start.bind(this))
element.addEventListener('touchstart', this.start.bind(this))
element.addEventListener('mousemove', throttle(this.move.bind(this), 100, { leading: true }))
element.addEventListener('touchmove', throttle(this.move.bind(this), 100, { leading: true }))
element.addEventListener('wheel', this.move.bind(this))
element.addEventListener('mouseup', this.end.bind(this))
element.addEventListener('mouseleave', this.end.bind(this))
element.addEventListener('touchend', this.end.bind(this))
element.addEventListener('touchcancel', this.end.bind(this))
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment