event/cl.event.handler.js

/**
 * Copyright (c) 2018-present clchart Contributors.
 *
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 *
 */

/*
  The canvas needs to have two methods
  eventCanvas: {
    addEventListener: () => {},
    removeEventListener: () => {}
  }
*/

/*
  // Event object required attributes
  event: {
    touches: [
      pageX: 0,
      pageY: 0,
      offsetX: 0,
      offsetY: 0,
    ],
    changedTouches: [],
    target: {
      getBoundingClientRect: () => {}
    },
    preventDefault: () => {}
  }
*/

/**
 * Get distance
 * @param {Number} xLen
 * @param {Number} yLen
 * @return {Number}
 */
function _getDistance (xLen, yLen) {
  return Math.sqrt(xLen * xLen + yLen * yLen)
}
/**
 * Get the rotation direction of the vector
 */
function _getRotateDirection (vector1, vector2) {
  return vector1.x * vector2.y - vector2.x * vector1.y
}

function _getRotateAngle (vector1, vector2) {
  let direction = _getRotateDirection(vector1, vector2)
  direction = direction > 0 ? -1 : 1
  const len1 = _getDistance(vector1.x, vector1.y)
  const len2 = _getDistance(vector2.x, vector2.y)
  const mr = len1 * len2
  if (mr === 0) return 0
  const dot = vector1.x * vector2.x + vector1.y * vector2.y
  let r = dot / mr
  if (r > 1) r = 1
  if (r < -1) r = -1
  return Math.acos(r) * direction * 180 / Math.PI
}

function _getTouchInfo (point, element) {
  const mouseInfo = {
    name: 'touch'
  }
  if (point.offsetX && point.offsetX === 0) {
    mouseInfo.offsetX = point.offsetX
    mouseInfo.offsetY = point.offsetY
  } else {
    let srcRect = {
      left: 0,
      top: 0
    }
    if (element && typeof element.getBoundingClientRect === 'function') srcRect = element.getBoundingClientRect()
    mouseInfo.offsetX = point.pageX - srcRect.left
    mouseInfo.offsetY = point.pageY - srcRect.top
  }
  return mouseInfo
}

function _getEventInfo (event) {
  return {
    offsetX: event.offsetX,
    offsetY: event.offsetY
  }
}

/**
 * Class representing ClEventHandler
 * @export
 * @class ClEventHandler
 */
