jquery-udraggable plugin and dependency
[infodrom.org/service.infodrom.org] / src / jquery.udraggable.js
1 /*
2  * jQuery udraggable plugin v0.3.0
3  * Copyright (c) 2013-2014 Grant McLean (grant@mclean.net.nz)
4  *
5  * Homepage: https://github.com/grantm/jquery-udraggable
6  *
7  * Dual licensed under the MIT and GPL (v2.0 or later) licenses:
8  *   http://opensource.org/licenses/MIT
9  *   http://opensource.org/licenses/GPL-2.0
10  *
11  * This library requires Michael S. Mikowski's unified mouse and touch
12  * event plugin: https://github.com/mmikowski/jquery.event.ue
13  *
14  */
15
16 (function($) {
17     "use strict";
18
19     var floor = Math.floor;
20     var min   = Math.min;
21     var max   = Math.max;
22
23     window.requestAnimationFrame = window.requestAnimationFrame || function(work) {
24         return setTimeout(work, 10);
25     };
26
27     window.cancelAnimationFrame = window.cancelAnimationFrame || function(id) {
28         return clearTimeout(id);
29     };
30
31
32     // Constructor function
33
34     var UDraggable = function (el, options) {
35         var that = this;
36         this.el  = el;
37         this.$el = $(el);
38         this.options = $.extend({}, $.fn.udraggable.defaults, options);
39         this.positionElement  = this.options.positionElement  || this.positionElement;
40         this.getStartPosition = this.options.getStartPosition || this.getStartPosition;
41         this.updatePositionFrameHandler = function() {
42             delete that.queuedUpdate;
43             var pos = that.ui.position;
44             that.positionElement(that.$el, that.started, pos.left, pos.top);
45             if (that.options.dragUpdate) {
46                 that.options.dragUpdate.apply(that.el, [that.ui]);
47             }
48         };
49         this.queuePositionUpdate = function() {
50             if (!that.queuedUpdate) {
51                 that.queuedUpdate = window.requestAnimationFrame(that.updatePositionFrameHandler);
52             }
53         };
54         this.init();
55     };
56
57     UDraggable.prototype = {
58
59         constructor: UDraggable,
60
61         init: function() {
62             var that = this;
63             this.disabled = false;
64             this.started = false;
65             this.normalisePosition();
66             var $target = this.options.handle ?
67                           this.$el.find( this.options.handle ) :
68                           this.$el;
69             if (this.options.longPress) {
70                 $target
71                     .on('uheldstart.udraggable', function(e) { that.start(e); })
72                     .on('uheldmove.udraggable',  function(e) { that.move(e);  })
73                     .on('uheldend.udraggable',   function(e) { that.end(e);   });
74             }
75             else {
76                 $target
77                     .on('udragstart.udraggable', function(e) { that.start(e); })
78                     .on('udragmove.udraggable',  function(e) { that.move(e);  })
79                     .on('udragend.udraggable',   function(e) { that.end(e);   });
80             }
81         },
82
83         destroy: function() {
84             var $target = this.options.handle ?
85                           this.$el.find( this.options.handle ) :
86                           this.$el;
87             $target.off('.udraggable');
88             this.$el.removeData('udraggable');
89         },
90
91         disable: function() {
92             this.disabled = true;
93         },
94
95         enable: function() {
96             this.disabled = false;
97         },
98
99         option: function() {
100             var name;
101             if (arguments.length === 0) {
102                 return this.options;
103             }
104             if (arguments.length === 2) {
105                 this.options[ arguments[0] ] = arguments[1];
106                 return;
107             }
108             if (arguments.length === 1) {
109                 if (typeof arguments[0] === 'string') {
110                     return this.options[ arguments[0] ];
111                 }
112                 if (typeof arguments[0] === 'object') {
113                     for(name in arguments[0]) {
114                         if (arguments[0].hasOwnProperty(name)) {
115                             this.options[name] = arguments[0][name];
116                         }
117                     }
118                 }
119             }
120             if (this.options.containment) {
121                 this._initContainment();
122             }
123         },
124
125         normalisePosition: function() {
126             var pos = this.$el.position();
127             this.$el.css({
128                 position: 'absolute',
129                 top: pos.top,
130                 left: pos.left,
131                 right: 'auto',
132                 bottom: 'auto'
133             });
134         },
135
136         start: function(e) {
137             if (this.disabled) {
138                 return;
139             }
140             var start = this.getStartPosition(this.$el);
141             this._initContainment();
142             this.ui = {
143                 helper:           this.$el,
144                 offset:           { top: start.y, left: start.x},
145                 originalPosition: { top: start.y, left: start.x},
146                 position:         { top: start.y, left: start.x},
147             };
148             if (this.options.longPress) {
149                 this._start(e);
150             }
151             return this._stopPropagation(e);
152         },
153
154         move: function(e) {
155             if (this.disabled || (!this.started && !this._start(e))) {
156                 return;
157             }
158             var delta_x = e.px_current_x - e.px_start_x;
159             var delta_y = e.px_current_y - e.px_start_y;
160             var axis = this.options.axis;
161             if (axis  &&  axis === "x") {
162                 delta_y = 0;
163             }
164             if (axis  &&  axis === "y") {
165                 delta_x = 0;
166             }
167             var cur = {
168                 left: this.ui.originalPosition.left,
169                 top:  this.ui.originalPosition.top
170             };
171             if (!axis  ||  (axis === "x")) {
172                 cur.left += delta_x;
173             }
174             if (!axis  ||  (axis === "y")) {
175                 cur.top += delta_y;
176             }
177             this._applyGrid(cur);
178             this._applyContainment(cur);
179             var pos = this.ui.position;
180             if ((cur.top !== pos.top)  ||  (cur.left !== pos.left)) {
181                 this.ui.position.left = cur.left;
182                 this.ui.position.top  = cur.top;
183                 this.ui.offset.left   = cur.left;
184                 this.ui.offset.top    = cur.top;
185                 if (this.options.drag) {
186                     this.options.drag.apply(this.el, [e, this.ui]);
187                 }
188                 this.queuePositionUpdate();
189             }
190             return this._stopPropagation(e);
191         },
192
193         end: function(e) {
194             if (this.started || this._start(e)) {
195                 this.$el.removeClass("udraggable-dragging");
196                 this.started = false;
197                 if (this.queuedUpdate) {
198                     window.cancelAnimationFrame(this.queuedUpdate);
199                 }
200                 this.updatePositionFrameHandler();
201                 if (this.options.stop) {
202                     this.options.stop.apply(this.el, [e, this.ui]);
203                 }
204             }
205             return this._stopPropagation(e);
206         },
207
208         // helper methods
209
210         _stopPropagation: function(e) {
211             e.stopPropagation();
212             e.preventDefault();
213             return false;
214         },
215
216         _start: function(e) {
217             if (!this._mouseDistanceMet(e) || !this._mouseDelayMet(e)) {
218                 return;
219             }
220             this.started = true;
221             this.queuePositionUpdate();
222             if (this.options.start) {
223                 this.options.start.apply(this.el, [e, this.ui]);
224             }
225             this.$el.addClass("udraggable-dragging");
226             return true;
227         },
228
229         _mouseDistanceMet: function(e) {
230             return max(
231                 Math.abs(e.px_start_x - e.px_current_x),
232                 Math.abs(e.px_start_y - e.px_current_y)
233             ) >= this.options.distance;
234         },
235
236         _mouseDelayMet: function(e) {
237             return e.ms_elapsed > this.options.delay;
238         },
239
240         _initContainment: function() {
241             var o = this.options;
242             var $c, ce;
243
244             if (!o.containment) {
245                 this.containment = null;
246                 return;
247             }
248
249             if (o.containment.constructor === Array) {
250                 this.containment = o.containment;
251                 return;
252             }
253
254             if (o.containment === "parent") {
255                 o.containment = this.$el.offsetParent();
256             }
257
258             $c = $( o.containment );
259             ce = $c[ 0 ];
260             if (!ce) {
261                 return;
262             }
263
264             this.containment = [
265                 0,
266                 0,
267                 $c.innerWidth() - this.$el.outerWidth(),
268                 $c.innerHeight() - this.$el.outerHeight(),
269             ];
270         },
271
272         _applyGrid: function(cur) {
273             if (this.options.grid) {
274                 var gx = this.options.grid[0];
275                 var gy = this.options.grid[1];
276                 cur.left = floor( (cur.left + gx / 2) / gx ) * gx;
277                 cur.top  = floor( (cur.top  + gy / 2) / gy ) * gy;
278             }
279         },
280
281         _applyContainment: function(cur) {
282             var cont = this.containment;
283             if (cont) {
284                 cur.left = min( max(cur.left, cont[0]), cont[2] );
285                 cur.top  = min( max(cur.top,  cont[1]), cont[3] );
286             }
287         },
288
289         getStartPosition: function($el) {
290             return {
291                 x: parseInt($el.css('left'), 10) || 0,
292                 y: parseInt($el.css('top'),  10) || 0
293             };
294         },
295
296         positionElement: function($el, dragging, left, top) {
297             $el.css({ left: left, top: top });
298         }
299
300     };
301
302
303     // jQuery plugin function
304
305     $.fn.udraggable = function(option) {
306         var args = Array.prototype.slice.call(arguments, 1);
307         var results = [];
308         this.each(function () {
309             var $this = $(this);
310             var data = $this.data('udraggable');
311             if (!data) {
312                 data = new UDraggable(this, option);
313                 $this.data('udraggable', data);
314             }
315             if (typeof option === 'string') {  // option is a method - call it
316                 if(typeof data[option] !== 'function') {
317                     throw "jquery.udraggable has no '" + option + "' method";
318                 }
319                 var result = data[option].apply(data, args);
320                 if (result !== undefined) {
321                     results.push( result );
322                 }
323             }
324         });
325         return results.length > 0 ? results[0] : this;
326     };
327
328     $.fn.udraggable.defaults = {
329          axis:        null,
330          delay:       0,
331          distance:    0,
332          longPress:   false,
333          // callbacks
334          drag:        null,
335          start:       null,
336          stop:        null
337     };
338
339
340 })(jQuery);
341