PageRenderTime 50ms CodeModel.GetById 15ms RepoModel.GetById 0ms app.codeStats 1ms

/files/gridster.js/0.1/jquery.gridster.js

https://gitlab.com/Mirros/jsdelivr
JavaScript | 1602 lines | 988 code | 287 blank | 327 comment | 136 complexity | 7df134791972188add789b7a74f90e19 MD5 | raw file
  1. /*! gridster.js - v0.1.0 - 2013-06-14
  2. * http://gridster.net/
  3. * Copyright (c) 2013 ducksboard; Licensed MIT */
  4. ;(function($, window, document, undefined){
  5. /**
  6. * Creates objects with coordinates (x1, y1, x2, y2, cx, cy, width, height)
  7. * to simulate DOM elements on the screen.
  8. * Coords is used by Gridster to create a faux grid with any DOM element can
  9. * collide.
  10. *
  11. * @class Coords
  12. * @param {HTMLElement|Object} obj The jQuery HTMLElement or a object with: left,
  13. * top, width and height properties.
  14. * @return {Object} Coords instance.
  15. * @constructor
  16. */
  17. function Coords(obj) {
  18. if (obj[0] && $.isPlainObject(obj[0])) {
  19. this.data = obj[0];
  20. }else {
  21. this.el = obj;
  22. }
  23. this.isCoords = true;
  24. this.coords = {};
  25. this.init();
  26. return this;
  27. }
  28. var fn = Coords.prototype;
  29. fn.init = function(){
  30. this.set();
  31. this.original_coords = this.get();
  32. };
  33. fn.set = function(update, not_update_offsets) {
  34. var el = this.el;
  35. if (el && !update) {
  36. this.data = el.offset();
  37. this.data.width = el.width();
  38. this.data.height = el.height();
  39. }
  40. if (el && update && !not_update_offsets) {
  41. var offset = el.offset();
  42. this.data.top = offset.top;
  43. this.data.left = offset.left;
  44. }
  45. var d = this.data;
  46. this.coords.x1 = d.left;
  47. this.coords.y1 = d.top;
  48. this.coords.x2 = d.left + d.width;
  49. this.coords.y2 = d.top + d.height;
  50. this.coords.cx = d.left + (d.width / 2);
  51. this.coords.cy = d.top + (d.height / 2);
  52. this.coords.width = d.width;
  53. this.coords.height = d.height;
  54. this.coords.el = el || false ;
  55. return this;
  56. };
  57. fn.update = function(data){
  58. if (!data && !this.el) {
  59. return this;
  60. }
  61. if (data) {
  62. var new_data = $.extend({}, this.data, data);
  63. this.data = new_data;
  64. return this.set(true, true);
  65. }
  66. this.set(true);
  67. return this;
  68. };
  69. fn.get = function(){
  70. return this.coords;
  71. };
  72. //jQuery adapter
  73. $.fn.coords = function() {
  74. if (this.data('coords') ) {
  75. return this.data('coords');
  76. }
  77. var ins = new Coords(this, arguments[0]);
  78. this.data('coords', ins);
  79. return ins;
  80. };
  81. }(jQuery, window, document));
  82. ;(function($, window, document, undefined){
  83. var defaults = {
  84. colliders_context: document.body
  85. // ,on_overlap: function(collider_data){},
  86. // on_overlap_start : function(collider_data){},
  87. // on_overlap_stop : function(collider_data){}
  88. };
  89. /**
  90. * Detects collisions between a DOM element against other DOM elements or
  91. * Coords objects.
  92. *
  93. * @class Collision
  94. * @uses Coords
  95. * @param {HTMLElement} el The jQuery wrapped HTMLElement.
  96. * @param {HTMLElement|Array} colliders Can be a jQuery collection
  97. * of HTMLElements or an Array of Coords instances.
  98. * @param {Object} [options] An Object with all options you want to
  99. * overwrite:
  100. * @param {Function} [options.on_overlap_start] Executes a function the first
  101. * time each `collider ` is overlapped.
  102. * @param {Function} [options.on_overlap_stop] Executes a function when a
  103. * `collider` is no longer collided.
  104. * @param {Function} [options.on_overlap] Executes a function when the
  105. * mouse is moved during the collision.
  106. * @return {Object} Collision instance.
  107. * @constructor
  108. */
  109. function Collision(el, colliders, options) {
  110. this.options = $.extend(defaults, options);
  111. this.$element = el;
  112. this.last_colliders = [];
  113. this.last_colliders_coords = [];
  114. if (typeof colliders === 'string' || colliders instanceof jQuery) {
  115. this.$colliders = $(colliders,
  116. this.options.colliders_context).not(this.$element);
  117. }else{
  118. this.colliders = $(colliders);
  119. }
  120. this.init();
  121. }
  122. var fn = Collision.prototype;
  123. fn.init = function() {
  124. this.find_collisions();
  125. };
  126. fn.overlaps = function(a, b) {
  127. var x = false;
  128. var y = false;
  129. if ((b.x1 >= a.x1 && b.x1 <= a.x2) ||
  130. (b.x2 >= a.x1 && b.x2 <= a.x2) ||
  131. (a.x1 >= b.x1 && a.x2 <= b.x2)
  132. ) { x = true; }
  133. if ((b.y1 >= a.y1 && b.y1 <= a.y2) ||
  134. (b.y2 >= a.y1 && b.y2 <= a.y2) ||
  135. (a.y1 >= b.y1 && a.y2 <= b.y2)
  136. ) { y = true; }
  137. return (x && y);
  138. };
  139. fn.detect_overlapping_region = function(a, b){
  140. var regionX = '';
  141. var regionY = '';
  142. if (a.y1 > b.cy && a.y1 < b.y2) { regionX = 'N'; }
  143. if (a.y2 > b.y1 && a.y2 < b.cy) { regionX = 'S'; }
  144. if (a.x1 > b.cx && a.x1 < b.x2) { regionY = 'W'; }
  145. if (a.x2 > b.x1 && a.x2 < b.cx) { regionY = 'E'; }
  146. return (regionX + regionY) || 'C';
  147. };
  148. fn.calculate_overlapped_area_coords = function(a, b){
  149. var x1 = Math.max(a.x1, b.x1);
  150. var y1 = Math.max(a.y1, b.y1);
  151. var x2 = Math.min(a.x2, b.x2);
  152. var y2 = Math.min(a.y2, b.y2);
  153. return $({
  154. left: x1,
  155. top: y1,
  156. width : (x2 - x1),
  157. height: (y2 - y1)
  158. }).coords().get();
  159. };
  160. fn.calculate_overlapped_area = function(coords){
  161. return (coords.width * coords.height);
  162. };
  163. fn.manage_colliders_start_stop = function(new_colliders_coords, start_callback, stop_callback){
  164. var last = this.last_colliders_coords;
  165. for (var i = 0, il = last.length; i < il; i++) {
  166. if ($.inArray(last[i], new_colliders_coords) === -1) {
  167. start_callback.call(this, last[i]);
  168. }
  169. }
  170. for (var j = 0, jl = new_colliders_coords.length; j < jl; j++) {
  171. if ($.inArray(new_colliders_coords[j], last) === -1) {
  172. stop_callback.call(this, new_colliders_coords[j]);
  173. }
  174. }
  175. };
  176. fn.find_collisions = function(player_data_coords){
  177. var self = this;
  178. var colliders_coords = [];
  179. var colliders_data = [];
  180. var $colliders = (this.colliders || this.$colliders);
  181. var count = $colliders.length;
  182. var player_coords = self.$element.coords()
  183. .update(player_data_coords || false).get();
  184. while(count--){
  185. var $collider = self.$colliders ?
  186. $($colliders[count]) : $colliders[count];
  187. var $collider_coords_ins = ($collider.isCoords) ?
  188. $collider : $collider.coords();
  189. var collider_coords = $collider_coords_ins.get();
  190. var overlaps = self.overlaps(player_coords, collider_coords);
  191. if (!overlaps) {
  192. continue;
  193. }
  194. var region = self.detect_overlapping_region(
  195. player_coords, collider_coords);
  196. //todo: make this an option
  197. if (region === 'C'){
  198. var area_coords = self.calculate_overlapped_area_coords(
  199. player_coords, collider_coords);
  200. var area = self.calculate_overlapped_area(area_coords);
  201. var collider_data = {
  202. area: area,
  203. area_coords : area_coords,
  204. region: region,
  205. coords: collider_coords,
  206. player_coords: player_coords,
  207. el: $collider
  208. };
  209. if (self.options.on_overlap) {
  210. self.options.on_overlap.call(this, collider_data);
  211. }
  212. colliders_coords.push($collider_coords_ins);
  213. colliders_data.push(collider_data);
  214. }
  215. }
  216. if (self.options.on_overlap_stop || self.options.on_overlap_start) {
  217. this.manage_colliders_start_stop(colliders_coords,
  218. self.options.on_overlap_start, self.options.on_overlap_stop);
  219. }
  220. this.last_colliders_coords = colliders_coords;
  221. return colliders_data;
  222. };
  223. fn.get_closest_colliders = function(player_data_coords){
  224. var colliders = this.find_collisions(player_data_coords);
  225. colliders.sort(function(a, b) {
  226. /* if colliders are being overlapped by the "C" (center) region,
  227. * we have to set a lower index in the array to which they are placed
  228. * above in the grid. */
  229. if (a.region === 'C' && b.region === 'C') {
  230. if (a.coords.y1 < b.coords.y1 || a.coords.x1 < b.coords.x1) {
  231. return - 1;
  232. }else{
  233. return 1;
  234. }
  235. }
  236. if (a.area < b.area) {
  237. return 1;
  238. }
  239. return 1;
  240. });
  241. return colliders;
  242. };
  243. //jQuery adapter
  244. $.fn.collision = function(collider, options) {
  245. return new Collision( this, collider, options );
  246. };
  247. }(jQuery, window, document));
  248. ;(function(window, undefined) {
  249. /* Debounce and throttle functions taken from underscore.js */
  250. window.debounce = function(func, wait, immediate) {
  251. var timeout;
  252. return function() {
  253. var context = this, args = arguments;
  254. var later = function() {
  255. timeout = null;
  256. if (!immediate) func.apply(context, args);
  257. };
  258. if (immediate && !timeout) func.apply(context, args);
  259. clearTimeout(timeout);
  260. timeout = setTimeout(later, wait);
  261. };
  262. };
  263. window.throttle = function(func, wait) {
  264. var context, args, timeout, throttling, more, result;
  265. var whenDone = debounce(
  266. function(){ more = throttling = false; }, wait);
  267. return function() {
  268. context = this; args = arguments;
  269. var later = function() {
  270. timeout = null;
  271. if (more) func.apply(context, args);
  272. whenDone();
  273. };
  274. if (!timeout) timeout = setTimeout(later, wait);
  275. if (throttling) {
  276. more = true;
  277. } else {
  278. result = func.apply(context, args);
  279. }
  280. whenDone();
  281. throttling = true;
  282. return result;
  283. };
  284. };
  285. })(window);
  286. ;(function($, window, document, undefined){
  287. var defaults = {
  288. items: '.gs_w',
  289. distance: 1,
  290. limit: true,
  291. offset_left: 0,
  292. autoscroll: true,
  293. ignore_dragging: ['INPUT', 'TEXTAREA', 'SELECT', 'BUTTON'],
  294. handle: null,
  295. container_width: 0 // 0 == auto
  296. // drag: function(e){},
  297. // start : function(e, ui){},
  298. // stop : function(e){}
  299. };
  300. var $window = $(window);
  301. var isTouch = !!('ontouchstart' in window);
  302. var pointer_events = {
  303. start: isTouch ? 'touchstart.gridster-draggable' : 'mousedown.gridster-draggable',
  304. move: isTouch ? 'touchmove.gridster-draggable' : 'mousemove.gridster-draggable',
  305. end: isTouch ? 'touchend.gridster-draggable' : 'mouseup.gridster-draggable'
  306. };
  307. /**
  308. * Basic drag implementation for DOM elements inside a container.
  309. * Provide start/stop/drag callbacks.
  310. *
  311. * @class Draggable
  312. * @param {HTMLElement} el The HTMLelement that contains all the widgets
  313. * to be dragged.
  314. * @param {Object} [options] An Object with all options you want to
  315. * overwrite:
  316. * @param {HTMLElement|String} [options.items] Define who will
  317. * be the draggable items. Can be a CSS Selector String or a
  318. * collection of HTMLElements.
  319. * @param {Number} [options.distance] Distance in pixels after mousedown
  320. * the mouse must move before dragging should start.
  321. * @param {Boolean} [options.limit] Constrains dragging to the width of
  322. * the container
  323. * @param {offset_left} [options.offset_left] Offset added to the item
  324. * that is being dragged.
  325. * @param {Number} [options.drag] Executes a callback when the mouse is
  326. * moved during the dragging.
  327. * @param {Number} [options.start] Executes a callback when the drag
  328. * starts.
  329. * @param {Number} [options.stop] Executes a callback when the drag stops.
  330. * @return {Object} Returns `el`.
  331. * @constructor
  332. */
  333. function Draggable(el, options) {
  334. this.options = $.extend({}, defaults, options);
  335. this.$body = $(document.body);
  336. this.$container = $(el);
  337. this.$dragitems = $(this.options.items, this.$container);
  338. this.is_dragging = false;
  339. this.player_min_left = 0 + this.options.offset_left;
  340. this.init();
  341. }
  342. var fn = Draggable.prototype;
  343. fn.init = function() {
  344. this.calculate_positions();
  345. this.$container.css('position', 'relative');
  346. this.disabled = false;
  347. this.events();
  348. $(window).bind('resize.gridster-draggable',
  349. throttle($.proxy(this.calculate_positions, this), 200));
  350. };
  351. fn.events = function() {
  352. this.$container.on('selectstart.gridster-draggable',
  353. $.proxy(this.on_select_start, this));
  354. this.$container.on(pointer_events.start, this.options.items,
  355. $.proxy(this.drag_handler, this));
  356. this.$body.on(pointer_events.end, $.proxy(function(e) {
  357. this.is_dragging = false;
  358. if (this.disabled) { return; }
  359. this.$body.off(pointer_events.move);
  360. if (this.drag_start) {
  361. this.on_dragstop(e);
  362. }
  363. }, this));
  364. };
  365. fn.get_actual_pos = function($el) {
  366. var pos = $el.position();
  367. return pos;
  368. };
  369. fn.get_mouse_pos = function(e) {
  370. if (isTouch) {
  371. var oe = e.originalEvent;
  372. e = oe.touches.length ? oe.touches[0] : oe.changedTouches[0];
  373. }
  374. return {
  375. left: e.clientX,
  376. top: e.clientY
  377. };
  378. };
  379. fn.get_offset = function(e) {
  380. e.preventDefault();
  381. var mouse_actual_pos = this.get_mouse_pos(e);
  382. var diff_x = Math.round(
  383. mouse_actual_pos.left - this.mouse_init_pos.left);
  384. var diff_y = Math.round(mouse_actual_pos.top - this.mouse_init_pos.top);
  385. var left = Math.round(this.el_init_offset.left + diff_x - this.baseX);
  386. var top = Math.round(
  387. this.el_init_offset.top + diff_y - this.baseY + this.scrollOffset);
  388. if (this.options.limit) {
  389. if (left > this.player_max_left) {
  390. left = this.player_max_left;
  391. }else if(left < this.player_min_left) {
  392. left = this.player_min_left;
  393. }
  394. }
  395. return {
  396. left: left,
  397. top: top,
  398. mouse_left: mouse_actual_pos.left,
  399. mouse_top: mouse_actual_pos.top
  400. };
  401. };
  402. fn.manage_scroll = function(offset) {
  403. /* scroll document */
  404. var nextScrollTop;
  405. var scrollTop = $window.scrollTop();
  406. var min_window_y = scrollTop;
  407. var max_window_y = min_window_y + this.window_height;
  408. var mouse_down_zone = max_window_y - 50;
  409. var mouse_up_zone = min_window_y + 50;
  410. var abs_mouse_left = offset.mouse_left;
  411. var abs_mouse_top = min_window_y + offset.mouse_top;
  412. var max_player_y = (this.doc_height - this.window_height +
  413. this.player_height);
  414. if (abs_mouse_top >= mouse_down_zone) {
  415. nextScrollTop = scrollTop + 30;
  416. if (nextScrollTop < max_player_y) {
  417. $window.scrollTop(nextScrollTop);
  418. this.scrollOffset = this.scrollOffset + 30;
  419. }
  420. }
  421. if (abs_mouse_top <= mouse_up_zone) {
  422. nextScrollTop = scrollTop - 30;
  423. if (nextScrollTop > 0) {
  424. $window.scrollTop(nextScrollTop);
  425. this.scrollOffset = this.scrollOffset - 30;
  426. }
  427. }
  428. };
  429. fn.calculate_positions = function(e) {
  430. this.window_height = $window.height();
  431. };
  432. fn.drag_handler = function(e) {
  433. var node = e.target.nodeName;
  434. if (this.disabled || e.which !== 1 && !isTouch) {
  435. return;
  436. }
  437. if (this.ignore_drag(e)) {
  438. return;
  439. }
  440. var self = this;
  441. var first = true;
  442. this.$player = $(e.currentTarget);
  443. this.el_init_pos = this.get_actual_pos(this.$player);
  444. this.mouse_init_pos = this.get_mouse_pos(e);
  445. this.offsetY = this.mouse_init_pos.top - this.el_init_pos.top;
  446. this.$body.on(pointer_events.move, function(mme){
  447. var mouse_actual_pos = self.get_mouse_pos(mme);
  448. var diff_x = Math.abs(
  449. mouse_actual_pos.left - self.mouse_init_pos.left);
  450. var diff_y = Math.abs(
  451. mouse_actual_pos.top - self.mouse_init_pos.top);
  452. if (!(diff_x > self.options.distance ||
  453. diff_y > self.options.distance)
  454. ) {
  455. return false;
  456. }
  457. if (first) {
  458. first = false;
  459. self.on_dragstart.call(self, mme);
  460. return false;
  461. }
  462. if (self.is_dragging === true) {
  463. self.on_dragmove.call(self, mme);
  464. }
  465. return false;
  466. });
  467. if (!isTouch) { return false; }
  468. };
  469. fn.on_dragstart = function(e) {
  470. e.preventDefault();
  471. this.drag_start = true;
  472. this.is_dragging = true;
  473. var offset = this.$container.offset();
  474. this.baseX = Math.round(offset.left);
  475. this.baseY = Math.round(offset.top);
  476. this.doc_height = $(document).height();
  477. if (this.options.helper === 'clone') {
  478. this.$helper = this.$player.clone()
  479. .appendTo(this.$container).addClass('helper');
  480. this.helper = true;
  481. }else{
  482. this.helper = false;
  483. }
  484. this.scrollOffset = 0;
  485. this.el_init_offset = this.$player.offset();
  486. this.player_width = this.$player.width();
  487. this.player_height = this.$player.height();
  488. var container_width = this.options.container_width || this.$container.width();
  489. this.player_max_left = (container_width - this.player_width +
  490. this.options.offset_left);
  491. if (this.options.start) {
  492. this.options.start.call(this.$player, e, {
  493. helper: this.helper ? this.$helper : this.$player
  494. });
  495. }
  496. return false;
  497. };
  498. fn.on_dragmove = function(e) {
  499. var offset = this.get_offset(e);
  500. this.options.autoscroll && this.manage_scroll(offset);
  501. (this.helper ? this.$helper : this.$player).css({
  502. 'position': 'absolute',
  503. 'left' : offset.left,
  504. 'top' : offset.top
  505. });
  506. var ui = {
  507. 'position': {
  508. 'left': offset.left,
  509. 'top': offset.top
  510. }
  511. };
  512. if (this.options.drag) {
  513. this.options.drag.call(this.$player, e, ui);
  514. }
  515. return false;
  516. };
  517. fn.on_dragstop = function(e) {
  518. var offset = this.get_offset(e);
  519. this.drag_start = false;
  520. var ui = {
  521. 'position': {
  522. 'left': offset.left,
  523. 'top': offset.top
  524. }
  525. };
  526. if (this.options.stop) {
  527. this.options.stop.call(this.$player, e, ui);
  528. }
  529. if (this.helper) {
  530. this.$helper.remove();
  531. }
  532. return false;
  533. };
  534. fn.on_select_start = function(e) {
  535. if (this.disabled) { return; }
  536. if (this.ignore_drag(e)) {
  537. return;
  538. }
  539. return false;
  540. };
  541. fn.enable = function() {
  542. this.disabled = false;
  543. };
  544. fn.disable = function() {
  545. this.disabled = true;
  546. };
  547. fn.destroy = function(){
  548. this.disable();
  549. this.$container.off('.gridster-draggable');
  550. this.$body.off('.gridster-draggable');
  551. $(window).off('.gridster-draggable');
  552. $.removeData(this.$container, 'drag');
  553. };
  554. fn.ignore_drag = function(event) {
  555. if (this.options.handle) {
  556. return !$(event.target).is(this.options.handle);
  557. }
  558. return $.inArray(event.target.nodeName, this.options.ignore_dragging) >= 0;
  559. };
  560. //jQuery adapter
  561. $.fn.drag = function ( options ) {
  562. return this.each(function () {
  563. if (!$.data(this, 'drag')) {
  564. $.data(this, 'drag', new Draggable( this, options ));
  565. }
  566. });
  567. };
  568. }(jQuery, window, document));
  569. ;(function($, window, document, undefined) {
  570. var defaults = {
  571. namespace: '',
  572. widget_selector: 'li',
  573. widget_margins: [10, 10],
  574. widget_base_dimensions: [400, 225],
  575. extra_rows: 0,
  576. extra_cols: 0,
  577. min_cols: 1,
  578. max_cols: null,
  579. min_rows: 15,
  580. max_size_x: 6,
  581. autogenerate_stylesheet: true,
  582. avoid_overlapped_widgets: true,
  583. serialize_params: function($w, wgd) {
  584. return {
  585. col: wgd.col,
  586. row: wgd.row,
  587. size_x: wgd.size_x,
  588. size_y: wgd.size_y
  589. };
  590. },
  591. collision: {},
  592. draggable: {
  593. distance: 4
  594. }
  595. };
  596. /**
  597. * @class Gridster
  598. * @uses Draggable
  599. * @uses Collision
  600. * @param {HTMLElement} el The HTMLelement that contains all the widgets.
  601. * @param {Object} [options] An Object with all options you want to
  602. * overwrite:
  603. * @param {HTMLElement|String} [options.widget_selector] Define who will
  604. * be the draggable widgets. Can be a CSS Selector String or a
  605. * collection of HTMLElements
  606. * @param {Array} [options.widget_margins] Margin between widgets.
  607. * The first index for the horizontal margin (left, right) and
  608. * the second for the vertical margin (top, bottom).
  609. * @param {Array} [options.widget_base_dimensions] Base widget dimensions
  610. * in pixels. The first index for the width and the second for the
  611. * height.
  612. * @param {Number} [options.extra_cols] Add more columns in addition to
  613. * those that have been calculated.
  614. * @param {Number} [options.extra_rows] Add more rows in addition to
  615. * those that have been calculated.
  616. * @param {Number} [options.min_cols] The minimum required columns.
  617. * @param {Number} [options.max_cols] The maximum columns possible (set to null
  618. * for no maximum).
  619. * @param {Number} [options.min_rows] The minimum required rows.
  620. * @param {Number} [options.max_size_x] The maximum number of columns
  621. * that a widget can span.
  622. * @param {Boolean} [options.autogenerate_stylesheet] If true, all the
  623. * CSS required to position all widgets in their respective columns
  624. * and rows will be generated automatically and injected to the
  625. * `<head>` of the document. You can set this to false, and write
  626. * your own CSS targeting rows and cols via data-attributes like so:
  627. * `[data-col="1"] { left: 10px; }`
  628. * @param {Boolean} [options.avoid_overlapped_widgets] Avoid that widgets loaded
  629. * from the DOM can be overlapped. It is helpful if the positions were
  630. * bad stored in the database or if there was any conflict.
  631. * @param {Function} [options.serialize_params] Return the data you want
  632. * for each widget in the serialization. Two arguments are passed:
  633. * `$w`: the jQuery wrapped HTMLElement, and `wgd`: the grid
  634. * coords object (`col`, `row`, `size_x`, `size_y`).
  635. * @param {Object} [options.collision] An Object with all options for
  636. * Collision class you want to overwrite. See Collision docs for
  637. * more info.
  638. * @param {Object} [options.draggable] An Object with all options for
  639. * Draggable class you want to overwrite. See Draggable docs for more
  640. * info.
  641. *
  642. * @constructor
  643. */
  644. function Gridster(el, options) {
  645. this.options = $.extend(true, defaults, options);
  646. this.$el = $(el);
  647. this.$wrapper = this.$el.parent();
  648. this.$widgets = this.$el.children(this.options.widget_selector).addClass('gs_w');
  649. this.widgets = [];
  650. this.$changed = $([]);
  651. this.wrapper_width = this.$wrapper.width();
  652. this.min_widget_width = (this.options.widget_margins[0] * 2) +
  653. this.options.widget_base_dimensions[0];
  654. this.min_widget_height = (this.options.widget_margins[1] * 2) +
  655. this.options.widget_base_dimensions[1];
  656. this.$style_tags = $([]);
  657. this.init();
  658. }
  659. Gridster.generated_stylesheets = [];
  660. var fn = Gridster.prototype;
  661. fn.init = function() {
  662. this.generate_grid_and_stylesheet();
  663. this.get_widgets_from_DOM();
  664. this.set_dom_grid_height();
  665. this.$wrapper.addClass('ready');
  666. this.draggable();
  667. $(window).bind('resize.gridster', throttle($.proxy(this.recalculate_faux_grid, this), 200));
  668. };
  669. /**
  670. * Disables dragging.
  671. *
  672. * @method disable
  673. * @return {Class} Returns the instance of the Gridster Class.
  674. */
  675. fn.disable = function() {
  676. this.$wrapper.find('.player-revert').removeClass('player-revert');
  677. this.drag_api.disable();
  678. return this;
  679. };
  680. /**
  681. * Enables dragging.
  682. *
  683. * @method enable
  684. * @return {Class} Returns the instance of the Gridster Class.
  685. */
  686. fn.enable = function() {
  687. this.drag_api.enable();
  688. return this;
  689. };
  690. /**
  691. * Add a new widget to the grid.
  692. *
  693. * @method add_widget
  694. * @param {String|HTMLElement} html The string representing the HTML of the widget
  695. * or the HTMLElement.
  696. * @param {Number} [size_x] The nº of rows the widget occupies horizontally.
  697. * @param {Number} [size_y] The nº of columns the widget occupies vertically.
  698. * @param {Number} [col] The column the widget should start in.
  699. * @param {Number} [row] The row the widget should start in.
  700. * @return {HTMLElement} Returns the jQuery wrapped HTMLElement representing.
  701. * the widget that was just created.
  702. */
  703. fn.add_widget = function(html, size_x, size_y, col, row) {
  704. var pos;
  705. size_x || (size_x = 1);
  706. size_y || (size_y = 1);
  707. if (!col & !row) {
  708. pos = this.next_position(size_x, size_y);
  709. }else{
  710. pos = {
  711. col: col,
  712. row: row
  713. };
  714. this.empty_cells(col, row, size_x, size_y);
  715. }
  716. var $w = $(html).attr({
  717. 'data-col': pos.col,
  718. 'data-row': pos.row,
  719. 'data-sizex' : size_x,
  720. 'data-sizey' : size_y
  721. }).addClass('gs_w').appendTo(this.$el).hide();
  722. this.$widgets = this.$widgets.add($w);
  723. this.register_widget($w);
  724. this.add_faux_rows(pos.size_y);
  725. //this.add_faux_cols(pos.size_x);
  726. this.set_dom_grid_height();
  727. return $w.fadeIn();
  728. };
  729. /**
  730. * Change the size of a widget.
  731. *
  732. * @method resize_widget
  733. * @param {HTMLElement} $widget The jQuery wrapped HTMLElement
  734. * representing the widget.
  735. * @param {Number} size_x The number of columns that will occupy the widget.
  736. * @param {Number} size_y The number of rows that will occupy the widget.
  737. * @param {Function} callback Function executed when the widget is removed.
  738. * @return {HTMLElement} Returns $widget.
  739. */
  740. fn.resize_widget = function($widget, size_x, size_y, callback) {
  741. var wgd = $widget.coords().grid;
  742. size_x || (size_x = wgd.size_x);
  743. size_y || (size_y = wgd.size_y);
  744. if (size_x > this.cols) {
  745. size_x = this.cols;
  746. }
  747. var old_cells_occupied = this.get_cells_occupied(wgd);
  748. var old_size_x = wgd.size_x;
  749. var old_size_y = wgd.size_y;
  750. var old_col = wgd.col;
  751. var new_col = old_col;
  752. var wider = size_x > old_size_x;
  753. var taller = size_y > old_size_y;
  754. if (old_col + size_x - 1 > this.cols) {
  755. var diff = old_col + (size_x - 1) - this.cols;
  756. var c = old_col - diff;
  757. new_col = Math.max(1, c);
  758. }
  759. var new_grid_data = {
  760. col: new_col,
  761. row: wgd.row,
  762. size_x: size_x,
  763. size_y: size_y
  764. };
  765. var new_cells_occupied = this.get_cells_occupied(new_grid_data);
  766. var empty_cols = [];
  767. $.each(old_cells_occupied.cols, function(i, col) {
  768. if ($.inArray(col, new_cells_occupied.cols) === -1) {
  769. empty_cols.push(col);
  770. }
  771. });
  772. var occupied_cols = [];
  773. $.each(new_cells_occupied.cols, function(i, col) {
  774. if ($.inArray(col, old_cells_occupied.cols) === -1) {
  775. occupied_cols.push(col);
  776. }
  777. });
  778. var empty_rows = [];
  779. $.each(old_cells_occupied.rows, function(i, row) {
  780. if ($.inArray(row, new_cells_occupied.rows) === -1) {
  781. empty_rows.push(row);
  782. }
  783. });
  784. var occupied_rows = [];
  785. $.each(new_cells_occupied.rows, function(i, row) {
  786. if ($.inArray(row, old_cells_occupied.rows) === -1) {
  787. occupied_rows.push(row);
  788. }
  789. });
  790. this.remove_from_gridmap(wgd);
  791. if (occupied_cols.length) {
  792. var cols_to_empty = [
  793. new_col, wgd.row, size_x, Math.min(old_size_y, size_y), $widget
  794. ];
  795. this.empty_cells.apply(this, cols_to_empty);
  796. }
  797. if (occupied_rows.length) {
  798. var rows_to_empty = [new_col, wgd.row, size_x, size_y, $widget];
  799. this.empty_cells.apply(this, rows_to_empty);
  800. }
  801. wgd.col = new_col;
  802. wgd.size_x = size_x;
  803. wgd.size_y = size_y;
  804. this.add_to_gridmap(new_grid_data, $widget);
  805. //update coords instance attributes
  806. $widget.data('coords').update({
  807. width: (size_x * this.options.widget_base_dimensions[0] +
  808. ((size_x - 1) * this.options.widget_margins[0]) * 2),
  809. height: (size_y * this.options.widget_base_dimensions[1] +
  810. ((size_y - 1) * this.options.widget_margins[1]) * 2)
  811. });
  812. if (size_y > old_size_y) {
  813. this.add_faux_rows(size_y - old_size_y);
  814. }
  815. if (size_x > old_size_x) {
  816. this.add_faux_cols(size_x - old_size_x);
  817. }
  818. $widget.attr({
  819. 'data-col': new_col,
  820. 'data-sizex': size_x,
  821. 'data-sizey': size_y
  822. });
  823. if (empty_cols.length) {
  824. var cols_to_remove_holes = [
  825. empty_cols[0], wgd.row,
  826. empty_cols.length,
  827. Math.min(old_size_y, size_y),
  828. $widget
  829. ];
  830. this.remove_empty_cells.apply(this, cols_to_remove_holes);
  831. }
  832. if (empty_rows.length) {
  833. var rows_to_remove_holes = [
  834. new_col, wgd.row, size_x, size_y, $widget
  835. ];
  836. this.remove_empty_cells.apply(this, rows_to_remove_holes);
  837. }
  838. if (callback) {
  839. callback.call(this, size_x, size_y);
  840. }
  841. return $widget;
  842. };
  843. /**
  844. * Move down widgets in cells represented by the arguments col, row, size_x,
  845. * size_y
  846. *
  847. * @method empty_cells
  848. * @param {Number} col The column where the group of cells begin.
  849. * @param {Number} row The row where the group of cells begin.
  850. * @param {Number} size_x The number of columns that the group of cells
  851. * occupy.
  852. * @param {Number} size_y The number of rows that the group of cells
  853. * occupy.
  854. * @param {HTMLElement} $exclude Exclude widgets from being moved.
  855. * @return {Class} Returns the instance of the Gridster Class.
  856. */
  857. fn.empty_cells = function(col, row, size_x, size_y, $exclude) {
  858. var $nexts = this.widgets_below({
  859. col: col,
  860. row: row - size_y,
  861. size_x: size_x,
  862. size_y: size_y
  863. });
  864. $nexts.not($exclude).each($.proxy(function(i, w) {
  865. var wgd = $(w).coords().grid;
  866. if (!(wgd.row <= (row + size_y - 1))) { return; }
  867. var diff = (row + size_y) - wgd.row;
  868. this.move_widget_down($(w), diff);
  869. }, this));
  870. this.set_dom_grid_height();
  871. return this;
  872. };
  873. /**
  874. * Move up widgets below cells represented by the arguments col, row, size_x,
  875. * size_y.
  876. *
  877. * @method remove_empty_cells
  878. * @param {Number} col The column where the group of cells begin.
  879. * @param {Number} row The row where the group of cells begin.
  880. * @param {Number} size_x The number of columns that the group of cells
  881. * occupy.
  882. * @param {Number} size_y The number of rows that the group of cells
  883. * occupy.
  884. * @param {HTMLElement} exclude Exclude widgets from being moved.
  885. * @return {Class} Returns the instance of the Gridster Class.
  886. */
  887. fn.remove_empty_cells = function(col, row, size_x, size_y, exclude) {
  888. var $nexts = this.widgets_below({
  889. col: col,
  890. row: row,
  891. size_x: size_x,
  892. size_y: size_y
  893. });
  894. $nexts.not(exclude).each($.proxy(function(i, widget) {
  895. this.move_widget_up( $(widget), size_y );
  896. }, this));
  897. this.set_dom_grid_height();
  898. return this;
  899. };
  900. /**
  901. * Get the most left column below to add a new widget.
  902. *
  903. * @method next_position
  904. * @param {Number} size_x The nº of rows the widget occupies horizontally.
  905. * @param {Number} size_y The nº of columns the widget occupies vertically.
  906. * @return {Object} Returns a grid coords object representing the future
  907. * widget coords.
  908. */
  909. fn.next_position = function(size_x, size_y) {
  910. size_x || (size_x = 1);
  911. size_y || (size_y = 1);
  912. var ga = this.gridmap;
  913. var cols_l = ga.length;
  914. var valid_pos = [];
  915. var rows_l;
  916. for (var c = 1; c < cols_l; c++) {
  917. rows_l = ga[c].length;
  918. for (var r = 1; r <= rows_l; r++) {
  919. var can_move_to = this.can_move_to({
  920. size_x: size_x,
  921. size_y: size_y
  922. }, c, r);
  923. if (can_move_to) {
  924. valid_pos.push({
  925. col: c,
  926. row: r,
  927. size_y: size_y,
  928. size_x: size_x
  929. });
  930. }
  931. }
  932. }
  933. if (valid_pos.length) {
  934. return this.sort_by_row_and_col_asc(valid_pos)[0];
  935. }
  936. return false;
  937. };
  938. /**
  939. * Remove a widget from the grid.
  940. *
  941. * @method remove_widget
  942. * @param {HTMLElement} el The jQuery wrapped HTMLElement you want to remove.
  943. * @param {Boolean|Function} silent If true, widgets below the removed one
  944. * will not move up. If a Function is passed it will be used as callback.
  945. * @param {Function} callback Function executed when the widget is removed.
  946. * @return {Class} Returns the instance of the Gridster Class.
  947. */
  948. fn.remove_widget = function(el, silent, callback) {
  949. var $el = el instanceof jQuery ? el : $(el);
  950. var wgd = $el.coords().grid;
  951. // if silent is a function assume it's a callback
  952. if ($.isFunction(silent)) {
  953. callback = silent;
  954. silent = false;
  955. }
  956. this.cells_occupied_by_placeholder = {};
  957. this.$widgets = this.$widgets.not($el);
  958. var $nexts = this.widgets_below($el);
  959. this.remove_from_gridmap(wgd);
  960. $el.fadeOut($.proxy(function() {
  961. $el.remove();
  962. if (!silent) {
  963. $nexts.each($.proxy(function(i, widget) {
  964. this.move_widget_up( $(widget), wgd.size_y );
  965. }, this));
  966. }
  967. this.set_dom_grid_height();
  968. if (callback) {
  969. callback.call(this, el);
  970. }
  971. }, this));
  972. };
  973. /**
  974. * Remove all widgets from the grid.
  975. *
  976. * @method remove_all_widgets
  977. * @param {Function} callback Function executed for each widget removed.
  978. * @return {Class} Returns the instance of the Gridster Class.
  979. */
  980. fn.remove_all_widgets = function(callback) {
  981. this.$widgets.each($.proxy(function(i, el){
  982. this.remove_widget(el, true, callback);
  983. }, this));
  984. return this;
  985. };
  986. /**
  987. * Returns a serialized array of the widgets in the grid.
  988. *
  989. * @method serialize
  990. * @param {HTMLElement} [$widgets] The collection of jQuery wrapped
  991. * HTMLElements you want to serialize. If no argument is passed all widgets
  992. * will be serialized.
  993. * @return {Array} Returns an Array of Objects with the data specified in
  994. * the serialize_params option.
  995. */
  996. fn.serialize = function($widgets) {
  997. $widgets || ($widgets = this.$widgets);
  998. var result = [];
  999. $widgets.each($.proxy(function(i, widget) {
  1000. result.push(this.options.serialize_params(
  1001. $(widget), $(widget).coords().grid ) );
  1002. }, this));
  1003. return result;
  1004. };
  1005. /**
  1006. * Returns a serialized array of the widgets that have changed their
  1007. * position.
  1008. *
  1009. * @method serialize_changed
  1010. * @return {Array} Returns an Array of Objects with the data specified in
  1011. * the serialize_params option.
  1012. */
  1013. fn.serialize_changed = function() {
  1014. return this.serialize(this.$changed);
  1015. };
  1016. /**
  1017. * Creates the grid coords object representing the widget a add it to the
  1018. * mapped array of positions.
  1019. *
  1020. * @method register_widget
  1021. * @return {Array} Returns the instance of the Gridster class.
  1022. */
  1023. fn.register_widget = function($el) {
  1024. var wgd = {
  1025. 'col': parseInt($el.attr('data-col'), 10),
  1026. 'row': parseInt($el.attr('data-row'), 10),
  1027. 'size_x': parseInt($el.attr('data-sizex'), 10),
  1028. 'size_y': parseInt($el.attr('data-sizey'), 10),
  1029. 'el': $el
  1030. };
  1031. if (this.options.avoid_overlapped_widgets &&
  1032. !this.can_move_to(
  1033. {size_x: wgd.size_x, size_y: wgd.size_y}, wgd.col, wgd.row)
  1034. ) {
  1035. wgd = this.next_position(wgd.size_x, wgd.size_y);
  1036. wgd.el = $el;
  1037. $el.attr({
  1038. 'data-col': wgd.col,
  1039. 'data-row': wgd.row,
  1040. 'data-sizex': wgd.size_x,
  1041. 'data-sizey': wgd.size_y
  1042. });
  1043. }
  1044. // attach Coord object to player data-coord attribute
  1045. $el.data('coords', $el.coords());
  1046. // Extend Coord object with grid position info
  1047. $el.data('coords').grid = wgd;
  1048. this.add_to_gridmap(wgd, $el);
  1049. return this;
  1050. };
  1051. /**
  1052. * Update in the mapped array of positions the value of cells represented by
  1053. * the grid coords object passed in the `grid_data` param.
  1054. *
  1055. * @param {Object} grid_data The grid coords object representing the cells
  1056. * to update in the mapped array.
  1057. * @param {HTMLElement|Boolean} value Pass `false` or the jQuery wrapped
  1058. * HTMLElement, depends if you want to delete an existing position or add
  1059. * a new one.
  1060. * @method update_widget_position
  1061. * @return {Class} Returns the instance of the Gridster Class.
  1062. */
  1063. fn.update_widget_position = function(grid_data, value) {
  1064. this.for_each_cell_occupied(grid_data, function(col, row) {
  1065. if (!this.gridmap[col]) { return this; }
  1066. this.gridmap[col][row] = value;
  1067. });
  1068. return this;
  1069. };
  1070. /**
  1071. * Remove a widget from the mapped array of positions.
  1072. *
  1073. * @method remove_from_gridmap
  1074. * @param {Object} grid_data The grid coords object representing the cells
  1075. * to update in the mapped array.
  1076. * @return {Class} Returns the instance of the Gridster Class.
  1077. */
  1078. fn.remove_from_gridmap = function(grid_data) {
  1079. return this.update_widget_position(grid_data, false);
  1080. };
  1081. /**
  1082. * Add a widget to the mapped array of positions.
  1083. *
  1084. * @method add_to_gridmap
  1085. * @param {Object} grid_data The grid coords object representing the cells
  1086. * to update in the mapped array.
  1087. * @param {HTMLElement|Boolean} value The value to set in the specified
  1088. * position .
  1089. * @return {Class} Returns the instance of the Gridster Class.
  1090. */
  1091. fn.add_to_gridmap = function(grid_data, value) {
  1092. this.update_widget_position(grid_data, value || grid_data.el);
  1093. if (grid_data.el) {
  1094. var $widgets = this.widgets_below(grid_data.el);
  1095. $widgets.each($.proxy(function(i, widget) {
  1096. this.move_widget_up( $(widget));
  1097. }, this));
  1098. }
  1099. };
  1100. /**
  1101. * Make widgets draggable.
  1102. *
  1103. * @uses Draggable
  1104. * @method draggable
  1105. * @return {Class} Returns the instance of the Gridster Class.
  1106. */
  1107. fn.draggable = function() {
  1108. var self = this;
  1109. var draggable_options = $.extend(true, {}, this.options.draggable, {
  1110. offset_left: this.options.widget_margins[0],
  1111. container_width: this.container_width,
  1112. start: function(event, ui) {
  1113. self.$widgets.filter('.player-revert')
  1114. .removeClass('player-revert');
  1115. self.$player = $(this);
  1116. self.$helper = self.options.draggable.helper === 'clone' ?
  1117. $(ui.helper) : self.$player;
  1118. self.helper = !self.$helper.is(self.$player);
  1119. self.on_start_drag.call(self, event, ui);
  1120. self.$el.trigger('gridster:dragstart');
  1121. },
  1122. stop: function(event, ui) {
  1123. self.on_stop_drag.call(self, event, ui);
  1124. self.$el.trigger('gridster:dragstop');
  1125. },
  1126. drag: throttle(function(event, ui) {
  1127. self.on_drag.call(self, event, ui);
  1128. self.$el.trigger('gridster:drag');
  1129. }, 60)
  1130. });
  1131. this.drag_api = this.$el.drag(draggable_options).data('drag');
  1132. return this;
  1133. };
  1134. /**
  1135. * This function is executed when the player begins to be dragged.
  1136. *
  1137. * @method on_start_drag
  1138. * @param {Event} event The original browser event
  1139. * @param {Object} ui A prepared ui object.
  1140. */
  1141. fn.on_start_drag = function(event, ui) {
  1142. this.$helper.add(this.$player).add(this.$wrapper).addClass('dragging');
  1143. this.$player.addClass('player');
  1144. this.player_grid_data = this.$player.coords().grid;
  1145. this.placeholder_grid_data = $.extend({}, this.player_grid_data);
  1146. //set new grid height along the dragging period
  1147. this.$el.css('height', this.$el.height() +
  1148. (this.player_grid_data.size_y * this.min_widget_height));
  1149. var colliders = this.faux_grid;
  1150. var coords = this.$player.data('coords').coords;
  1151. this.cells_occupied_by_player = this.get_cells_occupied(
  1152. this.player_grid_data);
  1153. this.cells_occupied_by_placeholder = this.get_cells_occupied(
  1154. this.placeholder_grid_data);
  1155. this.last_cols = [];
  1156. this.last_rows = [];
  1157. // see jquery.collision.js
  1158. this.collision_api = this.$helper.collision(
  1159. colliders, this.options.collision);
  1160. this.$preview_holder = $('<li />', {
  1161. 'class': 'preview-holder',
  1162. 'data-row': this.$player.attr('data-row'),
  1163. 'data-col': this.$player.attr('data-col'),
  1164. css: {
  1165. width: coords.width,
  1166. height: coords.height
  1167. }
  1168. }).appendTo(this.$el);
  1169. if (this.options.draggable.start) {
  1170. this.options.draggable.start.call(this, event, ui);
  1171. }
  1172. };
  1173. /**
  1174. * This function is executed when the player is being dragged.
  1175. *
  1176. * @method on_drag
  1177. * @param {Event} event The original browser event
  1178. * @param {Object} ui A prepared ui object.
  1179. */
  1180. fn.on_drag = function(event, ui) {
  1181. //break if dragstop has been fired
  1182. if (this.$player === null) {
  1183. return false;
  1184. }
  1185. var abs_offset = {
  1186. left: ui.position.left + this.baseX,
  1187. top: ui.position.top + this.baseY
  1188. };
  1189. this.colliders_data = this.collision_api.get_closest_colliders(
  1190. abs_offset);
  1191. this.on_overlapped_column_change(
  1192. this.on_start_overlapping_column,
  1193. this.on_stop_overlapping_column
  1194. );
  1195. this.on_overlapped_row_change(
  1196. this.on_start_overlapping_row,
  1197. this.on_stop_overlapping_row
  1198. );
  1199. if (this.helper && this.$player) {
  1200. this.$player.css({
  1201. 'left': ui.position.left,
  1202. 'top': ui.position.top
  1203. });
  1204. }
  1205. if (this.options.draggable.drag) {
  1206. this.options.draggable.drag.call(this, event, ui);
  1207. }
  1208. };
  1209. /**
  1210. * This function is executed when the player stops being dragged.
  1211. *
  1212. * @method on_stop_drag
  1213. * @param {Event} event The original browser event
  1214. * @param {Object} ui A prepared ui object.
  1215. */
  1216. fn.on_stop_drag = function(event, ui) {
  1217. this.$helper.add(this.$player).add(this.$wrapper)
  1218. .removeClass('dragging');
  1219. ui.position.left = ui.position.left + this.baseX;
  1220. ui.position.top = ui.position.top + this.baseY;
  1221. this.colliders_data = this.collision_api.get_closest_colliders(ui.position);
  1222. this.on_overlapped_column_change(
  1223. this.on_start_overlapping_column,
  1224. this.on_stop_overlapping_column
  1225. );
  1226. this.on_overlapped_row_change(
  1227. this.on_start_overlapping_row,
  1228. this.on_stop_overlapping_row
  1229. );
  1230. this.$player.addClass('player-revert').removeClass('player')
  1231. .attr({
  1232. 'data-col': this.placeholder_grid_data.col,
  1233. 'data-row': this.placeholder_grid_data.row
  1234. }).css({
  1235. 'left': '',
  1236. 'top': ''
  1237. });
  1238. this.$changed = this.$changed.add(this.$player);
  1239. this.cells_occupied_by_player = this.get_cells_occupied(
  1240. this.placeholder_grid_data);
  1241. this.set_cells_player_occupies(
  1242. this.placeholder_grid_data.col, this.placeholder_grid_data.row);
  1243. this.$player.coords().grid.row = this.placeholder_grid_data.row;
  1244. this.$player.coords().grid.col = this.placeholder_grid_data.col;
  1245. if (this.options.draggable.stop) {
  1246. this.options.draggable.stop.call(this, event, ui);
  1247. }
  1248. this.$preview_holder.remove();
  1249. this.$player = null;
  1250. this.$helper = null;
  1251. this.placeholder_grid_data = {};
  1252. this.player_grid_data = {};
  1253. this.cells_occupied_by_placeholder = {};
  1254. this.cells_occupied_by_player = {};
  1255. this.set_dom_grid_height();
  1256. };
  1257. /**
  1258. * Executes the callbacks passed as arguments when a column begins to be
  1259. * overlapped or stops being overlapped.
  1260. *
  1261. * @param {Function} start_callback Function executed when a new column
  1262. * begins to be overlapped. The column is passed as first argument.
  1263. * @param {Function} stop_callback Function executed when a column stops
  1264. * being overlapped. The column is passed as first argument.
  1265. * @method on_overlapped_column_change
  1266. * @return {Class} Returns the instance of the Gridster Class.
  1267. */
  1268. fn.on_overlapped_column_change = function(start_callback, stop_callback) {
  1269. if (!this.colliders_data.length) {
  1270. return this;
  1271. }
  1272. var cols = this.get_targeted_columns(
  1273. this.colliders_data[0].el.data.col);
  1274. var last_n_cols = this.last_cols.length;
  1275. var n_cols = cols.length;
  1276. var i;
  1277. for (i = 0; i < n_cols; i++) {
  1278. if ($.inArray(cols[i], this.last_cols) === -1) {
  1279. (start_callback || $.noop).call(this, cols[i]);
  1280. }
  1281. }
  1282. for (i = 0; i< last_n_cols; i++) {
  1283. if ($.inArray(this.last_cols[i], cols) === -1) {
  1284. (stop_callback || $.noop).call(this, this.last_cols[i]);
  1285. }
  1286. }
  1287. this.last_cols = cols;
  1288. return this;
  1289. };
  1290. /**
  1291. * Executes the callbacks passed as arguments when a row starts to be
  1292. * overlapped or stops being overlapped.
  1293. *
  1294. * @param {Function} start_callback Function executed when a new row begins
  1295. * to be overlapped. The row is passed as first argument.
  1296. * @param {Function} end_callback Function executed when a row stops being
  1297. * overlapped. The row is passed as first argument.
  1298. * @method on_overlapped_row_change
  1299. * @return {Class} Returns the instance of the Gridster Class.
  1300. */
  1301. fn.on_overlapped_row_change = function(start_callback, end_callback) {
  1302. if (!this.colliders_data.length) {
  1303. return this;
  1304. }
  1305. var rows = this.get_targeted_rows(this.colliders_data[0].el.data.row);
  1306. var last_n_rows = this.last_rows.length;
  1307. var n_rows = rows.length;
  1308. var i;
  1309. for (i = 0; i < n_rows; i++) {
  1310. if ($.inArray(rows[i], this.last_rows) === -1) {
  1311. (start_callback || $.noop).call(this, rows[i]);
  1312. }
  1313. }
  1314. for (i = 0; i < last_n_rows; i++) {
  1315. if ($.inArray(this.last_rows[i], rows) === -1) {