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