# 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;
        };
    }