API Docs for: 1.8.0
Show:

File: src/nonvisual/mousemanager.js

/*
 * Copyright (c) 2014-2015, Wanadev <http://www.wanadev.fr/>
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 *
 *   * Redistributions of source code must retain the above copyright notice, this
 *     list of conditions and the following disclaimer.
 *   * Redistributions in binary form must reproduce the above copyright notice,
 *     this list of conditions and the following disclaimer in the documentation
 *     and/or other materials provided with the distribution.
 *   * Neither the name of Wanadev nor the names of its contributors may be used
 *     to endorse or promote products derived from this software without specific
 *     prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
 * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
 * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
 * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE
 * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
 * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
 * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
 * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
 * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 *
 * Authored by: Fabien LOISON <http://flozz.fr/>
 */

/**
 * PhotonUI - Javascript Web User Interface.
 *
 * @module PhotonUI
 * @submodule NonVisual
 * @namespace photonui
 */

var Helpers = require("../helpers.js");
var Base = require("../base.js");
var Widget = require("../widget.js");

/**
 * Manage advanced mouse events on Widgets or HTMLElements.
 *
 * wEvents:
 *
 *   * mouse-event:
 *      - description: Called for *ALL* mouse events.
 *      - callback:    function(manager, mstate)
 *
 *   * mouse-down:
 *      - description: Mouse button pressed.
 *      - callback:    function(manager, mstate)
 *
 *   * mouse-up:
 *      - description: Mouse button released.
 *      - callback:    function(manager, mstate)
 *
 *   * click:
 *      - description: Click...
 *      - callback:    function(manager, mstate)
 *
 *   * double-click:
 *      - description: Double click...
 *      - callback:    function(manager, mstate)
 *
 *   * drag-start:
 *      - description: Start dragging.
 *      - callback:    function(manager, mstate)
 *
 *   * dragging:
 *      - description: dragging.
 *      - callback:    function(manager, mstate)
 *
 *   * drag-end:
 *      - description: Stop dragging.
 *      - callback:    function(manager, mstate)
 *
 *   * mouse-move:
 *      - description: Mouse move on the element.
 *      - callback:    function(manager, mstate)
 *
 *   * scroll-up:
 *      - description: Scroll up.
 *      - callback:    function(manager, mstate)
 *
 *   * scroll-down:
 *      - description: Scroll down.
 *      - callback:    function(manager, mstate)
 *
 *
 * mstate:
 *
 *   A snapshot of the mouse state ath the moment when the event occured.
 *
 *     {
 *         event: <Object>,       // The original js event
 *         action: <String>,      // The event name (mouse-down/up/move, click, double-click,
 *                                //    drag-start/end, dragging, scroll-up/down)
 *         pageX: <Number>,       // X position, relative to page top-left corner.
 *         pageY: <Number>,       // Y position, relative to page top-left corner.
 *         x: <Number>,           // X position, relative to the HTML element.
 *         y: <Number>,           // Y position, relative to the HTML element.
 *         deltaX: <Number>,      // Delta X (current_x - previous_x)
 *         deltaY: <Number>,      // Delta Y (current_y - previous_y)
 *         btnLeft: <Boolean>,    // Current state of the mouse left button.
 *         btnMiddle: <Boolean>,  // Current state of the mouse middle button.
 *         btnRight: <Boolean>,   // Current state of the mouse right button.
 *         button: <String>       // The button that triggered the last event (none, "left", "middle", "right").
 *     }
 *
 * @class MouseManager
 * @constructor
 * @extends photonui.Base
 * @param {photonui.Widget} element Any PhotonUI Widget (optional).
 * @param {HTMLElement} element Any HTML element (optional).
 * @param {Object} params additional params (optional).
 */
