Source: input/mouse.js

Object.assign(pc, function () {
    /**
     * @constructor
     * @name pc.MouseEvent
     * @classdesc MouseEvent object that is passed to events 'mousemove', 'mouseup', 'mousedown' and 'mousewheel'.
     * @description Create an new MouseEvent
     * @param {pc.Mouse} mouse The Mouse device that is firing this event
     * @param {MouseEvent} event The original browser event that fired
     * @property {Number} x The x co-ordinate of the mouse pointer relative to the element pc.Mouse is attached to
     * @property {Number} y The y co-ordinate of the mouse pointer relative to the element pc.Mouse is attached to
     * @property {Number} dx The change in x co-ordinate since the last mouse event
     * @property {Number} dy The change in y co-ordinate since the last mouse event
     * @property {Number} button The mouse button associated with this event. Can be:
     * <ul>
     *     <li>{@link pc.MOUSEBUTTON_LEFT}</li>
     *     <li>{@link pc.MOUSEBUTTON_MIDDLE}</li>
     *     <li>{@link pc.MOUSEBUTTON_RIGHT}</li>
     * </ul>
     * @property {Number} wheel A value representing the amount the mouse wheel has moved, only valid for {@link mousemove} events
     * @property {Element} element The element that the mouse was fired from
     * @property {Boolean} ctrlKey True if the ctrl key was pressed when this event was fired
     * @property {Boolean} shiftKey True if the shift key was pressed when this event was fired
     * @property {Boolean} altKey True if the alt key was pressed when this event was fired
     * @property {Boolean} metaKey True if the meta key was pressed when this event was fired
     * @property {MouseEvent} event The original browser event
     * @since 0.88.0
     */
    var MouseEvent = function (mouse, event) {
        var coords = {
            x: 0,
            y: 0
        };

        if (event) {
            if (event instanceof MouseEvent) {
                throw Error("Expected MouseEvent");
            }
            coords = mouse._getTargetCoords(event);
        } else {
            event = { };
        }

        if (coords) {
            this.x = coords.x;
            this.y = coords.y;
        } else if (pc.Mouse.isPointerLocked()) {
            this.x = 0;
            this.y = 0;
        } else {
            return;
        }

        // FF uses 'detail' and returns a value in 'no. of lines' to scroll
        // WebKit and Opera use 'wheelDelta', WebKit goes in multiples of 120 per wheel notch
        if (event.detail) {
            this.wheel = -1 * event.detail;
        } else if (event.wheelDelta) {
            this.wheel = event.wheelDelta / 120;
        } else {
            this.wheel = 0;
        }

        // Get the movement delta in this event
        if (pc.Mouse.isPointerLocked()) {
            this.dx = event.movementX || event.webkitMovementX || event.mozMovementX || 0;
            this.dy = event.movementY || event.webkitMovementY || event.mozMovementY || 0;
        } else {
            this.dx = this.x - mouse._lastX;
            this.dy = this.y - mouse._lastY;
        }

        if (event.type === 'mousedown' || event.type === 'mouseup') {
            this.button = event.button;
        } else {
            this.button = pc.MOUSEBUTTON_NONE;
        }
        this.buttons = mouse._buttons.slice(0);
        this.element = event.target;

        this.ctrlKey = event.ctrlKey || false;
        this.altKey = event.altKey || false;
        this.shiftKey = event.shiftKey || false;
        this.metaKey = event.metaKey || false;

        this.event = event;
    };

    // Events Documentation
    /**
     * @event
     * @name pc.Mouse#mousemove
     * @description Fired when the mouse is moved
     * @param {pc.MouseEvent} event The MouseEvent object
     */

    /**
     * @event
     * @name pc.Mouse#mousedown
     * @description Fired when a mouse button is pressed
     * @param {pc.MouseEvent} event The MouseEvent object
     */

    /**
     * @event
     * @name pc.Mouse#mouseup
     * @description Fired when a mouse button is released
     * @param {pc.MouseEvent} event The MouseEvent object
     */

    /**
     * @event
     * @name pc.Mouse#mousewheel
     * @description Fired when a mouse wheel is moved
     * @param {pc.MouseEvent} event The MouseEvent object
     */

    /**
     * @constructor
     * @name pc.Mouse
     * @classdesc A Mouse Device, bound to a DOM Element.
     * @description Create a new Mouse device
     * @param {Element} [element] The Element that the mouse events are attached to
     */
    var Mouse = function (element) {
        // Clear the mouse state
        this._lastX      = 0;
        this._lastY      = 0;
        this._buttons      = [false, false, false];
        this._lastbuttons  = [false, false, false];


        // Setup event handlers so they are bound to the correct 'this'
        this._upHandler = this._handleUp.bind(this);
        this._downHandler = this._handleDown.bind(this);
        this._moveHandler = this._handleMove.bind(this);
        this._wheelHandler = this._handleWheel.bind(this);
        this._contextMenuHandler = function (event) {
            event.preventDefault();
        };

        this._target = null;
        this._attached = false;

        this.attach(element);

        // Add events
        pc.events.attach(this);
    };

    /**
     * @function
     * @name pc.Mouse.isPointerLocked
     * @description Check if the mouse pointer has been locked, using {@link pc.Mouse#enabledPointerLock}
     * @returns {Boolean} True if locked
     */
    Mouse.isPointerLocked = function () {
        return !!(document.pointerLockElement || document.mozPointerLockElement || document.webkitPointerLockElement);
    };

    Object.assign(Mouse.prototype, {
        /**
         * @function
         * @name pc.Mouse#attach
         * @description Attach mouse events to an Element.
         * @param {Element} element The DOM element to attach the mouse to.
         */
        attach: function (element) {
            this._target = element;

            if (this._attached) return;
            this._attached = true;

            window.addEventListener("mouseup", this._upHandler, false);
            window.addEventListener("mousedown", this._downHandler, false);
            window.addEventListener("mousemove", this._moveHandler, false);
            window.addEventListener("mousewheel", this._wheelHandler, false); // WekKit
            window.addEventListener("DOMMouseScroll", this._wheelHandler, false); // Gecko
        },

        /**
         * @function
         * @name pc.Mouse#detach
         * @description Remove mouse events from the element that it is attached to
         */
        detach: function () {
            if (!this._attached) return;
            this._attached = false;
            this._target = null;

            window.removeEventListener("mouseup", this._upHandler);
            window.removeEventListener("mousedown", this._downHandler);
            window.removeEventListener("mousemove", this._moveHandler);
            window.removeEventListener("mousewheel", this._wheelHandler); // WekKit
            window.removeEventListener("DOMMouseScroll", this._wheelHandler); // Gecko
        },

        /**
         * @function
         * @name pc.Mouse#disableContextMenu
         * @description Disable the context menu usually activated with right-click
         */
        disableContextMenu: function () {
            if (!this._target) return;
            this._target.addEventListener("contextmenu", this._contextMenuHandler);
        },

        /**
         * @function
         * @name pc.Mouse#enableContextMenu
         * @description Enable the context menu usually activated with right-click. This option is active by default.
         */
        enableContextMenu: function () {
            if (!this._target) return;
            this._target.removeEventListener("contextmenu", this._contextMenuHandler);
        },

        /**
         * @function
         * @name pc.Mouse#enablePointerLock
         * @description Request that the browser hides the mouse cursor and locks the mouse to the element.
         * Allowing raw access to mouse movement input without risking the mouse exiting the element.
         * Notes: <br />
         * <ul>
         * <li>In some browsers this will only work when the browser is running in fullscreen mode. See {@link pc.Application#enableFullscreen}
         * <li>Enabling pointer lock can only be initiated by a user action e.g. in the event handler for a mouse or keyboard input.
         * </ul>
         * @param {Function} [success] Function called if the request for mouse lock is successful.
         * @param {Function} [error] Function called if the request for mouse lock is unsuccessful.
         */
        enablePointerLock: function (success, error) {
            if (!document.body.requestPointerLock) {
                if (error)
                    error();

                return;
            }

            var s = function () {
                success();
                document.removeEventListener('pointerlockchange', s);
            };
            var e = function () {
                error();
                document.removeEventListener('pointerlockerror', e);
            };

            if (success) {
                document.addEventListener('pointerlockchange', s, false);
            }

            if (error) {
                document.addEventListener('pointerlockerror', e, false);
            }

            document.body.requestPointerLock();
        },

        /**
         * @function
         * @name pc.Mouse#disablePointerLock
         * @description Return control of the mouse cursor to the user
         * @param {Function} [success] Function called when the mouse lock is disabled
         */
        disablePointerLock: function (success) {
            if (!document.exitPointerLock) {
                return;
            }

            var s = function () {
                success();
                document.removeEventListener('pointerlockchange', s);
            };
            if (success) {
                document.addEventListener('pointerlockchange', s, false);
            }
            document.exitPointerLock();
        },

        /**
         * @function
         * @name pc.Mouse#update
         * @description Update method, should be called once per frame
         */
        update: function () {
            // Copy current button state
            this._lastbuttons[0] = this._buttons[0];
            this._lastbuttons[1] = this._buttons[1];
            this._lastbuttons[2] = this._buttons[2];
        },

        /**
         * @function
         * @name pc.Mouse#isPressed
         * @description Returns true if the mouse button is currently pressed
         * @param {Number} button The mouse button to test. Can be:
         * <ul>
         *     <li>{@link pc.MOUSEBUTTON_LEFT}</li>
         *     <li>{@link pc.MOUSEBUTTON_MIDDLE}</li>
         *     <li>{@link pc.MOUSEBUTTON_RIGHT}</li>
         * </ul>
         * @returns {Boolean} True if the mouse button is current pressed
         */
        isPressed: function (button) {
            return this._buttons[button];
        },

        /**
         * @function
         * @name pc.Mouse#wasPressed
         * @description Returns true if the mouse button was pressed this frame (since the last call to update).
         * @param {Number} button The mouse button to test. Can be:
         * <ul>
         *     <li>{@link pc.MOUSEBUTTON_LEFT}</li>
         *     <li>{@link pc.MOUSEBUTTON_MIDDLE}</li>
         *     <li>{@link pc.MOUSEBUTTON_RIGHT}</li>
         * </ul>
         * @returns {Boolean} True if the mouse button was pressed since the last update
         */
        wasPressed: function (button) {
            return (this._buttons[button] && !this._lastbuttons[button]);
        },

        /**
         * @function
         * @name pc.Mouse#wasReleased
         * @description Returns true if the mouse button was released this frame (since the last call to update).
         * @param {Number} button The mouse button to test. Can be:
         * <ul>
         *     <li>{@link pc.MOUSEBUTTON_LEFT}</li>
         *     <li>{@link pc.MOUSEBUTTON_MIDDLE}</li>
         *     <li>{@link pc.MOUSEBUTTON_RIGHT}</li>
         * </ul>
         * @returns {Boolean} True if the mouse button was released since the last update
         */
        wasReleased: function (button) {
            return (!this._buttons[button] && this._lastbuttons[button]);
        },

        _handleUp: function (event) {
            // disable released button
            this._buttons[event.button] = false;

            var e = new MouseEvent(this, event);
            if (!e.event) return;

            // send 'mouseup' event
            this.fire(pc.EVENT_MOUSEUP, e);
        },

        _handleDown: function (event) {
            // Store which button has affected
            this._buttons[event.button] = true;

            var e = new MouseEvent(this, event);
            if (!e.event) return;

            this.fire(pc.EVENT_MOUSEDOWN, e);
        },

        _handleMove: function (event) {
            var e = new MouseEvent(this, event);
            if (!e.event) return;

            this.fire(pc.EVENT_MOUSEMOVE, e);

            // Store the last offset position to calculate deltas
            this._lastX = e.x;
            this._lastY = e.y;
        },

        _handleWheel: function (event) {
            var e = new MouseEvent(this, event);
            if (!e.event) return;

            this.fire(pc.EVENT_MOUSEWHEEL, e);
        },

        _getTargetCoords: function (event) {
            var rect = this._target.getBoundingClientRect();
            var left = Math.floor(rect.left);
            var top = Math.floor(rect.top);

            // mouse is outside of canvas
            if (event.clientX < left ||
                event.clientX >= left + this._target.clientWidth ||
                event.clientY < top ||
                event.clientY >= top + this._target.clientHeight) {

                return null;
            }

            return {
                x: event.clientX - left,
                y: event.clientY - top
            };
        }
    });

    // Public Interface
    return  {
        Mouse: Mouse,
        MouseEvent: MouseEvent
    };
}());