my_dashboard 0.4.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (148) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +13 -0
  3. data/.travis.yml +9 -0
  4. data/CHANGELOG.md +23 -0
  5. data/Gemfile +20 -0
  6. data/MIT-LICENSE +20 -0
  7. data/README.md +223 -0
  8. data/Rakefile +7 -0
  9. data/app/assets/javascripts/my_dashboard/application.js +17 -0
  10. data/app/assets/javascripts/my_dashboard/my_dashboard.coffee +0 -0
  11. data/app/assets/stylesheets/my_dashboard/application.css +13 -0
  12. data/app/controllers/my_dashboard/application_controller.rb +23 -0
  13. data/app/controllers/my_dashboard/dashboards_controller.rb +31 -0
  14. data/app/controllers/my_dashboard/events_controller.rb +28 -0
  15. data/app/controllers/my_dashboard/widgets_controller.rb +45 -0
  16. data/app/helpers/my_dashboard/application_helper.rb +4 -0
  17. data/app/views/layouts/my_dashboard/dashboard.html.erb +30 -0
  18. data/app/views/my_dashboard/widgets/clock.html +2 -0
  19. data/app/views/my_dashboard/widgets/comments.html +7 -0
  20. data/app/views/my_dashboard/widgets/graph.html +5 -0
  21. data/app/views/my_dashboard/widgets/iframe.html +1 -0
  22. data/app/views/my_dashboard/widgets/image.html +1 -0
  23. data/app/views/my_dashboard/widgets/list.html +18 -0
  24. data/app/views/my_dashboard/widgets/meter.html +7 -0
  25. data/app/views/my_dashboard/widgets/number.html +11 -0
  26. data/app/views/my_dashboard/widgets/text.html +7 -0
  27. data/bin/rails +8 -0
  28. data/config/routes.rb +15 -0
  29. data/lib/assets/javascripts/my_dashboard.gridster.coffee +0 -0
  30. data/lib/generators/my_dashboard/install_generator.rb +36 -0
  31. data/lib/generators/my_dashboard/job_generator.rb +15 -0
  32. data/lib/generators/my_dashboard/widget_generator.rb +17 -0
  33. data/lib/generators/templates/dashboards/sample.html.erb +28 -0
  34. data/lib/generators/templates/initializer.rb +58 -0
  35. data/lib/generators/templates/jobs/new.rb +3 -0
  36. data/lib/generators/templates/jobs/sample.rb +9 -0
  37. data/lib/generators/templates/layouts/dashboard.html.erb +30 -0
  38. data/lib/generators/templates/widgets/index.css +12 -0
  39. data/lib/generators/templates/widgets/index.js +13 -0
  40. data/lib/generators/templates/widgets/new.coffee +9 -0
  41. data/lib/generators/templates/widgets/new.html +1 -0
  42. data/lib/generators/templates/widgets/new.scss +10 -0
  43. data/lib/my_dashboard.rb +50 -0
  44. data/lib/my_dashboard/configuration.rb +63 -0
  45. data/lib/my_dashboard/engine.rb +7 -0
  46. data/lib/my_dashboard/railtie.rb +31 -0
  47. data/lib/my_dashboard/version.rb +3 -0
  48. data/lib/tasks/my_dashboard_tasks.rake +4 -0
  49. data/my_dashboard.gemspec +31 -0
  50. data/spec/controllers/my_dashboard/application_controller_spec.rb +21 -0
  51. data/spec/controllers/my_dashboard/dashboards_controller_spec.rb +46 -0
  52. data/spec/controllers/my_dashboard/events_controller_spec.rb +11 -0
  53. data/spec/controllers/my_dashboard/widgets_controller_spec.rb +72 -0
  54. data/spec/dummy/README.rdoc +28 -0
  55. data/spec/dummy/Rakefile +6 -0
  56. data/spec/dummy/app/assets/images/.keep +0 -0
  57. data/spec/dummy/app/assets/javascripts/application.js +13 -0
  58. data/spec/dummy/app/assets/javascripts/my_dashboard/widgets/foo.coffee +0 -0
  59. data/spec/dummy/app/assets/javascripts/my_dashboard/widgets/index.js +13 -0
  60. data/spec/dummy/app/assets/stylesheets/application.css +13 -0
  61. data/spec/dummy/app/assets/stylesheets/my_dashboard/widgets/foo.scss +0 -0
  62. data/spec/dummy/app/assets/stylesheets/my_dashboard/widgets/index.css +12 -0
  63. data/spec/dummy/app/controllers/application_controller.rb +5 -0
  64. data/spec/dummy/app/controllers/concerns/.keep +0 -0
  65. data/spec/dummy/app/helpers/application_helper.rb +2 -0
  66. data/spec/dummy/app/jobs/sample.rb +9 -0
  67. data/spec/dummy/app/mailers/.keep +0 -0
  68. data/spec/dummy/app/models/.keep +0 -0
  69. data/spec/dummy/app/models/concerns/.keep +0 -0
  70. data/spec/dummy/app/views/layouts/application.html.erb +14 -0
  71. data/spec/dummy/app/views/layouts/my_dashboard/dashboard.html.erb +30 -0
  72. data/spec/dummy/app/views/my_dashboard/dashboards/foo.erb +0 -0
  73. data/spec/dummy/app/views/my_dashboard/dashboards/sample.html.erb +28 -0
  74. data/spec/dummy/app/views/my_dashboard/widgets/foo.html +0 -0
  75. data/spec/dummy/bin/bundle +3 -0
  76. data/spec/dummy/bin/rails +4 -0
  77. data/spec/dummy/bin/rake +4 -0
  78. data/spec/dummy/config.ru +4 -0
  79. data/spec/dummy/config/application.rb +23 -0
  80. data/spec/dummy/config/boot.rb +5 -0
  81. data/spec/dummy/config/database.yml +25 -0
  82. data/spec/dummy/config/environment.rb +5 -0
  83. data/spec/dummy/config/environments/development.rb +29 -0
  84. data/spec/dummy/config/environments/production.rb +80 -0
  85. data/spec/dummy/config/environments/test.rb +36 -0
  86. data/spec/dummy/config/initializers/backtrace_silencers.rb +7 -0
  87. data/spec/dummy/config/initializers/filter_parameter_logging.rb +4 -0
  88. data/spec/dummy/config/initializers/inflections.rb +16 -0
  89. data/spec/dummy/config/initializers/mime_types.rb +5 -0
  90. data/spec/dummy/config/initializers/my_dashboard.rb +58 -0
  91. data/spec/dummy/config/initializers/secret_token.rb +12 -0
  92. data/spec/dummy/config/initializers/session_store.rb +3 -0
  93. data/spec/dummy/config/initializers/wrap_parameters.rb +14 -0
  94. data/spec/dummy/config/locales/en.yml +23 -0
  95. data/spec/dummy/config/routes.rb +3 -0
  96. data/spec/dummy/lib/assets/.keep +0 -0
  97. data/spec/dummy/log/.keep +0 -0
  98. data/spec/dummy/public/404.html +58 -0
  99. data/spec/dummy/public/422.html +58 -0
  100. data/spec/dummy/public/500.html +57 -0
  101. data/spec/dummy/public/favicon.ico +0 -0
  102. data/spec/lib/generators/widget_generator_spec.rb +26 -0
  103. data/spec/lib/my_dashboard/configuration_spec.rb +66 -0
  104. data/spec/lib/my_dashboard_spec.rb +55 -0
  105. data/spec/spec_helper.rb +52 -0
  106. data/spec/support/controller_spec_helpers.rb +9 -0
  107. data/vendor/assets/fonts/my_dashboard/fontawesome-webfont.eot +0 -0
  108. data/vendor/assets/fonts/my_dashboard/fontawesome-webfont.svg +414 -0
  109. data/vendor/assets/fonts/my_dashboard/fontawesome-webfont.ttf +0 -0
  110. data/vendor/assets/fonts/my_dashboard/fontawesome-webfont.woff +0 -0
  111. data/vendor/assets/javascripts/my_dashboard/batman.jquery.js +163 -0
  112. data/vendor/assets/javascripts/my_dashboard/batman.js +13680 -0
  113. data/vendor/assets/javascripts/my_dashboard/d3-3.2.8.min.js +5 -0
  114. data/vendor/assets/javascripts/my_dashboard/default_widgets/clock.coffee +18 -0
  115. data/vendor/assets/javascripts/my_dashboard/default_widgets/comments.coffee +24 -0
  116. data/vendor/assets/javascripts/my_dashboard/default_widgets/graph.coffee +37 -0
  117. data/vendor/assets/javascripts/my_dashboard/default_widgets/iframe.coffee +9 -0
  118. data/vendor/assets/javascripts/my_dashboard/default_widgets/image.coffee +9 -0
  119. data/vendor/assets/javascripts/my_dashboard/default_widgets/index.js +13 -0
  120. data/vendor/assets/javascripts/my_dashboard/default_widgets/list.coffee +6 -0
  121. data/vendor/assets/javascripts/my_dashboard/default_widgets/meter.coffee +14 -0
  122. data/vendor/assets/javascripts/my_dashboard/default_widgets/number.coffee +25 -0
  123. data/vendor/assets/javascripts/my_dashboard/default_widgets/text.coffee +1 -0
  124. data/vendor/assets/javascripts/my_dashboard/es5-shim.js +1021 -0
  125. data/vendor/assets/javascripts/my_dashboard/index.js +21 -0
  126. data/vendor/assets/javascripts/my_dashboard/jquery.gridster.js +3987 -0
  127. data/vendor/assets/javascripts/my_dashboard/jquery.js +4 -0
  128. data/vendor/assets/javascripts/my_dashboard/jquery.knob.js +646 -0
  129. data/vendor/assets/javascripts/my_dashboard/jquery.leanModal.min.js +5 -0
  130. data/vendor/assets/javascripts/my_dashboard/jquery.timeago.js +184 -0
  131. data/vendor/assets/javascripts/my_dashboard/moment.min.js +6 -0
  132. data/vendor/assets/javascripts/my_dashboard/my_dashboard.coffee +0 -0
  133. data/vendor/assets/javascripts/my_dashboard/rickshaw-1.5.1.min.js +3 -0
  134. data/vendor/assets/stylesheets/my_dashboard/font-awesome.scss +1338 -0
  135. data/vendor/assets/stylesheets/my_dashboard/index.css +15 -0
  136. data/vendor/assets/stylesheets/my_dashboard/jquery.gridster.css +121 -0
  137. data/vendor/assets/stylesheets/my_dashboard/my_dashboard.scss +302 -0
  138. data/vendor/assets/stylesheets/my_dashboard/widgets/clock.scss +13 -0
  139. data/vendor/assets/stylesheets/my_dashboard/widgets/comments.scss +33 -0
  140. data/vendor/assets/stylesheets/my_dashboard/widgets/graph.scss +65 -0
  141. data/vendor/assets/stylesheets/my_dashboard/widgets/iframe.scss +8 -0
  142. data/vendor/assets/stylesheets/my_dashboard/widgets/image.scss +13 -0
  143. data/vendor/assets/stylesheets/my_dashboard/widgets/index.css +12 -0
  144. data/vendor/assets/stylesheets/my_dashboard/widgets/list.scss +60 -0
  145. data/vendor/assets/stylesheets/my_dashboard/widgets/meter.scss +35 -0
  146. data/vendor/assets/stylesheets/my_dashboard/widgets/number.scss +39 -0
  147. data/vendor/assets/stylesheets/my_dashboard/widgets/text.scss +32 -0
  148. metadata +373 -0
