Source: Components/draggable.js

define(["lib/jquery", "lib/underscore", "common/util/binding_list", "common/util/check"], function( $, _, BindingList, check) {

    "use strict";

    // Movements less than this number of pixels are ignored
    var TRANSLATION_THRESHOLD = 0; //10;

    // Rotations less than this number of radians are ignored
    var ROTATION_THRESHOLD = 0; //0.1;

    // Scaling less than this fraction is ignored
    var SCALING_THRESHOLD = 0; //0.1;

    // returns true iff abs(a-b) > threshold
    var exceedsThreshold = function(a, b, threshold) {
        return Math.abs(a - b) > threshold;
    };

    /**
     * Makes an element draggable.
     *
     * @param  {string | DOMElement | jQuery} el
     *   Draggable element. Can be a CSS selector, a DOM element, or a
     *   jQuery object.
     *
     * @param  {boolean} [multitouch]
     *   If true, allows resizing and rotation with two fingers. Defaults to
     *   true.
     *
     * @class
     * Draggables are elements that can be moved and (optionally) rotated and
     * scaled using touch and pinch-to-zoom.
     *
     * @property {Boolean} enabled Whether this Draggable is enabled.
     *
     * @name Lens.Components.Draggable
     */
    var Draggable = function(el, multitouch) {
        check(el, check.Match.Elementish);
        check(multitouch, check.Match.Optional(Boolean));

        /** @alias Lens.Components.Draggable.prototype */
        var self = {};

        var enabled = true;
        var onUpdateBindings = BindingList();
        Object.defineProperty(self, "enabled", { get: function() { return enabled; } });

        if(multitouch === undefined) {
            multitouch = true;
        }
        el = $(el);

        /**
         * Enables the draggable. Draggables start out enabled, so you only
         * need to call this if you've disabled the Draggable with `disable`.
         */
        self.enable = function() {
            enabled = true;
        };

        /**
         * Disables the draggable. The draggable will no longer move when
         * touched.
         */
        self.disable = function() {
            enabled = false;
        };

        /**
         * Registers a callback to be run when the component is dragged.
         * `onUpdateCallback` is called once per touch move event with the new
         *  position.
         *
         * @param  {Function} [onUpdateCallback]
         *   Callback called with onUpdateCallback(x, y, el).
         */
        self.updated = function(onUpdateCallback) {
            check(onUpdateCallback, Function);
            return onUpdateBindings.add(onUpdateCallback);
        };

        // current state of mover
        var offset = el.offset();
        var x = offset.left;
        var y = offset.top;
        var r = 0;
        var scale = 1;

        // Binding object for the move handler
        var binding = null;

        // updates mover to match current x, y, r, w, h
        var update = function() {
            var transform = "translate(" + x     + "px, " + y     + "px)  ";
            transform    += "scale("     + scale + ","    + scale + ")    ";
            transform    += "rotate("    + r                      + "rad) ";
            el.css("-webkit-transform", transform);
            onUpdateBindings.callAll(x, y, el[0]);
        };
        update();

        /**
         * Set the x, y position of the draggable.
         *
         * @param  {Number} [newX]
         *   New x position.
         *
         * @param  {Number} [newY]
         *   New y position.
         */
        self.setPosition = function(newX, newY) {
            check(newX, Number);
            check(newY, Number);
            x = newX;
            y = newY;
            update();
        };

        // make mover fixed positioned and reset position. use transition for
        // smooth movement
        el.css("position", "fixed")
          .css("top",      0)
          .css("bottom",   null)
          .css("left",     0)
          .css("right",    null)
          .css("-webkit-transition", "all 0.1s linear");

        el[0].addEventListener("lens:touchstart", function(evt) {
            if(!self.enabled) {
                return;
            }

            var touches = evt.targetTouches;

            // keep track of x,y,r,w,h at start of gesture
            var origX = x;
            var origY = y;
            var origR = r;
            var origScale = scale;

            // keep track of previous dx, dy, rotation, and scale so we can
            // ignore jitter
            var prevDx    = 0;
            var prevDy    = 0;
            var prevR     = 0;
            var prevScale = 1;

            // set the origin so rotation works correctly
            el.css("-webkit-transform-origin", touches.elX + " " + touches.elY);

            // clear old binding
            if(binding) {
                binding.clear();
            }

            // bind to the move handler
            binding = touches.moved(function() {
                if(exceedsThreshold(prevDx, touches.dx, TRANSLATION_THRESHOLD)) {
                    x = origX + touches.dx;
                    prevDx = touches.dx;
                }

                if(exceedsThreshold(prevDy, touches.dy, TRANSLATION_THRESHOLD)) {
                    y = origY + touches.dy;
                    prevDy = touches.dy;
                }


                if(multitouch) {
                    if(touches.rotation &&
                       exceedsThreshold(prevR, touches.rotation, ROTATION_THRESHOLD)) {

                        r = origR + touches.rotation;
                        prevR = touches.rotation;
                    }

                    if(exceedsThreshold(prevScale, touches.scale, SCALING_THRESHOLD)) {
                        scale = origScale * touches.scale;
                        prevScale = touches.scale;
                    }
                }

                update();
            });

        });

        Object.freeze(self);
        return self;
    };

    Lens.Components.Draggable = Draggable;
    return Draggable;
});