var MouseManager = Base.$extend({

    // Constructor
    __init__: function (element, params) {
        this._registerWEvents([
            "mouse-event", "mouse-down", "mouse-up", "click", "double-click",
            "drag-start", "dragging", "drag-end", "mouse-move", "scroll-up",
            "scroll-down"
        ]);
        if (element && (element instanceof Widget || element instanceof HTMLElement)) {
            this.$super(params);
            this.element = element;
        } else {
            this.$super(element);
        }
    },

    //////////////////////////////////////////
    // Properties and Accessors             //
    //////////////////////////////////////////

    // ====== Public properties ======

    /**
     * The HTML Element on which the events are binded.
     *
     * NOTE: If a photonui.Widget object is assigned to this property,
     *       its HTML Element will be automatically assigned to the property instead.
     *
     * @property element
     * @type HTMLElement
     * @default null
     */
    _element: null,

    getElement: function () {
        return this._element || document;
    },

    setElement: function (element) {
        if (element instanceof Widget) {
            this._element = element.interactiveNode || element.html;
        } else if (element instanceof HTMLElement) {
            this._element = element;
        } else {
            this._element = null;
        }
        this._updateEvents();
    },

    /**
     * Minimum distance for triggering a drag-start, and maximum distance
     * to consider a mouse down/up as a click.
     *
     * @property threshold
     * @type Number
     * @default 5
     */
    _threshold: 5,

    getThreshold: function () {
        return this._threshold;
    },

    setThreshold: function (threshold) {
        this._threshold = threshold;
    },

    /**
     * Scale all position events by a factor. Use it when the canvas is scaled.
     *
     * @property scaleX
     * @type Number
     * @default 1
     */
    _scaleX: 1,

    getScaleX: function () {
        return this._scaleX;
    },

    setScaleX: function (scaleX) {
        this._scaleX = scaleX;
    },

    /**
     * Scale all position events by a factor. Use it when the canvas is scaled.
     *
     * @property scaleY
     * @type Number
     * @default 1
     */
    _scaleY: 1,

    getScaleY: function () {
        return this._scaleY;
    },

    setScaleY: function (scaleY) {
        this._scaleY = scaleY;
    },

    /**
     * Translate all position events by a scalar. Use it when the canvas is translated.
     *
     * @property translateX
     * @type Number
     * @default 0
     */
    _translateX: 0,

    getTranslateX: function () {
        return this._translateX;
    },

    setTranslateX: function (translateX) {
        this._translateX = translateX;
    },

    /**
     * Translate all position events by a scalar. Use it when the canvas is translated.
     *
     * @property translateY
     * @type Number
     * @default 0
     */
    _translateY: 0,

    getTranslateY: function () {
        return this._translateY;
    },

    setTranslateY: function (translateY) {
        this._translateY = translateY;
    },

    /**
     * X position, relative to page top-left corner.
     *
     * @property pageX
     * @readOnly
     * @type Number
     * @default 0
     */
    getPageX: function () {
        return this.__event.pageX || 0;
    },

    /**
     * Y position, relative to page top-left corner.
     *
     * @property pageY
     * @readOnly
     * @type Number
     * @default 0
     */
    getPageY: function () {
        return this.__event.pageY || 0;
    },

    /**
     * X position, relative to the HTML element.
     *
     * @property x
     * @readOnly
     * @type Number
     */
    getX: function () {
        var ex = Helpers.getAbsolutePosition(this.element).x;
        return (this.pageX - ex) * this.scaleX + this.translateX;
    },

    /**
     * Y position, relative to the HTML element.
     *
     * @property y
     * @readOnly
     * @type Number
     */
    getY: function () {
        var ey = Helpers.getAbsolutePosition(this.element).y;
        return (this.pageY - ey) * this.scaleY + this.translateY;
    },

    /**
     * Delta X (current_x - previous_x).
     *
     * @property deltaX
     * @readOnly
     * @type Number
     */
    getDeltaX: function () {
        return (this.pageX - ((this.__prevState.pageX !== undefined) ?
            this.__prevState.pageX : this.pageX)) * this.scaleX;
    },

    /**
     * Delta Y (current_y - previous_y).
     *
     * @property deltaY
     * @readOnly
     * @type Number
     */
    getDeltaY: function () {
        return (this.pageY - ((this.__prevState.pageY !== undefined) ?
            this.__prevState.pageY : this.pageY)) * this.scaleY;
    },

    /**
     * The action:
     *
     *   * "mouse-down"
     *   * "moues-up"
     *   * "click"
     *   * "double-click"
     *   * "drag-start"
     *   * "dragging"
     *   * "drag-end"
     *   * "scroll-down"
     *   * "scroll-up"
     *   * "mouse-move"
     *
     * @property action
     * @readOnly
     * @type String
     */
    _action: "",

    getAction: function () {
        return this._action;
    },

    /**
     * Current state of the mouse left button.
     *
     * @property btnLeft
     * @type Boolean
     * @readOnly
     */
    _btnLeft: false,

    getBtnLeft: function () {
        return this._btnLeft;
    },

    /**
     * Current state of the mouse middle button.
     *
     * @property btnMiddle
     * @type Boolean
     * @readOnly
     */
    _btnMiddle: false,

    getBtnMiddle: function () {
        return this._btnMiddle;
    },

    /**
     * Current state of the mouse right button.
     *
     * @property btnRight
     * @type Boolean
     * @readOnly
     */
    _btnRight: false,

    getBtnRight: function () {
        return this._btnRight;
    },

    /**
     * The button that triggered the last event.
     *
     *   * none
     *   * "left"
     *   * "middle"
     *   * "right"
     *
     * @property button
     * @readOnly
     * @type String
     */
    _button: null,

    getButton: function () {
        return this._button;
    },

    // ====== Private properties ======

    /**
     * Previous state.
     *
     * @property __prevState
     * @private
     * @type Object
     */
    __prevState: {},

    /**
     * Js event on mouse down.
     *
     * @property __mouseDownEvent
     * @private
     * @type Object
     */
    __mouseDownEvent: {},

    /**
     * Last event object.
     *
     * @property __event
     * @private
     * @type Object
     * @default {}
     */
    __event: {},

    /**
     * The button that triggered the drag start event
     *
     *   * null
     *   * "left"
     *   * "middle"
     *   * "right"
     *
     * @property __dragStartButton
     * @private
     * @type String
     * @default null
     */
    __dragStartButton: null,

    //////////////////////////////////////////
    // Methods                              //
    //////////////////////////////////////////

    /**
     * Bind events on the HTML Element.
     *
     * @method _updateEvents
     * @private
     */
    _updateEvents: function () {
        // Unbind all existing events
        for (var id in this.__events) {
            this._unbindEvent(id);
        }
        // Check if we have an html element
        if (!this.element) {
            return;
        }
        // Bind new events
        this._bindEvent("mouse-down", this.element, "mousedown", this.__onMouseDown.bind(this));
        this._bindEvent("mouse-up", this.element, "mouseup", this.__onMouseUp.bind(this));
        this._bindEvent("double-click", this.element, "dblclick", this.__onDoubleClick.bind(this));
        this._bindEvent("mouse-move", this.element, "mousemove", this.__onMouseMove.bind(this));
        this._bindEvent("wheel", this.element, "wheel", this.__onWheel.bind(this));

        this._bindEvent("document-mouse-up", document, "mouseup", this.__onDocumentMouseUp.bind(this));
        this._bindEvent("document-mouse-move", document, "mousemove", this.__onDocumentMouseMove.bind(this));
    },

    /**
     * Take a snapshot of the MouseManager
     *
     * @method _dump
     * @private
     * @return {Object}
     */
    _dump: function () {
        return {
            event: this.__event,
            action: this.action,
            pageX: this.pageX,
            pageY: this.pageY,
            x: this.x,
            y: this.y,
            deltaX: this.deltaX,
            deltaY: this.deltaY,
            btnLeft: this.btnLeft,
            btnMiddle: this.btnMiddle,
            btnRight: this.btnRight,
            button: this.button
        };
    },

    /**
     * Analyze and dispatche wEvents.
     *
     * @method _stateMachine
     * @private
     * @param {String} action The action name (e.g. "mouse-up").
     * @param {Object} event The js event.
     */
    _stateMachine: function (action, event) {
        // Save the previous state
        this.__prevState = this._dump();

        // Load the current state
        this._action = action;
        this.__event = event;
        this._button = null;
        if (this.__dragStartButton) {
            this._button = this.__dragStartButton;
        } else if (event.button === 0) {
            this._button = "left";
        } else if (event.button === 1) {
            this._button = "middle";
        } else if (event.button === 2) {
            this._button = "right";
        }

        // Analyze the event

        // Mouse Down / Mouse Up
        if (action == "mouse-down") {
            this.__mouseDownEvent = event;

            if (event.button === 0) {
                this._btnLeft = true;
            }
            if (event.button === 1) {
                this._btnMiddle = true;
            }
            if (event.button === 2) {
                this._btnRight = true;
            }

            this._callCallbacks("mouse-event", [this._dump()]);
            this._callCallbacks(this.action, [this._dump()]);
        } else if (action == "mouse-up") {
            if (event.button === 0) {
                this._btnLeft = false;
            }
            if (event.button === 1) {
                this._btnMiddle = false;
            }
            if (event.button === 2) {
                this._btnRight = false;
            }

            this._callCallbacks("mouse-event", [this._dump()]);
            this._callCallbacks(this.action, [this._dump()]);
        } else if (action == "drag-end") {
            if (event.button === 0) {
                this._btnLeft = false;
            }
            if (event.button === 1) {
                this._btnMiddle = false;
            }
            if (event.button === 2) {
                this._btnRight = false;
            }
        }

        // Click
        if (action == "mouse-up" && (Math.abs(this.pageX - this.__mouseDownEvent.pageX) <= this._threshold &&
            Math.abs(this.pageY - this.__mouseDownEvent.pageY) <= this._threshold)) {
            this._action = "click";
            this._callCallbacks("mouse-event", [this._dump()]);
            this._callCallbacks("click", [this._dump()]);
        }

        // Double Click
        if (action == "double-click" && this.__prevState.action == "click") {
            this._action = "double-click";
            this._callCallbacks("mouse-event", [this._dump()]);
            this._callCallbacks(this.action, [this._dump()]);
        }

        // Mouse move
        if (action == "mouse-move") {
            this._callCallbacks("mouse-event", [this._dump()]);
            this._callCallbacks(this.action, [this._dump()]);
        }

        // Drag Start
        if (action == "mouse-move" && this.__prevState.action != "drag-start" &&
            this.__prevState.action != "dragging" && (this.btnLeft || this.btnMiddle || this.btnRight)) {
            if (Math.abs(this.pageX - this.__mouseDownEvent.pageX) > this._threshold ||
                Math.abs(this.pageY - this.__mouseDownEvent.pageY) > this._threshold) {
                this.__dragStartButton = this._button;
                // Drag Start
                this._action = "drag-start";
                this.__event = this.__mouseDownEvent;
                this._callCallbacks("mouse-event", [this._dump()]);
                this._callCallbacks(this.action, [this._dump()]);
                // Dragging
                this._action = "dragging";
                this.__prevState.event = this.__mouseDownEvent;
                this.__event = event;
                this._callCallbacks("mouse-event", [this._dump()]);
                this._callCallbacks(this.action, [this._dump()]);
            }

        // Dragging
        } else if (action == "dragging" || (action == "mouse-move" && (this.__prevState.action == "drag-start" ||
                 this.__prevState.action == "dragging") && (this.btnLeft || this.btnMiddle || this.btnRight))) {
            this._action = "dragging";
            this._callCallbacks("mouse-event", [this._dump()]);
            this._callCallbacks(this.action, [this._dump()]);

        // Drag End
        } else if (action == "drag-end" || (action == "mouse-up" && (this.__prevState.action == "dragging" ||
                 this.__prevState.action == "drag-start") && !(this.btnLeft || this.btnMiddle || this.btnRight))) {
            this._action = "drag-end";
            this.__dragStartButton = null;
            this._callCallbacks("mouse-event", [this._dump()]);
            this._callCallbacks(this.action, [this._dump()]);
        }

        // Scroll Up / Scroll Down
        if (action == "scroll-up" || action == "scroll-down") {
            this._callCallbacks("mouse-event", [this._dump()]);
            this._callCallbacks(this.action, [this._dump()]);
        }
    },

    //////////////////////////////////////////
    // Internal Events Callbacks            //
    //////////////////////////////////////////

    /**
     * @method __onMouseDown
     * @private
     * @param event
     */
    __onMouseDown: function (event) {
        this._stateMachine("mouse-down", event);
    },

    /**
     * @method __onMouseUp
     * @private
     * @param event
     */
    __onMouseUp: function (event) {
        this._stateMachine("mouse-up", event);
    },

    /**
     * @method __onDoubleClick
     * @private
     * @param event
     */
    __onDoubleClick: function (event) {
        this._stateMachine("double-click", event);
    },

    /**
     * @method __onMouseMove
     * @private
     * @param event
     */
    __onMouseMove: function (event) {
        this._stateMachine("mouse-move", event);
    },

    /**
     * Used to detect drag-end outside the element.
     *
     * @method __onDocumentMouseUp
     * @private
     * @param event
     */
    __onDocumentMouseUp: function (event) {
        if (event.target === this._element) {
            return;
        }
        if (this.action == "dragging" || this.action == "drag-start") {
            this._stateMachine("drag-end", event);
        } else if (event.button === 0 && this._btnLeft ||
            event.button === 1 && this._btnMiddle ||
            event.button === 2 && this._btnRight) {

            this._stateMachine("mouse-up", event);
        }
    },

    /**
     * Used to detect dragging outside the element.
     *
     * @method __onDocumentMouseMove
     * @private
     * @param event
     */
    __onDocumentMouseMove: function (event) {
        if (event.target === this._element) {
            return;
        }
        if (this.action == "dragging" || this.action == "drag-start") {
            this._stateMachine("dragging", event);
        }
    },

    /**
     * @method __onWheel
     * @private
     * @param event
     */
    __onWheel: function (event) {
        var wheelDelta = null;

        if (event.deltaY !== undefined) {
            wheelDelta = -event.deltaY;
        }

        if (wheelDelta !== null) {
            if (wheelDelta >= 0) {
                this._stateMachine("scroll-up", event);
            } else {
                this._stateMachine("scroll-down", event);
            }
        }
    }
});

module.exports = MouseManager;