Source: composer/translate-composer.js

/*global require,exports */
/**
 * @module montage/composer/translate-composer
 * @requires montage/core/core
 * @requires montage/composer/composer
 * @requires montage/core/event/event-manager
 */
var Composer = require("./composer").Composer,
    defaultEventManager = require("../core/event/event-manager").defaultEventManager;

/**
 * Abstracts listening for touch and mouse events representing a drag. The
 * emitted events provide translateX and translateY properties that are updated
 * when the user drags on the given element. Should be used wherever a user
 * interacts with an element by dragging.
 *
 * @class TranslateComposer
 * @extends Composer
 * @fires translate
 * @fires translateStart
 * @fires translateEnd
 * @classdesc A composer that elevates touch and mouse events into drag events.
 */
var TranslateComposer = exports.TranslateComposer = Composer.specialize(/** @lends TranslateComposer# */ {

    /**
     * These elements perform some native action when clicked/touched and so we
     * should not preventDefault when a mousedown/touchstart happens on them.
     * @private
     */
    _NATIVE_ELEMENTS: {
        value: ["A", "IFRAME", "EMBED", "OBJECT", "VIDEO", "AUDIO", "CANVAS",
            "LABEL", "INPUT", "BUTTON", "SELECT", "TEXTAREA", "KEYGEN",
            "DETAILS", "COMMAND"
        ]
    },

    _WHEEL_POINTER: {
        value: "wheel",
        writable: false
    },

    _MOUSE_POINTER: {
        value: "mouse",
        writable: false
    },

    _TOUCH_POINTER: {
        value: "touch",
        writable: false
    },

    CLAIM_POINTER_POLICIES: {
        value: {
            DEFAULT: "default", // claiming pointers on the capture phase. (first move event)
            MOVE: "move" // claiming pointers on the capture phase + check if a real move has been encountered.
        }
    },

    // When set to true, do not respond to events, claim pointers, or prevent default
    enabled: {
        value: true
    },

    _externalUpdate: {
        value: true
    },

    isAnimating: {
        value: false
    },

    isMoving: {
        value: false
    },

    frame: {
        value: function () {
            if (this.isAnimating) {
                this._animationInterval();
            }
            this._externalUpdate = false;
        }
    },

    _pointerSpeedMultiplier: {
        value: 1
    },

    /**
     * How many pixels to translate by for each pixel of cursor movement.
     * @type {number}
     * @default 1
     */
    pointerSpeedMultiplier: {
        get: function () {
            return this._pointerSpeedMultiplier;
        },
        set: function (value) {
            this._pointerSpeedMultiplier = value;
        }
    },

    pointerStartEventPosition: {
        value: null
    },

    _shouldDispatchTranslate: {
        value: false
    },

    _isSelfUpdate: {
        value: false
    },

    _allowFloats: {
        value: false
    },

    /**
     * Allow (@link translateX} and {@link translateY} to be floats?
     * @type {boolean}
     * @default false
     */
    allowFloats: {
        get: function () {
            return this._allowFloats;
        },
        set: function (value) {
            if (this._allowFloats !== value) {
                this._allowFloats = value;
                this.translateX = this._translateX;
                this.translateY = this._translateY;
            }
        }
    },

    allowTranslateOuterExtreme: {
        value: false
    },

    _translateX: {
        value: 0
    },

    /**
     * Amount of translation in the X (left/right) direction. Can be inverted with
     * {@link invertXAxis}, and restricted to a range with
     * {@link minTranslateX} and {@link maxTranslateX}.
     * @type {number}
     * @default 0
     */
    translateX: {
        get: function () {
            return this._translateX;
        },
        set: function (value) {
            if (this._axis === "vertical") {
                this._translateX = this._minTranslateX || 0;
            } else {
                //jshint -W016
                var tmp = isNaN(value) ? 0 : this._allowFloats ? parseFloat(value) : value >> 0;
                //jshint +W016

                if (this._minTranslateX !== null && tmp < this._minTranslateX) {
                    tmp = this._minTranslateX;
                }
                if (this._maxTranslateX !== null && tmp > this._maxTranslateX) {
                    tmp = this._maxTranslateX;
                }
                if (!this._isSelfUpdate) {
                    this.isAnimating = false;
                }
                this._translateX = tmp;
            }
        }
    },

    _translateY: {
        value: 0
    },

    /**
     * Amount of translation in the Y (up/down) direction. Can be inverted with
     * {@link invertYAxis}, and restricted to a range with
     * {@link minTranslateY} and {@link maxTranslateY}.
     * @type {number}
     * @default 0
     */
    translateY: {
        get: function () {
            return this._translateY;
        },
        set: function (value) {
            if (this._axis === "horizontal") {
                this._translateY = this._minTranslateY || 0;
            } else {
                //jshint -W016
                var tmp = isNaN(value) ? 0 : this._allowFloats ? parseFloat(value) : value >> 0;
                //jshint +W016

                if (this._minTranslateY !== null && tmp < this._minTranslateY) {
                    tmp = this._minTranslateY;
                }
                if (this._maxTranslateY !== null && tmp > this._maxTranslateY) {
                    tmp = this._maxTranslateY;
                }
                if (!this._isSelfUpdate) {
                    this.isAnimating = false;
                }
                this._translateY = tmp;
            }
        }
    },

    _minTranslateX: {
        value: null
    },

    /**
     * The minimum value {@link translateX} can take. If set to null then
     * there is no minimum.
     * @type {?number}
     * @default null
    */
    minTranslateX: {
        get: function () {
            return this._minTranslateX;
        },
        set: function (value) {
            if (value !== null) {
                value = parseFloat(value);
            }

            if (this._minTranslateX !== value) {
                if (value !== null && this._translateX < value) {
                    this.translateX = value;
                }
                this._minTranslateX = value;
            }
        }
    },

    _maxTranslateX: {
        value: null
    },

    /**
     * The maximum value {@link translateX} can take. If set to null then
     * there is no maximum.
     * @type {?number}
     * @default null
     */
    maxTranslateX: {
        get: function () {
            return this._maxTranslateX;
        },
        set: function (value) {
            if (value !== null) {
                value = parseFloat(value);
            }

            if (this._maxTranslateX !== value) {
                if (value !== null && this._translateX > value) {
                    this.translateX = value;
                }
                this._maxTranslateX = value;
            }
        }
    },

    _minTranslateY: {
        value: null
    },

    /**
     * The minimum value {@link translateY} can take. If set to null then
     * there is no minimum.
     * @type {?number}
     * @default null
     */
    minTranslateY: {
        get: function () {
            return this._minTranslateY;
        },
        set: function (value) {
            if (value !== null) {
                value = parseFloat(value);
            }

            if (this._minTranslateY !== value) {
                if (value !== null && this._translateY < value) {
                    this.translateY = value;
                }
                this._minTranslateY = value;
            }
        }
    },

    _maxTranslateY: {
        value: null
    },

    /**
     * The maximum value {@link translateY} can take. If set to null then
     * there is no maximum.
     * @type {?number}
     * @default null
     */
    maxTranslateY: {
        get: function () {
            return this._maxTranslateY;
        },
        set: function (value) {
            if (value !== null) {
                value = parseFloat(value);
            }

            if (this._maxTranslateY !== value) {
                if (value !== null && this._translateY > value) {
                    this.translateY = value;
                }
                this._maxTranslateY = value;
            }
        }
    },

    _axis: {
        value: "both"
    },

    /**
     * Which axis translation is restricted to.
     *
     * Can be "vertical", "horizontal" or "both".
     * @type {string}
     * @default "both"
     */
    axis: {
        get: function () {
            return this._axis;
        },
        set: function (value) {
            switch (value) {
            case "vertical":
            case "horizontal":
                this._axis = value;
                this.translateX = this._translateX;
                this.translateY = this._translateY;
                break;
            default:
                this._axis = "both";
                break;
            }
        }
    },

    /**
     * Invert direction of translation on both axes.
     *
     * This inverts the effect of cursor motion on both axes. For example
     * if set to true moving the mouse up will increase the value of
     * translateY instead of decreasing it.
     *
     * Depends on invertXAxis and invertYAxis.
     * @type {boolean}
     * @default false
    */
    invertAxis: {
        depends: ["invertXAxis", "invertYAxis"],
        get: function () {
            return (this._invertXAxis === this._invertYAxis) ? this._invertXAxis : null;
        },
        set: function (value) {
            this.invertXAxis = value;
            this.invertYAxis = value;
        }
    },

    _invertXAxis: {
        value: false
    },

    /**
     * Invert direction of translation along the X axis.
     *
     * This inverts the effect of left/right cursor motion on translateX.
     * @type {boolean}
     * @default false
     */
    invertXAxis: {
        get: function () {
            return this._invertXAxis;
        },
        set: function (value) {
            this._invertXAxis = !!value;
        }
    },

    _invertYAxis: {
        value: false
    },

    /**
     * Invert direction of translation along the Y axis.
     *
     * This inverts the effect of up/down cursor motion on translateX.
     * @type {boolean}
     * @default false
     */
    invertYAxis: {
        get: function () {
            return this._invertYAxis;
        },
        set: function (value) {
            this._invertYAxis = !!value;
        }
    },

    _hasMomentum: {
        value: true
    },

    /**
     * Whether to keep translating after the user has releases the cursor.
     * @type {boolean}
     * @default true
     */
    hasMomentum: {
        get: function () {
            return this._hasMomentum;
        },
        set: function (value) {
            this._hasMomentum = value ? true : false;
        }
    },

    __momentumDuration: {
        value: 650
    },

    _momentumDuration: {
        get: function () {
            return this.__momentumDuration;
        },
        set: function (value) {
            //jshint -W016
            this.__momentumDuration = isNaN(value) ? 1 : value >> 0;
            //jshint +W016
            if (this.__momentumDuration < 1) {
                this.__momentumDuration = 1;
            }
        }
    },

    _pointerX: {
        value: null
    },

    _pointerY: {
        value: null
    },

    _touchIdentifier: {
        value: null
    },

    _isFirstMove: {
        value: false
    },

    _observedPointer: {
        value: null
    },

    eventManager: {
        get: function () {
            return defaultEventManager;
        }
    },

    _mouseRadiusThreshold: {
        value: 2 //px
    },

    _touchRadiusThreshold: {
        value: 8 //px
    },

    _listenToWheelEvent: {
        value: false
    },

    listenToWheelEvent: {
        set: function (_listenToWheelEvent) {
            _listenToWheelEvent = !!_listenToWheelEvent;

            if (this._listenToWheelEvent !== _listenToWheelEvent) {
                this._listenToWheelEvent = _listenToWheelEvent;

                if (this._isLoaded) {
                    if (_listenToWheelEvent) {
                        this._addWheelEventListener();
                    } else {
                        this._removeWheelEventListener();
                    }
                }
            }
        },
        get: function () {
            return this._listenToWheelEvent;
        }
    },

    _claimPointerPolicy: {
        value: null
    },

    load: {
        value: function () {
            if (window.PointerEvent) {
                this._element.addEventListener("pointerdown", this, true);

            } else if (window.MSPointerEvent && window.navigator.msPointerEnabled) {
                this._element.addEventListener("MSPointerDown", this, true);

            } else {
                this._element.addEventListener("touchstart", this, true);
                this._element.addEventListener("mousedown", this, true);
            }

            if (this._listenToWheelEvent) {
                this._addWheelEventListener();
            }

            this.eventManager.isStoringPointerEvents = true;
        }
    },

    unload: {
        value: function () {
            if (window.PointerEvent) {
                this._element.removeEventListener("pointerdown", this, true);

            } else if (window.MSPointerEvent && window.navigator.msPointerEnabled) {
                this._element.removeEventListener("MSPointerDown", this, true);

            } else {
                this._element.removeEventListener("touchstart", this, true);
                this._element.removeEventListener("mousedown", this, true);
            }

            if (this._listenToWheelEvent) {
                this._removeWheelEventListener();
            }
        }
    },

    surrenderPointer: {
        value: function (pointer, component) {
            var shouldSurrender = this.callDelegateMethod("surrenderPointer", pointer, component);
            if (typeof shouldSurrender !== "undefined" && shouldSurrender === false) {
                return false;
            }

            this._cancel();

            return true;
        }
    },

    /*
     * Add an event listener to receive events generated by the
     * `TranslateComposer`.
     * @param {string} event type
     * @param {object|function} listener object or function
     * @param {boolean} use capture instead of bubble
     */
    addEventListener: {
        value: function (type, listener, useCapture) {
            Composer.prototype.addEventListener.call(this, type, listener, useCapture);
            if (type === "translate") {
                this._shouldDispatchTranslate = true;
            }
        }
    },

    capturePointerdown: {
        value: function (event) {
            if (event.pointerType === this._MOUSE_POINTER || (window.MSPointerEvent && event.pointerType === window.MSPointerEvent.MSPOINTER_TYPE_MOUSE)) {
                this.captureMousedown(event);
            } else if (event.pointerType === this._TOUCH_POINTER || (window.MSPointerEvent && event.pointerType === window.MSPointerEvent.MSPOINTER_TYPE_TOUCH)) {
                this.captureTouchstart(event);
            }
        }
    },

    capturePointermove: {
        value: function (event) {
            if (event.pointerType === this._MOUSE_POINTER || (window.MSPointerEvent && event.pointerType === window.MSPointerEvent.MSPOINTER_TYPE_MOUSE)) {
                this.captureMousemove(event);
            } else if (event.pointerType === this._TOUCH_POINTER || (window.MSPointerEvent && event.pointerType === window.MSPointerEvent.MSPOINTER_TYPE_TOUCH)) {
                this.captureTouchmove(event);
            }
        }
    },

    handlePointerup: {
        value: function (event) {
            if (event.pointerType === this._MOUSE_POINTER || (window.MSPointerEvent && event.pointerType === window.MSPointerEvent.MSPOINTER_TYPE_MOUSE)) {
                this.handleMouseup(event);
            } else if (event.pointerType === this._TOUCH_POINTER || (window.MSPointerEvent && event.pointerType === window.MSPointerEvent.MSPOINTER_TYPE_TOUCH)) {
                this.handleTouchend(event);
            }
        }
    },

    handlePointercancel: {
        value: function (event) {
            if (event.pointerType === this._TOUCH_POINTER || (window.MSPointerEvent && event.pointerType === window.MSPointerEvent.MSPOINTER_TYPE_TOUCH)) {
                this.handleTouchcancel(event);
            }
        }
    },

    /**
     * Handle the mousedown
     * @function
     * @param {Event} event
     * @private
     */
    captureMousedown: {
        value: function (event) {
            if (!this.enabled) {
                return;
            }

            if (event.button === 0) {
                this._observedPointer = this._MOUSE_POINTER;
                this._start(event.clientX, event.clientY, event.target, event.timeStamp);
            }
        }
    },

    captureMousemove: {
        value: function (event) {
            if (this.enabled) {
                this._handleMove(event);                
            }

        }
    },

    handleMouseup: {
        value: function (event) {
            if (this.enabled) {
                this._end(event);
            }
        }
    },

    //Fixme: there is a graphic issue with popcorn with a chromebook if we don't prevent default.
    captureTouchstart: {
        value: function (event) {
            // If already scrolling, ignore any new touchstarts
            if (!this.enabled || this._observedPointer !== null) {
                return;
            }

            if (event.pointerId !== void 0) {
                this._observedPointer = event.pointerId;
                this._start(event.clientX, event.clientY, event.target, event.timeStamp);
                this._preventDefaultIfNeeded(event);

            } else {
                var targetTouches = event.targetTouches;

                if (targetTouches && targetTouches.length === 1) {
                    var touch = targetTouches[0];

                    this._preventDefaultIfNeeded(event);

                    this._observedPointer = touch.identifier;
                    this._start(touch.clientX, touch.clientY, touch.target, event.timeStamp);
                }
            }
        }
    },

    captureTouchmove: {
        value: function (event) {
            if (!this.enabled) {
                return;
            }
            if (event.pointerId !== void 0) {
                this._handleMove(event);
            } else {
                var touch = this._findObservedTouch(event.changedTouches);
                if (touch) {
                    this._handleMove(event, touch);
                }
            }
        }
    },

    handleTouchend: {
        value: function (event) {
            if (!this.enabled) {
                return;
            }

            if (event.pointerId !== void 0) {
                this._end(event);

            } else {
                var touch = this._findObservedTouch(event.changedTouches);

                if (touch) {
                    this._end(touch);
                }
            }
        }
    },

    handleTouchcancel: {
        value: function (event) {
            if (!this.enabled) {
                return;
            }

            if (event.pointerId !== void 0) {
                this._cancel(event);

            } else {
                var touch = this._findObservedTouch(event.changedTouches);

                if (touch) {
                    this._cancel(touch);
                }
            }
        }
    },

    captureScroll: {
        value: function (event) {
            if (event.target.contains(this.element)) {
                this._cancel(event);
            }
        }
    },


    /*------------------------------------------------------------------------------------------------------------------
     *                                             Private Functions.
     *-----------------------------------------------------------------------------------------------------------------/


    /**
     * Determines if the composer will call `preventDefault` on the DOM events it interprets.
     * @param {Event} The event
     * @returns {boolean} whether preventDefault should be called
     * @private
     **/
    _shouldPreventDefault: {
        value: function (event) {
            return !!event.target.tagName && this._NATIVE_ELEMENTS.indexOf(event.target.tagName) === -1 && !event.target.isContentEditable;
        }
    },

    _preventDefaultIfNeeded: {
        value: function (event) {
            if (!event.defaultPrevented && this._shouldPreventDefault(event)) {
                event.preventDefault();
            }
        }
    },

    _addWheelEventListener: {
        value: function () {
            if (this._element) {
                var wheelEventName = typeof window.onwheel !== "undefined" || typeof window.WheelEvent !== "undefined" ?
                    "wheel" : "mousewheel";

                this._element.addEventListener(wheelEventName, this, false);
                this._element.addEventListener(wheelEventName, this, true);
            }
        }
    },

    _removeWheelEventListener: {
        value: function () {
            if (this._element) {
                var wheelEventName = typeof window.onwheel !== "undefined" || typeof window.WheelEvent !== "undefined" ?
                    "wheel" : "mousewheel";

                this._element.removeEventListener(wheelEventName, this, false);
                this._element.removeEventListener(wheelEventName, this, true);
            }
        }
    },

    _start: {
        value: function (x, y, target, timeStamp) {
            this.pointerStartEventPosition = {
                pageX: x,
                pageY: y,
                target: target,
                timeStamp: timeStamp
            };

            this._pointerX = x;
            this._pointerY = y;

            if (window.PointerEvent) {
                this._element.addEventListener("pointerdown", this, false);
                document.addEventListener("pointermove", this, true);
                document.addEventListener("pointerup", this, false);
                document.addEventListener("pointercancel", this, false);

            } else if (window.MSPointerEvent && window.navigator.msPointerEnabled) {
                this._element.addEventListener("MSPointerDown", this, false);
                document.addEventListener("MSPointerMove", this, true);
                document.addEventListener("MSPointerUp", this, false);
                document.addEventListener("MSPointerCancel", this, false);

            } else {
                if (this._observedPointer === this._MOUSE_POINTER) {
                    this._element.addEventListener("mousedown", this, false);
                    document.addEventListener("mousemove", this, true);
                    document.addEventListener("mouseup", this, false);

                } else {
                    this._element.addEventListener("touchstart", this, false);
                    this._element.addEventListener("touchmove", this, true);
                    this._element.addEventListener("touchend", this, false);
                    this._element.addEventListener("touchcancel", this, false);
                }
            }

            document.addEventListener("scroll", this, true);

            if (this.isAnimating) {
                this.isAnimating = false;
                this._dispatchTranslateEnd();
            }

            this._isFirstMove = true;
        }
    },

    _handleStart: {
        value: function (event) {
            this._claimPointerPolicy = this.eventManager.componentClaimingPointer(this._observedPointer) ?
                this.CLAIM_POINTER_POLICIES.MOVE : this.CLAIM_POINTER_POLICIES.DEFAULT;
        }
    },

    _findObservedTouch: {
        value: function (changedTouches) {
            var touch = null, tmp;

            for (var i = 0; (tmp = changedTouches[i]) && touch === null; i++) {
                if (tmp.identifier === this._observedPointer) {
                    touch = tmp;
                }
            }

            return touch;
        }
    },

    _handleMove: {
        value: function (event, contactPoint) {
            var eventManager = this.eventManager,
                pointerStartEventPosition = this.pointerStartEventPosition;
            if (!contactPoint) {
                contactPoint = event;
            }

            if (this._isFirstMove) {
                var claimant = eventManager.componentClaimingPointer(this._observedPointer);

                if (claimant) {
                    var shouldClaimPointer = true;

                    if (this._claimPointerPolicy === this.CLAIM_POINTER_POLICIES.MOVE) {
                        var threshold = this._observedPointer === this._MOUSE_POINTER ? this._mouseRadiusThreshold : this._touchRadiusThreshold,
                            dX = pointerStartEventPosition.pageX - contactPoint.clientX,
                            dY = pointerStartEventPosition.pageY - contactPoint.clientY;

                        shouldClaimPointer = Composer.isCoordinateOutsideRadius(dX, dY, threshold);

                        if (shouldClaimPointer) {
                            // Updates translate start position when the claiming pointer policy is set to ”move".
                            pointerStartEventPosition.pageX = contactPoint.clientX;
                            pointerStartEventPosition.pageY = contactPoint.clientY;
                            pointerStartEventPosition.target = contactPoint.target;
                            pointerStartEventPosition.timeStamp = event.timeStamp;
                            this._pointerX = contactPoint.clientX;
                            this._pointerY = contactPoint.clientY;
                        }
                    }

                    if (!shouldClaimPointer) {
                        this._preventDefaultIfNeeded(event);
                        return void 0; // let's wait for the next move event
                    }
                }

                eventManager.claimPointer(this._observedPointer, this);
            }

            if (eventManager.isPointerClaimedByComponent(this._observedPointer, this)) {
                this._preventDefaultIfNeeded(event);

                if (this.allowTranslateOuterExtreme || this._shouldMove(event, contactPoint.clientX, contactPoint.clientY)) {
                    if (this._isFirstMove) {
                        this._firstMove();
                    } else {
                        this._move(contactPoint.clientX, contactPoint.clientY);
                    }
                }// else -> The translate composer will release interest when the movement will be over.
                // Indeed while moving the direction can change. (if reach max or min transalate for example)
            } else {
                // This component didn't claim the pointer so we stop
                // listening for further movement.
                this._releaseInterest();
            }
        }
    },

    _shouldMove: {
        value: function (event, x, y) {
            var translateY = this._translateY,
                translateX = this._translateX,
                minTranslateY = this._minTranslateY,
                maxTranslateY = this._maxTranslateY,
                minTranslateX = this._minTranslateX,
                maxTranslateX = this._maxTranslateX,
                canMove = true,
                isNegativeDeltaY,
                isNegativeDeltaX,
                deltaX,
                deltaY;

            if (event.type === "wheel" || event.type === "mousewheel") {
                if (this._axis !== "vertical") {
                    deltaX = ((event.wheelDeltaX || -event.deltaX || 0) * 20) / 120;
                }

                if (this._axis !== "horizontal") {
                    deltaY = ((event.wheelDeltaY || -event.deltaY || 0) * 20) / 120;
                }

                canMove = !(
                    (this._axis === "horizontal" && deltaX === 0) ||
                    (this._axis === "vertical" && deltaY === 0) ||
                    (deltaX ===  0 && deltaY === 0)
                );

            } else {
                if (this._axis !== "vertical") {
                    deltaX = -(this._invertXAxis ? (this._pointerX - x) : (x - this._pointerX));
                }

                if (this._axis !== "horizontal") {
                    deltaY = -(this._invertYAxis ? (this._pointerY - y) : (y - this._pointerY));
                }
            }

            if (canMove) {
                if (deltaY) {
                    isNegativeDeltaY = this._isNegativeNumber(deltaY);

                    if (minTranslateY !== null) {
                        // can moves if the current position is at the extreme top, but "scrolling" down or no extreme.
                        canMove = translateY !== minTranslateY || (translateY === minTranslateY && isNegativeDeltaY);
                    }


                    if (maxTranslateY !== null) {
                        if (canMove) {
                            // can moves if the current position is at the extreme bottom, but "scrolling" up or no extreme.
                            canMove = translateY !== maxTranslateY || (translateY === maxTranslateY && !isNegativeDeltaY);
                        }
                    }
                }

                if (deltaX) {
                    isNegativeDeltaX = this._isNegativeNumber(deltaX);

                    if (minTranslateX !== null) {
                        if (canMove) {
                            // can moves if the current position is at the extreme left, but "scrolling" right or no extreme.
                            canMove = translateX !== minTranslateX || (translateX === minTranslateX && isNegativeDeltaX);
                        }
                    }

                    if (maxTranslateX !== null) {
                        if (canMove) {
                            // can moves if the current position is at the extreme right, but "scrolling" left or no extreme.
                            canMove = translateX !== maxTranslateX || (translateX === maxTranslateX && !isNegativeDeltaX);
                        }
                    }
                }
            }


            return canMove;
        }
    },

    _firstMove: {
        value: function () {
            if (this._isFirstMove) {
                this._dispatchTranslateStart(this._translateX, this._translateY);
                this._isFirstMove = false;
                this.isMoving = true;
            }
        }
    },

    _move: {
        value: function (x, y) {
            var pointerDelta;
            this._isSelfUpdate = true;

            if (this._axis !== "vertical") {
                pointerDelta = this._invertXAxis ? (this._pointerX - x) : (x - this._pointerX);
                this.translateX += pointerDelta * this._pointerSpeedMultiplier;
            }

            if (this._axis !== "horizontal") {
                pointerDelta = this._invertYAxis ? (this._pointerY - y) : (y - this._pointerY);
                this.translateY += pointerDelta * this._pointerSpeedMultiplier;
            }

            this._isSelfUpdate = false;

            this._pointerX = x;
            this._pointerY = y;

            if (this._shouldDispatchTranslate) {
                this._dispatchTranslate();
            }
        }
    },

    _end: {
        value: function (event) {
            this.startTime = Date.now();

            this.endX = this.posX = this.startX=this._translateX;
            this.endY=this.posY=this.startY=this._translateY;

            var velocity = event.velocity;

            if ((this._hasMomentum) && ((velocity.speed>40) || this.translateStrideX || this.translateStrideY)) {
                if (this._axis !== "vertical") {
                    this.momentumX = velocity.x * this._pointerSpeedMultiplier * (this._invertXAxis ? 1 : -1);
                } else {
                    this.momentumX = 0;
                }
                if (this._axis !== "horizontal") {
                    this.momentumY = velocity.y * this._pointerSpeedMultiplier * (this._invertYAxis ? 1 : -1);
                } else {
                    this.momentumY=0;
                }
                this.endX = this.startX - (this.momentumX * this.__momentumDuration / 2000);
                this.endY = this.startY - (this.momentumY * this.__momentumDuration / 2000);
                this.startStrideXTime = null;
                this.startStrideYTime = null;
                this.animateMomentum = true;
            } else {
                this.animateMomentum = false;
            }

            if (this.animateMomentum) {
                this._animationInterval();
            } else if (!this._isFirstMove) {
                this.isMoving = false;
                // Only dispatch a translateEnd if a translate start has occured
                this._dispatchTranslateEnd();
            }

            this._releaseInterest();
        }
    },

    _cancel: {
        value: function (event) {
            this.startTime = Date.now();

            this.endX = this.posX = this.startX=this._translateX;
            this.endY=this.posY=this.startY=this._translateY;
            this.animateMomentum = false;

            if (!this._isFirstMove) {
                this.isMoving = false;
                // Only dispatch a translateCancel if a translate start has
                // occurred.
                this._dispatchTranslateCancel();
            }

            this._releaseInterest();
        }
    },

    _releaseInterest: {
        value: function () {
            if (window.PointerEvent) {
                this._element.removeEventListener("pointerdown", this, false);
                document.removeEventListener("pointermove", this, true);
                document.removeEventListener("pointerup", this, false);
                document.removeEventListener("pointercancel", this, false);

            } else if (window.MSPointerEvent && window.navigator.msPointerEnabled) {
                this._element.removeEventListener("MSPointerDown", this, false);
                document.removeEventListener("MSPointerMove", this, true);
                document.removeEventListener("MSPointerUp", this, false);
                document.removeEventListener("MSPointerCancel", this, false);

            } else {
                if (this._observedPointer === this._MOUSE_POINTER) {
                    this._element.removeEventListener("mousedown", this, false);
                    document.removeEventListener("mousemove", this, true);
                    document.removeEventListener("mouseup", this, false);

                } else {
                    this._element.removeEventListener("touchstart", this, false);
                    this._element.removeEventListener("touchmove", this, true);
                    this._element.removeEventListener("touchend", this, false);
                    this._element.removeEventListener("touchcancel", this, false);
                }
            }

            document.removeEventListener("scroll", this, true);

            if (this.eventManager.isPointerClaimedByComponent(this._observedPointer, this)) {
                this.eventManager.forfeitPointer(this._observedPointer, this);
            }

            this._observedPointer = null;
            this._isFirstMove = false;
            this.isMoving = false;
        }
    },

    _isAxisMovement: {
        value: function (event) {
            var velocity = event.velocity,
                lowerRight = 0.7853981633974483, // pi/4
                lowerLeft = 2.356194490192345, // 3pi/4
                upperLeft = -2.356194490192345, // 5pi/4
                upperRight = -0.7853981633974483, // 7pi/4
                isUp, isDown, isRight, isLeft,
                angle,
                dX, dY;

            if (this.axis === "both") {
                return true;
            }

            if (!velocity || 0 === velocity.speed || isNaN(velocity.speed)) {
                // If there's no speed then we calculate a vector from the
                // initial position to the current position.
                dX = this.pointerStartEventPosition.pageX - event.clientX;
                dY = this.pointerStartEventPosition.pageY - event.clientY;
                angle = Math.atan2(dY, dX);
            } else {
                angle = velocity.angle;
            }

            // The motion is with the grain of the element; we may want to see if we should claim the pointer
            if ("horizontal" === this.axis) {

                isRight = (angle <= lowerRight && angle >= upperRight);
                isLeft = (angle >= lowerLeft || angle <= upperLeft);

                if (isRight || isLeft) {
                    return true;
                }

            } else if ("vertical" === this.axis) {

                isUp = (angle <= upperRight && angle >= upperLeft);
                isDown = (angle >= lowerRight && angle <= lowerLeft);

                if (isUp || isDown) {
                    return true;
                }

            }

            return false;
        }
    },

    _translateEndTimeout: {
        value: null
    },

    _handleWheelTimeout: {
        value: null
    },

    _isNegativeNumber: {
        value: function (_number) {
            _number = +_number;

            if (!isNaN(_number)) {
                _number = 1/_number; // -> catch -0 (-infinity) && +0 (+infinity)
            }

            return _number < 0;
        }
    },

    captureWheel: {
        value: function (event) {
            if (this._shouldMove(event)) {
                if (!this.eventManager.isPointerClaimedByComponent(this._WHEEL_POINTER, this)) {
                    this.eventManager.claimPointer(this._WHEEL_POINTER, this);
                }

            } else if (this.eventManager.isPointerClaimedByComponent(this._WHEEL_POINTER, this)) {
                this.eventManager.forfeitPointer(this._WHEEL_POINTER, this);
            }
        }
    },

    handleWheel: {
        value: function (event) {
            if (!this.enabled) {
                return;
            }

            // If this composers' component is claiming the "wheel" pointer then handle the event
            if (this.eventManager.isPointerClaimedByComponent(this._WHEEL_POINTER, this)) {
                this._observedPointer = this._WHEEL_POINTER;

                if (this._translateEndTimeout) {
                    clearTimeout(this._translateEndTimeout);
                } else {
                    this._dispatchTranslateStart();
                }


                if (this.axis !== "vertical") {
                    this.translateX = this._translateX - ((event.wheelDeltaX || -event.deltaX || 0)* 20) / 120;
                }

                if (this.axis !== "horizontal") {
                    this.translateY = this._translateY - ((event.wheelDeltaY || -event.deltaY || 0)* 20) / 120;
                }

                this.isMoving = true;
                this._dispatchTranslate();

                if (typeof this._handleWheelTimeout !== "function") {
                    var _handleWheelTimeout = function () {
                        this._translateEndTimeout = null;
                        this._dispatchTranslateEnd();
                        this.isMoving = false;

                        if (this.eventManager.isPointerClaimedByComponent(this._WHEEL_POINTER, this)) {
                            this.eventManager.forfeitPointer(this._WHEEL_POINTER, this);
                        }
                    };

                    this._handleWheelTimeout = _handleWheelTimeout.bind(this);
                }

                this._translateEndTimeout = setTimeout(this._handleWheelTimeout, 400);

                // If we're not at one of the extremes (i.e. the scroll actually changed the translate)
                // then we want to preventDefault to stop the page scrolling.
                event.preventDefault();
            }
        }
    },

    _bezierTValue: {
        value: function (x, p1x, p1y, p2x, p2y) {
            var a = 1 - 3 * p2x + 3 * p1x,
                b = 3 * p2x - 6 * p1x,
                c = 3 * p1x,
                t = 0.5,
                der, i, k, tmp;

            for (i = 0; i < 10; i++) {
                tmp = t * t;
                der = 3 * a * tmp + 2 * b * t + c;
                k = 1 - t;
                t -= ((3 * (k * k * t * p1x + k * tmp * p2x) + tmp * t - x) / der); // der==0
            }
            tmp = t * t;
            k = 1 - t;
            return 3 * (k * k * t * p1y + k * tmp * p2y) + tmp * t;
        }
    },

    _dispatchTranslateStart: {
        value: function (x, y) {
            var translateStartEvent = document.createEvent("CustomEvent");

            translateStartEvent.initCustomEvent("translateStart", true, true, null);
            translateStartEvent.translateX = x;
            translateStartEvent.translateY = y;
            // Event needs to be the same shape as the one in flow-translate-composer
            translateStartEvent.scroll = 0;
            translateStartEvent.pointer = this._observedPointer;
            this.dispatchEvent(translateStartEvent);
        }
    },

    _dispatchTranslateEnd: {
        value: function () {
            var translateEndEvent = document.createEvent("CustomEvent");

            translateEndEvent.initCustomEvent("translateEnd", true, true, null);
            translateEndEvent.translateX = this._translateX;
            translateEndEvent.translateY = this._translateY;
            // Event needs to be the same shape as the one in flow-translate-composer
            translateEndEvent.scroll = 0;
            translateEndEvent.pointer = this._observedPointer;
            this.dispatchEvent(translateEndEvent);
        }
    },

    _dispatchTranslateCancel: {
        value: function () {
            var translateCancelEvent = document.createEvent("CustomEvent");

            translateCancelEvent.initCustomEvent("translateCancel", true, true, null);
            translateCancelEvent.translateX = this._translateX;
            translateCancelEvent.translateY = this._translateY;
            // Event needs to be the same shape as the one in flow-translate-composer
            translateCancelEvent.scroll = 0;
            translateCancelEvent.pointer = this._observedPointer;
            this.dispatchEvent(translateCancelEvent);
        }
    },

    _dispatchTranslate: {
        value: function () {
            var translateEvent = document.createEvent("CustomEvent");
            translateEvent.initCustomEvent("translate", true, true, null);
            translateEvent.translateX = this._translateX;
            translateEvent.translateY = this._translateY;
            // Event needs to be the same shape as the one in flow-translate-composer
            translateEvent.scroll = 0;
            translateEvent.pointer = this._observedPointer;
            this.dispatchEvent(translateEvent);
        }
    },

    animateBouncingX: {value: false, enumerable: false},
    startTimeBounceX: {value: false, enumerable: false},
    animateBouncingY: {value: false, enumerable: false},
    startTimeBounceY: {value: false, enumerable: false},
    animateMomentum: {value: false, enumerable: false},
    startTime: {value: null, enumerable: false},
    startX: {value: null, enumerable: false},
    posX: {value: null, enumerable: false},
    endX: {value: null, enumerable: false},
    startY: {value: null, enumerable: false},
    posY: {value: null, enumerable: false},
    endY: {value: null, enumerable: false},

    translateStrideX: {
        value: null
    },

    translateStrideY: {
        value: null
    },

    translateStrideDuration: {
        value: 330
    },

    _animationInterval: {
        value: function () {
            var time = Date.now(), t, tmp, tmpX, tmpY, animateStride = false;

            if (this.animateMomentum) {
                t=time-this.startTime;
                if (t<this.__momentumDuration) {
                    this.posX=this.startX-((this.momentumX+this.momentumX*(this.__momentumDuration-t)/this.__momentumDuration)*t/1000)/2;
                    this.posY=this.startY-((this.momentumY+this.momentumY*(this.__momentumDuration-t)/this.__momentumDuration)*t/1000)/2;
                    if (this.translateStrideX && (this.startStrideXTime === null) && ((this.__momentumDuration - t < this.translateStrideDuration) || (Math.abs(this.posX - this.endX) < this.translateStrideX * 0.75))) {
                        this.startStrideXTime = time;
                    }
                    if (this.translateStrideY && (this.startStrideYTime === null) && ((this.__momentumDuration - t < this.translateStrideDuration) || (Math.abs(this.posY - this.endY) < this.translateStrideY * 0.75))) {
                        this.startStrideYTime = time;
                    }
                } else {
                    this.animateMomentum = false;
                }
            }
            tmp = Math.round(this.endX / this.translateStrideX);
            if (this.startStrideXTime && (time - this.startStrideXTime > 0)) {
                if (time - this.startStrideXTime < this.translateStrideDuration) {
                    t = this._bezierTValue((time - this.startStrideXTime) / this.translateStrideDuration, 0.275, 0, 0.275, 1);
                    this.posX = this.posX * (1 - t) + (tmp *  this.translateStrideX) * t;
                    animateStride = true;
                } else {
                    this.posX = tmp * this.translateStrideX;
                }
            }
            tmp = Math.round(this.endY / this.translateStrideY);
            if (this.startStrideYTime && (time - this.startStrideYTime > 0)) {
                if (time - this.startStrideYTime < this.translateStrideDuration) {
                    t = this._bezierTValue((time - this.startStrideYTime) / this.translateStrideDuration, 0.275, 0, 0.275, 1);
                    this.posY = this.posY * (1 - t) + (tmp *  this.translateStrideY) * t;
                    animateStride = true;
                } else {
                    this.posY = tmp * this.translateStrideY;
                }
            }
            tmpX = this.posX;
            tmpY = this.posY;
            this._isSelfUpdate=true;
            this.translateX=tmpX;
            this.translateY=tmpY;
            if (this._shouldDispatchTranslate) {
                this._dispatchTranslate();
            }
            this._isSelfUpdate=false;
            this.isAnimating = this.animateMomentum || animateStride;
            if (this.isAnimating) {
                this.needsFrame=true;
            } else {
                this._dispatchTranslateEnd();
            }
        }
    }

});

TranslateComposer.prototype.captureMSPointerDown = TranslateComposer.prototype.capturePointerdown;
TranslateComposer.prototype.captureMSPointerMove = TranslateComposer.prototype.capturePointermove;
TranslateComposer.prototype.handleMSPointerUp = TranslateComposer.prototype.handlePointerup;
TranslateComposer.prototype.handleMSPointerCancel = TranslateComposer.prototype.handlePointercancel;
TranslateComposer.prototype.handleMousewheel = TranslateComposer.prototype.handleWheel;
TranslateComposer.prototype.handleMSPointerDown = TranslateComposer.prototype._handleStart;
TranslateComposer.prototype.handlePointerdown = TranslateComposer.prototype._handleStart;
TranslateComposer.prototype.handleMousedown = TranslateComposer.prototype._handleStart;
TranslateComposer.prototype.handleTouchstart = TranslateComposer.prototype._handleStart;

if(Math.sign !== void 0) {
    TranslateComposer.prototype._isNegativeNumber = function(_number) {
        return Math.sign(_number) === -1;
    };
}