export default class ClEventHandler {
  /**
   * Creates an instance of ClEventHandler.
   * @param {Object} {
   * father,
   * eventBuild,
   * isTouch
   * }
   * @constructor
   */
  constructor ({ father, eventBuild, isTouch }) {
    this.father = father
    this.eventCanvas = father.eventCanvas
    if (typeof eventBuild === 'function') {
      this.eventBuild = eventBuild
    } else {
      this.eventBuild = (e) => e
    }
    // Determine whether touch event is supported
    this.isTouch = !!isTouch

    // Remove long press popup button
    this.eventCanvas.addEventListener && this.eventCanvas.addEventListener('contextmenu', e => {
      e.preventDefault()
    })
  }
  /**
   * bind evnet
   * @memberof ClEventHandler
   */
  bindEvent () {
    if (this.isTouch) {
      this.addHandler('touchstart', this.touchstart.bind(this))
      this.addHandler('touchend', this.touchend.bind(this))
      this.addHandler('touchmove', this.touchmove.bind(this))
    } else {
      this.addHandler('mousemove', this.mousemove.bind(this))
      // this.addHandler('mousein', this.mousein.bind(this));
      this.addHandler('mouseout', this.mouseout.bind(this))
      this.addHandler('mousewheel', this.mousewheel.bind(this))
      this.addHandler('mouseup', this.mouseup.bind(this))
      this.addHandler('mousedown', this.mousedown.bind(this))
      this.addHandler('keyup', this.keyup.bind(this))
      this.addHandler('keydown', this.keydown.bind(this))

      this.addHandler('click', this.click.bind(this))
    }
  }
  /**
   * clear event listener
   * @memberof ClEventHandler
   */
  clearEvent () {
    if (this.isTouch) {
      this.clearHandler('touchstart', this.touchstart.bind(this))
      this.clearHandler('touchend', this.touchend.bind(this))
      this.clearHandler('touchmove', this.touchmove.bind(this))
    } else {
      this.clearHandler('mousemove', this.mousemove.bind(this))
      // this.clearHandler('mousein', this.mousein.bind(this));
      this.clearHandler('mouseout', this.mouseout.bind(this))
      this.clearHandler('mousewheel', this.mousewheel.bind(this))
      this.clearHandler('mouseup', this.mouseup.bind(this))
      this.clearHandler('mousedown', this.mousedown.bind(this))
      this.clearHandler('keyup', this.keyup.bind(this))
      this.clearHandler('keydown', this.keydown.bind(this))

      this.clearHandler('click', this.click.bind(this))
    }
  }
  /**
   * add handle for events
   * @param {String} eventName
   * @param {Function} handler
   * @memberof ClEventHandler
   */
  addHandler (eventName, handler) {
    if (this.eventCanvas.addEventListener) {
      this.eventCanvas.addEventListener(eventName, handler, false)
    } else if (this.eventCanvas.attachEvent) {
      this.eventCanvas.attachEvent('on' + eventName, handler)
    } else {
      this.eventCanvas['on' + eventName] = handler
    }
  }
  /**
   * Clean up all binding events
   * @param {String} eventName
   * @param {Function} handler
   * @memberof ClEventHandler
   */
  clearHandler (eventName, handler) {
    if (this.eventCanvas.removeEventListener) {
      this.eventCanvas.removeEventListener(eventName, handler, false)
    } else if (this.eventCanvas.deattachEvent) {
      this.eventCanvas.deattachEvent('on' + eventName, handler)
    } else {
      this.eventCanvas['on' + eventName] = null
    }
  }
  // The following is an event handler
  /**
   * mouse move
   * @param {Object} event
   * @memberof ClEventHandler
   */
  mousemove (event) {
    this.father.emitEvent('onMouseMove', _getEventInfo(event))
  }
  /**
   * mouse in
   * @param {Object} event
   * @memberof ClEventHandler
   */
  mousein (event) {
    this.father.emitEvent('onMouseIn', _getEventInfo(event))
  }
  /**
   * mouse out
   * @param {Object} event
   * @memberof ClEventHandler
   */
  mouseout (event) {
    this.father.emitEvent('onMouseOut', _getEventInfo(event))
  }
  /**
   * mouse whell
   * @param {Object} event
   * @memberof ClEventHandler
   */
  mousewheel (event) {
    const info = _getEventInfo(event)
    info.deltaY = event.deltaY
    this.father.emitEvent('onMouseWheel', info)
  }
  /**
   * mouse up
   * @param {Object} event
   * @memberof ClEventHandler
   */
  mouseup (event) {
    this.father.emitEvent('onMouseUp', _getEventInfo(event))
  }
  /**
   * mouse down
   * @param {Object} event
   * @memberof ClEventHandler
   */
  mousedown (event) {
    this.father.emitEvent('onMouseDown', _getEventInfo(event))
  }
  // keyboard event
  /**
   * key up
   * @param {Object} event
   * @memberof ClEventHandler
   */
  keyup (event) {
    const info = _getEventInfo(event)
    info.keyCode = event.keyCode
    this.father.emitEvent('onKeyUp', info)
  }
  /**
   * key down
   * @param {Object} event
   * @memberof ClEventHandler
   */
  keydown (event) {
    const info = _getEventInfo(event)
    info.keyCode = event.keyCode
    this.father.emitEvent('onKeyDown', info)
  }
  /**
   * click event
   * @param {Object} event
   * @memberof ClEventHandler
   */
  click (event) {
    this.father.emitEvent('onClick', _getEventInfo(event))
  }
  /**
   * touchstart
   * @param {Object} e
   * @memberof ClEventHandler
   */
  touchstart (e) {
    const event = this.eventBuild(e)
    this.__timestamp = new Date()
    const point = event.touches ? event.touches[0] : event
    this.startX = point.pageX
    this.startY = point.pageY
    clearTimeout(this.longTapTimeout)
    this.startTime = Date.now()
    // Two-point touch
    if (event.touches.length > 1) {
      const point2 = event.touches[1]
      const xLen = Math.abs(point2.pageX - this.startX)
      const yLen = Math.abs(point2.pageY - this.startY)
      this.touchDistance = _getDistance(xLen, yLen)
      this.touchVector = {
        x: point2.pageX - this.startX,
        y: point2.pageY - this.startY
      }
    } else {
      this.longTapTimeout = setTimeout(() => {
        this.father.emitEvent('onLongPress', _getTouchInfo(point, event.target))
      }, 600)
      if (this.previousTouchPoint) {
        if (Math.abs(this.startX - this.previousTouchPoint.startX) < 10 &&
          Math.abs(this.startY - this.previousTouchPoint.startY) < 10 &&
          Math.abs(this.startTime - this.previousTouchTime) < 300) {
          this.father.emitEvent('onDoubleClick', _getTouchInfo(point, event.target))
        }
      }
      this.previousTouchTime = this.startTime
      this.previousTouchPoint = {
        startX: this.startX,
        startY: this.startY
      }
    }
  }
  /**
   * touchend
   * @param {Object} e
   * @memberof ClEventHandler
   */
  touchend (e) {
    const event = this.eventBuild(e)
    clearTimeout(this.longTapTimeout)
    const point = event.changedTouches ? event.changedTouches[0] : event
    const timestamp = Date.now()
    if ((this.moveX !== null && Math.abs(this.moveX - this.startX) > 10) ||
      (this.moveY !== null && Math.abs(this.moveY - this.startY) > 10)) {
      if (timestamp - this.startTime < 500) {
        this.father.emitEvent('onSwipe', _getTouchInfo(point, event.target))
      }
    } else if (timestamp - this.startTime < 2000) {
      if (timestamp - this.startTime < 500) {
        this.father.emitEvent('onClick', _getTouchInfo(point, event.target))
      }
      if (timestamp - this.startTime > 500) {
        this.father.emitEvent('onLongPress', _getTouchInfo(point, event.target))
      }
    }
    this.startX = this.startY = this.moveX = this.moveY = null
    this.previousPinchScale = 1
    this.father.emitEvent('onMouseOut', _getTouchInfo(point, event.target))
  }
  /**
   * touchmove
   * @param {Object} e
   * @memberof ClEventHandler
   */
  touchmove (e) {
    const event = this.eventBuild(e)
    if (new Date() - this.__timestamp < 150) {
      return event
    }
    const timestamp = Date.now()
    if (event.touches.length > 1) {
      const xLen = Math.abs(event.touches[0].pageX - event.touches[1].pageX)
      const yLen = Math.abs(event.touches[0].pageY - event.touches[1].pageY)
      const touchDistance = _getDistance(xLen, yLen)
      // Calculate scaling events
      if (this.touchDistance) {
        const pinchScale = touchDistance / this.touchDistance
        const point = event.touches ? event.touches[0] : event
        // this._emitEvent('onPinch', { scale: pinchScale - this.previousPinchScale }); // 缩放
        const mouseinfo = _getTouchInfo(point, event.target)
        if ((timestamp - this.startTime) > 90 && this.previousPinchScale) {
          mouseinfo.scale = pinchScale - this.previousPinchScale
          if (Math.abs(mouseinfo.scale) > 0.01) {
            this.father.emitEvent('onPinch', mouseinfo)
          }
          this.startTime = Date.now()
        }
        this.previousPinchScale = pinchScale
      }
      // Calculating rotation events
      if (this.touchVector) {
        const vector = {
          x: event.touches[1].pageX - event.touches[0].pageX,
          y: event.touches[1].pageY - event.touches[0].pageY
        }
        const angle = _getRotateAngle(vector, this.touchVector)
        // this._emitEvent('onRotate', {
        //   angle
        // })
        this.father.emitEvent('onRotate', {
          angle
        })
        this.touchVector.x = vector.x
        this.touchVector.y = vector.y
      }
    } else {
      clearTimeout(this.longTapTimeout)
      const point = event.touches ? event.touches[0] : event
      const deltaX = this.moveX === null ? 0 : point.pageX - this.moveX
      const deltaY = this.moveY === null ? 0 : point.pageY - this.moveY
      // this._emitEvent('onMove', { deltaX, deltaY });
      const config = _getTouchInfo(point, event.target)
      config.deltaX = deltaX
      config.deltaY = deltaY
      this.father.emitEvent('onMouseMove', config)
      this.moveX = point.pageX
      this.moveY = point.pageY
    }
    if (typeof event.preventDefault === 'function') event.preventDefault()
  }
}