scroll-swipe
Version: 
a lightweight Events API for detecting scroll and touch events based on custom sensitivity
428 lines (330 loc) • 9.73 kB
JavaScript
const VERTICAL = 'VERTICAL';
const HORIZONTAL = 'HORIZONTAL';
const acceptedParams = new Set([
  'target',
  'scrollSensitivity',
  'touchSensitivity',
  'scrollCb',
  'touchCb',
  'scrollPreventDefault',
  'touchPreventDefault',
  'addEventListenerOptions'
]);
const noop = () => {};
if (typeof module !== 'undefined') {
  module.exports = ScrollSwipe;
}
function ScrollSwipe(opts) {
  Object.keys(opts).forEach(key => {
    if (acceptedParams.has(key)) {
      this[key] = opts[key];
      return;
    }
    throw new Error(`unknown options for ScrollSwipe: ${key}`)
  });
  if (!opts.target) {
    throw new Error('must provide DOM target element to ScrollSwipe');
  }
  // https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/addEventListener#parameters
  this.addEventListenerOptions = this.addEventListenerOptions || {};
  if (!this.scrollSensitivity || this.scrollSensitivity < 0) {
    this.scrollSensitivity = 0;
  }
  if (!this.touchSensitivity || this.touchSensitivity < 0) {
    this.touchSensitivity = 0;
  }
  if (this.target.style || this.target.style.touchAction === '') {
    this.target.style.touchAction += 'manipulation'
  }
  this.scrollPending = false;
  this.startTouchEvent = null;
  this.latestTouchEvent = null;
  this.latestTouch = null;
  this.startScrollEvent = null;
  this.latestScrollEvent = null;
  this.latestScroll = null;
  this.intent = 0;
  this.currentDirection = VERTICAL;
  this.touchArr = [];
  this.xArr = [];
  this.yArr = [];
  this.touchArrX = [];
  this.touchArrY = [];
  this.intentMap = {
    'VERTICAL': {
      0: 'UP',
      1: 'DOWN'
    },
    'HORIZONTAL': {
      0: 'LEFT',
      1: 'RIGHT'
    }
  };
  this.init();
  return this;
}
ScrollSwipe.prototype.init = function init() {
  // only init if true
  if (this.scrollCb) {
    this.initScroll();
  }
  if (this.touchCb) {
    this.initTouch();
  }
  return this;
}
ScrollSwipe.prototype.listen = function listen() {
  this.flush();
  this.scrollPending = false;
  return this;
}
ScrollSwipe.prototype.onWheel = function onWheel(e) {
    if (this.scrollPreventDefault && !this.addEventListenerOptions.passive) {
      e.preventDefault();
    }
    if (this.scrollPending) {
      return;
    }
    this.startScrollEvent = e;
    const x = e.deltaX;
    const y = e.deltaY;
    this.addXScroll(x);
    this.addYScroll(y);
    this.scrollFulfilled((fulfilled, direction, intent) => {
      if (!fulfilled) {
        return;
      }
      this.lockout();
      this.latestScrollEvent = e;
      const result = this.buildResult({
        startEvent: this.latestScrollEvent,
        lastEvent: this.latestScrollEvent,
        direction,
        intent
      });
      this.scrollCb(result, this);
      this.undoLockout();
    });
}
ScrollSwipe.prototype.initScroll = function initScroll() {
  this.newOnWheel = this.onWheel.bind(this);
  if (this.target && this.target.addEventListener) {
    this.target.addEventListener('wheel', this.newOnWheel, this.addEventListenerOptions);
  }
  return this;
}
ScrollSwipe.prototype.touchMove = function touchMove(e) {
  if (this.touch && !this.addEventListenerOptions.passive) {
    e.preventDefault();
  }
  const changedTouches = e.changedTouches[0];
  const x = changedTouches.clientX;
  const y = changedTouches.clientY;
  this.startTouchEvent = e;
  this.addXTouch(x);
  this.addYTouch(y);
}
ScrollSwipe.prototype.buildResult = function buildResult({ startEvent, lastEvent, direction, intent }) {
  return {
    startEvent,
    lastEvent,
    direction,
    intent,
    scrollPending: this.scrollPending,
    mappedIntent: this.intentMap[direction][intent]
  };
}
ScrollSwipe.prototype.touchEnd = function touchEnd(e) {
  this.touchFulfilled(e, (fulfilled, direction, intent) => {
    if (!fulfilled) {
      return;
    }
    const result = this.buildResult({
      startEvent: this.startTouchEvent,
      lastEvent: this.latestTouchEvent,
      direction,
      intent
    });
    this.touchCb(result, this);
  });
}
ScrollSwipe.prototype.initTouch = function initTouch() {
  this.newTouchMove = this.touchMove.bind(this);
  this.newTouchEnd = this.touchEnd.bind(this);
  this.target.addEventListener('touchmove', this.newTouchMove, this.addEventListenerOptions);
  this.target.addEventListener('touchend', this.newTouchEnd, this.addEventListenerOptions);
  return this;
}
//touch events
ScrollSwipe.prototype.touchFulfilled = function touchFulfilled(e, cb) {
  if (!e) {
    throw new Error('must provide event to touchFulfilled');
  }
  if (!cb) {
    throw new Error('must provide callback to touchFulfilled');
  }
  const { touchSensitivity, touchArrX, touchArrY } = this;
  const bool = (touchArrX.length > touchSensitivity && touchArrY.length > touchSensitivity);
  if (!bool) {
    return cb(false, null);
  }
  const changedTouches = e.changedTouches[0];
  const xStart = touchArrX[0];
  const yStart = touchArrY[0];
  const xEnd = changedTouches.clientX;
  const yEnd = changedTouches.clientY;
  const xIntent = xStart < xEnd ? 0 : 1;
  const yIntent = yStart < yEnd ? 0 : 1;
  let direction = VERTICAL;
  //determine vertical or horizontal based on the greatest difference
  if ( Math.abs(xStart - xEnd) > Math.abs(yStart - yEnd) ) {
    direction = HORIZONTAL;
  }
  const intent = direction === VERTICAL ? yIntent : xIntent;
  swap.call(this, intent, direction);
  this.resetTouches();
  this.scrollPending = true;
  this.latestTouchEvent = e;
  cb(this.scrollPending, this.currentDirection, this.currentIntent);
  return this;
}
ScrollSwipe.prototype.getTouch = function getTouch(idx) {
  return this.touchArr[idx];
}
ScrollSwipe.prototype.addXTouch = function addTouch(touch) {
  if (this.pending()) {
    return this;
  }
  this.latestTouch = touch;
  this.touchArrX.push(touch);
  return this;
}
ScrollSwipe.prototype.addYTouch = function addTouch(touch) {
  if (this.pending()) {
    return this;
  }
  this.latestTouch = touch;
  this.touchArrY.push(touch);
  return this;
}
ScrollSwipe.prototype.resetTouches = function resetTouches() {
  this.touchArrX = [];
  this.touchArrY = [];
  return this;
}
//wheel events
ScrollSwipe.prototype.addXScroll = function addXScroll(s) {
  if (this.pending()) {
    return this;
  }
  this.latestScroll = s;
  this.xArr.push(s);
  return this;
}
ScrollSwipe.prototype.addYScroll = function addYScroll(s) {
  if (this.pending()) {
    return this;
  }
  this.latestScroll = s;
  this.yArr.push(s);
  return this;
}
ScrollSwipe.prototype.getDirection = function getDirection() {
  return this.currentDirection;
}
ScrollSwipe.prototype.resetScroll = function resetScroll() {
  this.xArr = [];
  this.yArr = [];
  return this;
}
ScrollSwipe.prototype.flush = function flush() {
  this.resetScroll();
  this.resetTouches();
  return this;
}
ScrollSwipe.prototype.lockout = function lockout() {
  this.originalAddXTouch = this.addXTouch;
  this.originalAddYTouch = this.addYTouch;
  this.originalAddXScroll = this.addXScroll;
  this.originalAddYScroll = this.addYScroll;
  this.addXScroll = noop;
  this.addYScroll = noop;
  this.addXTouch = noop;
  this.addYTouch = noop;
  return this;
};
ScrollSwipe.prototype.undoLockout = function undoLockout() {
  this.addXScroll = this.originalAddXScroll;
  this.addYScroll = this.originalAddYScroll;
  this.addXTouch = this.originalAddXTouch;
  this.addYTouch = this.originalAddYTouch;
  return this;
}
ScrollSwipe.prototype.scrollFulfilled = function scrollFulfilled(cb) {
  if (!cb) {
    throw new Error('must provide callback to scrollFulfilled');
  }
  const { xArr, yArr, scrollSensitivity } = this;
  const bool = (xArr.length > scrollSensitivity && yArr.length > scrollSensitivity);
  const { direction, intent } = this.evalScrollDirection();
  if (bool) {
    swap.call(this, intent, direction);
    this.resetScroll();
    this.scrollPending = true;
  }
  cb(this.scrollPending, this.currentDirection, this.currentIntent);
  return this;
}
ScrollSwipe.prototype.evalScrollDirection = function evalScrollDirection() {
  const { x, y, xIntent, yIntent } = this.getSums();
  const direction = x > y ? HORIZONTAL : VERTICAL;
  const base = direction === VERTICAL ? yIntent : xIntent;
  let intent = 0;
  if (base > 0) {
    intent = 1;
  }
  return { direction, intent };
}
ScrollSwipe.prototype.getSums = function getSums() {
  const { xArr, yArr } = this;
  let xIntent = 0;
  let yIntent = 0;
  const x = xArr.reduce((result, curr) => {
      xIntent = xIntent + curr;
      return result += Math.abs(curr);
    }, 0);
  const y = yArr.reduce((result, curr) => {
      yIntent = yIntent + curr;
      return result += Math.abs(curr);
    }, 0);
  return {x, y, xIntent, yIntent};
}
ScrollSwipe.prototype.getScrollDirection = function getScrollDirection() {
  return this.currentDirection;
}
ScrollSwipe.prototype.pending = function pending() {
  return this.scrollPending;
}
ScrollSwipe.prototype.killScroll = function killScroll() {
  if (this.target && this.target.removeEventListener) {
    this.target.removeEventListener('wheel', this.newOnWheel, false);
  }
  return this;
}
ScrollSwipe.prototype.killTouch = function killTouch() {
  if (this.target && this.target.removeEventListener) {
    this.target.removeEventListener('touchmove', this.newTouchMove, false);
    this.target.removeEventListener('touchend', this.newTouchEnd, false);
  }
  return this;
}
ScrollSwipe.prototype.killAll = function teardown() {
  this.killScroll().killTouch().flush();
  return this;
}
function swap(intent, direction) {
  this.previousIntent = this.currentIntent;
  this.currentIntent = intent;
  this.previousDirection = this.currentDirection;
  this.currentDirection = direction;
}