Source: lib/EventHandler.js

/* EventHandler.js 
 * 
 * copyright (c) 2010-2017, Christian Mayer and the CometVisu contributers.
 * 
 * This program is free software; you can redistribute it and/or modify it
 * under the terms of the GNU General Public License as published by the Free
 * Software Foundation; either version 3 of the License, or (at your option)
 * any later version.
 *
 * This program is distributed in the hope that it will be useful, but WITHOUT
 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
 * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
 * more details.
 *
 * You should have received a copy of the GNU General Public License along
 * with this program; if not, write to the Free Software Foundation, Inc.,
 * 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA
 */


define([], function() {

  /**
   * General handler for all mouse and touch actions.
   *
   * The general flow of mouse actions are:
   * 1. mousedown                           - "button pressed"
   * 2. mouseout                            - "button released"
   * 3. mouseout (mouse moved inside again) - "button pressed"
   * 4. mouseup                             - "button released"
   *
   * 2. gets mapped to a action cancel event
   * 3. gets mapped to a mousedown event
   * 2. and 3. can be repeated unlimited - or also be left out.
   * 4. triggers the real action
   *
   * For touch it's a little different as a touchmove cancels the current
   * action and translates into a scroll.
   *
   * All of this is the default or when the mousemove callback is returning
   * restrict=true (or undefined).
   * When restrict=false the widget captures the mouse until it is released.
   */
  function EventHandler(templateEngine) {

    this._navbarRegEx = /navbar/;
    this._isTouchDevice = !!('ontouchstart' in window) ||    // works on most browsers
      !!('onmsgesturechange' in window); // works on ie10
    this._isWidget = false;
    this._scrollElement = null;
    // object to hold the coordinated of the current mouse / touch event
    this._mouseEvent = templateEngine.handleMouseEvent = {
      moveFn: undefined,
      moveRestrict: true,
      actor: undefined,
      widget: undefined,
      widgetCreator: undefined,
      downtime: 0,
      alreadyCanceled: false
    };
    this._touchStartX = null;
    this._touchStartY = null;

    // helper function to get the current actor and widget out of an event:
    this.getWidgetActor = function (element) {
      var actor, widget;

      while (element) {
        if (element.classList.contains('actor') || (element.classList.contains('group') && element.classList.contains('clickable'))) {
          actor = element;
        }

        if (element.classList.contains('widget_container')) {
          widget = element;
          if (templateEngine.design.creators[widget.dataset.type].action !== undefined) {
            return {actor: actor, widget: widget};
          }
        }
        if (element.classList.contains('page')) {
          // abort traversal
          return {actor: actor, widget: widget};
        }
        element = element.parentElement;
      }
      return false;
    };

    // helper function to determine the element to scroll (or undefined)
    this.getScrollElement = function (element) {
      while (element) {
        if (element.classList.contains('page')) {
          return this._navbarRegEx.test(element.id) ? undefined : element;
        }
        if (element.classList.contains('navbar')) {
          var parent = element.parentElement;
          if ('navbarTop' === parent.id || 'navbarBottom' === parent.id) {
            return element;
          }
          return;
        }

        element = element.parentElement;
      }
    };

    this.onPointerDown = function (event) {
      // search if a widget was hit
      var widgetActor = this.getWidgetActor(event.target),
        bindWidget = widgetActor.widget ? templateEngine.widgetDataGet(widgetActor.widget.id).bind_click_to_widget : false;

      var touchobj;

      if (event.changedTouches) {
        touchobj = event.changedTouches[0];
        this._touchStartX = parseInt(touchobj.clientX);
        this._touchStartY = parseInt(touchobj.clientY);
      } else {
        this._touchStartX = parseInt(event.clientX);
        this._touchStartY = parseInt(event.clientY);
      }

      this._isWidget = widgetActor.widget !== undefined && (bindWidget || widgetActor.actor !== undefined);
      if (this._isWidget) {
        this._mouseEvent.actor = widgetActor.actor;
        this._mouseEvent.widget = widgetActor.widget;
        this._mouseEvent.widgetCreator = templateEngine.design.creators[widgetActor.widget.dataset.type];
        this._mouseEvent.downtime = Date.now();
        this._mouseEvent.alreadyCanceled = false;

        var actionFn = this._mouseEvent.widgetCreator.downaction;

        if (actionFn !== undefined) {
          var moveFnInfo = actionFn.call(this._mouseEvent.widget, this._mouseEvent.widget.id, this._mouseEvent.actor, false, event);
          if (moveFnInfo) {
            this._mouseEvent.moveFn = moveFnInfo.callback;
            this._mouseEvent.moveRestrict = moveFnInfo.restrict !== undefined ? moveFnInfo.restrict : true;
          }
        }
      } else {
        this._mouseEvent.actor = undefined;
      }

      if (this._mouseEvent.moveRestrict) {
        this._scrollElement = this.getScrollElement(event.target);
      }
      // stop the propagation if scrollable is at the end
      // inspired by 
      if (this._scrollElement) {
        var startTopScroll = this._scrollElement.scrollTop;

        if (startTopScroll <= 0) {
          this._scrollElement.scrollTop = 1;
        }
        if (startTopScroll + this._scrollElement.offsetHeight >= this._scrollElement.scrollHeight) {
          this._scrollElement.scrollTop = this._scrollElement.scrollHeight - this._scrollElement.offsetHeight - 1;
        }
      }
    };

    this.onPointerUp = function (event) {
      if (this._isWidget) {
        var
          widgetActor = this.getWidgetActor(event.target),
          widget = this._mouseEvent.widget,
          actionFn = this._mouseEvent.widgetCreator.action,
          bindWidget = templateEngine.widgetDataGet(widget.id).bind_click_to_widget,
          inCurrent = widgetActor.widget === widget && (bindWidget || widgetActor.actor === this._mouseEvent.actor);

        if (
          actionFn !== undefined &&
          inCurrent && !this._mouseEvent.alreadyCanceled
        ) {
          actionFn.call(widget, widget.id, this._mouseEvent.actor, !inCurrent, event);
        }
        this._mouseEvent.moveFn = undefined;
        this._mouseEvent.moveRestrict = true;
        this._scrollElement = undefined;
        this._isWidget = false;
      } else if( 'touchend' === event.type && 'A' === event.target.nodeName )
      {
        // simulate a click on link elements on touch devices as we are
        // prevented default
        var 
          x = event.changedTouches[0].pageX - window.pageXOffset,
          y = event.changedTouches[0].pageY - window.pageYOffset,
          target = document.elementFromPoint(x, y);
        if( target === event.target ) // did the touch stay on the element?
          target.click();
      }
    };

    /**
     * mouse move: let the user cancel an action by dragging the mouse outside
     * and reactivate it when the dragged cursor is returning
     * @param event {Event}
     * @private
     */
    this._onPointerMoveNoTouch = function (event) {
      if (this._isWidget) {
        var
          actionFn = null,
          widgetActor = this.getWidgetActor(event.target),
          widget = this._mouseEvent.widget,
          bindWidget = templateEngine.widgetDataGet(widget.id).bind_click_to_widget,
          inCurrent = !this._mouseEvent.moveRestrict || (widgetActor.widget === widget && (bindWidget || widgetActor.actor === this._mouseEvent.actor));

        if (inCurrent && this._mouseEvent.moveFn) {
          this._mouseEvent.moveFn(event);
        }

        if (inCurrent && this._mouseEvent.alreadyCanceled) { // reactivate
          this._mouseEvent.alreadyCanceled = false;
          actionFn = this._mouseEvent.widgetCreator.downaction;
          if (actionFn) {
            actionFn.call(widget, widget.id, this._mouseEvent.actor, false, event);
          }
        }
        else if ((!inCurrent && !this._mouseEvent.alreadyCanceled)) {
          // cancel
          this._mouseEvent.alreadyCanceled = true;
          actionFn = this._mouseEvent.widgetCreator.action;
          if (actionFn) {
            actionFn.call(widget, widget.id, this._mouseEvent.actor, true, event);
          }
        }
      }
    };

    /**
     * touch move: scroll when the finger is moving and cancel any pending
     * actions at the same time
     * @private
     */
    this._onPointerMoveTouch = function (event) {
      if (this._isWidget) {
        var
          widget = this._mouseEvent.widget,
          touchobj = event.changedTouches[0];

        if (this._mouseEvent.moveFn) {
          this._mouseEvent.moveFn(event);
        }
        // cancel when finger moved more than 5px
        if (this._mouseEvent.moveRestrict && !this._mouseEvent.alreadyCanceled &&
          (Math.abs(this._touchStartX - parseInt(touchobj.clientX)) > 5 ||
          Math.abs(this._touchStartY - parseInt(touchobj.clientY)) > 5 )) { // cancel
          this._mouseEvent.alreadyCanceled = true;
          var actionFn = this._mouseEvent.widgetCreator.action;
          if (actionFn) {
            actionFn.call(widget, widget.id, this._mouseEvent.actor, true, event);
          }
        }
      }

      // take care to prevent overscroll
      if (this._scrollElement) {
        var scrollTop = this._scrollElement.scrollTop,
          scrollLeft = this._scrollElement.scrollLeft;
        // prevent scrolling of an element that takes full height and width
        // as it doesn't need scrolling
        if ((scrollTop <= 0) && (scrollTop + this._scrollElement.offsetHeight >= this._scrollElement.scrollHeight) &&
          (scrollLeft <= 0) && (scrollLeft + this._scrollElement.offsetWidth >= this._scrollElement.scrollWidth )) {
          return;
        }
        event.stopPropagation();
      } else {
        event.preventDefault();
      }
    };

    /**
     * The dispatcher registers listeners for all relevant events to the window object
     * and dispatched the event to the EventHandler. The dispatcher listens to similar events
     * like touchstart and mousedown but makes sure that these events are not fired
     *
     * @param handler
     * @constructor
     */
    var Dispatcher = function(handler) {

      /**
       * register to all events
       */
      this.register = function() {
        window.addEventListener('mousedown', this._onDown);
        window.addEventListener('touchstart', this._onDown);

        window.addEventListener('mouseup', this._onUp);
        window.addEventListener('touchend', this._onUp);

        window.addEventListener('mousemove', this._onMove);
        window.addEventListener('touchmove', this._onMove);
      };

      this._onDown = function(event) {
        handler.onPointerDown(event);
      };

      this._onUp = function(event) {
        handler.onPointerUp(event);
        if (event.type === "touchend") {
          // prevent mouseup beeing fired
          event.preventDefault();
        }
      };

      this._onMove = function(event) {
        // dispatch by event type
        if (event.type === "mousemove") {
          handler._onPointerMoveNoTouch(event);
        } else if (event.type === "touchmove") {
          handler._onPointerMoveTouch(event);
        } else{
          console.error("onhandled event type "+event.type);
        }
      };
    };

    this.dispatcher = new Dispatcher(this);
    this.dispatcher.register();

  }
  return EventHandler;
});