@@ -0,0 +1,21 @@
1
+ // This is a manifest file that'll be compiled into application.js, which will include all the files
2
+ // listed below.
3
+ //
4
+ // Any JavaScript/Coffee file within this directory, lib/assets/javascripts, vendor/assets/javascripts,
5
+ // or vendor/assets/javascripts of plugins, if any, can be referenced here using a relative path.
6
+ //
7
+ // It's not advisable to add code directly here, but if you do, it'll appear at the bottom of the
8
+ // the compiled file.
9
+ //
10
+ // WARNING: THE FIRST BLANK LINE MARKS THE END OF WHAT'S TO BE PROCESSED, ANY BLANK LINE SHOULD
11
+ // GO AFTER THE REQUIRES BELOW.
12
+ //
13
+ //= require jquery
14
+ //= require jquery.knob
15
+ //= require jquery.leanModal.min
16
+ //= require jquery.timeago
17
+ //= require moment.min
18
+ //= require d3-3.2.8.min
19
+ //= require rickshaw-1.5.1.min
20
+ //= require es5-shim
21
+ //= require my_dashboard
@@ -0,0 +1,3987 @@
1
+ /*! gridster.js - v0.5.6 - 2014-09-25
2
+ * http://gridster.net/
3
+ * Copyright (c) 2014 ducksboard; Licensed MIT */
4
+
5
+ ;(function(root, factory) {
6
+
7
+ if (typeof define === 'function' && define.amd) {
8
+ define('gridster-coords', ['jquery'], factory);
9
+ } else {
10
+ root.GridsterCoords = factory(root.$ || root.jQuery);
11
+ }
12
+
13
+ }(this, function($) {
14
+ /**
15
+ * Creates objects with coordinates (x1, y1, x2, y2, cx, cy, width, height)
16
+ * to simulate DOM elements on the screen.
17
+ * Coords is used by Gridster to create a faux grid with any DOM element can
18
+ * collide.
19
+ *
20
+ * @class Coords
21
+ * @param {HTMLElement|Object} obj The jQuery HTMLElement or a object with: left,
22
+ * top, width and height properties.
23
+ * @return {Object} Coords instance.
24
+ * @constructor
25
+ */
26
+ function Coords(obj) {
27
+ if (obj[0] && $.isPlainObject(obj[0])) {
28
+ this.data = obj[0];
29
+ }else {
30
+ this.el = obj;
31
+ }
32
+
33
+ this.isCoords = true;
34
+ this.coords = {};
35
+ this.init();
36
+ return this;
37
+ }
38
+
39
+
40
+ var fn = Coords.prototype;
41
+
42
+
43
+ fn.init = function(){
44
+ this.set();
45
+ this.original_coords = this.get();
46
+ };
47
+
48
+
49
+ fn.set = function(update, not_update_offsets) {
50
+ var el = this.el;
51
+
52
+ if (el && !update) {
53
+ this.data = el.offset();
54
+ this.data.width = el.width();
55
+ this.data.height = el.height();
56
+ }
57
+
58
+ if (el && update && !not_update_offsets) {
59
+ var offset = el.offset();
60
+ this.data.top = offset.top;
61
+ this.data.left = offset.left;
62
+ }
63
+
64
+ var d = this.data;
65
+
66
+ typeof d.left === 'undefined' && (d.left = d.x1);
67
+ typeof d.top === 'undefined' && (d.top = d.y1);
68
+
69
+ this.coords.x1 = d.left;
70
+ this.coords.y1 = d.top;
71
+ this.coords.x2 = d.left + d.width;
72
+ this.coords.y2 = d.top + d.height;
73
+ this.coords.cx = d.left + (d.width / 2);
74
+ this.coords.cy = d.top + (d.height / 2);
75
+ this.coords.width = d.width;
76
+ this.coords.height = d.height;
77
+ this.coords.el = el || false ;
78
+
79
+ return this;
80
+ };
81
+
82
+
83
+ fn.update = function(data){
84
+ if (!data && !this.el) {
85
+ return this;
86
+ }
87
+
88
+ if (data) {
89
+ var new_data = $.extend({}, this.data, data);
90
+ this.data = new_data;
91
+ return this.set(true, true);
92
+ }
93
+
94
+ this.set(true);
95
+ return this;
96
+ };
97
+
98
+
99
+ fn.get = function(){
100
+ return this.coords;
101
+ };
102
+
103
+ fn.destroy = function() {
104
+ this.el.removeData('coords');
105
+ delete this.el;
106
+ };
107
+
108
+ //jQuery adapter
109
+ $.fn.coords = function() {
110
+ if (this.data('coords') ) {
111
+ return this.data('coords');
112
+ }
113
+
114
+ var ins = new Coords(this, arguments[0]);
115
+ this.data('coords', ins);
116
+ return ins;
117
+ };
118
+
119
+ return Coords;
120
+
121
+ }));
122
+
123
+ ;(function(root, factory) {
124
+
125
+ if (typeof define === 'function' && define.amd) {
126
+ define('gridster-collision', ['jquery', 'gridster-coords'], factory);
127
+ } else {
128
+ root.GridsterCollision = factory(root.$ || root.jQuery,
129
+ root.GridsterCoords);
130
+ }
131
+
132
+ }(this, function($, Coords) {
133
+
134
+ var defaults = {
135
+ colliders_context: document.body,
136
+ overlapping_region: 'C'
137
+ // ,on_overlap: function(collider_data){},
138
+ // on_overlap_start : function(collider_data){},
139
+ // on_overlap_stop : function(collider_data){}
140
+ };
141
+
142
+
143
+ /**
144
+ * Detects collisions between a DOM element against other DOM elements or
145
+ * Coords objects.
146
+ *
147
+ * @class Collision
148
+ * @uses Coords
149
+ * @param {HTMLElement} el The jQuery wrapped HTMLElement.
150
+ * @param {HTMLElement|Array} colliders Can be a jQuery collection
151
+ * of HTMLElements or an Array of Coords instances.
152
+ * @param {Object} [options] An Object with all options you want to
153
+ * overwrite:
154
+ * @param {String} [options.overlapping_region] Determines when collision
155
+ * is valid, depending on the overlapped area. Values can be: 'N', 'S',
156
+ * 'W', 'E', 'C' or 'all'. Default is 'C'.
157
+ * @param {Function} [options.on_overlap_start] Executes a function the first
158
+ * time each `collider ` is overlapped.
159
+ * @param {Function} [options.on_overlap_stop] Executes a function when a
160
+ * `collider` is no longer collided.
161
+ * @param {Function} [options.on_overlap] Executes a function when the
162
+ * mouse is moved during the collision.
163
+ * @return {Object} Collision instance.
164
+ * @constructor
165
+ */
166
+ function Collision(el, colliders, options) {
167
+ this.options = $.extend(defaults, options);
168
+ this.$element = el;
169
+ this.last_colliders = [];
170
+ this.last_colliders_coords = [];
171
+ this.set_colliders(colliders);
172
+
173
+ this.init();
174
+ }
175
+
176
+ Collision.defaults = defaults;
177
+
178
+ var fn = Collision.prototype;
179
+
180
+
181
+ fn.init = function() {
182
+ this.find_collisions();
183
+ };
184
+
185
+
186
+ fn.overlaps = function(a, b) {
187
+ var x = false;
188
+ var y = false;
189
+
190
+ if ((b.x1 >= a.x1 && b.x1 <= a.x2) ||
191
+ (b.x2 >= a.x1 && b.x2 <= a.x2) ||
192
+ (a.x1 >= b.x1 && a.x2 <= b.x2)
193
+ ) { x = true; }
194
+
195
+ if ((b.y1 >= a.y1 && b.y1 <= a.y2) ||
196
+ (b.y2 >= a.y1 && b.y2 <= a.y2) ||
197
+ (a.y1 >= b.y1 && a.y2 <= b.y2)
198
+ ) { y = true; }
199
+
200
+ return (x && y);
201
+ };
202
+
203
+
204
+ fn.detect_overlapping_region = function(a, b){
205
+ var regionX = '';
206
+ var regionY = '';
207
+
208
+ if (a.y1 > b.cy && a.y1 < b.y2) { regionX = 'N'; }
209
+ if (a.y2 > b.y1 && a.y2 < b.cy) { regionX = 'S'; }
210
+ if (a.x1 > b.cx && a.x1 < b.x2) { regionY = 'W'; }
211
+ if (a.x2 > b.x1 && a.x2 < b.cx) { regionY = 'E'; }
212
+
213
+ return (regionX + regionY) || 'C';
214
+ };
215
+
216
+
217
+ fn.calculate_overlapped_area_coords = function(a, b){
218
+ var x1 = Math.max(a.x1, b.x1);
219
+ var y1 = Math.max(a.y1, b.y1);
220
+ var x2 = Math.min(a.x2, b.x2);
221
+ var y2 = Math.min(a.y2, b.y2);
222
+
223
+ return $({
224
+ left: x1,
225
+ top: y1,
226
+ width : (x2 - x1),
227
+ height: (y2 - y1)
228
+ }).coords().get();
229
+ };
230
+
231
+
232
+ fn.calculate_overlapped_area = function(coords){
233
+ return (coords.width * coords.height);
234
+ };
235
+
236
+
237
+ fn.manage_colliders_start_stop = function(new_colliders_coords, start_callback, stop_callback){
238
+ var last = this.last_colliders_coords;
239
+
240
+ for (var i = 0, il = last.length; i < il; i++) {
241
+ if ($.inArray(last[i], new_colliders_coords) === -1) {
242
+ start_callback.call(this, last[i]);
243
+ }
244
+ }
245
+
246
+ for (var j = 0, jl = new_colliders_coords.length; j < jl; j++) {
247
+ if ($.inArray(new_colliders_coords[j], last) === -1) {
248
+ stop_callback.call(this, new_colliders_coords[j]);
249
+ }
250
+
251
+ }
252
+ };
253
+
254
+
255
+ fn.find_collisions = function(player_data_coords){
256
+ var self = this;
257
+ var overlapping_region = this.options.overlapping_region;
258
+ var colliders_coords = [];
259
+ var colliders_data = [];
260
+ var $colliders = (this.colliders || this.$colliders);
261
+ var count = $colliders.length;
262
+ var player_coords = self.$element.coords()
263
+ .update(player_data_coords || false).get();
264
+
265
+ while(count--){
266
+ var $collider = self.$colliders ?
267
+ $($colliders[count]) : $colliders[count];
268
+ var $collider_coords_ins = ($collider.isCoords) ?
269
+ $collider : $collider.coords();
270
+ var collider_coords = $collider_coords_ins.get();
271
+ var overlaps = self.overlaps(player_coords, collider_coords);
272
+
273
+ if (!overlaps) {
274
+ continue;
275
+ }
276
+
277
+ var region = self.detect_overlapping_region(
278
+ player_coords, collider_coords);
279
+
280
+ //todo: make this an option
281
+ if (region === overlapping_region || overlapping_region === 'all') {
282
+
283
+ var area_coords = self.calculate_overlapped_area_coords(
284
+ player_coords, collider_coords);
285
+ var area = self.calculate_overlapped_area(area_coords);
286
+ var collider_data = {
287
+ area: area,
288
+ area_coords : area_coords,
289
+ region: region,
290
+ coords: collider_coords,
291
+ player_coords: player_coords,
292
+ el: $collider
293
+ };
294
+
295
+ if (self.options.on_overlap) {
296
+ self.options.on_overlap.call(this, collider_data);
297
+ }
298
+ colliders_coords.push($collider_coords_ins);
299
+ colliders_data.push(collider_data);
300
+ }
301
+ }
302
+
303
+ if (self.options.on_overlap_stop || self.options.on_overlap_start) {
304
+ this.manage_colliders_start_stop(colliders_coords,
305
+ self.options.on_overlap_start, self.options.on_overlap_stop);
306
+ }
307
+
308
+ this.last_colliders_coords = colliders_coords;
309
+
310
+ return colliders_data;
311
+ };
312
+
313
+
314
+ fn.get_closest_colliders = function(player_data_coords){
315
+ var colliders = this.find_collisions(player_data_coords);
316
+
317
+ colliders.sort(function(a, b) {
318
+ /* if colliders are being overlapped by the "C" (center) region,
319
+ * we have to set a lower index in the array to which they are placed
320
+ * above in the grid. */
321
+ if (a.region === 'C' && b.region === 'C') {
322
+ if (a.coords.y1 < b.coords.y1 || a.coords.x1 < b.coords.x1) {
323
+ return - 1;
324
+ }else{
325
+ return 1;
326
+ }
327
+ }
328
+
329
+ if (a.area < b.area) {
330
+ return 1;
331
+ }
332
+
333
+ return 1;
334
+ });
335
+ return colliders;
336
+ };
337
+
338
+
339
+ fn.set_colliders = function(colliders) {
340
+ if (typeof colliders === 'string' || colliders instanceof $) {
341
+ this.$colliders = $(colliders,
342
+ this.options.colliders_context).not(this.$element);
343
+ }else{
344
+ this.colliders = $(colliders);
345
+ }
346
+ };
347
+
348
+
349
+ //jQuery adapter
350
+ $.fn.collision = function(collider, options) {
351
+ return new Collision( this, collider, options );
352
+ };
353
+
354
+ return Collision;
355
+
356
+ }));
357
+
358
+ ;(function(window, undefined) {
359
+
360
+ /* Delay, debounce and throttle functions taken from underscore.js
361
+ *
362
+ * Copyright (c) 2009-2013 Jeremy Ashkenas, DocumentCloud and
363
+ * Investigative Reporters & Editors
364
+ *
365
+ * Permission is hereby granted, free of charge, to any person
366
+ * obtaining a copy of this software and associated documentation
367
+ * files (the "Software"), to deal in the Software without
368
+ * restriction, including without limitation the rights to use,
369
+ * copy, modify, merge, publish, distribute, sublicense, and/or sell
370
+ * copies of the Software, and to permit persons to whom the
371
+ * Software is furnished to do so, subject to the following
372
+ * conditions:
373
+ *
374
+ * The above copyright notice and this permission notice shall be
375
+ * included in all copies or substantial portions of the Software.
376
+ *
377
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
378
+ * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
379
+ * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
380
+ * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
381
+ * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
382
+ * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
383
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
384
+ * OTHER DEALINGS IN THE SOFTWARE.
385
+ */
386
+
387
+ window.delay = function(func, wait) {
388
+ var args = Array.prototype.slice.call(arguments, 2);
389
+ return setTimeout(function(){ return func.apply(null, args); }, wait);
390
+ };
391
+
392
+ window.debounce = function(func, wait, immediate) {
393
+ var timeout;
394
+ return function() {
395
+ var context = this, args = arguments;
396
+ var later = function() {
397
+ timeout = null;
398
+ if (!immediate) func.apply(context, args);
399
+ };
400
+ if (immediate && !timeout) func.apply(context, args);
401
+ clearTimeout(timeout);
402
+ timeout = setTimeout(later, wait);
403
+ };
404
+ };
405
+
406
+ window.throttle = function(func, wait) {
407
+ var context, args, timeout, throttling, more, result;
408
+ var whenDone = debounce(
409
+ function(){ more = throttling = false; }, wait);
410
+ return function() {
411
+ context = this; args = arguments;
412
+ var later = function() {
413
+ timeout = null;
414
+ if (more) func.apply(context, args);
415
+ whenDone();
416
+ };
417
+ if (!timeout) timeout = setTimeout(later, wait);
418
+ if (throttling) {
419
+ more = true;
420
+ } else {
421
+ result = func.apply(context, args);
422
+ }
423
+ whenDone();
424
+ throttling = true;
425
+ return result;
426
+ };
427
+ };
428
+
429
+ })(window);
430
+
431
+ ;(function(root, factory) {
432
+
433
+ if (typeof define === 'function' && define.amd) {
434
+ define('gridster-draggable', ['jquery'], factory);
435
+ } else {
436
+ root.GridsterDraggable = factory(root.$ || root.jQuery);
437
+ }
438
+
439
+ }(this, function($) {
440
+
441
+ var defaults = {
442
+ items: 'li',
443
+ distance: 1,
444
+ limit: true,
445
+ offset_left: 0,
446
+ autoscroll: true,
447
+ ignore_dragging: ['INPUT', 'TEXTAREA', 'SELECT', 'BUTTON'], // or function
448
+ handle: null,
449
+ container_width: 0, // 0 == auto
450
+ move_element: true,
451
+ helper: false, // or 'clone'
452
+ remove_helper: true
453
+ // drag: function(e) {},
454
+ // start : function(e, ui) {},
455
+ // stop : function(e) {}
456
+ };
457
+
458
+ var $window = $(window);
459
+ var dir_map = { x : 'left', y : 'top' };
460
+ var isTouch = !!('ontouchstart' in window);
461
+
462
+ var capitalize = function(str) {
463
+ return str.charAt(0).toUpperCase() + str.slice(1);
464
+ };
465
+
466
+ var idCounter = 0;
467
+ var uniqId = function() {
468
+ return ++idCounter + '';
469
+ }
470
+
471
+ /**
472
+ * Basic drag implementation for DOM elements inside a container.
473
+ * Provide start/stop/drag callbacks.
474
+ *
475
+ * @class Draggable
476
+ * @param {HTMLElement} el The HTMLelement that contains all the widgets
477
+ * to be dragged.
478
+ * @param {Object} [options] An Object with all options you want to
479
+ * overwrite:
480
+ * @param {HTMLElement|String} [options.items] Define who will
481
+ * be the draggable items. Can be a CSS Selector String or a
482
+ * collection of HTMLElements.
483
+ * @param {Number} [options.distance] Distance in pixels after mousedown
484
+ * the mouse must move before dragging should start.
485
+ * @param {Boolean} [options.limit] Constrains dragging to the width of
486
+ * the container
487
+ * @param {Object|Function} [options.ignore_dragging] Array of node names
488
+ * that sould not trigger dragging, by default is `['INPUT', 'TEXTAREA',
489
+ * 'SELECT', 'BUTTON']`. If a function is used return true to ignore dragging.
490
+ * @param {offset_left} [options.offset_left] Offset added to the item
491
+ * that is being dragged.
492
+ * @param {Number} [options.drag] Executes a callback when the mouse is
493
+ * moved during the dragging.
494
+ * @param {Number} [options.start] Executes a callback when the drag
495
+ * starts.
496
+ * @param {Number} [options.stop] Executes a callback when the drag stops.
497
+ * @return {Object} Returns `el`.
498
+ * @constructor
499
+ */
500
+ function Draggable(el, options) {
501
+ this.options = $.extend({}, defaults, options);
502
+ this.$document = $(document);
503
+ this.$container = $(el);
504
+ this.$dragitems = $(this.options.items, this.$container);
505
+ this.is_dragging = false;
506
+ this.player_min_left = 0 + this.options.offset_left;
507
+ this.id = uniqId();
508
+ this.ns = '.gridster-draggable-' + this.id;
509
+ this.init();
510
+ }
511
+
512
+ Draggable.defaults = defaults;
513
+
514
+ var fn = Draggable.prototype;
515
+
516
+ fn.init = function() {
517
+ var pos = this.$container.css('position');
518
+ this.calculate_dimensions();
519
+ this.$container.css('position', pos === 'static' ? 'relative' : pos);
520
+ this.disabled = false;
521
+ this.events();
522
+
523
+ $(window).bind(this.nsEvent('resize'),
524
+ throttle($.proxy(this.calculate_dimensions, this), 200));
525
+ };
526
+
527
+ fn.nsEvent = function(ev) {
528
+ return (ev || '') + this.ns;
529
+ };
530
+
531
+ fn.events = function() {
532
+ this.pointer_events = {
533
+ start: this.nsEvent('touchstart') + ' ' + this.nsEvent('mousedown'),
534
+ move: this.nsEvent('touchmove') + ' ' + this.nsEvent('mousemove'),
535
+ end: this.nsEvent('touchend') + ' ' + this.nsEvent('mouseup'),
536
+ };
537
+
538
+ this.$container.on(this.nsEvent('selectstart'),
539
+ $.proxy(this.on_select_start, this));
540
+
541
+ this.$container.on(this.pointer_events.start, this.options.items,
542
+ $.proxy(this.drag_handler, this));
543
+
544
+ this.$document.on(this.pointer_events.end, $.proxy(function(e) {
545
+ this.is_dragging = false;
546
+ if (this.disabled) { return; }
547
+ this.$document.off(this.pointer_events.move);
548
+ if (this.drag_start) {
549
+ this.on_dragstop(e);
550
+ }
551
+ }, this));
552
+ };
553
+
554
+ fn.get_actual_pos = function($el) {
555
+ var pos = $el.position();
556
+ return pos;
557
+ };
558
+
559
+
560
+ fn.get_mouse_pos = function(e) {
561
+ if (e.originalEvent && e.originalEvent.touches) {
562
+ var oe = e.originalEvent;
563
+ e = oe.touches.length ? oe.touches[0] : oe.changedTouches[0];
564
+ }
565
+
566
+ return {
567
+ left: e.clientX,
568
+ top: e.clientY
569
+ };
570
+ };
571
+
572
+
573
+ fn.get_offset = function(e) {
574
+ e.preventDefault();
575
+ var mouse_actual_pos = this.get_mouse_pos(e);
576
+ var diff_x = Math.round(
577
+ mouse_actual_pos.left - this.mouse_init_pos.left);
578
+ var diff_y = Math.round(mouse_actual_pos.top - this.mouse_init_pos.top);
579
+
580
+ var left = Math.round(this.el_init_offset.left +
581
+ diff_x - this.baseX + $(window).scrollLeft() - this.win_offset_x);
582
+ var top = Math.round(this.el_init_offset.top +
583
+ diff_y - this.baseY + $(window).scrollTop() - this.win_offset_y);
584
+
585
+ if (this.options.limit) {
586
+ if (left > this.player_max_left) {
587
+ left = this.player_max_left;
588
+ } else if(left < this.player_min_left) {
589
+ left = this.player_min_left;
590
+ }
591
+ }
592
+
593
+ return {
594
+ position: {
595
+ left: left,
596
+ top: top
597
+ },
598
+ pointer: {
599
+ left: mouse_actual_pos.left,
600
+ top: mouse_actual_pos.top,
601
+ diff_left: diff_x + ($(window).scrollLeft() - this.win_offset_x),
602
+ diff_top: diff_y + ($(window).scrollTop() - this.win_offset_y)
603
+ }
604
+ };
605
+ };
606
+
607
+
608
+ fn.get_drag_data = function(e) {
609
+ var offset = this.get_offset(e);
610
+ offset.$player = this.$player;
611
+ offset.$helper = this.helper ? this.$helper : this.$player;
612
+
613
+ return offset;
614
+ };
615
+
616
+
617
+ fn.set_limits = function(container_width) {
618
+ container_width || (container_width = this.$container.width());
619
+ this.player_max_left = (container_width - this.player_width +
620
+ - this.options.offset_left);
621
+
622
+ this.options.container_width = container_width;
623
+
624
+ return this;
625
+ };
626
+
627
+
628
+ fn.scroll_in = function(axis, data) {
629
+ var dir_prop = dir_map[axis];
630
+
631
+ var area_size = 50;
632
+ var scroll_inc = 30;
633
+
634
+ var is_x = axis === 'x';
635
+ var window_size = is_x ? this.window_width : this.window_height;
636
+ var doc_size = is_x ? $(document).width() : $(document).height();
637
+ var player_size = is_x ? this.$player.width() : this.$player.height();
638
+
639
+ var next_scroll;
640
+ var scroll_offset = $window['scroll' + capitalize(dir_prop)]();
641
+ var min_window_pos = scroll_offset;
642
+ var max_window_pos = min_window_pos + window_size;
643
+
644
+ var mouse_next_zone = max_window_pos - area_size; // down/right
645
+ var mouse_prev_zone = min_window_pos + area_size; // up/left
646
+
647
+ var abs_mouse_pos = min_window_pos + data.pointer[dir_prop];
648
+
649
+ var max_player_pos = (doc_size - window_size + player_size);
650
+
651
+ if (abs_mouse_pos >= mouse_next_zone) {
652
+ next_scroll = scroll_offset + scroll_inc;
653
+ if (next_scroll < max_player_pos) {
654
+ $window['scroll' + capitalize(dir_prop)](next_scroll);
655
+ this['scroll_offset_' + axis] += scroll_inc;
656
+ }
657
+ }
658
+
659
+ if (abs_mouse_pos <= mouse_prev_zone) {
660
+ next_scroll = scroll_offset - scroll_inc;
661
+ if (next_scroll > 0) {
662
+ $window['scroll' + capitalize(dir_prop)](next_scroll);
663
+ this['scroll_offset_' + axis] -= scroll_inc;
664
+ }
665
+ }
666
+
667
+ return this;
668
+ };
669
+
670
+
671
+ fn.manage_scroll = function(data) {
672
+ this.scroll_in('x', data);
673
+ this.scroll_in('y', data);
674
+ };
675
+
676
+
677
+ fn.calculate_dimensions = function(e) {
678
+ this.window_height = $window.height();
679
+ this.window_width = $window.width();
680
+ };
681
+
682
+
683
+ fn.drag_handler = function(e) {
684
+ var node = e.target.nodeName;
685
+ // skip if drag is disabled, or click was not done with the mouse primary button
686
+ if (this.disabled || e.which !== 1 && !isTouch) {
687
+ return;
688
+ }
689
+
690
+ if (this.ignore_drag(e)) {
691
+ return;
692
+ }
693
+
694
+ var self = this;
695
+ var first = true;
696
+ this.$player = $(e.currentTarget);
697
+
698
+ this.el_init_pos = this.get_actual_pos(this.$player);
699
+ this.mouse_init_pos = this.get_mouse_pos(e);
700
+ this.offsetY = this.mouse_init_pos.top - this.el_init_pos.top;
701
+
702
+ this.$document.on(this.pointer_events.move, function(mme) {
703
+ var mouse_actual_pos = self.get_mouse_pos(mme);
704
+ var diff_x = Math.abs(
705
+ mouse_actual_pos.left - self.mouse_init_pos.left);
706
+ var diff_y = Math.abs(
707
+ mouse_actual_pos.top - self.mouse_init_pos.top);
708
+ if (!(diff_x > self.options.distance ||
709
+ diff_y > self.options.distance)
710
+ ) {
711
+ return false;
712
+ }
713
+
714
+ if (first) {
715
+ first = false;
716
+ self.on_dragstart.call(self, mme);
717
+ return false;
718
+ }
719
+
720
+ if (self.is_dragging === true) {
721
+ self.on_dragmove.call(self, mme);
722
+ }
723
+
724
+ return false;
725
+ });
726
+
727
+ if (!isTouch) { return false; }
728
+ };
729
+
730
+
731
+ fn.on_dragstart = function(e) {
732
+ e.preventDefault();
733
+
734
+ if (this.is_dragging) { return this; }
735
+
736
+ this.drag_start = this.is_dragging = true;
737
+ var offset = this.$container.offset();
738
+ this.baseX = Math.round(offset.left);
739
+ this.baseY = Math.round(offset.top);
740
+ this.initial_container_width = this.options.container_width || this.$container.width();
741
+
742
+ if (this.options.helper === 'clone') {
743
+ this.$helper = this.$player.clone()
744
+ .appendTo(this.$container).addClass('helper');
745
+ this.helper = true;
746
+ } else {
747
+ this.helper = false;
748
+ }
749
+
750
+ this.win_offset_y = $(window).scrollTop();
751
+ this.win_offset_x = $(window).scrollLeft();
752
+ this.scroll_offset_y = 0;
753
+ this.scroll_offset_x = 0;
754
+ this.el_init_offset = this.$player.offset();
755
+ this.player_width = this.$player.width();
756
+ this.player_height = this.$player.height();
757
+
758
+ this.set_limits(this.options.container_width);
759
+
760
+ if (this.options.start) {
761
+ this.options.start.call(this.$player, e, this.get_drag_data(e));
762
+ }
763
+ return false;
764
+ };
765
+
766
+
767
+ fn.on_dragmove = function(e) {
768
+ var data = this.get_drag_data(e);
769
+
770
+ this.options.autoscroll && this.manage_scroll(data);
771
+
772
+ if (this.options.move_element) {
773
+ (this.helper ? this.$helper : this.$player).css({
774
+ 'position': 'absolute',
775
+ 'left' : data.position.left,
776
+ 'top' : data.position.top
777
+ });
778
+ }
779
+
780
+ var last_position = this.last_position || data.position;
781
+ data.prev_position = last_position;
782
+
783
+ if (this.options.drag) {
784
+ this.options.drag.call(this.$player, e, data);
785
+ }
786
+
787
+ this.last_position = data.position;
788
+ return false;
789
+ };
790
+
791
+
792
+ fn.on_dragstop = function(e) {
793
+ var data = this.get_drag_data(e);
794
+ this.drag_start = false;
795
+
796
+ if (this.options.stop) {
797
+ this.options.stop.call(this.$player, e, data);
798
+ }
799
+
800
+ if (this.helper && this.options.remove_helper) {
801
+ this.$helper.remove();
802
+ }
803
+
804
+ return false;
805
+ };
806
+
807
+ fn.on_select_start = function(e) {
808
+ if (this.disabled) { return; }
809
+
810
+ if (this.ignore_drag(e)) {
811
+ return;
812
+ }
813
+
814
+ return false;
815
+ };
816
+
817
+ fn.enable = function() {
818
+ this.disabled = false;
819
+ };
820
+
821
+ fn.disable = function() {
822
+ this.disabled = true;
823
+ };
824
+
825
+ fn.destroy = function() {
826
+ this.disable();
827
+
828
+ this.$container.off(this.ns);
829
+ this.$document.off(this.ns);
830
+ $(window).off(this.ns);
831
+
832
+ $.removeData(this.$container, 'drag');
833
+ };
834
+
835
+ fn.ignore_drag = function(event) {
836
+ if (this.options.handle) {
837
+ return !$(event.target).is(this.options.handle);
838
+ }
839
+
840
+ if ($.isFunction(this.options.ignore_dragging)) {
841
+ return this.options.ignore_dragging(event);
842
+ }
843
+
844
+ return $(event.target).is(this.options.ignore_dragging.join(', '));
845
+ };
846
+
847
+ //jQuery adapter
848
+ $.fn.drag = function ( options ) {
849
+ return new Draggable(this, options);
850
+ };
851
+
852
+ return Draggable;
853
+
854
+ }));
855
+
856
+ ;(function(root, factory) {
857
+
858
+ if (typeof define === 'function' && define.amd) {
859
+ define(['jquery', 'gridster-draggable', 'gridster-collision'], factory);
860
+ } else {
861
+ root.Gridster = factory(root.$ || root.jQuery, root.GridsterDraggable,
862
+ root.GridsterCollision);
863
+ }
864
+
865
+ }(this, function($, Draggable, Collision) {
866
+
867
+ var defaults = {
868
+ namespace: '',
869
+ widget_selector: 'li',
870
+ widget_margins: [10, 10],
871
+ widget_base_dimensions: [400, 225],
872
+ extra_rows: 0,
873
+ extra_cols: 0,
874
+ min_cols: 1,
875
+ max_cols: Infinity,
876
+ min_rows: 15,
877
+ max_size_x: false,
878
+ autogrow_cols: false,
879
+ autogenerate_stylesheet: true,
880
+ avoid_overlapped_widgets: true,
881
+ auto_init: true,
882
+ serialize_params: function($w, wgd) {
883
+ return {
884
+ col: wgd.col,
885
+ row: wgd.row,
886
+ size_x: wgd.size_x,
887
+ size_y: wgd.size_y
888
+ };
889
+ },
890
+ collision: {},
891
+ draggable: {
892
+ items: '.gs-w',
893
+ distance: 4,
894
+ ignore_dragging: Draggable.defaults.ignore_dragging.slice(0)
895
+ },
896
+ resize: {
897
+ enabled: false,
898
+ axes: ['both'],
899
+ handle_append_to: '',
900
+ handle_class: 'gs-resize-handle',
901
+ max_size: [Infinity, Infinity],
902
+ min_size: [1, 1]
903
+ }
904
+ };
905
+
906
+ /**
907
+ * @class Gridster
908
+ * @uses Draggable
909
+ * @uses Collision
910
+ * @param {HTMLElement} el The HTMLelement that contains all the widgets.
911
+ * @param {Object} [options] An Object with all options you want to
912
+ * overwrite:
913
+ * @param {HTMLElement|String} [options.widget_selector] Define who will
914
+ * be the draggable widgets. Can be a CSS Selector String or a
915
+ * collection of HTMLElements
916
+ * @param {Array} [options.widget_margins] Margin between widgets.
917
+ * The first index for the horizontal margin (left, right) and
918
+ * the second for the vertical margin (top, bottom).
919
+ * @param {Array} [options.widget_base_dimensions] Base widget dimensions
920
+ * in pixels. The first index for the width and the second for the
921
+ * height.
922
+ * @param {Number} [options.extra_cols] Add more columns in addition to
923
+ * those that have been calculated.
924
+ * @param {Number} [options.extra_rows] Add more rows in addition to
925
+ * those that have been calculated.
926
+ * @param {Number} [options.min_cols] The minimum required columns.
927
+ * @param {Number} [options.max_cols] The maximum columns possible (set to null
928
+ * for no maximum).
929
+ * @param {Number} [options.min_rows] The minimum required rows.
930
+ * @param {Number} [options.max_size_x] The maximum number of columns
931
+ * that a widget can span.
932
+ * @param {Boolean} [options.autogenerate_stylesheet] If true, all the
933
+ * CSS required to position all widgets in their respective columns
934
+ * and rows will be generated automatically and injected to the
935
+ * `<head>` of the document. You can set this to false, and write
936
+ * your own CSS targeting rows and cols via data-attributes like so:
937
+ * `[data-col="1"] { left: 10px; }`
938
+ * @param {Boolean} [options.avoid_overlapped_widgets] Avoid that widgets loaded
939
+ * from the DOM can be overlapped. It is helpful if the positions were
940
+ * bad stored in the database or if there was any conflict.
941
+ * @param {Boolean} [options.auto_init] Automatically call gridster init
942
+ * method or not when the plugin is instantiated.
943
+ * @param {Function} [options.serialize_params] Return the data you want
944
+ * for each widget in the serialization. Two arguments are passed:
945
+ * `$w`: the jQuery wrapped HTMLElement, and `wgd`: the grid
946
+ * coords object (`col`, `row`, `size_x`, `size_y`).
947
+ * @param {Object} [options.collision] An Object with all options for
948
+ * Collision class you want to overwrite. See Collision docs for
949
+ * more info.
950
+ * @param {Object} [options.draggable] An Object with all options for
951
+ * Draggable class you want to overwrite. See Draggable docs for more
952
+ * info.
953
+ * @param {Object|Function} [options.draggable.ignore_dragging] Note that
954
+ * if you use a Function, and resize is enabled, you should ignore the
955
+ * resize handlers manually (options.resize.handle_class).
956
+ * @param {Object} [options.resize] An Object with resize config options.
957
+ * @param {Boolean} [options.resize.enabled] Set to true to enable
958
+ * resizing.
959
+ * @param {Array} [options.resize.axes] Axes in which widgets can be
960
+ * resized. Possible values: ['x', 'y', 'both'].
961
+ * @param {String} [options.resize.handle_append_to] Set a valid CSS
962
+ * selector to append resize handles to.
963
+ * @param {String} [options.resize.handle_class] CSS class name used
964
+ * by resize handles.
965
+ * @param {Array} [options.resize.max_size] Limit widget dimensions
966
+ * when resizing. Array values should be integers:
967
+ * `[max_cols_occupied, max_rows_occupied]`
968
+ * @param {Array} [options.resize.min_size] Limit widget dimensions
969
+ * when resizing. Array values should be integers:
970
+ * `[min_cols_occupied, min_rows_occupied]`
971
+ * @param {Function} [options.resize.start] Function executed
972
+ * when resizing starts.
973
+ * @param {Function} [otions.resize.resize] Function executed
974
+ * during the resizing.
975
+ * @param {Function} [options.resize.stop] Function executed
976
+ * when resizing stops.
977
+ *
978
+ * @constructor
979
+ */
980
+ function Gridster(el, options) {
981
+ this.options = $.extend(true, {}, defaults, options);
982
+ this.$el = $(el);
983
+ this.$wrapper = this.$el.parent();
984
+ this.$widgets = this.$el.children(
985
+ this.options.widget_selector).addClass('gs-w');
986
+ this.widgets = [];
987
+ this.$changed = $([]);
988
+ this.wrapper_width = this.$wrapper.width();
989
+ this.min_widget_width = (this.options.widget_margins[0] * 2) +
990
+ this.options.widget_base_dimensions[0];
991
+ this.min_widget_height = (this.options.widget_margins[1] * 2) +
992
+ this.options.widget_base_dimensions[1];
993
+
994
+ this.generated_stylesheets = [];
995
+ this.$style_tags = $([]);
996
+
997
+ this.options.auto_init && this.init();
998
+ }
999
+
1000
+ Gridster.defaults = defaults;
1001
+ Gridster.generated_stylesheets = [];
1002
+
1003
+
1004
+ /**
1005
+ * Sorts an Array of grid coords objects (representing the grid coords of
1006
+ * each widget) in ascending way.
1007
+ *
1008
+ * @method sort_by_row_asc
1009
+ * @param {Array} widgets Array of grid coords objects
1010
+ * @return {Array} Returns the array sorted.
1011
+ */
1012
+ Gridster.sort_by_row_asc = function(widgets) {
1013
+ widgets = widgets.sort(function(a, b) {
1014
+ if (!a.row) {
1015
+ a = $(a).coords().grid;
1016
+ b = $(b).coords().grid;
1017
+ }
1018
+
1019
+ if (a.row > b.row) {
1020
+ return 1;
1021
+ }
1022
+ return -1;
1023
+ });
1024
+
1025
+ return widgets;
1026
+ };
1027
+
1028
+
1029
+ /**
1030
+ * Sorts an Array of grid coords objects (representing the grid coords of
1031
+ * each widget) placing first the empty cells upper left.
1032
+ *
1033
+ * @method sort_by_row_and_col_asc
1034
+ * @param {Array} widgets Array of grid coords objects
1035
+ * @return {Array} Returns the array sorted.
1036
+ */
1037
+ Gridster.sort_by_row_and_col_asc = function(widgets) {
1038
+ widgets = widgets.sort(function(a, b) {
1039
+ if (a.row > b.row || a.row === b.row && a.col > b.col) {
1040
+ return 1;
1041
+ }
1042
+ return -1;
1043
+ });
1044
+
1045
+ return widgets;
1046
+ };
1047
+
1048
+
1049
+ /**
1050
+ * Sorts an Array of grid coords objects by column (representing the grid
1051
+ * coords of each widget) in ascending way.
1052
+ *
1053
+ * @method sort_by_col_asc
1054
+ * @param {Array} widgets Array of grid coords objects
1055
+ * @return {Array} Returns the array sorted.
1056
+ */
1057
+ Gridster.sort_by_col_asc = function(widgets) {
1058
+ widgets = widgets.sort(function(a, b) {
1059
+ if (a.col > b.col) {
1060
+ return 1;
1061
+ }
1062
+ return -1;
1063
+ });
1064
+
1065
+ return widgets;
1066
+ };
1067
+
1068
+
1069
+ /**
1070
+ * Sorts an Array of grid coords objects (representing the grid coords of
1071
+ * each widget) in descending way.
1072
+ *
1073
+ * @method sort_by_row_desc
1074
+ * @param {Array} widgets Array of grid coords objects
1075
+ * @return {Array} Returns the array sorted.
1076
+ */
1077
+ Gridster.sort_by_row_desc = function(widgets) {
1078
+ widgets = widgets.sort(function(a, b) {
1079
+ if (a.row + a.size_y < b.row + b.size_y) {
1080
+ return 1;
1081
+ }
1082
+ return -1;
1083
+ });
1084
+ return widgets;
1085
+ };
1086
+
1087
+
1088
+
1089
+ /** Instance Methods **/
1090
+
1091
+ var fn = Gridster.prototype;
1092
+
1093
+ fn.init = function() {
1094
+ this.options.resize.enabled && this.setup_resize();
1095
+ this.generate_grid_and_stylesheet();
1096
+ this.get_widgets_from_DOM();
1097
+ this.set_dom_grid_height();
1098
+ this.set_dom_grid_width();
1099
+ this.$wrapper.addClass('ready');
1100
+ this.draggable();
1101
+ this.options.resize.enabled && this.resizable();
1102
+
1103
+ $(window).bind('resize.gridster', throttle(
1104
+ $.proxy(this.recalculate_faux_grid, this), 200));
1105
+ };
1106
+
1107
+
1108
+ /**
1109
+ * Disables dragging.
1110
+ *
1111
+ * @method disable
1112
+ * @return {Class} Returns the instance of the Gridster Class.
1113
+ */
1114
+ fn.disable = function() {
1115
+ this.$wrapper.find('.player-revert').removeClass('player-revert');
1116
+ this.drag_api.disable();
1117
+ return this;
1118
+ };
1119
+
1120
+
1121
+ /**
1122
+ * Enables dragging.
1123
+ *
1124
+ * @method enable
1125
+ * @return {Class} Returns the instance of the Gridster Class.
1126
+ */
1127
+ fn.enable = function() {
1128
+ this.drag_api.enable();
1129
+ return this;
1130
+ };
1131
+
1132
+
1133
+
1134
+ /**
1135
+ * Disables drag-and-drop widget resizing.
1136
+ *
1137
+ * @method disable
1138
+ * @return {Class} Returns instance of gridster Class.
1139
+ */
1140
+ fn.disable_resize = function() {
1141
+ this.$el.addClass('gs-resize-disabled');
1142
+ this.resize_api.disable();
1143
+ return this;
1144
+ };
1145
+
1146
+
1147
+ /**
1148
+ * Enables drag-and-drop widget resizing.
1149
+ *
1150
+ * @method enable
1151
+ * @return {Class} Returns instance of gridster Class.
1152
+ */
1153
+ fn.enable_resize = function() {
1154
+ this.$el.removeClass('gs-resize-disabled');
1155
+ this.resize_api.enable();
1156
+ return this;
1157
+ };
1158
+
1159
+
1160
+ /**
1161
+ * Add a new widget to the grid.
1162
+ *
1163
+ * @method add_widget
1164
+ * @param {String|HTMLElement} html The string representing the HTML of the widget
1165
+ * or the HTMLElement.
1166
+ * @param {Number} [size_x] The nº of rows the widget occupies horizontally.
1167
+ * @param {Number} [size_y] The nº of columns the widget occupies vertically.
1168
+ * @param {Number} [col] The column the widget should start in.
1169
+ * @param {Number} [row] The row the widget should start in.
1170
+ * @param {Array} [max_size] max_size Maximun size (in units) for width and height.
1171
+ * @param {Array} [min_size] min_size Minimum size (in units) for width and height.
1172
+ * @return {HTMLElement} Returns the jQuery wrapped HTMLElement representing.
1173
+ * the widget that was just created.
1174
+ */
1175
+ fn.add_widget = function(html, size_x, size_y, col, row, max_size, min_size) {
1176
+ var pos;
1177
+ size_x || (size_x = 1);
1178
+ size_y || (size_y = 1);
1179
+
1180
+ if (!col & !row) {
1181
+ pos = this.next_position(size_x, size_y);
1182
+ } else {
1183
+ pos = {
1184
+ col: col,
1185
+ row: row,
1186
+ size_x: size_x,
1187
+ size_y: size_y
1188
+ };
1189
+
1190
+ this.empty_cells(col, row, size_x, size_y);
1191
+ }
1192
+
1193
+ var $w = $(html).attr({
1194
+ 'data-col': pos.col,
1195
+ 'data-row': pos.row,
1196
+ 'data-sizex' : size_x,
1197
+ 'data-sizey' : size_y
1198
+ }).addClass('gs-w').appendTo(this.$el).hide();
1199
+
1200
+ this.$widgets = this.$widgets.add($w);
1201
+
1202
+ this.register_widget($w);
1203
+
1204
+ this.add_faux_rows(pos.size_y);
1205
+ //this.add_faux_cols(pos.size_x);
1206
+
1207
+ if (max_size) {
1208
+ this.set_widget_max_size($w, max_size);
1209
+ }
1210
+
1211
+ if (min_size) {
1212
+ this.set_widget_min_size($w, min_size);
1213
+ }
1214
+
1215
+ this.set_dom_grid_width();
1216
+ this.set_dom_grid_height();
1217
+
1218
+ this.drag_api.set_limits(this.cols * this.min_widget_width);
1219
+
1220
+ return $w.fadeIn();
1221
+ };
1222
+
1223
+
1224
+ /**
1225
+ * Change widget size limits.
1226
+ *
1227
+ * @method set_widget_min_size
1228
+ * @param {HTMLElement|Number} $widget The jQuery wrapped HTMLElement
1229
+ * representing the widget or an index representing the desired widget.
1230
+ * @param {Array} min_size Minimum size (in units) for width and height.
1231
+ * @return {HTMLElement} Returns instance of gridster Class.
1232
+ */
1233
+ fn.set_widget_min_size = function($widget, min_size) {
1234
+ $widget = typeof $widget === 'number' ?
1235
+ this.$widgets.eq($widget) : $widget;
1236
+
1237
+ if (!$widget.length) { return this; }
1238
+
1239
+ var wgd = $widget.data('coords').grid;
1240
+ wgd.min_size_x = min_size[0];
1241
+ wgd.min_size_y = min_size[1];
1242
+
1243
+ return this;
1244
+ };
1245
+
1246
+
1247
+ /**
1248
+ * Change widget size limits.
1249
+ *
1250
+ * @method set_widget_max_size
1251
+ * @param {HTMLElement|Number} $widget The jQuery wrapped HTMLElement
1252
+ * representing the widget or an index representing the desired widget.
1253
+ * @param {Array} max_size Maximun size (in units) for width and height.
1254
+ * @return {HTMLElement} Returns instance of gridster Class.
1255
+ */
1256
+ fn.set_widget_max_size = function($widget, max_size) {
1257
+ $widget = typeof $widget === 'number' ?
1258
+ this.$widgets.eq($widget) : $widget;
1259
+
1260
+ if (!$widget.length) { return this; }
1261
+
1262
+ var wgd = $widget.data('coords').grid;
1263
+ wgd.max_size_x = max_size[0];
1264
+ wgd.max_size_y = max_size[1];
1265
+
1266
+ return this;
1267
+ };
1268
+
1269
+
1270
+ /**
1271
+ * Append the resize handle into a widget.
1272
+ *
1273
+ * @method add_resize_handle
1274
+ * @param {HTMLElement} $widget The jQuery wrapped HTMLElement
1275
+ * representing the widget.
1276
+ * @return {HTMLElement} Returns instance of gridster Class.
1277
+ */
1278
+ fn.add_resize_handle = function($w) {
1279
+ var append_to = this.options.resize.handle_append_to;
1280
+ $(this.resize_handle_tpl).appendTo( append_to ? $(append_to, $w) : $w);
1281
+
1282
+ return this;
1283
+ };
1284
+
1285
+
1286
+ /**
1287
+ * Change the size of a widget. Width is limited to the current grid width.
1288
+ *
1289
+ * @method resize_widget
1290
+ * @param {HTMLElement} $widget The jQuery wrapped HTMLElement
1291
+ * representing the widget.
1292
+ * @param {Number} size_x The number of columns that will occupy the widget.
1293
+ * By default <code>size_x</code> is limited to the space available from
1294
+ * the column where the widget begins, until the last column to the right.
1295
+ * @param {Number} size_y The number of rows that will occupy the widget.
1296
+ * @param {Function} [callback] Function executed when the widget is removed.
1297
+ * @return {HTMLElement} Returns $widget.
1298
+ */
1299
+ fn.resize_widget = function($widget, size_x, size_y, callback) {
1300
+ var wgd = $widget.coords().grid;
1301
+ var col = wgd.col;
1302
+ var max_cols = this.options.max_cols;
1303
+ var old_size_y = wgd.size_y;
1304
+ var old_col = wgd.col;
1305
+ var new_col = old_col;
1306
+
1307
+ size_x || (size_x = wgd.size_x);
1308
+ size_y || (size_y = wgd.size_y);
1309
+
1310
+ if (max_cols !== Infinity) {
1311
+ size_x = Math.min(size_x, max_cols - col + 1);
1312
+ }
1313
+
1314
+ if (size_y > old_size_y) {
1315
+ this.add_faux_rows(Math.max(size_y - old_size_y, 0));
1316
+ }
1317
+
1318
+ var player_rcol = (col + size_x - 1);
1319
+ if (player_rcol > this.cols) {
1320
+ this.add_faux_cols(player_rcol - this.cols);
1321
+ }
1322
+
1323
+ var new_grid_data = {
1324
+ col: new_col,
1325
+ row: wgd.row,
1326
+ size_x: size_x,
1327
+ size_y: size_y
1328
+ };
1329
+
1330
+ this.mutate_widget_in_gridmap($widget, wgd, new_grid_data);
1331
+
1332
+ this.set_dom_grid_height();
1333
+ this.set_dom_grid_width();
1334
+
1335
+ if (callback) {
1336
+ callback.call(this, new_grid_data.size_x, new_grid_data.size_y);
1337
+ }
1338
+
1339
+ return $widget;
1340
+ };
1341
+
1342
+
1343
+ /**
1344
+ * Mutate widget dimensions and position in the grid map.
1345
+ *
1346
+ * @method mutate_widget_in_gridmap
1347
+ * @param {HTMLElement} $widget The jQuery wrapped HTMLElement
1348
+ * representing the widget to mutate.
1349
+ * @param {Object} wgd Current widget grid data (col, row, size_x, size_y).
1350
+ * @param {Object} new_wgd New widget grid data.
1351
+ * @return {HTMLElement} Returns instance of gridster Class.
1352
+ */
1353
+ fn.mutate_widget_in_gridmap = function($widget, wgd, new_wgd) {
1354
+ var old_size_x = wgd.size_x;
1355
+ var old_size_y = wgd.size_y;
1356
+
1357
+ var old_cells_occupied = this.get_cells_occupied(wgd);
1358
+ var new_cells_occupied = this.get_cells_occupied(new_wgd);
1359
+
1360
+ var empty_cols = [];
1361
+ $.each(old_cells_occupied.cols, function(i, col) {
1362
+ if ($.inArray(col, new_cells_occupied.cols) === -1) {
1363
+ empty_cols.push(col);
1364
+ }
1365
+ });
1366
+
1367
+ var occupied_cols = [];
1368
+ $.each(new_cells_occupied.cols, function(i, col) {
1369
+ if ($.inArray(col, old_cells_occupied.cols) === -1) {
1370
+ occupied_cols.push(col);
1371
+ }
1372
+ });
1373
+
1374
+ var empty_rows = [];
1375
+ $.each(old_cells_occupied.rows, function(i, row) {
1376
+ if ($.inArray(row, new_cells_occupied.rows) === -1) {
1377
+ empty_rows.push(row);
1378
+ }
1379
+ });
1380
+
1381
+ var occupied_rows = [];
1382
+ $.each(new_cells_occupied.rows, function(i, row) {
1383
+ if ($.inArray(row, old_cells_occupied.rows) === -1) {
1384
+ occupied_rows.push(row);
1385
+ }
1386
+ });
1387
+
1388
+ this.remove_from_gridmap(wgd);
1389
+
1390
+ if (occupied_cols.length) {
1391
+ var cols_to_empty = [
1392
+ new_wgd.col, new_wgd.row, new_wgd.size_x, Math.min(old_size_y, new_wgd.size_y), $widget
1393
+ ];
1394
+ this.empty_cells.apply(this, cols_to_empty);
1395
+ }
1396
+
1397
+ if (occupied_rows.length) {
1398
+ var rows_to_empty = [new_wgd.col, new_wgd.row, new_wgd.size_x, new_wgd.size_y, $widget];
1399
+ this.empty_cells.apply(this, rows_to_empty);
1400
+ }
1401
+
1402
+ // not the same that wgd = new_wgd;
1403
+ wgd.col = new_wgd.col;
1404
+ wgd.row = new_wgd.row;
1405
+ wgd.size_x = new_wgd.size_x;
1406
+ wgd.size_y = new_wgd.size_y;
1407
+
1408
+ this.add_to_gridmap(new_wgd, $widget);
1409
+
1410
+ $widget.removeClass('player-revert');
1411
+
1412
+ //update coords instance attributes
1413
+ $widget.data('coords').update({
1414
+ width: (new_wgd.size_x * this.options.widget_base_dimensions[0] +
1415
+ ((new_wgd.size_x - 1) * this.options.widget_margins[0]) * 2),
1416
+ height: (new_wgd.size_y * this.options.widget_base_dimensions[1] +
1417
+ ((new_wgd.size_y - 1) * this.options.widget_margins[1]) * 2)
1418
+ });
1419
+
1420
+ $widget.attr({
1421
+ 'data-col': new_wgd.col,
1422
+ 'data-row': new_wgd.row,
1423
+ 'data-sizex': new_wgd.size_x,
1424
+ 'data-sizey': new_wgd.size_y
1425
+ });
1426
+
1427
+ if (empty_cols.length) {
1428
+ var cols_to_remove_holes = [
1429
+ empty_cols[0], new_wgd.row,
1430
+ empty_cols.length,
1431
+ Math.min(old_size_y, new_wgd.size_y),
1432
+ $widget
1433
+ ];
1434
+
1435
+ this.remove_empty_cells.apply(this, cols_to_remove_holes);
1436
+ }
1437
+
1438
+ if (empty_rows.length) {
1439
+ var rows_to_remove_holes = [
1440
+ new_wgd.col, new_wgd.row, new_wgd.size_x, new_wgd.size_y, $widget
1441
+ ];
1442
+ this.remove_empty_cells.apply(this, rows_to_remove_holes);
1443
+ }
1444
+
1445
+ this.move_widget_up($widget);
1446
+
1447
+ return this;
1448
+ };
1449
+
1450
+
1451
+ /**
1452
+ * Move down widgets in cells represented by the arguments col, row, size_x,
1453
+ * size_y
1454
+ *
1455
+ * @method empty_cells
1456
+ * @param {Number} col The column where the group of cells begin.
1457
+ * @param {Number} row The row where the group of cells begin.
1458
+ * @param {Number} size_x The number of columns that the group of cells
1459
+ * occupy.
1460
+ * @param {Number} size_y The number of rows that the group of cells
1461
+ * occupy.
1462
+ * @param {HTMLElement} $exclude Exclude widgets from being moved.
1463
+ * @return {Class} Returns the instance of the Gridster Class.
1464
+ */
1465
+ fn.empty_cells = function(col, row, size_x, size_y, $exclude) {
1466
+ var $nexts = this.widgets_below({
1467
+ col: col,
1468
+ row: row - size_y,
1469
+ size_x: size_x,
1470
+ size_y: size_y
1471
+ });
1472
+
1473
+ $nexts.not($exclude).each($.proxy(function(i, w) {
1474
+ var wgd = $(w).coords().grid;
1475
+ if ( !(wgd.row <= (row + size_y - 1))) { return; }
1476
+ var diff = (row + size_y) - wgd.row;
1477
+ this.move_widget_down($(w), diff);
1478
+ }, this));
1479
+
1480
+ this.set_dom_grid_height();
1481
+
1482
+ return this;
1483
+ };
1484
+
1485
+
1486
+ /**
1487
+ * Move up widgets below cells represented by the arguments col, row, size_x,
1488
+ * size_y.
1489
+ *
1490
+ * @method remove_empty_cells
1491
+ * @param {Number} col The column where the group of cells begin.
1492
+ * @param {Number} row The row where the group of cells begin.
1493
+ * @param {Number} size_x The number of columns that the group of cells
1494
+ * occupy.
1495
+ * @param {Number} size_y The number of rows that the group of cells
1496
+ * occupy.
1497
+ * @param {HTMLElement} exclude Exclude widgets from being moved.
1498
+ * @return {Class} Returns the instance of the Gridster Class.
1499
+ */
1500
+ fn.remove_empty_cells = function(col, row, size_x, size_y, exclude) {
1501
+ var $nexts = this.widgets_below({
1502
+ col: col,
1503
+ row: row,
1504
+ size_x: size_x,
1505
+ size_y: size_y
1506
+ });
1507
+
1508
+ $nexts.not(exclude).each($.proxy(function(i, widget) {
1509
+ this.move_widget_up( $(widget), size_y );
1510
+ }, this));
1511
+
1512
+ this.set_dom_grid_height();
1513
+
1514
+ return this;
1515
+ };
1516
+
1517
+
1518
+ /**
1519
+ * Get the most left column below to add a new widget.
1520
+ *
1521
+ * @method next_position
1522
+ * @param {Number} size_x The nº of rows the widget occupies horizontally.
1523
+ * @param {Number} size_y The nº of columns the widget occupies vertically.
1524
+ * @return {Object} Returns a grid coords object representing the future
1525
+ * widget coords.
1526
+ */
1527
+ fn.next_position = function(size_x, size_y) {
1528
+ size_x || (size_x = 1);
1529
+ size_y || (size_y = 1);
1530
+ var ga = this.gridmap;
1531
+ var cols_l = ga.length;
1532
+ var valid_pos = [];
1533
+ var rows_l;
1534
+
1535
+ for (var c = 1; c < cols_l; c++) {
1536
+ rows_l = ga[c].length;
1537
+ for (var r = 1; r <= rows_l; r++) {
1538
+ var can_move_to = this.can_move_to({
1539
+ size_x: size_x,
1540
+ size_y: size_y
1541
+ }, c, r);
1542
+
1543
+ if (can_move_to) {
1544
+ valid_pos.push({
1545
+ col: c,
1546
+ row: r,
1547
+ size_y: size_y,
1548
+ size_x: size_x
1549
+ });
1550
+ }
1551
+ }
1552
+ }
1553
+
1554
+ if (valid_pos.length) {
1555
+ return Gridster.sort_by_row_and_col_asc(valid_pos)[0];
1556
+ }
1557
+ return false;
1558
+ };
1559
+
1560
+
1561
+ /**
1562
+ * Remove a widget from the grid.
1563
+ *
1564
+ * @method remove_widget
1565
+ * @param {HTMLElement} el The jQuery wrapped HTMLElement you want to remove.
1566
+ * @param {Boolean|Function} silent If true, widgets below the removed one
1567
+ * will not move up. If a Function is passed it will be used as callback.
1568
+ * @param {Function} callback Function executed when the widget is removed.
1569
+ * @return {Class} Returns the instance of the Gridster Class.
1570
+ */
1571
+ fn.remove_widget = function(el, silent, callback) {
1572
+ var $el = el instanceof $ ? el : $(el);
1573
+ var wgd = $el.coords().grid;
1574
+
1575
+ // if silent is a function assume it's a callback
1576
+ if ($.isFunction(silent)) {
1577
+ callback = silent;
1578
+ silent = false;
1579
+ }
1580
+
1581
+ this.cells_occupied_by_placeholder = {};
1582
+ this.$widgets = this.$widgets.not($el);
1583
+
1584
+ var $nexts = this.widgets_below($el);
1585
+
1586
+ this.remove_from_gridmap(wgd);
1587
+
1588
+ $el.fadeOut($.proxy(function() {
1589
+ $el.remove();
1590
+
1591
+ if (!silent) {
1592
+ $nexts.each($.proxy(function(i, widget) {
1593
+ this.move_widget_up( $(widget), wgd.size_y );
1594
+ }, this));
1595
+ }
1596
+
1597
+ this.set_dom_grid_height();
1598
+
1599
+ if (callback) {
1600
+ callback.call(this, el);
1601
+ }
1602
+ }, this));
1603
+
1604
+ return this;
1605
+ };
1606
+
1607
+
1608
+ /**
1609
+ * Remove all widgets from the grid.
1610
+ *
1611
+ * @method remove_all_widgets
1612
+ * @param {Function} callback Function executed for each widget removed.
1613
+ * @return {Class} Returns the instance of the Gridster Class.
1614
+ */
1615
+ fn.remove_all_widgets = function(callback) {
1616
+ this.$widgets.each($.proxy(function(i, el){
1617
+ this.remove_widget(el, true, callback);
1618
+ }, this));
1619
+
1620
+ return this;
1621
+ };
1622
+
1623
+
1624
+ /**
1625
+ * Returns a serialized array of the widgets in the grid.
1626
+ *
1627
+ * @method serialize
1628
+ * @param {HTMLElement} [$widgets] The collection of jQuery wrapped
1629
+ * HTMLElements you want to serialize. If no argument is passed all widgets
1630
+ * will be serialized.
1631
+ * @return {Array} Returns an Array of Objects with the data specified in
1632
+ * the serialize_params option.
1633
+ */
1634
+ fn.serialize = function($widgets) {
1635
+ $widgets || ($widgets = this.$widgets);
1636
+
1637
+ return $widgets.map($.proxy(function(i, widget) {
1638
+ var $w = $(widget);
1639
+ return this.options.serialize_params($w, $w.coords().grid);
1640
+ }, this)).get();
1641
+ };
1642
+
1643
+
1644
+ /**
1645
+ * Returns a serialized array of the widgets that have changed their
1646
+ * position.
1647
+ *
1648
+ * @method serialize_changed
1649
+ * @return {Array} Returns an Array of Objects with the data specified in
1650
+ * the serialize_params option.
1651
+ */
1652
+ fn.serialize_changed = function() {
1653
+ return this.serialize(this.$changed);
1654
+ };
1655
+
1656
+
1657
+ /**
1658
+ * Convert widgets from DOM elements to "widget grid data" Objects.
1659
+ *
1660
+ * @method dom_to_coords
1661
+ * @param {HTMLElement} $widget The widget to be converted.
1662
+ */
1663
+ fn.dom_to_coords = function($widget) {
1664
+ return {
1665
+ 'col': parseInt($widget.attr('data-col'), 10),
1666
+ 'row': parseInt($widget.attr('data-row'), 10),
1667
+ 'size_x': parseInt($widget.attr('data-sizex'), 10) || 1,
1668
+ 'size_y': parseInt($widget.attr('data-sizey'), 10) || 1,
1669
+ 'max_size_x': parseInt($widget.attr('data-max-sizex'), 10) || false,
1670
+ 'max_size_y': parseInt($widget.attr('data-max-sizey'), 10) || false,
1671
+ 'min_size_x': parseInt($widget.attr('data-min-sizex'), 10) || false,
1672
+ 'min_size_y': parseInt($widget.attr('data-min-sizey'), 10) || false,
1673
+ 'el': $widget
1674
+ };
1675
+ };
1676
+
1677
+
1678
+ /**
1679
+ * Creates the grid coords object representing the widget an add it to the
1680
+ * mapped array of positions.
1681
+ *
1682
+ * @method register_widget
1683
+ * @param {HTMLElement|Object} $el jQuery wrapped HTMLElement representing
1684
+ * the widget, or an "widget grid data" Object with (col, row, el ...).
1685
+ * @return {Boolean} Returns true if the widget final position is different
1686
+ * than the original.
1687
+ */
1688
+ fn.register_widget = function($el) {
1689
+ var isDOM = $el instanceof jQuery;
1690
+ var wgd = isDOM ? this.dom_to_coords($el) : $el;
1691
+ var posChanged = false;
1692
+ isDOM || ($el = wgd.el);
1693
+
1694
+ var empty_upper_row = this.can_go_widget_up(wgd);
1695
+ if (empty_upper_row) {
1696
+ wgd.row = empty_upper_row;
1697
+ $el.attr('data-row', empty_upper_row);
1698
+ this.$el.trigger('gridster:positionchanged', [wgd]);
1699
+ posChanged = true;
1700
+ }
1701
+
1702
+ if (this.options.avoid_overlapped_widgets &&
1703
+ !this.can_move_to(
1704
+ {size_x: wgd.size_x, size_y: wgd.size_y}, wgd.col, wgd.row)
1705
+ ) {
1706
+ $.extend(wgd, this.next_position(wgd.size_x, wgd.size_y));
1707
+ $el.attr({
1708
+ 'data-col': wgd.col,
1709
+ 'data-row': wgd.row,
1710
+ 'data-sizex': wgd.size_x,
1711
+ 'data-sizey': wgd.size_y
1712
+ });
1713
+ posChanged = true;
1714
+ }
1715
+
1716
+ // attach Coord object to player data-coord attribute
1717
+ $el.data('coords', $el.coords());
1718
+ // Extend Coord object with grid position info
1719
+ $el.data('coords').grid = wgd;
1720
+
1721
+ this.add_to_gridmap(wgd, $el);
1722
+
1723
+ this.options.resize.enabled && this.add_resize_handle($el);
1724
+
1725
+ return posChanged;
1726
+ };
1727
+
1728
+
1729
+ /**
1730
+ * Update in the mapped array of positions the value of cells represented by
1731
+ * the grid coords object passed in the `grid_data` param.
1732
+ *
1733
+ * @param {Object} grid_data The grid coords object representing the cells
1734
+ * to update in the mapped array.
1735
+ * @param {HTMLElement|Boolean} value Pass `false` or the jQuery wrapped
1736
+ * HTMLElement, depends if you want to delete an existing position or add
1737
+ * a new one.
1738
+ * @method update_widget_position
1739
+ * @return {Class} Returns the instance of the Gridster Class.
1740
+ */
1741
+ fn.update_widget_position = function(grid_data, value) {
1742
+ this.for_each_cell_occupied(grid_data, function(col, row) {
1743
+ if (!this.gridmap[col]) { return this; }
1744
+ this.gridmap[col][row] = value;
1745
+ });
1746
+ return this;
1747
+ };
1748
+
1749
+
1750
+ /**
1751
+ * Remove a widget from the mapped array of positions.
1752
+ *
1753
+ * @method remove_from_gridmap
1754
+ * @param {Object} grid_data The grid coords object representing the cells
1755
+ * to update in the mapped array.
1756
+ * @return {Class} Returns the instance of the Gridster Class.
1757
+ */
1758
+ fn.remove_from_gridmap = function(grid_data) {
1759
+ return this.update_widget_position(grid_data, false);
1760
+ };
1761
+
1762
+
1763
+ /**
1764
+ * Add a widget to the mapped array of positions.
1765
+ *
1766
+ * @method add_to_gridmap
1767
+ * @param {Object} grid_data The grid coords object representing the cells
1768
+ * to update in the mapped array.
1769
+ * @param {HTMLElement|Boolean} value The value to set in the specified
1770
+ * position .
1771
+ * @return {Class} Returns the instance of the Gridster Class.
1772
+ */
1773
+ fn.add_to_gridmap = function(grid_data, value) {
1774
+ this.update_widget_position(grid_data, value || grid_data.el);
1775
+
1776
+ if (grid_data.el) {
1777
+ var $widgets = this.widgets_below(grid_data.el);
1778
+ $widgets.each($.proxy(function(i, widget) {
1779
+ this.move_widget_up( $(widget));
1780
+ }, this));
1781
+ }
1782
+ };
1783
+
1784
+
1785
+ /**
1786
+ * Make widgets draggable.
1787
+ *
1788
+ * @uses Draggable
1789
+ * @method draggable
1790
+ * @return {Class} Returns the instance of the Gridster Class.
1791
+ */
1792
+ fn.draggable = function() {
1793
+ var self = this;
1794
+ var draggable_options = $.extend(true, {}, this.options.draggable, {
1795
+ offset_left: this.options.widget_margins[0],
1796
+ offset_top: this.options.widget_margins[1],
1797
+ container_width: this.cols * this.min_widget_width,
1798
+ limit: true,
1799
+ start: function(event, ui) {
1800
+ self.$widgets.filter('.player-revert')
1801
+ .removeClass('player-revert');
1802
+
1803
+ self.$player = $(this);
1804
+ self.$helper = $(ui.$helper);
1805
+
1806
+ self.helper = !self.$helper.is(self.$player);
1807
+
1808
+ self.on_start_drag.call(self, event, ui);
1809
+ self.$el.trigger('gridster:dragstart');
1810
+ },
1811
+ stop: function(event, ui) {
1812
+ self.on_stop_drag.call(self, event, ui);
1813
+ self.$el.trigger('gridster:dragstop');
1814
+ },
1815
+ drag: throttle(function(event, ui) {
1816
+ self.on_drag.call(self, event, ui);
1817
+ self.$el.trigger('gridster:drag');
1818
+ }, 60)
1819
+ });
1820
+
1821
+ this.drag_api = this.$el.drag(draggable_options);
1822
+ return this;
1823
+ };
1824
+
1825
+
1826
+ /**
1827
+ * Bind resize events to get resize working.
1828
+ *
1829
+ * @method resizable
1830
+ * @return {Class} Returns instance of gridster Class.
1831
+ */
1832
+ fn.resizable = function() {
1833
+ this.resize_api = this.$el.drag({
1834
+ items: '.' + this.options.resize.handle_class,
1835
+ offset_left: this.options.widget_margins[0],
1836
+ container_width: this.container_width,
1837
+ move_element: false,
1838
+ resize: true,
1839
+ limit: this.options.autogrow_cols ? false : true,
1840
+ start: $.proxy(this.on_start_resize, this),
1841
+ stop: $.proxy(function(event, ui) {
1842
+ delay($.proxy(function() {
1843
+ this.on_stop_resize(event, ui);
1844
+ }, this), 120);
1845
+ }, this),
1846
+ drag: throttle($.proxy(this.on_resize, this), 60)
1847
+ });
1848
+
1849
+ return this;
1850
+ };
1851
+
1852
+
1853
+ /**
1854
+ * Setup things required for resizing. Like build templates for drag handles.
1855
+ *
1856
+ * @method setup_resize
1857
+ * @return {Class} Returns instance of gridster Class.
1858
+ */
1859
+ fn.setup_resize = function() {
1860
+ this.resize_handle_class = this.options.resize.handle_class;
1861
+ var axes = this.options.resize.axes;
1862
+ var handle_tpl = '<span class="' + this.resize_handle_class + ' ' +
1863
+ this.resize_handle_class + '-{type}" />';
1864
+
1865
+ this.resize_handle_tpl = $.map(axes, function(type) {
1866
+ return handle_tpl.replace('{type}', type);
1867
+ }).join('');
1868
+
1869
+ if ($.isArray(this.options.draggable.ignore_dragging)) {
1870
+ this.options.draggable.ignore_dragging.push(
1871
+ '.' + this.resize_handle_class);
1872
+ }
1873
+
1874
+ return this;
1875
+ };
1876
+
1877
+
1878
+ /**
1879
+ * This function is executed when the player begins to be dragged.
1880
+ *
1881
+ * @method on_start_drag
1882
+ * @param {Event} event The original browser event
1883
+ * @param {Object} ui A prepared ui object with useful drag-related data
1884
+ */
1885
+ fn.on_start_drag = function(event, ui) {
1886
+ this.$helper.add(this.$player).add(this.$wrapper).addClass('dragging');
1887
+
1888
+ this.highest_col = this.get_highest_occupied_cell().col;
1889
+
1890
+ this.$player.addClass('player');
1891
+ this.player_grid_data = this.$player.coords().grid;
1892
+ this.placeholder_grid_data = $.extend({}, this.player_grid_data);
1893
+
1894
+ this.set_dom_grid_height(this.$el.height() +
1895
+ (this.player_grid_data.size_y * this.min_widget_height));
1896
+
1897
+ this.set_dom_grid_width(this.cols);
1898
+
1899
+ var pgd_sizex = this.player_grid_data.size_x;
1900
+ var cols_diff = this.cols - this.highest_col;
1901
+
1902
+ if (this.options.autogrow_cols && cols_diff <= pgd_sizex) {
1903
+ this.add_faux_cols(Math.min(pgd_sizex - cols_diff, 1));
1904
+ }
1905
+
1906
+ var colliders = this.faux_grid;
1907
+ var coords = this.$player.data('coords').coords;
1908
+
1909
+ this.cells_occupied_by_player = this.get_cells_occupied(
1910
+ this.player_grid_data);
1911
+ this.cells_occupied_by_placeholder = this.get_cells_occupied(
1912
+ this.placeholder_grid_data);
1913
+
1914
+ this.last_cols = [];
1915
+ this.last_rows = [];
1916
+
1917
+ // see jquery.collision.js
1918
+ this.collision_api = this.$helper.collision(
1919
+ colliders, this.options.collision);
1920
+
1921
+ this.$preview_holder = $('<' + this.$player.get(0).tagName + ' />', {
1922
+ 'class': 'preview-holder',
1923
+ 'data-row': this.$player.attr('data-row'),
1924
+ 'data-col': this.$player.attr('data-col'),
1925
+ css: {
1926
+ width: coords.width,
1927
+ height: coords.height
1928
+ }
1929
+ }).appendTo(this.$el);
1930
+
1931
+ if (this.options.draggable.start) {
1932
+ this.options.draggable.start.call(this, event, ui);
1933
+ }
1934
+ };
1935
+
1936
+
1937
+ /**
1938
+ * This function is executed when the player is being dragged.
1939
+ *
1940
+ * @method on_drag
1941
+ * @param {Event} event The original browser event
1942
+ * @param {Object} ui A prepared ui object with useful drag-related data
1943
+ */
1944
+ fn.on_drag = function(event, ui) {
1945
+ //break if dragstop has been fired
1946
+ if (this.$player === null) {
1947
+ return false;
1948
+ }
1949
+
1950
+ var abs_offset = {
1951
+ left: ui.position.left + this.baseX,
1952
+ top: ui.position.top + this.baseY
1953
+ };
1954
+
1955
+ // auto grow cols
1956
+ if (this.options.autogrow_cols) {
1957
+ var prcol = this.placeholder_grid_data.col +
1958
+ this.placeholder_grid_data.size_x - 1;
1959
+
1960
+ // "- 1" due to adding at least 1 column in on_start_drag
1961
+ if (prcol >= this.cols - 1 && this.options.max_cols >= this.cols + 1) {
1962
+ this.add_faux_cols(1);
1963
+ this.set_dom_grid_width(this.cols + 1);
1964
+ this.drag_api.set_limits(this.container_width);
1965
+ }
1966
+
1967
+ this.collision_api.set_colliders(this.faux_grid);
1968
+ }
1969
+
1970
+ this.colliders_data = this.collision_api.get_closest_colliders(
1971
+ abs_offset);
1972
+
1973
+ this.on_overlapped_column_change(
1974
+ this.on_start_overlapping_column, this.on_stop_overlapping_column);
1975
+
1976
+ this.on_overlapped_row_change(
1977
+ this.on_start_overlapping_row, this.on_stop_overlapping_row);
1978
+
1979
+
1980
+ if (this.helper && this.$player) {
1981
+ this.$player.css({
1982
+ 'left': ui.position.left,
1983
+ 'top': ui.position.top
1984
+ });
1985
+ }
1986
+
1987
+ if (this.options.draggable.drag) {
1988
+ this.options.draggable.drag.call(this, event, ui);
1989
+ }
1990
+ };
1991
+
1992
+
1993
+ /**
1994
+ * This function is executed when the player stops being dragged.
1995
+ *
1996
+ * @method on_stop_drag
1997
+ * @param {Event} event The original browser event
1998
+ * @param {Object} ui A prepared ui object with useful drag-related data
1999
+ */
2000
+ fn.on_stop_drag = function(event, ui) {
2001
+ this.$helper.add(this.$player).add(this.$wrapper)
2002
+ .removeClass('dragging');
2003
+
2004
+ ui.position.left = ui.position.left + this.baseX;
2005
+ ui.position.top = ui.position.top + this.baseY;
2006
+ this.colliders_data = this.collision_api.get_closest_colliders(
2007
+ ui.position);
2008
+
2009
+ this.on_overlapped_column_change(
2010
+ this.on_start_overlapping_column,
2011
+ this.on_stop_overlapping_column
2012
+ );
2013
+
2014
+ this.on_overlapped_row_change(
2015
+ this.on_start_overlapping_row,
2016
+ this.on_stop_overlapping_row
2017
+ );
2018
+
2019
+ this.$player.addClass('player-revert').removeClass('player')
2020
+ .attr({
2021
+ 'data-col': this.placeholder_grid_data.col,
2022
+ 'data-row': this.placeholder_grid_data.row
2023
+ }).css({
2024
+ 'left': '',
2025
+ 'top': ''
2026
+ });
2027
+
2028
+ this.$changed = this.$changed.add(this.$player);
2029
+
2030
+ this.cells_occupied_by_player = this.get_cells_occupied(
2031
+ this.placeholder_grid_data);
2032
+ this.set_cells_player_occupies(
2033
+ this.placeholder_grid_data.col, this.placeholder_grid_data.row);
2034
+
2035
+ this.$player.coords().grid.row = this.placeholder_grid_data.row;
2036
+ this.$player.coords().grid.col = this.placeholder_grid_data.col;
2037
+
2038
+ if (this.options.draggable.stop) {
2039
+ this.options.draggable.stop.call(this, event, ui);
2040
+ }
2041
+
2042
+ this.$preview_holder.remove();
2043
+
2044
+ this.$player = null;
2045
+ this.$helper = null;
2046
+ this.placeholder_grid_data = {};
2047
+ this.player_grid_data = {};
2048
+ this.cells_occupied_by_placeholder = {};
2049
+ this.cells_occupied_by_player = {};
2050
+
2051
+ this.set_dom_grid_height();
2052
+ this.set_dom_grid_width();
2053
+
2054
+ if (this.options.autogrow_cols) {
2055
+ this.drag_api.set_limits(this.cols * this.min_widget_width);
2056
+ }
2057
+ };
2058
+
2059
+
2060
+ /**
2061
+ * This function is executed every time a widget starts to be resized.
2062
+ *
2063
+ * @method on_start_resize
2064
+ * @param {Event} event The original browser event
2065
+ * @param {Object} ui A prepared ui object with useful drag-related data
2066
+ */
2067
+ fn.on_start_resize = function(event, ui) {
2068
+ this.$resized_widget = ui.$player.closest('.gs-w');
2069
+ this.resize_coords = this.$resized_widget.coords();
2070
+ this.resize_wgd = this.resize_coords.grid;
2071
+ this.resize_initial_width = this.resize_coords.coords.width;
2072
+ this.resize_initial_height = this.resize_coords.coords.height;
2073
+ this.resize_initial_sizex = this.resize_coords.grid.size_x;
2074
+ this.resize_initial_sizey = this.resize_coords.grid.size_y;
2075
+ this.resize_initial_col = this.resize_coords.grid.col;
2076
+ this.resize_last_sizex = this.resize_initial_sizex;
2077
+ this.resize_last_sizey = this.resize_initial_sizey;
2078
+
2079
+ this.resize_max_size_x = Math.min(this.resize_wgd.max_size_x ||
2080
+ this.options.resize.max_size[0],
2081
+ this.options.max_cols - this.resize_initial_col + 1);
2082
+ this.resize_max_size_y = this.resize_wgd.max_size_y ||
2083
+ this.options.resize.max_size[1];
2084
+
2085
+ this.resize_min_size_x = (this.resize_wgd.min_size_x ||
2086
+ this.options.resize.min_size[0] || 1);
2087
+ this.resize_min_size_y = (this.resize_wgd.min_size_y ||
2088
+ this.options.resize.min_size[1] || 1);
2089
+
2090
+ this.resize_initial_last_col = this.get_highest_occupied_cell().col;
2091
+
2092
+ this.set_dom_grid_width(this.cols);
2093
+
2094
+ this.resize_dir = {
2095
+ right: ui.$player.is('.' + this.resize_handle_class + '-x'),
2096
+ bottom: ui.$player.is('.' + this.resize_handle_class + '-y')
2097
+ };
2098
+
2099
+ this.$resized_widget.css({
2100
+ 'min-width': this.options.widget_base_dimensions[0],
2101
+ 'min-height': this.options.widget_base_dimensions[1]
2102
+ });
2103
+
2104
+ var nodeName = this.$resized_widget.get(0).tagName;
2105
+ this.$resize_preview_holder = $('<' + nodeName + ' />', {
2106
+ 'class': 'preview-holder resize-preview-holder',
2107
+ 'data-row': this.$resized_widget.attr('data-row'),
2108
+ 'data-col': this.$resized_widget.attr('data-col'),
2109
+ 'css': {
2110
+ 'width': this.resize_initial_width,
2111
+ 'height': this.resize_initial_height
2112
+ }
2113
+ }).appendTo(this.$el);
2114
+
2115
+ this.$resized_widget.addClass('resizing');
2116
+
2117
+ if (this.options.resize.start) {
2118
+ this.options.resize.start.call(this, event, ui, this.$resized_widget);
2119
+ }
2120
+
2121
+ this.$el.trigger('gridster:resizestart');
2122
+ };
2123
+
2124
+
2125
+ /**
2126
+ * This function is executed every time a widget stops being resized.
2127
+ *
2128
+ * @method on_stop_resize
2129
+ * @param {Event} event The original browser event
2130
+ * @param {Object} ui A prepared ui object with useful drag-related data
2131
+ */
2132
+ fn.on_stop_resize = function(event, ui) {
2133
+ this.$resized_widget
2134
+ .removeClass('resizing')
2135
+ .css({
2136
+ 'width': '',
2137
+ 'height': ''
2138
+ });
2139
+
2140
+ delay($.proxy(function() {
2141
+ this.$resize_preview_holder
2142
+ .remove()
2143
+ .css({
2144
+ 'min-width': '',
2145
+ 'min-height': ''
2146
+ });
2147
+
2148
+ if (this.options.resize.stop) {
2149
+ this.options.resize.stop.call(this, event, ui, this.$resized_widget);
2150
+ }
2151
+
2152
+ this.$el.trigger('gridster:resizestop');
2153
+ }, this), 300);
2154
+
2155
+ this.set_dom_grid_width();
2156
+
2157
+ if (this.options.autogrow_cols) {
2158
+ this.drag_api.set_limits(this.cols * this.min_widget_width);
2159
+ }
2160
+ };
2161
+
2162
+
2163
+ /**
2164
+ * This function is executed when a widget is being resized.
2165
+ *
2166
+ * @method on_resize
2167
+ * @param {Event} event The original browser event
2168
+ * @param {Object} ui A prepared ui object with useful drag-related data
2169
+ */
2170
+ fn.on_resize = function(event, ui) {
2171
+ var rel_x = (ui.pointer.diff_left);
2172
+ var rel_y = (ui.pointer.diff_top);
2173
+ var wbd_x = this.options.widget_base_dimensions[0];
2174
+ var wbd_y = this.options.widget_base_dimensions[1];
2175
+ var margin_x = this.options.widget_margins[0];
2176
+ var margin_y = this.options.widget_margins[1];
2177
+ var max_size_x = this.resize_max_size_x;
2178
+ var min_size_x = this.resize_min_size_x;
2179
+ var max_size_y = this.resize_max_size_y;
2180
+ var min_size_y = this.resize_min_size_y;
2181
+ var autogrow = this.options.autogrow_cols;
2182
+ var width;
2183
+ var max_width = Infinity;
2184
+ var max_height = Infinity;
2185
+
2186
+ var inc_units_x = Math.ceil((rel_x / (wbd_x + margin_x * 2)) - 0.2);
2187
+ var inc_units_y = Math.ceil((rel_y / (wbd_y + margin_y * 2)) - 0.2);
2188
+
2189
+ var size_x = Math.max(1, this.resize_initial_sizex + inc_units_x);
2190
+ var size_y = Math.max(1, this.resize_initial_sizey + inc_units_y);
2191
+
2192
+ var max_cols = (this.container_width / this.min_widget_width) -
2193
+ this.resize_initial_col + 1;
2194
+ var limit_width = ((max_cols * this.min_widget_width) - margin_x * 2);
2195
+
2196
+ size_x = Math.max(Math.min(size_x, max_size_x), min_size_x);
2197
+ size_x = Math.min(max_cols, size_x);
2198
+ width = (max_size_x * wbd_x) + ((size_x - 1) * margin_x * 2);
2199
+ max_width = Math.min(width, limit_width);
2200
+ min_width = (min_size_x * wbd_x) + ((size_x - 1) * margin_x * 2);
2201
+
2202
+ size_y = Math.max(Math.min(size_y, max_size_y), min_size_y);
2203
+ max_height = (max_size_y * wbd_y) + ((size_y - 1) * margin_y * 2);
2204
+ min_height = (min_size_y * wbd_y) + ((size_y - 1) * margin_y * 2);
2205
+
2206
+ if (this.resize_dir.right) {
2207
+ size_y = this.resize_initial_sizey;
2208
+ } else if (this.resize_dir.bottom) {
2209
+ size_x = this.resize_initial_sizex;
2210
+ }
2211
+
2212
+ if (autogrow) {
2213
+ var last_widget_col = this.resize_initial_col + size_x - 1;
2214
+ if (autogrow && this.resize_initial_last_col <= last_widget_col) {
2215
+ this.set_dom_grid_width(Math.max(last_widget_col + 1, this.cols));
2216
+
2217
+ if (this.cols < last_widget_col) {
2218
+ this.add_faux_cols(last_widget_col - this.cols);
2219
+ }
2220
+ }
2221
+ }
2222
+
2223
+ var css_props = {};
2224
+ !this.resize_dir.bottom && (css_props.width = Math.max(Math.min(
2225
+ this.resize_initial_width + rel_x, max_width), min_width));
2226
+ !this.resize_dir.right && (css_props.height = Math.max(Math.min(
2227
+ this.resize_initial_height + rel_y, max_height), min_height));
2228
+
2229
+ this.$resized_widget.css(css_props);
2230
+
2231
+ if (size_x !== this.resize_last_sizex ||
2232
+ size_y !== this.resize_last_sizey) {
2233
+
2234
+ this.resize_widget(this.$resized_widget, size_x, size_y);
2235
+ this.set_dom_grid_width(this.cols);
2236
+
2237
+ this.$resize_preview_holder.css({
2238
+ 'width': '',
2239
+ 'height': ''
2240
+ }).attr({
2241
+ 'data-row': this.$resized_widget.attr('data-row'),
2242
+ 'data-sizex': size_x,
2243
+ 'data-sizey': size_y
2244
+ });
2245
+ }
2246
+
2247
+ if (this.options.resize.resize) {
2248
+ this.options.resize.resize.call(this, event, ui, this.$resized_widget);
2249
+ }
2250
+
2251
+ this.$el.trigger('gridster:resize');
2252
+
2253
+ this.resize_last_sizex = size_x;
2254
+ this.resize_last_sizey = size_y;
2255
+ };
2256
+
2257
+
2258
+ /**
2259
+ * Executes the callbacks passed as arguments when a column begins to be
2260
+ * overlapped or stops being overlapped.
2261
+ *
2262
+ * @param {Function} start_callback Function executed when a new column
2263
+ * begins to be overlapped. The column is passed as first argument.
2264
+ * @param {Function} stop_callback Function executed when a column stops
2265
+ * being overlapped. The column is passed as first argument.
2266
+ * @method on_overlapped_column_change
2267
+ * @return {Class} Returns the instance of the Gridster Class.
2268
+ */
2269
+ fn.on_overlapped_column_change = function(start_callback, stop_callback) {
2270
+ if (!this.colliders_data.length) {
2271
+ return this;
2272
+ }
2273
+ var cols = this.get_targeted_columns(
2274
+ this.colliders_data[0].el.data.col);
2275
+
2276
+ var last_n_cols = this.last_cols.length;
2277
+ var n_cols = cols.length;
2278
+ var i;
2279
+
2280
+ for (i = 0; i < n_cols; i++) {
2281
+ if ($.inArray(cols[i], this.last_cols) === -1) {
2282
+ (start_callback || $.noop).call(this, cols[i]);
2283
+ }
2284
+ }
2285
+
2286
+ for (i = 0; i< last_n_cols; i++) {
2287
+ if ($.inArray(this.last_cols[i], cols) === -1) {
2288
+ (stop_callback || $.noop).call(this, this.last_cols[i]);
2289
+ }
2290
+ }
2291
+
2292
+ this.last_cols = cols;
2293
+
2294
+ return this;
2295
+ };
2296
+
2297
+
2298
+ /**
2299
+ * Executes the callbacks passed as arguments when a row starts to be
2300
+ * overlapped or stops being overlapped.
2301
+ *
2302
+ * @param {Function} start_callback Function executed when a new row begins
2303
+ * to be overlapped. The row is passed as first argument.
2304
+ * @param {Function} end_callback Function executed when a row stops being
2305
+ * overlapped. The row is passed as first argument.
2306
+ * @method on_overlapped_row_change
2307
+ * @return {Class} Returns the instance of the Gridster Class.
2308
+ */
2309
+ fn.on_overlapped_row_change = function(start_callback, end_callback) {
2310
+ if (!this.colliders_data.length) {
2311
+ return this;
2312
+ }
2313
+ var rows = this.get_targeted_rows(this.colliders_data[0].el.data.row);
2314
+ var last_n_rows = this.last_rows.length;
2315
+ var n_rows = rows.length;
2316
+ var i;
2317
+
2318
+ for (i = 0; i < n_rows; i++) {
2319
+ if ($.inArray(rows[i], this.last_rows) === -1) {
2320
+ (start_callback || $.noop).call(this, rows[i]);
2321
+ }
2322
+ }
2323
+
2324
+ for (i = 0; i < last_n_rows; i++) {
2325
+ if ($.inArray(this.last_rows[i], rows) === -1) {
2326
+ (end_callback || $.noop).call(this, this.last_rows[i]);
2327
+ }
2328
+ }
2329
+
2330
+ this.last_rows = rows;
2331
+ };
2332
+
2333
+
2334
+ /**
2335
+ * Sets the current position of the player
2336
+ *
2337
+ * @param {Number} col
2338
+ * @param {Number} row
2339
+ * @param {Boolean} no_player
2340
+ * @method set_player
2341
+ * @return {object}
2342
+ */
2343
+ fn.set_player = function(col, row, no_player) {
2344
+ var self = this;
2345
+ if (!no_player) {
2346
+ this.empty_cells_player_occupies();
2347
+ }
2348
+ var cell = !no_player ? self.colliders_data[0].el.data : {col: col};
2349
+ var to_col = cell.col;
2350
+ var to_row = row || cell.row;
2351
+
2352
+ this.player_grid_data = {
2353
+ col: to_col,
2354
+ row: to_row,
2355
+ size_y : this.player_grid_data.size_y,
2356
+ size_x : this.player_grid_data.size_x
2357
+ };
2358
+
2359
+ this.cells_occupied_by_player = this.get_cells_occupied(
2360
+ this.player_grid_data);
2361
+
2362
+ var $overlapped_widgets = this.get_widgets_overlapped(
2363
+ this.player_grid_data);
2364
+
2365
+ var constraints = this.widgets_constraints($overlapped_widgets);
2366
+
2367
+ this.manage_movements(constraints.can_go_up, to_col, to_row);
2368
+ this.manage_movements(constraints.can_not_go_up, to_col, to_row);
2369
+
2370
+ /* if there is not widgets overlapping in the new player position,
2371
+ * update the new placeholder position. */
2372
+ if (!$overlapped_widgets.length) {
2373
+ var pp = this.can_go_player_up(this.player_grid_data);
2374
+ if (pp !== false) {
2375
+ to_row = pp;
2376
+ }
2377
+ this.set_placeholder(to_col, to_row);
2378
+ }
2379
+
2380
+ return {
2381
+ col: to_col,
2382
+ row: to_row
2383
+ };
2384
+ };
2385
+
2386
+
2387
+ /**
2388
+ * See which of the widgets in the $widgets param collection can go to
2389
+ * a upper row and which not.
2390
+ *
2391
+ * @method widgets_contraints
2392
+ * @param {jQuery} $widgets A jQuery wrapped collection of
2393
+ * HTMLElements.
2394
+ * @return {object} Returns a literal Object with two keys: `can_go_up` &
2395
+ * `can_not_go_up`. Each contains a set of HTMLElements.
2396
+ */
2397
+ fn.widgets_constraints = function($widgets) {
2398
+ var $widgets_can_go_up = $([]);
2399
+ var $widgets_can_not_go_up;
2400
+ var wgd_can_go_up = [];
2401
+ var wgd_can_not_go_up = [];
2402
+
2403
+ $widgets.each($.proxy(function(i, w) {
2404
+ var $w = $(w);
2405
+ var wgd = $w.coords().grid;
2406
+ if (this.can_go_widget_up(wgd)) {
2407
+ $widgets_can_go_up = $widgets_can_go_up.add($w);
2408
+ wgd_can_go_up.push(wgd);
2409
+ } else {
2410
+ wgd_can_not_go_up.push(wgd);
2411
+ }
2412
+ }, this));
2413
+
2414
+ $widgets_can_not_go_up = $widgets.not($widgets_can_go_up);
2415
+
2416
+ return {
2417
+ can_go_up: Gridster.sort_by_row_asc(wgd_can_go_up),
2418
+ can_not_go_up: Gridster.sort_by_row_desc(wgd_can_not_go_up)
2419
+ };
2420
+ };
2421
+
2422
+
2423
+ /**
2424
+ * Sorts an Array of grid coords objects (representing the grid coords of
2425
+ * each widget) in descending way.
2426
+ *
2427
+ * @method manage_movements
2428
+ * @param {jQuery} $widgets A jQuery collection of HTMLElements
2429
+ * representing the widgets you want to move.
2430
+ * @param {Number} to_col The column to which we want to move the widgets.
2431
+ * @param {Number} to_row The row to which we want to move the widgets.
2432
+ * @return {Class} Returns the instance of the Gridster Class.
2433
+ */
2434
+ fn.manage_movements = function($widgets, to_col, to_row) {
2435
+ $.each($widgets, $.proxy(function(i, w) {
2436
+ var wgd = w;
2437
+ var $w = wgd.el;
2438
+
2439
+ var can_go_widget_up = this.can_go_widget_up(wgd);
2440
+
2441
+ if (can_go_widget_up) {
2442
+ //target CAN go up
2443
+ //so move widget up
2444
+ this.move_widget_to($w, can_go_widget_up);
2445
+ this.set_placeholder(to_col, can_go_widget_up + wgd.size_y);
2446
+
2447
+ } else {
2448
+ //target can't go up
2449
+ var can_go_player_up = this.can_go_player_up(
2450
+ this.player_grid_data);
2451
+
2452
+ if (!can_go_player_up) {
2453
+ // target can't go up
2454
+ // player cant't go up
2455
+ // so we need to move widget down to a position that dont
2456
+ // overlaps player
2457
+ var y = (to_row + this.player_grid_data.size_y) - wgd.row;
2458
+
2459
+ this.move_widget_down($w, y);
2460
+ this.set_placeholder(to_col, to_row);
2461
+ }
2462
+ }
2463
+ }, this));
2464
+
2465
+ return this;
2466
+ };
2467
+
2468
+ /**
2469
+ * Determines if there is a widget in the row and col given. Or if the
2470
+ * HTMLElement passed as first argument is the player.
2471
+ *
2472
+ * @method is_player
2473
+ * @param {Number|HTMLElement} col_or_el A jQuery wrapped collection of
2474
+ * HTMLElements.
2475
+ * @param {Number} [row] The column to which we want to move the widgets.
2476
+ * @return {Boolean} Returns true or false.
2477
+ */
2478
+ fn.is_player = function(col_or_el, row) {
2479
+ if (row && !this.gridmap[col_or_el]) { return false; }
2480
+ var $w = row ? this.gridmap[col_or_el][row] : col_or_el;
2481
+ return $w && ($w.is(this.$player) || $w.is(this.$helper));
2482
+ };
2483
+
2484
+
2485
+ /**
2486
+ * Determines if the widget that is being dragged is currently over the row
2487
+ * and col given.
2488
+ *
2489
+ * @method is_player_in
2490
+ * @param {Number} col The column to check.
2491
+ * @param {Number} row The row to check.
2492
+ * @return {Boolean} Returns true or false.
2493
+ */
2494
+ fn.is_player_in = function(col, row) {
2495
+ var c = this.cells_occupied_by_player || {};
2496
+ return $.inArray(col, c.cols) >= 0 && $.inArray(row, c.rows) >= 0;
2497
+ };
2498
+
2499
+
2500
+ /**
2501
+ * Determines if the placeholder is currently over the row and col given.
2502
+ *
2503
+ * @method is_placeholder_in
2504
+ * @param {Number} col The column to check.
2505
+ * @param {Number} row The row to check.
2506
+ * @return {Boolean} Returns true or false.
2507
+ */
2508
+ fn.is_placeholder_in = function(col, row) {
2509
+ var c = this.cells_occupied_by_placeholder || {};
2510
+ return this.is_placeholder_in_col(col) && $.inArray(row, c.rows) >= 0;
2511
+ };
2512
+
2513
+
2514
+ /**
2515
+ * Determines if the placeholder is currently over the column given.
2516
+ *
2517
+ * @method is_placeholder_in_col
2518
+ * @param {Number} col The column to check.
2519
+ * @return {Boolean} Returns true or false.
2520
+ */
2521
+ fn.is_placeholder_in_col = function(col) {
2522
+ var c = this.cells_occupied_by_placeholder || [];
2523
+ return $.inArray(col, c.cols) >= 0;
2524
+ };
2525
+
2526
+
2527
+ /**
2528
+ * Determines if the cell represented by col and row params is empty.
2529
+ *
2530
+ * @method is_empty
2531
+ * @param {Number} col The column to check.
2532
+ * @param {Number} row The row to check.
2533
+ * @return {Boolean} Returns true or false.
2534
+ */
2535
+ fn.is_empty = function(col, row) {
2536
+ if (typeof this.gridmap[col] !== 'undefined') {
2537
+ if(typeof this.gridmap[col][row] !== 'undefined' &&
2538
+ this.gridmap[col][row] === false
2539
+ ) {
2540
+ return true;
2541
+ }
2542
+ return false;
2543
+ }
2544
+ return true;
2545
+ };
2546
+
2547
+
2548
+ /**
2549
+ * Determines if the cell represented by col and row params is occupied.
2550
+ *
2551
+ * @method is_occupied
2552
+ * @param {Number} col The column to check.
2553
+ * @param {Number} row The row to check.
2554
+ * @return {Boolean} Returns true or false.
2555
+ */
2556
+ fn.is_occupied = function(col, row) {
2557
+ if (!this.gridmap[col]) {
2558
+ return false;
2559
+ }
2560
+
2561
+ if (this.gridmap[col][row]) {
2562
+ return true;
2563
+ }
2564
+ return false;
2565
+ };
2566
+
2567
+
2568
+ /**
2569
+ * Determines if there is a widget in the cell represented by col/row params.
2570
+ *
2571
+ * @method is_widget
2572
+ * @param {Number} col The column to check.
2573
+ * @param {Number} row The row to check.
2574
+ * @return {Boolean|HTMLElement} Returns false if there is no widget,
2575
+ * else returns the jQuery HTMLElement
2576
+ */
2577
+ fn.is_widget = function(col, row) {
2578
+ var cell = this.gridmap[col];
2579
+ if (!cell) {
2580
+ return false;
2581
+ }
2582
+
2583
+ cell = cell[row];
2584
+
2585
+ if (cell) {
2586
+ return cell;
2587
+ }
2588
+
2589
+ return false;
2590
+ };
2591
+
2592
+
2593
+ /**
2594
+ * Determines if there is a widget in the cell represented by col/row
2595
+ * params and if this is under the widget that is being dragged.
2596
+ *
2597
+ * @method is_widget_under_player
2598
+ * @param {Number} col The column to check.
2599
+ * @param {Number} row The row to check.
2600
+ * @return {Boolean} Returns true or false.
2601
+ */
2602
+ fn.is_widget_under_player = function(col, row) {
2603
+ if (this.is_widget(col, row)) {
2604
+ return this.is_player_in(col, row);
2605
+ }
2606
+ return false;
2607
+ };
2608
+
2609
+
2610
+ /**
2611
+ * Get widgets overlapping with the player or with the object passed
2612
+ * representing the grid cells.
2613
+ *
2614
+ * @method get_widgets_under_player
2615
+ * @return {HTMLElement} Returns a jQuery collection of HTMLElements
2616
+ */
2617
+ fn.get_widgets_under_player = function(cells) {
2618
+ cells || (cells = this.cells_occupied_by_player || {cols: [], rows: []});
2619
+ var $widgets = $([]);
2620
+
2621
+ $.each(cells.cols, $.proxy(function(i, col) {
2622
+ $.each(cells.rows, $.proxy(function(i, row) {
2623
+ if(this.is_widget(col, row)) {
2624
+ $widgets = $widgets.add(this.gridmap[col][row]);
2625
+ }
2626
+ }, this));
2627
+ }, this));
2628
+
2629
+ return $widgets;
2630
+ };
2631
+
2632
+
2633
+ /**
2634
+ * Put placeholder at the row and column specified.
2635
+ *
2636
+ * @method set_placeholder
2637
+ * @param {Number} col The column to which we want to move the
2638
+ * placeholder.
2639
+ * @param {Number} row The row to which we want to move the
2640
+ * placeholder.
2641
+ * @return {Class} Returns the instance of the Gridster Class.
2642
+ */
2643
+ fn.set_placeholder = function(col, row) {
2644
+ var phgd = $.extend({}, this.placeholder_grid_data);
2645
+ var $nexts = this.widgets_below({
2646
+ col: phgd.col,
2647
+ row: phgd.row,
2648
+ size_y: phgd.size_y,
2649
+ size_x: phgd.size_x
2650
+ });
2651
+
2652
+ // Prevents widgets go out of the grid
2653
+ var right_col = (col + phgd.size_x - 1);
2654
+ if (right_col > this.cols) {
2655
+ col = col - (right_col - col);
2656
+ }
2657
+
2658
+ var moved_down = this.placeholder_grid_data.row < row;
2659
+ var changed_column = this.placeholder_grid_data.col !== col;
2660
+
2661
+ this.placeholder_grid_data.col = col;
2662
+ this.placeholder_grid_data.row = row;
2663
+
2664
+ this.cells_occupied_by_placeholder = this.get_cells_occupied(
2665
+ this.placeholder_grid_data);
2666
+
2667
+ this.$preview_holder.attr({
2668
+ 'data-row' : row,
2669
+ 'data-col' : col
2670
+ });
2671
+
2672
+ if (moved_down || changed_column) {
2673
+ $nexts.each($.proxy(function(i, widget) {
2674
+ this.move_widget_up(
2675
+ $(widget), this.placeholder_grid_data.col - col + phgd.size_y);
2676
+ }, this));
2677
+ }
2678
+
2679
+ var $widgets_under_ph = this.get_widgets_under_player(
2680
+ this.cells_occupied_by_placeholder);
2681
+
2682
+ if ($widgets_under_ph.length) {
2683
+ $widgets_under_ph.each($.proxy(function(i, widget) {
2684
+ var $w = $(widget);
2685
+ this.move_widget_down(
2686
+ $w, row + phgd.size_y - $w.data('coords').grid.row);
2687
+ }, this));
2688
+ }
2689
+
2690
+ };
2691
+
2692
+
2693
+ /**
2694
+ * Determines whether the player can move to a position above.
2695
+ *
2696
+ * @method can_go_player_up
2697
+ * @param {Object} widget_grid_data The actual grid coords object of the
2698
+ * player.
2699
+ * @return {Number|Boolean} If the player can be moved to an upper row
2700
+ * returns the row number, else returns false.
2701
+ */
2702
+ fn.can_go_player_up = function(widget_grid_data) {
2703
+ var p_bottom_row = widget_grid_data.row + widget_grid_data.size_y - 1;
2704
+ var result = true;
2705
+ var upper_rows = [];
2706
+ var min_row = 10000;
2707
+ var $widgets_under_player = this.get_widgets_under_player();
2708
+
2709
+ /* generate an array with columns as index and array with upper rows
2710
+ * empty as value */
2711
+ this.for_each_column_occupied(widget_grid_data, function(tcol) {
2712
+ var grid_col = this.gridmap[tcol];
2713
+ var r = p_bottom_row + 1;
2714
+ upper_rows[tcol] = [];
2715
+
2716
+ while (--r > 0) {
2717
+ if (this.is_empty(tcol, r) || this.is_player(tcol, r) ||
2718
+ this.is_widget(tcol, r) &&
2719
+ grid_col[r].is($widgets_under_player)
2720
+ ) {
2721
+ upper_rows[tcol].push(r);
2722
+ min_row = r < min_row ? r : min_row;
2723
+ } else {
2724
+ break;
2725
+ }
2726
+ }
2727
+
2728
+ if (upper_rows[tcol].length === 0) {
2729
+ result = false;
2730
+ return true; //break
2731
+ }
2732
+
2733
+ upper_rows[tcol].sort(function(a, b) {
2734
+ return a - b;
2735
+ });
2736
+ });
2737
+
2738
+ if (!result) { return false; }
2739
+
2740
+ return this.get_valid_rows(widget_grid_data, upper_rows, min_row);
2741
+ };
2742
+
2743
+
2744
+ /**
2745
+ * Determines whether a widget can move to a position above.
2746
+ *
2747
+ * @method can_go_widget_up
2748
+ * @param {Object} widget_grid_data The actual grid coords object of the
2749
+ * widget we want to check.
2750
+ * @return {Number|Boolean} If the widget can be moved to an upper row
2751
+ * returns the row number, else returns false.
2752
+ */
2753
+ fn.can_go_widget_up = function(widget_grid_data) {
2754
+ var p_bottom_row = widget_grid_data.row + widget_grid_data.size_y - 1;
2755
+ var result = true;
2756
+ var upper_rows = [];
2757
+ var min_row = 10000;
2758
+
2759
+ /* generate an array with columns as index and array with topmost rows
2760
+ * empty as value */
2761
+ this.for_each_column_occupied(widget_grid_data, function(tcol) {
2762
+ var grid_col = this.gridmap[tcol];
2763
+ upper_rows[tcol] = [];
2764
+
2765
+ var r = p_bottom_row + 1;
2766
+ // iterate over each row
2767
+ while (--r > 0) {
2768
+ if (this.is_widget(tcol, r) && !this.is_player_in(tcol, r)) {
2769
+ if (!grid_col[r].is(widget_grid_data.el)) {
2770
+ break;
2771
+ }
2772
+ }
2773
+
2774
+ if (!this.is_player(tcol, r) &&
2775
+ !this.is_placeholder_in(tcol, r) &&
2776
+ !this.is_player_in(tcol, r)) {
2777
+ upper_rows[tcol].push(r);
2778
+ }
2779
+
2780
+ if (r < min_row) {
2781
+ min_row = r;
2782
+ }
2783
+ }
2784
+
2785
+ if (upper_rows[tcol].length === 0) {
2786
+ result = false;
2787
+ return true; //break
2788
+ }
2789
+
2790
+ upper_rows[tcol].sort(function(a, b) {
2791
+ return a - b;
2792
+ });
2793
+ });
2794
+
2795
+ if (!result) { return false; }
2796
+
2797
+ return this.get_valid_rows(widget_grid_data, upper_rows, min_row);
2798
+ };
2799
+
2800
+
2801
+ /**
2802
+ * Search a valid row for the widget represented by `widget_grid_data' in
2803
+ * the `upper_rows` array. Iteration starts from row specified in `min_row`.
2804
+ *
2805
+ * @method get_valid_rows
2806
+ * @param {Object} widget_grid_data The actual grid coords object of the
2807
+ * player.
2808
+ * @param {Array} upper_rows An array with columns as index and arrays
2809
+ * of valid rows as values.
2810
+ * @param {Number} min_row The upper row from which the iteration will start.
2811
+ * @return {Number|Boolean} Returns the upper row valid from the `upper_rows`
2812
+ * for the widget in question.
2813
+ */
2814
+ fn.get_valid_rows = function(widget_grid_data, upper_rows, min_row) {
2815
+ var p_top_row = widget_grid_data.row;
2816
+ var p_bottom_row = widget_grid_data.row + widget_grid_data.size_y - 1;
2817
+ var size_y = widget_grid_data.size_y;
2818
+ var r = min_row - 1;
2819
+ var valid_rows = [];
2820
+
2821
+ while (++r <= p_bottom_row ) {
2822
+ var common = true;
2823
+ $.each(upper_rows, function(col, rows) {
2824
+ if ($.isArray(rows) && $.inArray(r, rows) === -1) {
2825
+ common = false;
2826
+ }
2827
+ });
2828
+
2829
+ if (common === true) {
2830
+ valid_rows.push(r);
2831
+ if (valid_rows.length === size_y) {
2832
+ break;
2833
+ }
2834
+ }
2835
+ }
2836
+
2837
+ var new_row = false;
2838
+ if (size_y === 1) {
2839
+ if (valid_rows[0] !== p_top_row) {
2840
+ new_row = valid_rows[0] || false;
2841
+ }
2842
+ } else {
2843
+ if (valid_rows[0] !== p_top_row) {
2844
+ new_row = this.get_consecutive_numbers_index(
2845
+ valid_rows, size_y);
2846
+ }
2847
+ }
2848
+
2849
+ return new_row;
2850
+ };
2851
+
2852
+
2853
+ fn.get_consecutive_numbers_index = function(arr, size_y) {
2854
+ var max = arr.length;
2855
+ var result = [];
2856
+ var first = true;
2857
+ var prev = -1; // or null?
2858
+
2859
+ for (var i=0; i < max; i++) {
2860
+ if (first || arr[i] === prev + 1) {
2861
+ result.push(i);
2862
+ if (result.length === size_y) {
2863
+ break;
2864
+ }
2865
+ first = false;
2866
+ } else {
2867
+ result = [];
2868
+ first = true;
2869
+ }
2870
+
2871
+ prev = arr[i];
2872
+ }
2873
+
2874
+ return result.length >= size_y ? arr[result[0]] : false;
2875
+ };
2876
+
2877
+
2878
+ /**
2879
+ * Get widgets overlapping with the player.
2880
+ *
2881
+ * @method get_widgets_overlapped
2882
+ * @return {jQuery} Returns a jQuery collection of HTMLElements.
2883
+ */
2884
+ fn.get_widgets_overlapped = function() {
2885
+ var $w;
2886
+ var $widgets = $([]);
2887
+ var used = [];
2888
+ var rows_from_bottom = this.cells_occupied_by_player.rows.slice(0);
2889
+ rows_from_bottom.reverse();
2890
+
2891
+ $.each(this.cells_occupied_by_player.cols, $.proxy(function(i, col) {
2892
+ $.each(rows_from_bottom, $.proxy(function(i, row) {
2893
+ // if there is a widget in the player position
2894
+ if (!this.gridmap[col]) { return true; } //next iteration
2895
+ var $w = this.gridmap[col][row];
2896
+ if (this.is_occupied(col, row) && !this.is_player($w) &&
2897
+ $.inArray($w, used) === -1
2898
+ ) {
2899
+ $widgets = $widgets.add($w);
2900
+ used.push($w);
2901
+ }
2902
+
2903
+ }, this));
2904
+ }, this));
2905
+
2906
+ return $widgets;
2907
+ };
2908
+
2909
+
2910
+ /**
2911
+ * This callback is executed when the player begins to collide with a column.
2912
+ *
2913
+ * @method on_start_overlapping_column
2914
+ * @param {Number} col The collided column.
2915
+ * @return {jQuery} Returns a jQuery collection of HTMLElements.
2916
+ */
2917
+ fn.on_start_overlapping_column = function(col) {
2918
+ this.set_player(col, false);
2919
+ };
2920
+
2921
+
2922
+ /**
2923
+ * A callback executed when the player begins to collide with a row.
2924
+ *
2925
+ * @method on_start_overlapping_row
2926
+ * @param {Number} row The collided row.
2927
+ * @return {jQuery} Returns a jQuery collection of HTMLElements.
2928
+ */
2929
+ fn.on_start_overlapping_row = function(row) {
2930
+ this.set_player(false, row);
2931
+ };
2932
+
2933
+
2934
+ /**
2935
+ * A callback executed when the the player ends to collide with a column.
2936
+ *
2937
+ * @method on_stop_overlapping_column
2938
+ * @param {Number} col The collided row.
2939
+ * @return {jQuery} Returns a jQuery collection of HTMLElements.
2940
+ */
2941
+ fn.on_stop_overlapping_column = function(col) {
2942
+ this.set_player(col, false);
2943
+
2944
+ var self = this;
2945
+ this.for_each_widget_below(col, this.cells_occupied_by_player.rows[0],
2946
+ function(tcol, trow) {
2947
+ self.move_widget_up(this, self.player_grid_data.size_y);
2948
+ });
2949
+ };
2950
+
2951
+
2952
+ /**
2953
+ * This callback is executed when the player ends to collide with a row.
2954
+ *
2955
+ * @method on_stop_overlapping_row
2956
+ * @param {Number} row The collided row.
2957
+ * @return {jQuery} Returns a jQuery collection of HTMLElements.
2958
+ */
2959
+ fn.on_stop_overlapping_row = function(row) {
2960
+ this.set_player(false, row);
2961
+
2962
+ var self = this;
2963
+ var cols = this.cells_occupied_by_player.cols;
2964
+ for (var c = 0, cl = cols.length; c < cl; c++) {
2965
+ this.for_each_widget_below(cols[c], row, function(tcol, trow) {
2966
+ self.move_widget_up(this, self.player_grid_data.size_y);
2967
+ });
2968
+ }
2969
+ };
2970
+
2971
+
2972
+ /**
2973
+ * Move a widget to a specific row. The cell or cells must be empty.
2974
+ * If the widget has widgets below, all of these widgets will be moved also
2975
+ * if they can.
2976
+ *
2977
+ * @method move_widget_to
2978
+ * @param {HTMLElement} $widget The jQuery wrapped HTMLElement of the
2979
+ * widget is going to be moved.
2980
+ * @return {Class} Returns the instance of the Gridster Class.
2981
+ */
2982
+ fn.move_widget_to = function($widget, row) {
2983
+ var self = this;
2984
+ var widget_grid_data = $widget.coords().grid;
2985
+ var diff = row - widget_grid_data.row;
2986
+ var $next_widgets = this.widgets_below($widget);
2987
+
2988
+ var can_move_to_new_cell = this.can_move_to(
2989
+ widget_grid_data, widget_grid_data.col, row, $widget);
2990
+
2991
+ if (can_move_to_new_cell === false) {
2992
+ return false;
2993
+ }
2994
+
2995
+ this.remove_from_gridmap(widget_grid_data);
2996
+ widget_grid_data.row = row;
2997
+ this.add_to_gridmap(widget_grid_data);
2998
+ $widget.attr('data-row', row);
2999
+ this.$changed = this.$changed.add($widget);
3000
+
3001
+
3002
+ $next_widgets.each(function(i, widget) {
3003
+ var $w = $(widget);
3004
+ var wgd = $w.coords().grid;
3005
+ var can_go_up = self.can_go_widget_up(wgd);
3006
+ if (can_go_up && can_go_up !== wgd.row) {
3007
+ self.move_widget_to($w, can_go_up);
3008
+ }
3009
+ });
3010
+
3011
+ return this;
3012
+ };
3013
+
3014
+
3015
+ /**
3016
+ * Move up the specified widget and all below it.
3017
+ *
3018
+ * @method move_widget_up
3019
+ * @param {HTMLElement} $widget The widget you want to move.
3020
+ * @param {Number} [y_units] The number of cells that the widget has to move.
3021
+ * @return {Class} Returns the instance of the Gridster Class.
3022
+ */
3023
+ fn.move_widget_up = function($widget, y_units) {
3024
+ var el_grid_data = $widget.coords().grid;
3025
+ var actual_row = el_grid_data.row;
3026
+ var moved = [];
3027
+ var can_go_up = true;
3028
+ y_units || (y_units = 1);
3029
+
3030
+ if (!this.can_go_up($widget)) { return false; } //break;
3031
+
3032
+ this.for_each_column_occupied(el_grid_data, function(col) {
3033
+ // can_go_up
3034
+ if ($.inArray($widget, moved) === -1) {
3035
+ var widget_grid_data = $widget.coords().grid;
3036
+ var next_row = actual_row - y_units;
3037
+ next_row = this.can_go_up_to_row(
3038
+ widget_grid_data, col, next_row);
3039
+
3040
+ if (!next_row) {
3041
+ return true;
3042
+ }
3043
+
3044
+ var $next_widgets = this.widgets_below($widget);
3045
+
3046
+ this.remove_from_gridmap(widget_grid_data);
3047
+ widget_grid_data.row = next_row;
3048
+ this.add_to_gridmap(widget_grid_data);
3049
+ $widget.attr('data-row', widget_grid_data.row);
3050
+ this.$changed = this.$changed.add($widget);
3051
+
3052
+ moved.push($widget);
3053
+
3054
+ $next_widgets.each($.proxy(function(i, widget) {
3055
+ this.move_widget_up($(widget), y_units);
3056
+ }, this));
3057
+ }
3058
+ });
3059
+
3060
+ };
3061
+
3062
+
3063
+ /**
3064
+ * Move down the specified widget and all below it.
3065
+ *
3066
+ * @method move_widget_down
3067
+ * @param {jQuery} $widget The jQuery object representing the widget
3068
+ * you want to move.
3069
+ * @param {Number} y_units The number of cells that the widget has to move.
3070
+ * @return {Class} Returns the instance of the Gridster Class.
3071
+ */
3072
+ fn.move_widget_down = function($widget, y_units) {
3073
+ var el_grid_data, actual_row, moved, y_diff;
3074
+
3075
+ if (y_units <= 0) { return false; }
3076
+
3077
+ el_grid_data = $widget.coords().grid;
3078
+ actual_row = el_grid_data.row;
3079
+ moved = [];
3080
+ y_diff = y_units;
3081
+
3082
+ if (!$widget) { return false; }
3083
+
3084
+ if ($.inArray($widget, moved) === -1) {
3085
+
3086
+ var widget_grid_data = $widget.coords().grid;
3087
+ var next_row = actual_row + y_units;
3088
+ var $next_widgets = this.widgets_below($widget);
3089
+
3090
+ this.remove_from_gridmap(widget_grid_data);
3091
+
3092
+ $next_widgets.each($.proxy(function(i, widget) {
3093
+ var $w = $(widget);
3094
+ var wd = $w.coords().grid;
3095
+ var tmp_y = this.displacement_diff(
3096
+ wd, widget_grid_data, y_diff);
3097
+
3098
+ if (tmp_y > 0) {
3099
+ this.move_widget_down($w, tmp_y);
3100
+ }
3101
+ }, this));
3102
+
3103
+ widget_grid_data.row = next_row;
3104
+ this.update_widget_position(widget_grid_data, $widget);
3105
+ $widget.attr('data-row', widget_grid_data.row);
3106
+ this.$changed = this.$changed.add($widget);
3107
+
3108
+ moved.push($widget);
3109
+ }
3110
+ };
3111
+
3112
+
3113
+ /**
3114
+ * Check if the widget can move to the specified row, else returns the
3115
+ * upper row possible.
3116
+ *
3117
+ * @method can_go_up_to_row
3118
+ * @param {Number} widget_grid_data The current grid coords object of the
3119
+ * widget.
3120
+ * @param {Number} col The target column.
3121
+ * @param {Number} row The target row.
3122
+ * @return {Boolean|Number} Returns the row number if the widget can move
3123
+ * to the target position, else returns false.
3124
+ */
3125
+ fn.can_go_up_to_row = function(widget_grid_data, col, row) {
3126
+ var ga = this.gridmap;
3127
+ var result = true;
3128
+ var urc = []; // upper_rows_in_columns
3129
+ var actual_row = widget_grid_data.row;
3130
+ var r;
3131
+
3132
+ /* generate an array with columns as index and array with
3133
+ * upper rows empty in the column */
3134
+ this.for_each_column_occupied(widget_grid_data, function(tcol) {
3135
+ var grid_col = ga[tcol];
3136
+ urc[tcol] = [];
3137
+
3138
+ r = actual_row;
3139
+ while (r--) {
3140
+ if (this.is_empty(tcol, r) &&
3141
+ !this.is_placeholder_in(tcol, r)
3142
+ ) {
3143
+ urc[tcol].push(r);
3144
+ } else {
3145
+ break;
3146
+ }
3147
+ }
3148
+
3149
+ if (!urc[tcol].length) {
3150
+ result = false;
3151
+ return true;
3152
+ }
3153
+
3154
+ });
3155
+
3156
+ if (!result) { return false; }
3157
+
3158
+ /* get common rows starting from upper position in all the columns
3159
+ * that widget occupies */
3160
+ r = row;
3161
+ for (r = 1; r < actual_row; r++) {
3162
+ var common = true;
3163
+
3164
+ for (var uc = 0, ucl = urc.length; uc < ucl; uc++) {
3165
+ if (urc[uc] && $.inArray(r, urc[uc]) === -1) {
3166
+ common = false;
3167
+ }
3168
+ }
3169
+
3170
+ if (common === true) {
3171
+ result = r;
3172
+ break;
3173
+ }
3174
+ }
3175
+
3176
+ return result;
3177
+ };
3178
+
3179
+
3180
+ fn.displacement_diff = function(widget_grid_data, parent_bgd, y_units) {
3181
+ var actual_row = widget_grid_data.row;
3182
+ var diffs = [];
3183
+ var parent_max_y = parent_bgd.row + parent_bgd.size_y;
3184
+
3185
+ this.for_each_column_occupied(widget_grid_data, function(col) {
3186
+ var temp_y_units = 0;
3187
+
3188
+ for (var r = parent_max_y; r < actual_row; r++) {
3189
+ if (this.is_empty(col, r)) {
3190
+ temp_y_units = temp_y_units + 1;
3191
+ }
3192
+ }
3193
+
3194
+ diffs.push(temp_y_units);
3195
+ });
3196
+
3197
+ var max_diff = Math.max.apply(Math, diffs);
3198
+ y_units = (y_units - max_diff);
3199
+
3200
+ return y_units > 0 ? y_units : 0;
3201
+ };
3202
+
3203
+
3204
+ /**
3205
+ * Get widgets below a widget.
3206
+ *
3207
+ * @method widgets_below
3208
+ * @param {HTMLElement} $el The jQuery wrapped HTMLElement.
3209
+ * @return {jQuery} A jQuery collection of HTMLElements.
3210
+ */
3211
+ fn.widgets_below = function($el) {
3212
+ var el_grid_data = $.isPlainObject($el) ? $el : $el.coords().grid;
3213
+ var self = this;
3214
+ var ga = this.gridmap;
3215
+ var next_row = el_grid_data.row + el_grid_data.size_y - 1;
3216
+ var $nexts = $([]);
3217
+
3218
+ this.for_each_column_occupied(el_grid_data, function(col) {
3219
+ self.for_each_widget_below(col, next_row, function(tcol, trow) {
3220
+ if (!self.is_player(this) && $.inArray(this, $nexts) === -1) {
3221
+ $nexts = $nexts.add(this);
3222
+ return true; // break
3223
+ }
3224
+ });
3225
+ });
3226
+
3227
+ return Gridster.sort_by_row_asc($nexts);
3228
+ };
3229
+
3230
+
3231
+ /**
3232
+ * Update the array of mapped positions with the new player position.
3233
+ *
3234
+ * @method set_cells_player_occupies
3235
+ * @param {Number} col The new player col.
3236
+ * @param {Number} col The new player row.
3237
+ * @return {Class} Returns the instance of the Gridster Class.
3238
+ */
3239
+ fn.set_cells_player_occupies = function(col, row) {
3240
+ this.remove_from_gridmap(this.placeholder_grid_data);
3241
+ this.placeholder_grid_data.col = col;
3242
+ this.placeholder_grid_data.row = row;
3243
+ this.add_to_gridmap(this.placeholder_grid_data, this.$player);
3244
+ return this;
3245
+ };
3246
+
3247
+
3248
+ /**
3249
+ * Remove from the array of mapped positions the reference to the player.
3250
+ *
3251
+ * @method empty_cells_player_occupies
3252
+ * @return {Class} Returns the instance of the Gridster Class.
3253
+ */
3254
+ fn.empty_cells_player_occupies = function() {
3255
+ this.remove_from_gridmap(this.placeholder_grid_data);
3256
+ return this;
3257
+ };
3258
+
3259
+
3260
+ fn.can_go_up = function($el) {
3261
+ var el_grid_data = $el.coords().grid;
3262
+ var initial_row = el_grid_data.row;
3263
+ var prev_row = initial_row - 1;
3264
+ var ga = this.gridmap;
3265
+ var upper_rows_by_column = [];
3266
+
3267
+ var result = true;
3268
+ if (initial_row === 1) { return false; }
3269
+
3270
+ this.for_each_column_occupied(el_grid_data, function(col) {
3271
+ var $w = this.is_widget(col, prev_row);
3272
+
3273
+ if (this.is_occupied(col, prev_row) ||
3274
+ this.is_player(col, prev_row) ||
3275
+ this.is_placeholder_in(col, prev_row) ||
3276
+ this.is_player_in(col, prev_row)
3277
+ ) {
3278
+ result = false;
3279
+ return true; //break
3280
+ }
3281
+ });
3282
+
3283
+ return result;
3284
+ };
3285
+
3286
+
3287
+ /**
3288
+ * Check if it's possible to move a widget to a specific col/row. It takes
3289
+ * into account the dimensions (`size_y` and `size_x` attrs. of the grid
3290
+ * coords object) the widget occupies.
3291
+ *
3292
+ * @method can_move_to
3293
+ * @param {Object} widget_grid_data The grid coords object that represents
3294
+ * the widget.
3295
+ * @param {Object} col The col to check.
3296
+ * @param {Object} row The row to check.
3297
+ * @param {Number} [max_row] The max row allowed.
3298
+ * @return {Boolean} Returns true if all cells are empty, else return false.
3299
+ */
3300
+ fn.can_move_to = function(widget_grid_data, col, row, max_row) {
3301
+ var ga = this.gridmap;
3302
+ var $w = widget_grid_data.el;
3303
+ var future_wd = {
3304
+ size_y: widget_grid_data.size_y,
3305
+ size_x: widget_grid_data.size_x,
3306
+ col: col,
3307
+ row: row
3308
+ };
3309
+ var result = true;
3310
+
3311
+ //Prevents widgets go out of the grid
3312
+ var right_col = col + widget_grid_data.size_x - 1;
3313
+ if (right_col > this.cols) {
3314
+ return false;
3315
+ }
3316
+
3317
+ if (max_row && max_row < row + widget_grid_data.size_y - 1) {
3318
+ return false;
3319
+ }
3320
+
3321
+ this.for_each_cell_occupied(future_wd, function(tcol, trow) {
3322
+ var $tw = this.is_widget(tcol, trow);
3323
+ if ($tw && (!widget_grid_data.el || $tw.is($w))) {
3324
+ result = false;
3325
+ }
3326
+ });
3327
+
3328
+ return result;
3329
+ };
3330
+
3331
+
3332
+ /**
3333
+ * Given the leftmost column returns all columns that are overlapping
3334
+ * with the player.
3335
+ *
3336
+ * @method get_targeted_columns
3337
+ * @param {Number} [from_col] The leftmost column.
3338
+ * @return {Array} Returns an array with column numbers.
3339
+ */
3340
+ fn.get_targeted_columns = function(from_col) {
3341
+ var max = (from_col || this.player_grid_data.col) +
3342
+ (this.player_grid_data.size_x - 1);
3343
+ var cols = [];
3344
+ for (var col = from_col; col <= max; col++) {
3345
+ cols.push(col);
3346
+ }
3347
+ return cols;
3348
+ };
3349
+
3350
+
3351
+ /**
3352
+ * Given the upper row returns all rows that are overlapping with the player.
3353
+ *
3354
+ * @method get_targeted_rows
3355
+ * @param {Number} [from_row] The upper row.
3356
+ * @return {Array} Returns an array with row numbers.
3357
+ */
3358
+ fn.get_targeted_rows = function(from_row) {
3359
+ var max = (from_row || this.player_grid_data.row) +
3360
+ (this.player_grid_data.size_y - 1);
3361
+ var rows = [];
3362
+ for (var row = from_row; row <= max; row++) {
3363
+ rows.push(row);
3364
+ }
3365
+ return rows;
3366
+ };
3367
+
3368
+ /**
3369
+ * Get all columns and rows that a widget occupies.
3370
+ *
3371
+ * @method get_cells_occupied
3372
+ * @param {Object} el_grid_data The grid coords object of the widget.
3373
+ * @return {Object} Returns an object like `{ cols: [], rows: []}`.
3374
+ */
3375
+ fn.get_cells_occupied = function(el_grid_data) {
3376
+ var cells = { cols: [], rows: []};
3377
+ var i;
3378
+ if (arguments[1] instanceof $) {
3379
+ el_grid_data = arguments[1].coords().grid;
3380
+ }
3381
+
3382
+ for (i = 0; i < el_grid_data.size_x; i++) {
3383
+ var col = el_grid_data.col + i;
3384
+ cells.cols.push(col);
3385
+ }
3386
+
3387
+ for (i = 0; i < el_grid_data.size_y; i++) {
3388
+ var row = el_grid_data.row + i;
3389
+ cells.rows.push(row);
3390
+ }
3391
+
3392
+ return cells;
3393
+ };
3394
+
3395
+
3396
+ /**
3397
+ * Iterate over the cells occupied by a widget executing a function for
3398
+ * each one.
3399
+ *
3400
+ * @method for_each_cell_occupied
3401
+ * @param {Object} el_grid_data The grid coords object that represents the
3402
+ * widget.
3403
+ * @param {Function} callback The function to execute on each column
3404
+ * iteration. Column and row are passed as arguments.
3405
+ * @return {Class} Returns the instance of the Gridster Class.
3406
+ */
3407
+ fn.for_each_cell_occupied = function(grid_data, callback) {
3408
+ this.for_each_column_occupied(grid_data, function(col) {
3409
+ this.for_each_row_occupied(grid_data, function(row) {
3410
+ callback.call(this, col, row);
3411
+ });
3412
+ });
3413
+ return this;
3414
+ };
3415
+
3416
+
3417
+ /**
3418
+ * Iterate over the columns occupied by a widget executing a function for
3419
+ * each one.
3420
+ *
3421
+ * @method for_each_column_occupied
3422
+ * @param {Object} el_grid_data The grid coords object that represents
3423
+ * the widget.
3424
+ * @param {Function} callback The function to execute on each column
3425
+ * iteration. The column number is passed as first argument.
3426
+ * @return {Class} Returns the instance of the Gridster Class.
3427
+ */
3428
+ fn.for_each_column_occupied = function(el_grid_data, callback) {
3429
+ for (var i = 0; i < el_grid_data.size_x; i++) {
3430
+ var col = el_grid_data.col + i;
3431
+ callback.call(this, col, el_grid_data);
3432
+ }
3433
+ };
3434
+
3435
+
3436
+ /**
3437
+ * Iterate over the rows occupied by a widget executing a function for
3438
+ * each one.
3439
+ *
3440
+ * @method for_each_row_occupied
3441
+ * @param {Object} el_grid_data The grid coords object that represents
3442
+ * the widget.
3443
+ * @param {Function} callback The function to execute on each column
3444
+ * iteration. The row number is passed as first argument.
3445
+ * @return {Class} Returns the instance of the Gridster Class.
3446
+ */
3447
+ fn.for_each_row_occupied = function(el_grid_data, callback) {
3448
+ for (var i = 0; i < el_grid_data.size_y; i++) {
3449
+ var row = el_grid_data.row + i;
3450
+ callback.call(this, row, el_grid_data);
3451
+ }
3452
+ };
3453
+
3454
+
3455
+
3456
+ fn._traversing_widgets = function(type, direction, col, row, callback) {
3457
+ var ga = this.gridmap;
3458
+ if (!ga[col]) { return; }
3459
+
3460
+ var cr, max;
3461
+ var action = type + '/' + direction;
3462
+ if (arguments[2] instanceof $) {
3463
+ var el_grid_data = arguments[2].coords().grid;
3464
+ col = el_grid_data.col;
3465
+ row = el_grid_data.row;
3466
+ callback = arguments[3];
3467
+ }
3468
+ var matched = [];
3469
+ var trow = row;
3470
+
3471
+
3472
+ var methods = {
3473
+ 'for_each/above': function() {
3474
+ while (trow--) {
3475
+ if (trow > 0 && this.is_widget(col, trow) &&
3476
+ $.inArray(ga[col][trow], matched) === -1
3477
+ ) {
3478
+ cr = callback.call(ga[col][trow], col, trow);
3479
+ matched.push(ga[col][trow]);
3480
+ if (cr) { break; }
3481
+ }
3482
+ }
3483
+ },
3484
+ 'for_each/below': function() {
3485
+ for (trow = row + 1, max = ga[col].length; trow < max; trow++) {
3486
+ if (this.is_widget(col, trow) &&
3487
+ $.inArray(ga[col][trow], matched) === -1
3488
+ ) {
3489
+ cr = callback.call(ga[col][trow], col, trow);
3490
+ matched.push(ga[col][trow]);
3491
+ if (cr) { break; }
3492
+ }
3493
+ }
3494
+ }
3495
+ };
3496
+
3497
+ if (methods[action]) {
3498
+ methods[action].call(this);
3499
+ }
3500
+ };
3501
+
3502
+
3503
+ /**
3504
+ * Iterate over each widget above the column and row specified.
3505
+ *
3506
+ * @method for_each_widget_above
3507
+ * @param {Number} col The column to start iterating.
3508
+ * @param {Number} row The row to start iterating.
3509
+ * @param {Function} callback The function to execute on each widget
3510
+ * iteration. The value of `this` inside the function is the jQuery
3511
+ * wrapped HTMLElement.
3512
+ * @return {Class} Returns the instance of the Gridster Class.
3513
+ */
3514
+ fn.for_each_widget_above = function(col, row, callback) {
3515
+ this._traversing_widgets('for_each', 'above', col, row, callback);
3516
+ return this;
3517
+ };
3518
+
3519
+
3520
+ /**
3521
+ * Iterate over each widget below the column and row specified.
3522
+ *
3523
+ * @method for_each_widget_below
3524
+ * @param {Number} col The column to start iterating.
3525
+ * @param {Number} row The row to start iterating.
3526
+ * @param {Function} callback The function to execute on each widget
3527
+ * iteration. The value of `this` inside the function is the jQuery wrapped
3528
+ * HTMLElement.
3529
+ * @return {Class} Returns the instance of the Gridster Class.
3530
+ */
3531
+ fn.for_each_widget_below = function(col, row, callback) {
3532
+ this._traversing_widgets('for_each', 'below', col, row, callback);
3533
+ return this;
3534
+ };
3535
+
3536
+
3537
+ /**
3538
+ * Returns the highest occupied cell in the grid.
3539
+ *
3540
+ * @method get_highest_occupied_cell
3541
+ * @return {Object} Returns an object with `col` and `row` numbers.
3542
+ */
3543
+ fn.get_highest_occupied_cell = function() {
3544
+ var r;
3545
+ var gm = this.gridmap;
3546
+ var rl = gm[1].length;
3547
+ var rows = [], cols = [];
3548
+ var row_in_col = [];
3549
+ for (var c = gm.length - 1; c >= 1; c--) {
3550
+ for (r = rl - 1; r >= 1; r--) {
3551
+ if (this.is_widget(c, r)) {
3552
+ rows.push(r);
3553
+ cols.push(c);
3554
+ break;
3555
+ }
3556
+ }
3557
+ }
3558
+
3559
+ return {
3560
+ col: Math.max.apply(Math, cols),
3561
+ row: Math.max.apply(Math, rows)
3562
+ };
3563
+ };
3564
+
3565
+
3566
+ fn.get_widgets_from = function(col, row) {
3567
+ var ga = this.gridmap;
3568
+ var $widgets = $();
3569
+
3570
+ if (col) {
3571
+ $widgets = $widgets.add(
3572
+ this.$widgets.filter(function() {
3573
+ var tcol = $(this).attr('data-col');
3574
+ return (tcol === col || tcol > col);
3575
+ })
3576
+ );
3577
+ }
3578
+
3579
+ if (row) {
3580
+ $widgets = $widgets.add(
3581
+ this.$widgets.filter(function() {
3582
+ var trow = $(this).attr('data-row');
3583
+ return (trow === row || trow > row);
3584
+ })
3585
+ );
3586
+ }
3587
+
3588
+ return $widgets;
3589
+ };
3590
+
3591
+
3592
+ /**
3593
+ * Set the current height of the parent grid.
3594
+ *
3595
+ * @method set_dom_grid_height
3596
+ * @return {Object} Returns the instance of the Gridster class.
3597
+ */
3598
+ fn.set_dom_grid_height = function(height) {
3599
+ if (typeof height === 'undefined') {
3600
+ var r = this.get_highest_occupied_cell().row;
3601
+ height = r * this.min_widget_height;
3602
+ }
3603
+
3604
+ this.container_height = height;
3605
+ this.$el.css('height', this.container_height);
3606
+ return this;
3607
+ };
3608
+
3609
+ /**
3610
+ * Set the current width of the parent grid.
3611
+ *
3612
+ * @method set_dom_grid_width
3613
+ * @return {Object} Returns the instance of the Gridster class.
3614
+ */
3615
+ fn.set_dom_grid_width = function(cols) {
3616
+ if (typeof cols === 'undefined') {
3617
+ cols = this.get_highest_occupied_cell().col;
3618
+ }
3619
+
3620
+ var max_cols = (this.options.autogrow_cols ? this.options.max_cols :
3621
+ this.cols);
3622
+
3623
+ cols = Math.min(max_cols, Math.max(cols, this.options.min_cols));
3624
+ this.container_width = cols * this.min_widget_width;
3625
+ this.$el.css('width', this.container_width);
3626
+ return this;
3627
+ };
3628
+
3629
+
3630
+ /**
3631
+ * It generates the neccessary styles to position the widgets.
3632
+ *
3633
+ * @method generate_stylesheet
3634
+ * @param {Number} rows Number of columns.
3635
+ * @param {Number} cols Number of rows.
3636
+ * @return {Object} Returns the instance of the Gridster class.
3637
+ */
3638
+ fn.generate_stylesheet = function(opts) {
3639
+ var styles = '';
3640
+ var max_size_x = this.options.max_size_x || this.cols;
3641
+ var max_rows = 0;
3642
+ var max_cols = 0;
3643
+ var i;
3644
+ var rules;
3645
+
3646
+ opts || (opts = {});
3647
+ opts.cols || (opts.cols = this.cols);
3648
+ opts.rows || (opts.rows = this.rows);
3649
+ opts.namespace || (opts.namespace = this.options.namespace);
3650
+ opts.widget_base_dimensions ||
3651
+ (opts.widget_base_dimensions = this.options.widget_base_dimensions);
3652
+ opts.widget_margins ||
3653
+ (opts.widget_margins = this.options.widget_margins);
3654
+ opts.min_widget_width = (opts.widget_margins[0] * 2) +
3655
+ opts.widget_base_dimensions[0];
3656
+ opts.min_widget_height = (opts.widget_margins[1] * 2) +
3657
+ opts.widget_base_dimensions[1];
3658
+
3659
+ // don't duplicate stylesheets for the same configuration
3660
+ var serialized_opts = $.param(opts);
3661
+ if ($.inArray(serialized_opts, Gridster.generated_stylesheets) >= 0) {
3662
+ return false;
3663
+ }
3664
+
3665
+ this.generated_stylesheets.push(serialized_opts);
3666
+ Gridster.generated_stylesheets.push(serialized_opts);
3667
+
3668
+ /* generate CSS styles for cols */
3669
+ for (i = opts.cols; i >= 0; i--) {
3670
+ styles += (opts.namespace + ' [data-col="'+ (i + 1) + '"] { left:' +
3671
+ ((i * opts.widget_base_dimensions[0]) +
3672
+ (i * opts.widget_margins[0]) +
3673
+ ((i + 1) * opts.widget_margins[0])) + 'px; }\n');
3674
+ }
3675
+
3676
+ /* generate CSS styles for rows */
3677
+ for (i = opts.rows; i >= 0; i--) {
3678
+ styles += (opts.namespace + ' [data-row="' + (i + 1) + '"] { top:' +
3679
+ ((i * opts.widget_base_dimensions[1]) +
3680
+ (i * opts.widget_margins[1]) +
3681
+ ((i + 1) * opts.widget_margins[1]) ) + 'px; }\n');
3682
+ }
3683
+
3684
+ for (var y = 1; y <= opts.rows; y++) {
3685
+ styles += (opts.namespace + ' [data-sizey="' + y + '"] { height:' +
3686
+ (y * opts.widget_base_dimensions[1] +
3687
+ (y - 1) * (opts.widget_margins[1] * 2)) + 'px; }\n');
3688
+ }
3689
+
3690
+ for (var x = 1; x <= max_size_x; x++) {
3691
+ styles += (opts.namespace + ' [data-sizex="' + x + '"] { width:' +
3692
+ (x * opts.widget_base_dimensions[0] +
3693
+ (x - 1) * (opts.widget_margins[0] * 2)) + 'px; }\n');
3694
+ }
3695
+
3696
+ this.remove_style_tags();
3697
+
3698
+ return this.add_style_tag(styles);
3699
+ };
3700
+
3701
+
3702
+ /**
3703
+ * Injects the given CSS as string to the head of the document.
3704
+ *
3705
+ * @method add_style_tag
3706
+ * @param {String} css The styles to apply.
3707
+ * @return {Object} Returns the instance of the Gridster class.
3708
+ */
3709
+ fn.add_style_tag = function(css) {
3710
+ var d = document;
3711
+ var tag = d.createElement('style');
3712
+
3713
+ d.getElementsByTagName('head')[0].appendChild(tag);
3714
+ tag.setAttribute('type', 'text/css');
3715
+
3716
+ if (tag.styleSheet) {
3717
+ tag.styleSheet.cssText = css;
3718
+ } else {
3719
+ tag.appendChild(document.createTextNode(css));
3720
+ }
3721
+
3722
+ this.$style_tags = this.$style_tags.add(tag);
3723
+
3724
+ return this;
3725
+ };
3726
+
3727
+
3728
+ /**
3729
+ * Remove the style tag with the associated id from the head of the document
3730
+ *
3731
+ * @method remove_style_tag
3732
+ * @return {Object} Returns the instance of the Gridster class.
3733
+ */
3734
+ fn.remove_style_tags = function() {
3735
+ var all_styles = Gridster.generated_stylesheets;
3736
+ var ins_styles = this.generated_stylesheets;
3737
+
3738
+ this.$style_tags.remove();
3739
+
3740
+ Gridster.generated_stylesheets = $.map(all_styles, function(s) {
3741
+ if ($.inArray(s, ins_styles) === -1) { return s; }
3742
+ });
3743
+ };
3744
+
3745
+
3746
+ /**
3747
+ * Generates a faux grid to collide with it when a widget is dragged and
3748
+ * detect row or column that we want to go.
3749
+ *
3750
+ * @method generate_faux_grid
3751
+ * @param {Number} rows Number of columns.
3752
+ * @param {Number} cols Number of rows.
3753
+ * @return {Object} Returns the instance of the Gridster class.
3754
+ */
3755
+ fn.generate_faux_grid = function(rows, cols) {
3756
+ this.faux_grid = [];
3757
+ this.gridmap = [];
3758
+ var col;
3759
+ var row;
3760
+ for (col = cols; col > 0; col--) {
3761
+ this.gridmap[col] = [];
3762
+ for (row = rows; row > 0; row--) {
3763
+ this.add_faux_cell(row, col);
3764
+ }
3765
+ }
3766
+ return this;
3767
+ };
3768
+
3769
+
3770
+ /**
3771
+ * Add cell to the faux grid.
3772
+ *
3773
+ * @method add_faux_cell
3774
+ * @param {Number} row The row for the new faux cell.
3775
+ * @param {Number} col The col for the new faux cell.
3776
+ * @return {Object} Returns the instance of the Gridster class.
3777
+ */
3778
+ fn.add_faux_cell = function(row, col) {
3779
+ var coords = $({
3780
+ left: this.baseX + ((col - 1) * this.min_widget_width),
3781
+ top: this.baseY + (row -1) * this.min_widget_height,
3782
+ width: this.min_widget_width,
3783
+ height: this.min_widget_height,
3784
+ col: col,
3785
+ row: row,
3786
+ original_col: col,
3787
+ original_row: row
3788
+ }).coords();
3789
+
3790
+ if (!$.isArray(this.gridmap[col])) {
3791
+ this.gridmap[col] = [];
3792
+ }
3793
+
3794
+ this.gridmap[col][row] = false;
3795
+ this.faux_grid.push(coords);
3796
+
3797
+ return this;
3798
+ };
3799
+
3800
+
3801
+ /**
3802
+ * Add rows to the faux grid.
3803
+ *
3804
+ * @method add_faux_rows
3805
+ * @param {Number} rows The number of rows you want to add to the faux grid.
3806
+ * @return {Object} Returns the instance of the Gridster class.
3807
+ */
3808
+ fn.add_faux_rows = function(rows) {
3809
+ var actual_rows = this.rows;
3810
+ var max_rows = actual_rows + (rows || 1);
3811
+
3812
+ for (var r = max_rows; r > actual_rows; r--) {
3813
+ for (var c = this.cols; c >= 1; c--) {
3814
+ this.add_faux_cell(r, c);
3815
+ }
3816
+ }
3817
+
3818
+ this.rows = max_rows;
3819
+
3820
+ if (this.options.autogenerate_stylesheet) {
3821
+ this.generate_stylesheet();
3822
+ }
3823
+
3824
+ return this;
3825
+ };
3826
+
3827
+ /**
3828
+ * Add cols to the faux grid.
3829
+ *
3830
+ * @method add_faux_cols
3831
+ * @param {Number} cols The number of cols you want to add to the faux grid.
3832
+ * @return {Object} Returns the instance of the Gridster class.
3833
+ */
3834
+ fn.add_faux_cols = function(cols) {
3835
+ var actual_cols = this.cols;
3836
+ var max_cols = actual_cols + (cols || 1);
3837
+ max_cols = Math.min(max_cols, this.options.max_cols);
3838
+
3839
+ for (var c = actual_cols + 1; c <= max_cols; c++) {
3840
+ for (var r = this.rows; r >= 1; r--) {
3841
+ this.add_faux_cell(r, c);
3842
+ }
3843
+ }
3844
+
3845
+ this.cols = max_cols;
3846
+
3847
+ if (this.options.autogenerate_stylesheet) {
3848
+ this.generate_stylesheet();
3849
+ }
3850
+
3851
+ return this;
3852
+ };
3853
+
3854
+
3855
+ /**
3856
+ * Recalculates the offsets for the faux grid. You need to use it when
3857
+ * the browser is resized.
3858
+ *
3859
+ * @method recalculate_faux_grid
3860
+ * @return {Object} Returns the instance of the Gridster class.
3861
+ */
3862
+ fn.recalculate_faux_grid = function() {
3863
+ var aw = this.$wrapper.width();
3864
+ this.baseX = ($(window).width() - aw) / 2;
3865
+ this.baseY = this.$wrapper.offset().top;
3866
+
3867
+ $.each(this.faux_grid, $.proxy(function(i, coords) {
3868
+ this.faux_grid[i] = coords.update({
3869
+ left: this.baseX + (coords.data.col -1) * this.min_widget_width,
3870
+ top: this.baseY + (coords.data.row -1) * this.min_widget_height
3871
+ });
3872
+ }, this));
3873
+
3874
+ return this;
3875
+ };
3876
+
3877
+
3878
+ /**
3879
+ * Get all widgets in the DOM and register them.
3880
+ *
3881
+ * @method get_widgets_from_DOM
3882
+ * @return {Object} Returns the instance of the Gridster class.
3883
+ */
3884
+ fn.get_widgets_from_DOM = function() {
3885
+ var widgets_coords = this.$widgets.map($.proxy(function(i, widget) {
3886
+ var $w = $(widget);
3887
+ return this.dom_to_coords($w);
3888
+ }, this));
3889
+
3890
+ widgets_coords = Gridster.sort_by_row_and_col_asc(widgets_coords);
3891
+
3892
+ var changes = $(widgets_coords).map($.proxy(function(i, wgd) {
3893
+ return this.register_widget(wgd) || null;
3894
+ }, this));
3895
+
3896
+ if (changes.length) {
3897
+ this.$el.trigger('gridster:positionschanged');
3898
+ }
3899
+
3900
+ return this;
3901
+ };
3902
+
3903
+
3904
+ /**
3905
+ * Calculate columns and rows to be set based on the configuration
3906
+ * parameters, grid dimensions, etc ...
3907
+ *
3908
+ * @method generate_grid_and_stylesheet
3909
+ * @return {Object} Returns the instance of the Gridster class.
3910
+ */
3911
+ fn.generate_grid_and_stylesheet = function() {
3912
+ var aw = this.$wrapper.width();
3913
+ var max_cols = this.options.max_cols;
3914
+
3915
+ var cols = Math.floor(aw / this.min_widget_width) +
3916
+ this.options.extra_cols;
3917
+
3918
+ var actual_cols = this.$widgets.map(function() {
3919
+ return $(this).attr('data-col');
3920
+ }).get();
3921
+
3922
+ //needed to pass tests with phantomjs
3923
+ actual_cols.length || (actual_cols = [0]);
3924
+
3925
+ var min_cols = Math.max.apply(Math, actual_cols);
3926
+
3927
+ this.cols = Math.max(min_cols, cols, this.options.min_cols);
3928
+
3929
+ if (max_cols !== Infinity && max_cols >= min_cols && max_cols < this.cols) {
3930
+ this.cols = max_cols;
3931
+ }
3932
+
3933
+ // get all rows that could be occupied by the current widgets
3934
+ var max_rows = this.options.extra_rows;
3935
+ this.$widgets.each(function(i, w) {
3936
+ max_rows += (+$(w).attr('data-sizey'));
3937
+ });
3938
+
3939
+ this.rows = Math.max(max_rows, this.options.min_rows);
3940
+
3941
+ this.baseX = ($(window).width() - aw) / 2;
3942
+ this.baseY = this.$wrapper.offset().top;
3943
+
3944
+ if (this.options.autogenerate_stylesheet) {
3945
+ this.generate_stylesheet();
3946
+ }
3947
+
3948
+ return this.generate_faux_grid(this.rows, this.cols);
3949
+ };
3950
+
3951
+ /**
3952
+ * Destroy this gridster by removing any sign of its presence, making it easy to avoid memory leaks
3953
+ *
3954
+ * @method destroy
3955
+ * @param {Boolean} remove If true, remove gridster from DOM.
3956
+ * @return {Object} Returns the instance of the Gridster class.
3957
+ */
3958
+ fn.destroy = function(remove) {
3959
+ this.$el.removeData('gridster');
3960
+
3961
+ // remove bound callback on window resize
3962
+ $(window).unbind('.gridster');
3963
+
3964
+ if (this.drag_api) {
3965
+ this.drag_api.destroy();
3966
+ }
3967
+
3968
+ this.remove_style_tags();
3969
+
3970
+ remove && this.$el.remove();
3971
+
3972
+ return this;
3973
+ };
3974
+
3975
+
3976
+ //jQuery adapter
3977
+ $.fn.gridster = function(options) {
3978
+ return this.each(function() {
3979
+ if (! $(this).data('gridster')) {
3980
+ $(this).data('gridster', new Gridster( this, options ));
3981
+ }
3982
+ });
3983
+ };
3984
+
3985
+ return Gridster;
3986
+
3987
+ }));