├── .bowerrc ├── .gitignore ├── LICENSE ├── package.json ├── test ├── jquery.gridder_test.js └── jquery.gridder.html ├── README.md ├── src ├── utils.js ├── jquery.coords.js ├── jquery.gridster.css ├── jquery.gridster.extras.js ├── jquery.collision.js ├── jquery.draggable.js └── jquery.gridster.js ├── CONTRIBUTING.md ├── Gruntfile.js └── CHANGELOG.md /.bowerrc: -------------------------------------------------------------------------------- 1 | { 2 | "directory": "libs", 3 | "json": "package.json" 4 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | dist/ 3 | libs/ 4 | gh-pages/ 5 | demo/ 6 | .idea 7 | .DS_Store 8 | .idea 9 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2012 Ducksboard 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "gridster", 3 | "title": "gridster.js", 4 | "description": "a drag-and-drop multi-column jQuery grid plugin", 5 | "version": "0.5.6", 6 | "homepage": "http://gridster.net/", 7 | "author": { 8 | "name": "ducksboard", 9 | "email": "hackers@ducksboard.com" 10 | }, 11 | "repository": { 12 | "type": "git", 13 | "url": "git://github.com/ducksboard/gridster.js.git" 14 | }, 15 | "bugs": { 16 | "url": "https://github.com/ducksboard/gridster.js/issues" 17 | }, 18 | "licenses": [ 19 | { 20 | "type": "MIT", 21 | "url": "https://github.com/ducksboard/gridster.js/blob/master/LICENSE" 22 | } 23 | ], 24 | "keywords": [], 25 | "dependencies": { 26 | "jquery": "git+https://github.com/jquery/jquery.git#2.0.3" 27 | }, 28 | "devDependencies": { 29 | "grunt": "~0.4.1", 30 | "grunt-contrib-uglify": "~0.2.0", 31 | "grunt-contrib-jshint": "~0.3.0", 32 | "grunt-contrib-concat": "~0.1.3", 33 | "grunt-contrib-watch": "~0.3.1", 34 | "grunt-contrib-cssmin": "~0.5.0", 35 | "grunt-contrib-yuidoc": "~0.4.0", 36 | "bower": "~0.9.2", 37 | "qunitjs": "~1.11.0", 38 | "grunt-bump": "0.0.11", 39 | "grunt-conventional-changelog": "~1.0.0" 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /test/jquery.gridder_test.js: -------------------------------------------------------------------------------- 1 | /*global QUnit:false, module:false, test:false, asyncTest:false, expect:false*/ 2 | /*global start:false, stop:false ok:false, equal:false, notEqual:false, deepEqual:false*/ 3 | /*global notDeepEqual:false, strictEqual:false, notStrictEqual:false, raises:false*/ 4 | (function($) { 5 | 6 | /* 7 | ======== A Handy Little QUnit Reference ======== 8 | http://docs.jquery.com/QUnit 9 | 10 | Test methods: 11 | expect(numAssertions) 12 | stop(increment) 13 | start(decrement) 14 | Test assertions: 15 | ok(value, [message]) 16 | equal(actual, expected, [message]) 17 | notEqual(actual, expected, [message]) 18 | deepEqual(actual, expected, [message]) 19 | notDeepEqual(actual, expected, [message]) 20 | strictEqual(actual, expected, [message]) 21 | notStrictEqual(actual, expected, [message]) 22 | raises(block, [expected], [message]) 23 | */ 24 | 25 | module('jQuery#gridster', { 26 | setup: function() { 27 | 28 | this.el = $('#qunit-fixture').find(".wrapper ul"); 29 | 30 | } 31 | }); 32 | 33 | // test('is chainable', 1, function() { 34 | // // Not a bad test to run on collection methods. 35 | // strictEqual(this.el, this.el.gridster(), 'should be chaninable'); 36 | // }); 37 | 38 | }(jQuery)); 39 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Gridster.js 2 | =========== 3 | 4 | [![Bitdeli Badge](https://d2weczhvl823v0.cloudfront.net/ducksboard/gridster.js/trend.png)](https://bitdeli.com/free "Bitdeli Badge") 5 | 6 | Gridster is a jQuery plugin that makes building intuitive draggable 7 | layouts from elements spanning multiple columns. You can even 8 | dynamically add and remove elements from the grid. 9 | 10 | More at [http://gridster.net/](http://gridster.net/). 11 | 12 | [Releases](https://github.com/ducksboard/gridster.js/releases) 13 | 14 | [CHANGELOG](https://github.com/ducksboard/gridster.js/blob/master/CHANGELOG.md) 15 | 16 | Gridster is maintained by Ducksboard occasionally but not actively. 17 | @dustmoo and @pushmatrix have also write permissions as Gridster maintainers 18 | they are. Thank you guys! 19 | 20 | ## Forks 21 | 22 | Mr @dustmoo (maintainer of Gridster) has his own fork of gridster.js 23 | with some new interesting features like widget-swapping and static widgets. 24 | 25 | Can be found here: [dustmoo/gridster.js](https://github.com/dustmoo/gridster.js) 26 | 27 | @dustmoo is working in his spare time to merge all these changes into 28 | ducksboard/gridster.js 29 | 30 | If anyone would like to help @dustmoo improve his fork and reconcile 31 | it with the main library he would be happy for the help. 32 | 33 | 34 | ## Contributing to this project 35 | 36 | Anyone and everyone is welcome to contribute. Please take a moment to review the guidelines for contributing. 37 | 38 | * [Bug reports](CONTRIBUTING.md#bugs) 39 | * [Feature requests](CONTRIBUTING.md#features) 40 | * [Pull requests](CONTRIBUTING.md#pull-requests) 41 | 42 | 43 | ## License 44 | 45 | Distributed under the MIT license. 46 | 47 | ## Whodunit 48 | 49 | Gridster is built by [Ducksboard](http://ducksboard.com/) with the help of all 50 | these [wonderful people](https://github.com/ducksboard/gridster.js/graphs/contributors). 51 | -------------------------------------------------------------------------------- /test/jquery.gridder.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | gridster.js Test Suite 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 |

gridster.js Test Suite

29 |

30 |
31 |

32 |
    33 |
    34 | 35 |
    36 | 62 |
    63 |
    64 | 65 | 66 | 67 | 68 | -------------------------------------------------------------------------------- /src/utils.js: -------------------------------------------------------------------------------- 1 | ;(function(window, undefined) { 2 | 3 | /* Delay, debounce and throttle functions taken from underscore.js 4 | * 5 | * Copyright (c) 2009-2013 Jeremy Ashkenas, DocumentCloud and 6 | * Investigative Reporters & Editors 7 | * 8 | * Permission is hereby granted, free of charge, to any person 9 | * obtaining a copy of this software and associated documentation 10 | * files (the "Software"), to deal in the Software without 11 | * restriction, including without limitation the rights to use, 12 | * copy, modify, merge, publish, distribute, sublicense, and/or sell 13 | * copies of the Software, and to permit persons to whom the 14 | * Software is furnished to do so, subject to the following 15 | * conditions: 16 | * 17 | * The above copyright notice and this permission notice shall be 18 | * included in all copies or substantial portions of the Software. 19 | * 20 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 21 | * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 22 | * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 23 | * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 24 | * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 25 | * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 26 | * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 27 | * OTHER DEALINGS IN THE SOFTWARE. 28 | */ 29 | 30 | window.delay = function(func, wait) { 31 | var args = Array.prototype.slice.call(arguments, 2); 32 | return setTimeout(function(){ return func.apply(null, args); }, wait); 33 | }; 34 | 35 | window.debounce = function(func, wait, immediate) { 36 | var timeout; 37 | return function() { 38 | var context = this, args = arguments; 39 | var later = function() { 40 | timeout = null; 41 | if (!immediate) func.apply(context, args); 42 | }; 43 | if (immediate && !timeout) func.apply(context, args); 44 | clearTimeout(timeout); 45 | timeout = setTimeout(later, wait); 46 | }; 47 | }; 48 | 49 | window.throttle = function(func, wait) { 50 | var context, args, timeout, throttling, more, result; 51 | var whenDone = debounce( 52 | function(){ more = throttling = false; }, wait); 53 | return function() { 54 | context = this; args = arguments; 55 | var later = function() { 56 | timeout = null; 57 | if (more) func.apply(context, args); 58 | whenDone(); 59 | }; 60 | if (!timeout) timeout = setTimeout(later, wait); 61 | if (throttling) { 62 | more = true; 63 | } else { 64 | result = func.apply(context, args); 65 | } 66 | whenDone(); 67 | throttling = true; 68 | return result; 69 | }; 70 | }; 71 | 72 | })(window); 73 | -------------------------------------------------------------------------------- /src/jquery.coords.js: -------------------------------------------------------------------------------- 1 | /* 2 | * jquery.coords 3 | * https://github.com/ducksboard/gridster.js 4 | * 5 | * Copyright (c) 2012 ducksboard 6 | * Licensed under the MIT licenses. 7 | */ 8 | 9 | ;(function(root, factory) { 10 | 11 | if (typeof define === 'function' && define.amd) { 12 | define('gridster-coords', ['jquery'], factory); 13 | } else { 14 | root.GridsterCoords = factory(root.$ || root.jQuery); 15 | } 16 | 17 | }(this, function($) { 18 | /** 19 | * Creates objects with coordinates (x1, y1, x2, y2, cx, cy, width, height) 20 | * to simulate DOM elements on the screen. 21 | * Coords is used by Gridster to create a faux grid with any DOM element can 22 | * collide. 23 | * 24 | * @class Coords 25 | * @param {HTMLElement|Object} obj The jQuery HTMLElement or a object with: left, 26 | * top, width and height properties. 27 | * @return {Object} Coords instance. 28 | * @constructor 29 | */ 30 | function Coords(obj) { 31 | if (obj[0] && $.isPlainObject(obj[0])) { 32 | this.data = obj[0]; 33 | }else { 34 | this.el = obj; 35 | } 36 | 37 | this.isCoords = true; 38 | this.coords = {}; 39 | this.init(); 40 | return this; 41 | } 42 | 43 | 44 | var fn = Coords.prototype; 45 | 46 | 47 | fn.init = function(){ 48 | this.set(); 49 | this.original_coords = this.get(); 50 | }; 51 | 52 | 53 | fn.set = function(update, not_update_offsets) { 54 | var el = this.el; 55 | 56 | if (el && !update) { 57 | this.data = el.offset(); 58 | this.data.width = el.width(); 59 | this.data.height = el.height(); 60 | } 61 | 62 | if (el && update && !not_update_offsets) { 63 | var offset = el.offset(); 64 | this.data.top = offset.top; 65 | this.data.left = offset.left; 66 | } 67 | 68 | var d = this.data; 69 | 70 | typeof d.left === 'undefined' && (d.left = d.x1); 71 | typeof d.top === 'undefined' && (d.top = d.y1); 72 | 73 | this.coords.x1 = d.left; 74 | this.coords.y1 = d.top; 75 | this.coords.x2 = d.left + d.width; 76 | this.coords.y2 = d.top + d.height; 77 | this.coords.cx = d.left + (d.width / 2); 78 | this.coords.cy = d.top + (d.height / 2); 79 | this.coords.width = d.width; 80 | this.coords.height = d.height; 81 | this.coords.el = el || false ; 82 | 83 | return this; 84 | }; 85 | 86 | 87 | fn.update = function(data){ 88 | if (!data && !this.el) { 89 | return this; 90 | } 91 | 92 | if (data) { 93 | var new_data = $.extend({}, this.data, data); 94 | this.data = new_data; 95 | return this.set(true, true); 96 | } 97 | 98 | this.set(true); 99 | return this; 100 | }; 101 | 102 | 103 | fn.get = function(){ 104 | return this.coords; 105 | }; 106 | 107 | fn.destroy = function() { 108 | this.el.removeData('coords'); 109 | delete this.el; 110 | }; 111 | 112 | //jQuery adapter 113 | $.fn.coords = function() { 114 | if (this.data('coords') ) { 115 | return this.data('coords'); 116 | } 117 | 118 | var ins = new Coords(this, arguments[0]); 119 | this.data('coords', ins); 120 | return ins; 121 | }; 122 | 123 | return Coords; 124 | 125 | })); 126 | -------------------------------------------------------------------------------- /src/jquery.gridster.css: -------------------------------------------------------------------------------- 1 | .gridster { 2 | position:relative; 3 | } 4 | 5 | .gridster > * { 6 | margin: 0 auto; 7 | -webkit-transition: height .4s, width .4s; 8 | -moz-transition: height .4s, width .4s; 9 | -o-transition: height .4s, width .4s; 10 | -ms-transition: height .4s, width .4s; 11 | transition: height .4s, width .4s; 12 | } 13 | 14 | .gridster .gs-w { 15 | z-index: 2; 16 | position: absolute; 17 | } 18 | 19 | .ready .gs-w:not(.preview-holder) { 20 | -webkit-transition: opacity .3s, left .3s, top .3s; 21 | -moz-transition: opacity .3s, left .3s, top .3s; 22 | -o-transition: opacity .3s, left .3s, top .3s; 23 | transition: opacity .3s, left .3s, top .3s; 24 | } 25 | 26 | .ready .gs-w:not(.preview-holder), 27 | .ready .resize-preview-holder { 28 | -webkit-transition: opacity .3s, left .3s, top .3s, width .3s, height .3s; 29 | -moz-transition: opacity .3s, left .3s, top .3s, width .3s, height .3s; 30 | -o-transition: opacity .3s, left .3s, top .3s, width .3s, height .3s; 31 | transition: opacity .3s, left .3s, top .3s, width .3s, height .3s; 32 | } 33 | 34 | .gridster .preview-holder { 35 | z-index: 1; 36 | position: absolute; 37 | background-color: #fff; 38 | border-color: #fff; 39 | opacity: 0.3; 40 | } 41 | 42 | .gridster .player-revert { 43 | z-index: 10!important; 44 | -webkit-transition: left .3s, top .3s!important; 45 | -moz-transition: left .3s, top .3s!important; 46 | -o-transition: left .3s, top .3s!important; 47 | transition: left .3s, top .3s!important; 48 | } 49 | 50 | .gridster .dragging, 51 | .gridster .resizing { 52 | z-index: 10!important; 53 | -webkit-transition: all 0s !important; 54 | -moz-transition: all 0s !important; 55 | -o-transition: all 0s !important; 56 | transition: all 0s !important; 57 | } 58 | 59 | 60 | .gs-resize-handle { 61 | position: absolute; 62 | z-index: 1; 63 | } 64 | 65 | .gs-resize-handle-both { 66 | width: 20px; 67 | height: 20px; 68 | bottom: -8px; 69 | right: -8px; 70 | background-image: url('data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBzdGFuZGFsb25lPSJubyI/Pg08IS0tIEdlbmVyYXRvcjogQWRvYmUgRmlyZXdvcmtzIENTNiwgRXhwb3J0IFNWRyBFeHRlbnNpb24gYnkgQWFyb24gQmVhbGwgKGh0dHA6Ly9maXJld29ya3MuYWJlYWxsLmNvbSkgLiBWZXJzaW9uOiAwLjYuMSAgLS0+DTwhRE9DVFlQRSBzdmcgUFVCTElDICItLy9XM0MvL0RURCBTVkcgMS4xLy9FTiIgImh0dHA6Ly93d3cudzMub3JnL0dyYXBoaWNzL1NWRy8xLjEvRFREL3N2ZzExLmR0ZCI+DTxzdmcgaWQ9IlVudGl0bGVkLVBhZ2UlMjAxIiB2aWV3Qm94PSIwIDAgNiA2IiBzdHlsZT0iYmFja2dyb3VuZC1jb2xvcjojZmZmZmZmMDAiIHZlcnNpb249IjEuMSINCXhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIHhtbDpzcGFjZT0icHJlc2VydmUiDQl4PSIwcHgiIHk9IjBweCIgd2lkdGg9IjZweCIgaGVpZ2h0PSI2cHgiDT4NCTxnIG9wYWNpdHk9IjAuMzAyIj4NCQk8cGF0aCBkPSJNIDYgNiBMIDAgNiBMIDAgNC4yIEwgNCA0LjIgTCA0LjIgNC4yIEwgNC4yIDAgTCA2IDAgTCA2IDYgTCA2IDYgWiIgZmlsbD0iIzAwMDAwMCIvPg0JPC9nPg08L3N2Zz4='); 71 | background-position: top left; 72 | background-repeat: no-repeat; 73 | cursor: se-resize; 74 | z-index: 20; 75 | } 76 | 77 | .gs-resize-handle-x { 78 | top: 0; 79 | bottom: 13px; 80 | right: -5px; 81 | width: 10px; 82 | cursor: e-resize; 83 | } 84 | 85 | .gs-resize-handle-y { 86 | left: 0; 87 | right: 13px; 88 | bottom: -5px; 89 | height: 10px; 90 | cursor: s-resize; 91 | } 92 | 93 | .gs-w:hover .gs-resize-handle, 94 | .resizing .gs-resize-handle { 95 | opacity: 1; 96 | } 97 | 98 | .gs-resize-handle, 99 | .gs-w.dragging .gs-resize-handle { 100 | opacity: 0; 101 | } 102 | 103 | .gs-resize-disabled .gs-resize-handle { 104 | display: none!important; 105 | } 106 | 107 | [data-max-sizex="1"] .gs-resize-handle-x, 108 | [data-max-sizey="1"] .gs-resize-handle-y, 109 | [data-max-sizey="1"][data-max-sizex="1"] .gs-resize-handle { 110 | display: none !important; 111 | } 112 | 113 | /* Uncomment this if you set helper : "clone" in draggable options */ 114 | /*.gridster .player { 115 | opacity:0; 116 | } 117 | */ -------------------------------------------------------------------------------- /src/jquery.gridster.extras.js: -------------------------------------------------------------------------------- 1 | ;(function(root, factory) { 2 | 3 | if (typeof define === 'function' && define.amd) { 4 | define(['jquery', 'gridster'], factory); 5 | } else { 6 | root.Gridster = factory(root.$ || root.jQuery, root.Gridster); 7 | } 8 | 9 | }(this, function($, Gridster) { 10 | 11 | var fn = Gridster.prototype; 12 | 13 | fn.widgets_in_col = function(col) { 14 | if (!this.gridmap[col]) { 15 | return false; 16 | } 17 | 18 | for (var i = this.gridmap[col].length - 1; i >= 0; i--) { 19 | if (this.is_widget(col, i) !== false) { 20 | return true; 21 | } 22 | } 23 | 24 | return false; 25 | }; 26 | 27 | fn.widgets_in_row = function(row) { 28 | for (var i = this.gridmap.length; i >= 1; i--) { 29 | if (this.is_widget(i, row) !== false) { 30 | return true; 31 | } 32 | } 33 | 34 | return false; 35 | }; 36 | 37 | 38 | fn.widgets_in_range = function(col1, row1, col2, row2) { 39 | var valid_cols = []; 40 | var valid_rows = []; 41 | var $widgets = $([]); 42 | var c, r, $w, wgd; 43 | 44 | for (c = col2; c >= col1; c--) { 45 | for (r = row2; r >= row1; r--) { 46 | $w = this.is_widget(c, r); 47 | 48 | if ($w !== false) { 49 | wgd = $w.data('coords').grid; 50 | if (wgd.col >= col1 && wgd.col <= col2 && 51 | wgd.row >= row1 && wgd.row <= row2 52 | ) { 53 | $widgets = $widgets.add($w); 54 | } 55 | } 56 | } 57 | } 58 | 59 | return $widgets; 60 | }; 61 | 62 | 63 | fn.get_bottom_most_occupied_cell = function() { 64 | var row = 0; 65 | var col = 0; 66 | this.for_each_cell(function($el, c, r) { 67 | if ($el && r > row) { 68 | row = r; 69 | col = c; 70 | } 71 | }); 72 | 73 | return {col: col, row: row}; 74 | }; 75 | 76 | 77 | fn.get_right_most_occupied_cell = function() { 78 | var row = 0; 79 | var col = 0; 80 | this.for_each_cell(function($el, c, r) { 81 | if ($el) { 82 | row = r; 83 | col = c; 84 | return false; 85 | } 86 | }); 87 | 88 | return {col: col, row: row}; 89 | }; 90 | 91 | 92 | fn.for_each_cell = function(callback, gridmap) { 93 | gridmap || (gridmap = this.gridmap); 94 | var cols = gridmap.length; 95 | var rows = gridmap[1].length; 96 | 97 | cols_iter: 98 | for (var c = cols - 1; c >= 1; c--) { 99 | for (var r = rows - 1; r >= 1; r--) { 100 | var $el = gridmap[c] && gridmap[c][r]; 101 | if (callback) { 102 | if (callback.call(this, $el, c, r) === false) { 103 | break cols_iter; 104 | } else { continue; } 105 | } 106 | } 107 | } 108 | }; 109 | 110 | 111 | fn.next_position_in_range = function(size_x, size_y, max_rows) { 112 | size_x || (size_x = 1); 113 | size_y || (size_y = 1); 114 | var ga = this.gridmap; 115 | var cols_l = ga.length; 116 | var valid_pos = []; 117 | var rows_l; 118 | 119 | for (var c = 1; c < cols_l; c++) { 120 | rows_l = max_rows || ga[c].length; 121 | for (var r = 1; r <= rows_l; r++) { 122 | var can_move_to = this.can_move_to({ 123 | size_x: size_x, 124 | size_y: size_y 125 | }, c, r, max_rows); 126 | 127 | if (can_move_to) { 128 | valid_pos.push({ 129 | col: c, 130 | row: r, 131 | size_y: size_y, 132 | size_x: size_x 133 | }); 134 | } 135 | } 136 | } 137 | 138 | if (valid_pos.length >= 1) { 139 | return Gridster.sort_by_col_asc(valid_pos)[0]; 140 | } 141 | 142 | return false; 143 | }; 144 | 145 | 146 | fn.closest_to_right = function(col, row) { 147 | if (!this.gridmap[col]) { return false; } 148 | var cols_l = this.gridmap.length - 1; 149 | 150 | for (var c = col; c <= cols_l; c++) { 151 | if (this.gridmap[c][row]) { 152 | return { col: c, row: row }; 153 | } 154 | } 155 | 156 | return false; 157 | }; 158 | 159 | 160 | fn.closest_to_left = function(col, row) { 161 | var cols_l = this.gridmap.length - 1; 162 | if (!this.gridmap[col]) { return false; } 163 | 164 | for (var c = col; c >= 1; c--) { 165 | if (this.gridmap[c][row]) { 166 | return { col: c, row: row }; 167 | } 168 | } 169 | 170 | return false; 171 | }; 172 | 173 | return Gridster; 174 | 175 | })); 176 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to this project 2 | 3 | Please take a moment to review this document in order to make the contribution 4 | process easy and effective for everyone involved. 5 | 6 | Following these guidelines helps to communicate that you respect the time of 7 | the developers managing and developing this open source project. In return, 8 | they should reciprocate that respect in addressing your issue or assessing 9 | patches and features. 10 | 11 | 12 | ## Using the issue tracker 13 | 14 | The issue tracker is the preferred channel for [bug reports](#bugs), 15 | [features requests](#features) and [submitting pull 16 | requests](#pull-requests), but please respect the following restrictions: 17 | 18 | * Please **do not** use the issue tracker for personal support requests (use 19 | [Stack Overflow](http://stackoverflow.com)). 20 | 21 | * Please **do not** derail or troll issues. Keep the discussion on topic and 22 | respect the opinions of others. 23 | 24 | 25 | 26 | ## Bug reports 27 | 28 | A bug is a _demonstrable problem_ that is caused by the code in the repository. 29 | Good bug reports are extremely helpful - thank you! 30 | 31 | Guidelines for bug reports: 32 | 33 | 1. **Use the GitHub issue search** — check if the issue has already been 34 | reported. 35 | 36 | 2. **Check if the issue has been fixed** — try to reproduce it using the 37 | latest `master` or development branch in the repository. 38 | 39 | 3. **Isolate the problem** — ideally create a [reduced test 40 | case](http://css-tricks.com/6263-reduced-test-cases/) and a live example (you can use something like [jsfiddle](http://jsfiddle.net/) or [jsbin](http://jsbin.com/)) . 41 | 42 | A good bug report shouldn't leave others needing to chase you up for more 43 | information. Please try to be as detailed as possible in your report. What is 44 | your environment? What steps will reproduce the issue? What browser(s) and OS 45 | experience the problem? What would you expect to be the outcome? All these 46 | details will help people to fix any potential bugs. 47 | 48 | Example: 49 | 50 | > Short and descriptive example bug report title 51 | > 52 | > A summary of the issue and the browser/OS environment in which it occurs. If 53 | > suitable, include the steps required to reproduce the bug. 54 | > 55 | > 1. This is the first step 56 | > 2. This is the second step 57 | > 3. Further steps, etc. 58 | > 59 | > `` - a link to the reduced test case 60 | > 61 | > Any other information you want to share that is relevant to the issue being 62 | > reported. This might include the lines of code that you have identified as 63 | > causing the bug, and potential solutions (and your opinions on their 64 | > merits). 65 | 66 | 67 | 68 | ## Feature requests 69 | 70 | Feature requests are welcome. But take a moment to find out whether your idea 71 | fits with the scope and aims of the project. It's up to *you* to make a strong 72 | case to convince the project's developers of the merits of this feature. Please 73 | provide as much detail and context as possible. 74 | 75 | **Please, use the GitHub issue search** to check if the feature has already been requested. 76 | 77 | 78 | 79 | ## Pull requests 80 | 81 | Good pull requests - patches, improvements, new features - are a fantastic 82 | help. They should remain focused in scope and avoid containing unrelated 83 | commits. 84 | 85 | **Please ask first** before embarking on any significant pull request (e.g. 86 | implementing features, refactoring code, porting to a different language), 87 | otherwise you risk spending a lot of time working on something that the 88 | project's developers might not want to merge into the project. 89 | 90 | Code must follow, mostly, these [coding conventions](http://javascript.crockford.com/code.html) . 91 | 92 | Adhering to the following this process is the best way to get your work 93 | included in the project: 94 | 95 | 1. [Fork](http://help.github.com/fork-a-repo/) the project, clone your fork, 96 | and configure the remotes: 97 | 98 | ```bash 99 | # Clone your fork of the repo into the current directory 100 | git clone https://github.com//gridster.js 101 | # Navigate to the newly cloned directory 102 | cd gridster.js 103 | # Assign the original repo to a remote called "upstream" 104 | git remote add upstream https://github.com/ducksboard/gridster.js 105 | ``` 106 | 107 | 2. If you cloned a while ago, get the latest changes from upstream: 108 | 109 | ```bash 110 | git checkout master 111 | git pull upstream master 112 | ``` 113 | 114 | 3. Create a new topic branch (off the main project development branch) to 115 | contain your feature, change, or fix: 116 | 117 | ```bash 118 | git checkout -b 119 | ``` 120 | 121 | 4. Commit your changes in logical chunks. Please adhere to these [git commit 122 | message guidelines](https://docs.google.com/document/d/1QrDFcIiPjSLDn3EL15IJygNPiHORgU1_OOAqWjiDU5Y) 123 | or your code is unlikely be merged into the main project. Use Git's 124 | [interactive rebase](https://help.github.com/articles/interactive-rebase) 125 | feature to tidy up your commits before making them public. 126 | 127 | 5. Merge or rebase the upstream development branch into your topic branch: 128 | 129 | ```bash 130 | git pull --rebase upstream master 131 | ``` 132 | 133 | 6. Push your topic branch up to your fork: 134 | 135 | ```bash 136 | git push origin 137 | ``` 138 | 139 | 7. [Open a Pull Request](https://help.github.com/articles/using-pull-requests/) 140 | with a clear title and description. 141 | 142 | **IMPORTANT**: By submitting a patch, you agree to allow the project owner to 143 | license your work under the same license as that used by the project. 144 | -------------------------------------------------------------------------------- /Gruntfile.js: -------------------------------------------------------------------------------- 1 | /*global module:false*/ 2 | module.exports = function(grunt) { 3 | 4 | // Project configuration. 5 | grunt.initConfig({ 6 | pkg: grunt.file.readJSON('package.json'), 7 | meta: { 8 | banner: '/*! <%= pkg.title || pkg.name %> - v<%= pkg.version %> - ' + 9 | '<%= grunt.template.today("yyyy-mm-dd") %>\n' + 10 | '<%= pkg.homepage ? "* " + pkg.homepage : "" %>\n' + 11 | '* Copyright (c) <%= grunt.template.today("yyyy") %> <%= pkg.author.name %>;' + 12 | ' Licensed <%= _.pluck(pkg.licenses, "type").join(", ") %> */\n\n', 13 | 14 | minibanner: '/*! <%= pkg.title || pkg.name %> - v<%= pkg.version %> - ' + 15 | '<%= grunt.template.today("yyyy-mm-dd") %> - ' + 16 | '<%= pkg.homepage ? "* " + pkg.homepage + " - " : "" %>' + 17 | 'Copyright (c) <%= grunt.template.today("yyyy") %> <%= pkg.author.name %>;' + 18 | ' Licensed <%= _.pluck(pkg.licenses, "type").join(", ") %> */ ' 19 | }, 20 | concat: { 21 | options: { 22 | stripBanners: true, 23 | banner: '<%= meta.banner %>' 24 | }, 25 | dist_js: { 26 | src: ['src/jquery.coords.js', 'src/jquery.collision.js', 'src/utils.js', 'src/jquery.draggable.js', 'src/jquery.<%= pkg.name %>.js'], 27 | dest: 'dist/jquery.<%= pkg.name %>.js' 28 | }, 29 | 30 | dist_extras_js: { 31 | src: ['src/jquery.coords.js', 'src/jquery.collision.js', 'src/utils.js', 'src/jquery.draggable.js', 'src/jquery.<%= pkg.name %>.js', 'src/jquery.<%= pkg.name %>.extras.js'], 32 | dest: 'dist/jquery.<%= pkg.name %>.with-extras.js' 33 | }, 34 | 35 | dist_css: { 36 | src: ['src/jquery.<%= pkg.name %>.css'], 37 | dest: 'dist/jquery.<%= pkg.name %>.css' 38 | }, 39 | 40 | dist_demo_js: { 41 | src: ['src/jquery.coords.js', 'src/jquery.collision.js', 'src/utils.js', 'src/jquery.draggable.js', 'src/jquery.<%= pkg.name %>.js'], 42 | dest: 'gh-pages/dist/jquery.<%= pkg.name %>.js' 43 | }, 44 | 45 | dist_extras_demo_js: { 46 | src: ['src/jquery.coords.js', 'src/jquery.collision.js', 'src/utils.js', 'src/jquery.draggable.js', 'src/jquery.<%= pkg.name %>.js', 'src/jquery.<%= pkg.name %>.extras.js'], 47 | dest: 'gh-pages/dist/jquery.<%= pkg.name %>.with-extras.js' 48 | }, 49 | 50 | dist_demo_css: { 51 | src: ['src/jquery.<%= pkg.name %>.css'], 52 | dest: 'gh-pages/dist/jquery.<%= pkg.name %>.css' 53 | } 54 | }, 55 | uglify: { 56 | options: { 57 | banner: '<%= meta.minibanner %>' 58 | }, 59 | dist: { 60 | files: { 61 | 'dist/jquery.<%= pkg.name %>.min.js': ['<%= concat.dist_js.dest %>'] 62 | } 63 | }, 64 | 65 | dist_extras: { 66 | files: { 67 | 'dist/jquery.<%= pkg.name %>.with-extras.min.js': ['<%= concat.dist_extras_js.dest %>'] 68 | } 69 | }, 70 | 71 | dist_demo: { 72 | files: { 73 | 'gh-pages/dist/jquery.<%= pkg.name %>.min.js': ['<%= concat.dist_js.dest %>'], 74 | } 75 | }, 76 | 77 | dist_extras_demo: { 78 | files: { 79 | 'gh-pages/dist/jquery.<%= pkg.name %>.with-extras.min.js': ['<%= concat.dist_extras_js.dest %>'] 80 | } 81 | } 82 | }, 83 | cssmin: { 84 | compress: { 85 | options: { 86 | keepSpecialComments: 0, 87 | banner: '<%= meta.minibanner %>' 88 | }, 89 | files: { 90 | "dist/jquery.<%= pkg.name %>.min.css": ["dist/jquery.<%= pkg.name %>.css"], 91 | "gh-pages/dist/jquery.<%= pkg.name %>.min.css": ["dist/jquery.<%= pkg.name %>.css"] 92 | } 93 | } 94 | }, 95 | jshint: { 96 | files: ['grunt.js', 'src/**/*.js', 'test/**/*.js'] 97 | }, 98 | watch: { 99 | files: ['<%= lint.files %>', 'src/jquery.<%= pkg.name %>.css'], 100 | tasks: 'min concat' 101 | }, 102 | jshint: { 103 | options: { 104 | curly: true, 105 | eqeqeq: true, 106 | immed: true, 107 | latedef: true, 108 | newcap: true, 109 | noarg: true, 110 | sub: true, 111 | undef: true, 112 | boss: true, 113 | eqnull: true, 114 | browser: true 115 | }, 116 | globals: { 117 | jQuery: true 118 | } 119 | }, 120 | yuidoc: { 121 | compile: { 122 | "name": 'gridster.js', 123 | "description": 'gridster.js, a drag-and-drop multi-column jQuery grid plugin', 124 | "version": '0.1.0', 125 | "url": 'http://gridster.net/', 126 | "logo": 'https://ducksboard.com/static/images/svg/logo-ducksboard-black-small.svg', 127 | options: { 128 | paths: "src/", 129 | outdir: "gh-pages/docs/" 130 | } 131 | } 132 | }, 133 | 134 | bump: { 135 | options: { 136 | files: ['package.json'], 137 | updateConfigs: ['pkg'], 138 | commit: true, 139 | commitMessage: 'Release v%VERSION%', 140 | commitFiles: ['package.json', 'CHANGELOG.md', 'dist/'], // '-a' for all files 141 | createTag: true, 142 | tagName: 'v%VERSION%', 143 | tagMessage: 'Version %VERSION%', 144 | push: false, 145 | pushTo: 'origin', 146 | gitDescribeOptions: '--tags --always --abbrev=1 --dirty=-d' // options to use with '$ git describe' 147 | } 148 | }, 149 | 150 | changelog: { 151 | options: { 152 | dest: 'CHANGELOG.md' 153 | } 154 | }, 155 | 156 | watch: { 157 | files: ['libs/*.js', 'src/*.js', 'src/*.css', 'Gruntfile.js'], 158 | tasks: ['concat', 'uglify', 'cssmin'] 159 | } 160 | }); 161 | 162 | 163 | grunt.loadNpmTasks('grunt-contrib-watch'); 164 | grunt.loadNpmTasks('grunt-contrib-jshint'); 165 | grunt.loadNpmTasks('grunt-contrib-concat'); 166 | grunt.loadNpmTasks('grunt-contrib-uglify'); 167 | grunt.loadNpmTasks('grunt-contrib-cssmin'); 168 | grunt.loadNpmTasks('grunt-contrib-yuidoc'); 169 | grunt.loadNpmTasks('grunt-bump'); 170 | grunt.loadNpmTasks('grunt-conventional-changelog'); 171 | 172 | // Default task. 173 | grunt.registerTask('default', ['jshint', 'concat', 'uglify', 'cssmin']); 174 | grunt.registerTask('build', ['default']); 175 | grunt.registerTask('docs', ['yuidoc']); 176 | 177 | grunt.registerTask('release', ['build', 'bump-only:patch', 'build', 'docs', 'changelog']); 178 | grunt.registerTask('release:minor', ['build', 'bump-only:minor', 'build', 'docs', 'changelog']); 179 | grunt.registerTask('release:major', ['build', 'bump-only:major', 'build', 'docs', 'changelog']); 180 | grunt.registerTask('release:git', ['build', 'bump-only:git', 'build', 'docs', 'changelog', 'bump-commit']); 181 | grunt.registerTask('release:commit', ['bump-commit']); 182 | 183 | }; 184 | -------------------------------------------------------------------------------- /src/jquery.collision.js: -------------------------------------------------------------------------------- 1 | /* 2 | * jquery.collision 3 | * https://github.com/ducksboard/gridster.js 4 | * 5 | * Copyright (c) 2012 ducksboard 6 | * Licensed under the MIT licenses. 7 | */ 8 | 9 | ;(function(root, factory) { 10 | 11 | if (typeof define === 'function' && define.amd) { 12 | define('gridster-collision', ['jquery', 'gridster-coords'], factory); 13 | } else { 14 | root.GridsterCollision = factory(root.$ || root.jQuery, 15 | root.GridsterCoords); 16 | } 17 | 18 | }(this, function($, Coords) { 19 | 20 | var defaults = { 21 | colliders_context: document.body, 22 | overlapping_region: 'C' 23 | // ,on_overlap: function(collider_data){}, 24 | // on_overlap_start : function(collider_data){}, 25 | // on_overlap_stop : function(collider_data){} 26 | }; 27 | 28 | 29 | /** 30 | * Detects collisions between a DOM element against other DOM elements or 31 | * Coords objects. 32 | * 33 | * @class Collision 34 | * @uses Coords 35 | * @param {HTMLElement} el The jQuery wrapped HTMLElement. 36 | * @param {HTMLElement|Array} colliders Can be a jQuery collection 37 | * of HTMLElements or an Array of Coords instances. 38 | * @param {Object} [options] An Object with all options you want to 39 | * overwrite: 40 | * @param {String} [options.overlapping_region] Determines when collision 41 | * is valid, depending on the overlapped area. Values can be: 'N', 'S', 42 | * 'W', 'E', 'C' or 'all'. Default is 'C'. 43 | * @param {Function} [options.on_overlap_start] Executes a function the first 44 | * time each `collider ` is overlapped. 45 | * @param {Function} [options.on_overlap_stop] Executes a function when a 46 | * `collider` is no longer collided. 47 | * @param {Function} [options.on_overlap] Executes a function when the 48 | * mouse is moved during the collision. 49 | * @return {Object} Collision instance. 50 | * @constructor 51 | */ 52 | function Collision(el, colliders, options) { 53 | this.options = $.extend(defaults, options); 54 | this.$element = el; 55 | this.last_colliders = []; 56 | this.last_colliders_coords = []; 57 | this.set_colliders(colliders); 58 | 59 | this.init(); 60 | } 61 | 62 | Collision.defaults = defaults; 63 | 64 | var fn = Collision.prototype; 65 | 66 | 67 | fn.init = function() { 68 | this.find_collisions(); 69 | }; 70 | 71 | 72 | fn.overlaps = function(a, b) { 73 | var x = false; 74 | var y = false; 75 | 76 | if ((b.x1 >= a.x1 && b.x1 <= a.x2) || 77 | (b.x2 >= a.x1 && b.x2 <= a.x2) || 78 | (a.x1 >= b.x1 && a.x2 <= b.x2) 79 | ) { x = true; } 80 | 81 | if ((b.y1 >= a.y1 && b.y1 <= a.y2) || 82 | (b.y2 >= a.y1 && b.y2 <= a.y2) || 83 | (a.y1 >= b.y1 && a.y2 <= b.y2) 84 | ) { y = true; } 85 | 86 | return (x && y); 87 | }; 88 | 89 | 90 | fn.detect_overlapping_region = function(a, b){ 91 | var regionX = ''; 92 | var regionY = ''; 93 | 94 | if (a.y1 > b.cy && a.y1 < b.y2) { regionX = 'N'; } 95 | if (a.y2 > b.y1 && a.y2 < b.cy) { regionX = 'S'; } 96 | if (a.x1 > b.cx && a.x1 < b.x2) { regionY = 'W'; } 97 | if (a.x2 > b.x1 && a.x2 < b.cx) { regionY = 'E'; } 98 | 99 | return (regionX + regionY) || 'C'; 100 | }; 101 | 102 | 103 | fn.calculate_overlapped_area_coords = function(a, b){ 104 | var x1 = Math.max(a.x1, b.x1); 105 | var y1 = Math.max(a.y1, b.y1); 106 | var x2 = Math.min(a.x2, b.x2); 107 | var y2 = Math.min(a.y2, b.y2); 108 | 109 | return $({ 110 | left: x1, 111 | top: y1, 112 | width : (x2 - x1), 113 | height: (y2 - y1) 114 | }).coords().get(); 115 | }; 116 | 117 | 118 | fn.calculate_overlapped_area = function(coords){ 119 | return (coords.width * coords.height); 120 | }; 121 | 122 | 123 | fn.manage_colliders_start_stop = function(new_colliders_coords, start_callback, stop_callback){ 124 | var last = this.last_colliders_coords; 125 | 126 | for (var i = 0, il = last.length; i < il; i++) { 127 | if ($.inArray(last[i], new_colliders_coords) === -1) { 128 | start_callback.call(this, last[i]); 129 | } 130 | } 131 | 132 | for (var j = 0, jl = new_colliders_coords.length; j < jl; j++) { 133 | if ($.inArray(new_colliders_coords[j], last) === -1) { 134 | stop_callback.call(this, new_colliders_coords[j]); 135 | } 136 | 137 | } 138 | }; 139 | 140 | 141 | fn.find_collisions = function(player_data_coords){ 142 | var self = this; 143 | var overlapping_region = this.options.overlapping_region; 144 | var colliders_coords = []; 145 | var colliders_data = []; 146 | var $colliders = (this.colliders || this.$colliders); 147 | var count = $colliders.length; 148 | var player_coords = self.$element.coords() 149 | .update(player_data_coords || false).get(); 150 | 151 | while(count--){ 152 | var $collider = self.$colliders ? 153 | $($colliders[count]) : $colliders[count]; 154 | var $collider_coords_ins = ($collider.isCoords) ? 155 | $collider : $collider.coords(); 156 | var collider_coords = $collider_coords_ins.get(); 157 | var overlaps = self.overlaps(player_coords, collider_coords); 158 | 159 | if (!overlaps) { 160 | continue; 161 | } 162 | 163 | var region = self.detect_overlapping_region( 164 | player_coords, collider_coords); 165 | 166 | //todo: make this an option 167 | if (region === overlapping_region || overlapping_region === 'all') { 168 | 169 | var area_coords = self.calculate_overlapped_area_coords( 170 | player_coords, collider_coords); 171 | var area = self.calculate_overlapped_area(area_coords); 172 | var collider_data = { 173 | area: area, 174 | area_coords : area_coords, 175 | region: region, 176 | coords: collider_coords, 177 | player_coords: player_coords, 178 | el: $collider 179 | }; 180 | 181 | if (self.options.on_overlap) { 182 | self.options.on_overlap.call(this, collider_data); 183 | } 184 | colliders_coords.push($collider_coords_ins); 185 | colliders_data.push(collider_data); 186 | } 187 | } 188 | 189 | if (self.options.on_overlap_stop || self.options.on_overlap_start) { 190 | this.manage_colliders_start_stop(colliders_coords, 191 | self.options.on_overlap_start, self.options.on_overlap_stop); 192 | } 193 | 194 | this.last_colliders_coords = colliders_coords; 195 | 196 | return colliders_data; 197 | }; 198 | 199 | 200 | fn.get_closest_colliders = function(player_data_coords){ 201 | var colliders = this.find_collisions(player_data_coords); 202 | 203 | colliders.sort(function(a, b) { 204 | /* if colliders are being overlapped by the "C" (center) region, 205 | * we have to set a lower index in the array to which they are placed 206 | * above in the grid. */ 207 | if (a.region === 'C' && b.region === 'C') { 208 | if (a.coords.y1 < b.coords.y1 || a.coords.x1 < b.coords.x1) { 209 | return - 1; 210 | }else{ 211 | return 1; 212 | } 213 | } 214 | 215 | if (a.area < b.area) { 216 | return 1; 217 | } 218 | 219 | return 1; 220 | }); 221 | return colliders; 222 | }; 223 | 224 | 225 | fn.set_colliders = function(colliders) { 226 | if (typeof colliders === 'string' || colliders instanceof $) { 227 | this.$colliders = $(colliders, 228 | this.options.colliders_context).not(this.$element); 229 | }else{ 230 | this.colliders = $(colliders); 231 | } 232 | }; 233 | 234 | 235 | //jQuery adapter 236 | $.fn.collision = function(collider, options) { 237 | return new Collision( this, collider, options ); 238 | }; 239 | 240 | return Collision; 241 | 242 | })); 243 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | 2 | ### v0.5.6 (2014-09-25) 3 | 4 | 5 | #### Bug Fixes 6 | 7 | * **draggable:** namespace events with unique ids ([79aff38c](http://github.com/ducksboard/gridster.js/commit/79aff38c60cc6ce2c0f0160bd3c6f93cb2511642)) 8 | 9 | 10 | ### v0.5.5 (2014-07-25) 11 | 12 | 13 | #### Bug Fixes 14 | 15 | * **gridster:** fire `positionschanged` when widget orig position changes ([9926ceff](http://github.com/ducksboard/gridster.js/commit/9926ceff59cba49c71542e45aa095be35eb1df58)) 16 | 17 | 18 | ### v0.5.4 (2014-07-16) 19 | 20 | 21 | #### Bug Fixes 22 | 23 | * **gridster:** serialize returns an Array object, not a jQuery object ([93df6cf6](http://github.com/ducksboard/gridster.js/commit/93df6cf6907fd0fb8787b3d068c9a9c467dcc020), closes [#394](http://github.com/ducksboard/gridster.js/issues/394)) 24 | 25 | 26 | ### v0.5.3 (2014-07-04) 27 | 28 | 29 | #### Bug Fixes 30 | 31 | * **gridster:** 32 | * custom `ignore_dragging` overwrites the default value ([6bcfa6e1](http://github.com/ducksboard/gridster.js/commit/6bcfa6e16e4a88cbb5efff1ce29308737884a89d)) 33 | * sort widgets appropriately when reading them from DOM ([5c6d25cb](http://github.com/ducksboard/gridster.js/commit/5c6d25cbbe3de021806408f3cff6cb1e139c0a25)) 34 | 35 | 36 | #### Features 37 | 38 | * make gridster AMD compatible ([589d7fd5](http://github.com/ducksboard/gridster.js/commit/589d7fd509a570fd02666c2f8231545211d6c83f)) 39 | * **gridster:** move widget up when added if there is space available ([8ec307b6](http://github.com/ducksboard/gridster.js/commit/8ec307b6f7173e94610409adcb1671372cc2c67d)) 40 | 41 | 42 | ### v0.5.2 (2014-06-16) 43 | 44 | 45 | #### Bug Fixes 46 | 47 | * **draggable:** 48 | * handle both touch and click events ([021a6c23](http://github.com/ducksboard/gridster.js/commit/021a6c23e851210c1b817bd353a1e5e19ce10b90), closes [#207](http://github.com/ducksboard/gridster.js/issues/207), [#236](http://github.com/ducksboard/gridster.js/issues/236), [#329](http://github.com/ducksboard/gridster.js/issues/329), [#380](http://github.com/ducksboard/gridster.js/issues/380)) 49 | * replaced scrollX/Y with scrollLeft/Top ([bb7463a3](http://github.com/ducksboard/gridster.js/commit/bb7463a3241750397492dfbac133cea193f0254f)) 50 | * fix offset during drag ([c726c4ad](http://github.com/ducksboard/gridster.js/commit/c726c4ad9c18fea95e4b46b9bacd36c42aa9691c)) 51 | * bind drag events to $document ([dd6c7420](http://github.com/ducksboard/gridster.js/commit/dd6c7420087d5810a9f6b02bf9d81a04a60ae840)) 52 | * **gridster:** 53 | * fix add_widget to use correct size_y when adding rows ([7d22e6c8](http://github.com/ducksboard/gridster.js/commit/7d22e6c8b201de33e33def77a93dc9009d0aa4cb)) 54 | * Removing previously added style tags before adding new one. ([93c46ff4](http://github.com/ducksboard/gridster.js/commit/93c46ff45ebe59f3658b7f32f05b67109aa87311)) 55 | 56 | 57 | #### Features 58 | 59 | * **draggable:** 60 | * allow ignore_dragging config option to be a function ([69fcfe45](http://github.com/ducksboard/gridster.js/commit/69fcfe459678e833cb53de040b9fbc96dd687543)) 61 | * option to not remove helper on drag stop ([03910df9](http://github.com/ducksboard/gridster.js/commit/03910df967a1ae7bcb2fa3aadd58255e0bcbf327)) 62 | 63 | 64 | ### v0.5.1 (2014-03-05) 65 | 66 | 67 | #### Features 68 | 69 | * **collision:** overlapping region as a config option ([720d487e](http://github.com/ducksboard/gridster.js/commit/720d487e3988593e2c60909c88aaff13fbd4f842)) 70 | * **coords:** 71 | * allow both (left/x1) and (top/y1) attr keys ([6f22217f](http://github.com/ducksboard/gridster.js/commit/6f22217f056e4fc52f6405f2af49596105aae150)) 72 | * add destroy method ([fdeee4f6](http://github.com/ducksboard/gridster.js/commit/fdeee4f636266c7a0579ced833f04fec013b6863)) 73 | * **draggable:** keep container position prop if different than static ([04868a38](http://github.com/ducksboard/gridster.js/commit/04868a384d655d110f2d153d2fddb94b1c6d54a9)) 74 | * **gridster:** destroy element's data and optionally remove from DOM ([dc09f191](http://github.com/ducksboard/gridster.js/commit/dc09f191d8503669cfa4737122c77cb0f5b9c3d2)) 75 | 76 | 77 | ## v0.5.0 (2014-02-14) 78 | 79 | 80 | #### Bug Fixes 81 | 82 | * **autogrow:** refining autogrow_cols behavior and grid width issues ([835c2df8](http://github.com/ducksboard/gridster.js/commit/835c2df84419a92b1641b687fcf083f3ff102627)) 83 | * **resize.stop:** Call resize.stop at the latest possible moment ([e21f63a0](http://github.com/ducksboard/gridster.js/commit/e21f63a05a539f5c611eb49cd6861b1e38b36531)) 84 | 85 | 86 | #### Features 87 | 88 | * **draggable:** Add toggle draggable method. ([073fdc40](http://github.com/ducksboard/gridster.js/commit/073fdc40e0a94dd371646fc54cd420e3ddab0254)) 89 | 90 | 91 | ### v0.4.4 (2014-02-13) 92 | 93 | 94 | #### Features 95 | 96 | * **resize:** add start/stop/resize event triggers ([7ca8deec](http://github.com/ducksboard/gridster.js/commit/7ca8deec8559d950097a6dc351cb0c6fcef3458d)) 97 | 98 | 99 | ### v0.4.3 (2014-02-11) 100 | 101 | 102 | #### Bug Fixes 103 | 104 | * **generated-styles:** cleaning cached serializations properly ([f8b04f29](http://github.com/ducksboard/gridster.js/commit/f8b04f298e12e46ca9b07f0bae0abc6b08ed6e18)) 105 | 106 | 107 | ### v0.4.2 (2014-02-07) 108 | 109 | 110 | #### Bug Fixes 111 | 112 | * recalculate grid width when adding widgets ([47745978](http://github.com/ducksboard/gridster.js/commit/4774597834300601fc81d5111a31a8c1672c55e1)) 113 | 114 | 115 | ### v0.4.1 (2014-02-07) 116 | 117 | #### Bug Fixes 118 | 119 | * add resize.min_size option to default config object ([5672edb0](http://github.com/ducksboard/gridster.js/commit/5672edb05e39c6b9ff5e3ca31d68c9e94dfaa617)) 120 | 121 | 122 | ## v0.4.0 (2014-02-07) 123 | 124 | 125 | #### Bug Fixes 126 | 127 | * **gridster:** 128 | * leaking options with multiple Gridster instances ([07c71097](http://github.com/ducksboard/gridster.js/commit/07c7109771094d98be51d68448a20e1d2987b35d)) 129 | * resize.axes default option only 'both' ([62988780](http://github.com/ducksboard/gridster.js/commit/6298878077d5db129daa9780939fec5237b82af9)) 130 | * **licenses:** add required copyright message for underscore ([b563c094](http://github.com/ducksboard/gridster.js/commit/b563c094cf0f3a5da2288492f95759ae32e8967c)) 131 | * **readme:** link title jsfiddle -> jsbin, edit 5) of process steps ([0641aa89](http://github.com/ducksboard/gridster.js/commit/0641aa89833ecf9d167f7d8e89ee8bd5b4304248)) 132 | 133 | 134 | #### Features 135 | 136 | * **draggable:** 137 | * method to set drag limits dynamically ([d4482ec1](http://github.com/ducksboard/gridster.js/commit/d4482ec1476f8a0b6fb6cdeb25b7774ef678d81c)) 138 | * support horizontal scrolling while dragging ([ae4921b7](http://github.com/ducksboard/gridster.js/commit/ae4921b70798944211267cacf8a89e62d0818369)) 139 | * **gridster:** increase grid width when dragging or resizing ([37c4e943](http://github.com/ducksboard/gridster.js/commit/37c4e94358b9392710452b9e7f96454837bf9845)) 140 | * **resize:** add option to set min_size of a widget ([ff511872](http://github.com/ducksboard/gridster.js/commit/ff511872e65992ee89bd2a88d862caaf99733f38)) 141 | 142 | 143 | ## v0.3.0 (2013-11-18) 144 | 145 | 146 | #### Features 147 | 148 | * **draggable:** 149 | * method to set drag limits dynamically ([d4482ec1](http://github.com/ducksboard/gridster.js/commit/d4482ec1476f8a0b6fb6cdeb25b7774ef678d81c)) 150 | * support horizontal scrolling while dragging ([ae4921b7](http://github.com/ducksboard/gridster.js/commit/ae4921b70798944211267cacf8a89e62d0818369)) 151 | * **gridster:** increase grid width when dragging or resizing ([b61df653](http://github.com/ducksboard/gridster.js/commit/b61df6535f728970fb8c6f25a208275dbde66550)) 152 | 153 | 154 | ### v0.2.1 (2013-10-28) 155 | 156 | 157 | #### Features 158 | 159 | * **resize:** Add start/stop/resize callbacks ([d4ec7140](http://github.com/ducksboard/gridster.js/commit/d4ec7140f736bc30697c75b54ed3242ddf1d75b9)) 160 | 161 | 162 | ## v0.2.0 (2013-10-26) 163 | 164 | 165 | #### Bug Fixes 166 | 167 | * fixes and improvements in widget-resizing. ([ae02b32b](http://github.com/ducksboard/gridster.js/commit/ae02b32b9210c6328f4acc339e215ae50c134f77), closes [#32](http://github.com/ducksboard/gridster.js/issues/32)) 168 | * **gridster:** 169 | * the preview holder should not always use `li` ([1ade74e2](http://github.com/ducksboard/gridster.js/commit/1ade74e239485b07e870fca44e1eafb3ff1ae283)) 170 | * overlapping widget problem ([31fd8d6b](http://github.com/ducksboard/gridster.js/commit/31fd8d6ba893e4c39b91ba30d429e37f3da30b24)) 171 | * Orphan preview holder when dragging is interrupted ([1b13617d](http://github.com/ducksboard/gridster.js/commit/1b13617df2ce53235bdf3a1e38f1555f529663c3)) 172 | * remove_widget Returns the instance of the Gridster Class ([5bfbc5c0](http://github.com/ducksboard/gridster.js/commit/5bfbc5c0b5ab49c2a7c651327ce2e0f30f594985)) 173 | 174 | 175 | #### Features 176 | 177 | * **draggable:** 178 | * new config option to move or not the dragged element ([4d9b2a84](http://github.com/ducksboard/gridster.js/commit/4d9b2a84f11cb7cb2ddad51c158d92b82e7bc447)) 179 | * CSS selectors support in `ignore_dragging` config opt ([0f956249](http://github.com/ducksboard/gridster.js/commit/0f95624925be97aee7a8450707e04e887e4dac58)) 180 | * pass previous position to the drag callback ([055cc0e4](http://github.com/ducksboard/gridster.js/commit/055cc0e4f6f9de5721986515656ac894855f9e02)) 181 | * Don't start new drag if previous one hasn't stopped ([91ca6572](http://github.com/ducksboard/gridster.js/commit/91ca65721c2eb32b5dec82cdc5e5e7f81dac329e)) 182 | * pass useful data to all drag callbacks ([8dda2410](http://github.com/ducksboard/gridster.js/commit/8dda2410f300592706985c05141ca6b702977dc0)) 183 | * **gridster:** drag-and-drop widget resizing ([e1924053](http://github.com/ducksboard/gridster.js/commit/e19240532de0bad35ffe6e5fc63934819390adc5)) 184 | * **utils:** add delay helper to utils ([faa6c5db](http://github.com/ducksboard/gridster.js/commit/faa6c5db0002feccf681e9f919ed583eef152773)) 185 | 186 | -------------------------------------------------------------------------------- /src/jquery.draggable.js: -------------------------------------------------------------------------------- 1 | /* 2 | * jquery.draggable 3 | * https://github.com/ducksboard/gridster.js 4 | * 5 | * Copyright (c) 2012 ducksboard 6 | * Licensed under the MIT licenses. 7 | */ 8 | 9 | ;(function(root, factory) { 10 | 11 | if (typeof define === 'function' && define.amd) { 12 | define('gridster-draggable', ['jquery'], factory); 13 | } else { 14 | root.GridsterDraggable = factory(root.$ || root.jQuery); 15 | } 16 | 17 | }(this, function($) { 18 | 19 | var defaults = { 20 | items: 'li', 21 | distance: 1, 22 | limit: true, 23 | offset_left: 0, 24 | autoscroll: true, 25 | ignore_dragging: ['INPUT', 'TEXTAREA', 'SELECT', 'BUTTON'], // or function 26 | handle: null, 27 | container_width: 0, // 0 == auto 28 | move_element: true, 29 | helper: false, // or 'clone' 30 | remove_helper: true 31 | // drag: function(e) {}, 32 | // start : function(e, ui) {}, 33 | // stop : function(e) {} 34 | }; 35 | 36 | var $window = $(window); 37 | var dir_map = { x : 'left', y : 'top' }; 38 | var isTouch = !!('ontouchstart' in window); 39 | 40 | var capitalize = function(str) { 41 | return str.charAt(0).toUpperCase() + str.slice(1); 42 | }; 43 | 44 | var idCounter = 0; 45 | var uniqId = function() { 46 | return ++idCounter + ''; 47 | } 48 | 49 | /** 50 | * Basic drag implementation for DOM elements inside a container. 51 | * Provide start/stop/drag callbacks. 52 | * 53 | * @class Draggable 54 | * @param {HTMLElement} el The HTMLelement that contains all the widgets 55 | * to be dragged. 56 | * @param {Object} [options] An Object with all options you want to 57 | * overwrite: 58 | * @param {HTMLElement|String} [options.items] Define who will 59 | * be the draggable items. Can be a CSS Selector String or a 60 | * collection of HTMLElements. 61 | * @param {Number} [options.distance] Distance in pixels after mousedown 62 | * the mouse must move before dragging should start. 63 | * @param {Boolean} [options.limit] Constrains dragging to the width of 64 | * the container 65 | * @param {Object|Function} [options.ignore_dragging] Array of node names 66 | * that sould not trigger dragging, by default is `['INPUT', 'TEXTAREA', 67 | * 'SELECT', 'BUTTON']`. If a function is used return true to ignore dragging. 68 | * @param {offset_left} [options.offset_left] Offset added to the item 69 | * that is being dragged. 70 | * @param {Number} [options.drag] Executes a callback when the mouse is 71 | * moved during the dragging. 72 | * @param {Number} [options.start] Executes a callback when the drag 73 | * starts. 74 | * @param {Number} [options.stop] Executes a callback when the drag stops. 75 | * @return {Object} Returns `el`. 76 | * @constructor 77 | */ 78 | function Draggable(el, options) { 79 | this.options = $.extend({}, defaults, options); 80 | this.$document = $(document); 81 | this.$container = $(el); 82 | this.$dragitems = $(this.options.items, this.$container); 83 | this.is_dragging = false; 84 | this.player_min_left = 0 + this.options.offset_left; 85 | this.id = uniqId(); 86 | this.ns = '.gridster-draggable-' + this.id; 87 | this.init(); 88 | } 89 | 90 | Draggable.defaults = defaults; 91 | 92 | var fn = Draggable.prototype; 93 | 94 | fn.init = function() { 95 | var pos = this.$container.css('position'); 96 | this.calculate_dimensions(); 97 | this.$container.css('position', pos === 'static' ? 'relative' : pos); 98 | this.disabled = false; 99 | this.events(); 100 | 101 | $(window).bind(this.nsEvent('resize'), 102 | throttle($.proxy(this.calculate_dimensions, this), 200)); 103 | }; 104 | 105 | fn.nsEvent = function(ev) { 106 | return (ev || '') + this.ns; 107 | }; 108 | 109 | fn.events = function() { 110 | this.pointer_events = { 111 | start: this.nsEvent('touchstart') + ' ' + this.nsEvent('mousedown'), 112 | move: this.nsEvent('touchmove') + ' ' + this.nsEvent('mousemove'), 113 | end: this.nsEvent('touchend') + ' ' + this.nsEvent('mouseup'), 114 | }; 115 | 116 | this.$container.on(this.nsEvent('selectstart'), 117 | $.proxy(this.on_select_start, this)); 118 | 119 | this.$container.on(this.pointer_events.start, this.options.items, 120 | $.proxy(this.drag_handler, this)); 121 | 122 | this.$document.on(this.pointer_events.end, $.proxy(function(e) { 123 | this.is_dragging = false; 124 | if (this.disabled) { return; } 125 | this.$document.off(this.pointer_events.move); 126 | if (this.drag_start) { 127 | this.on_dragstop(e); 128 | } 129 | }, this)); 130 | }; 131 | 132 | fn.get_actual_pos = function($el) { 133 | var pos = $el.position(); 134 | return pos; 135 | }; 136 | 137 | 138 | fn.get_mouse_pos = function(e) { 139 | if (e.originalEvent && e.originalEvent.touches) { 140 | var oe = e.originalEvent; 141 | e = oe.touches.length ? oe.touches[0] : oe.changedTouches[0]; 142 | } 143 | 144 | return { 145 | left: e.clientX, 146 | top: e.clientY 147 | }; 148 | }; 149 | 150 | 151 | fn.get_offset = function(e) { 152 | e.preventDefault(); 153 | var mouse_actual_pos = this.get_mouse_pos(e); 154 | var diff_x = Math.round( 155 | mouse_actual_pos.left - this.mouse_init_pos.left); 156 | var diff_y = Math.round(mouse_actual_pos.top - this.mouse_init_pos.top); 157 | 158 | var left = Math.round(this.el_init_offset.left + 159 | diff_x - this.baseX + $(window).scrollLeft() - this.win_offset_x); 160 | var top = Math.round(this.el_init_offset.top + 161 | diff_y - this.baseY + $(window).scrollTop() - this.win_offset_y); 162 | 163 | if (this.options.limit) { 164 | if (left > this.player_max_left) { 165 | left = this.player_max_left; 166 | } else if(left < this.player_min_left) { 167 | left = this.player_min_left; 168 | } 169 | } 170 | 171 | return { 172 | position: { 173 | left: left, 174 | top: top 175 | }, 176 | pointer: { 177 | left: mouse_actual_pos.left, 178 | top: mouse_actual_pos.top, 179 | diff_left: diff_x + ($(window).scrollLeft() - this.win_offset_x), 180 | diff_top: diff_y + ($(window).scrollTop() - this.win_offset_y) 181 | } 182 | }; 183 | }; 184 | 185 | 186 | fn.get_drag_data = function(e) { 187 | var offset = this.get_offset(e); 188 | offset.$player = this.$player; 189 | offset.$helper = this.helper ? this.$helper : this.$player; 190 | 191 | return offset; 192 | }; 193 | 194 | 195 | fn.set_limits = function(container_width) { 196 | container_width || (container_width = this.$container.width()); 197 | this.player_max_left = (container_width - this.player_width + 198 | - this.options.offset_left); 199 | 200 | this.options.container_width = container_width; 201 | 202 | return this; 203 | }; 204 | 205 | 206 | fn.scroll_in = function(axis, data) { 207 | var dir_prop = dir_map[axis]; 208 | 209 | var area_size = 50; 210 | var scroll_inc = 30; 211 | 212 | var is_x = axis === 'x'; 213 | var window_size = is_x ? this.window_width : this.window_height; 214 | var doc_size = is_x ? $(document).width() : $(document).height(); 215 | var player_size = is_x ? this.$player.width() : this.$player.height(); 216 | 217 | var next_scroll; 218 | var scroll_offset = $window['scroll' + capitalize(dir_prop)](); 219 | var min_window_pos = scroll_offset; 220 | var max_window_pos = min_window_pos + window_size; 221 | 222 | var mouse_next_zone = max_window_pos - area_size; // down/right 223 | var mouse_prev_zone = min_window_pos + area_size; // up/left 224 | 225 | var abs_mouse_pos = min_window_pos + data.pointer[dir_prop]; 226 | 227 | var max_player_pos = (doc_size - window_size + player_size); 228 | 229 | if (abs_mouse_pos >= mouse_next_zone) { 230 | next_scroll = scroll_offset + scroll_inc; 231 | if (next_scroll < max_player_pos) { 232 | $window['scroll' + capitalize(dir_prop)](next_scroll); 233 | this['scroll_offset_' + axis] += scroll_inc; 234 | } 235 | } 236 | 237 | if (abs_mouse_pos <= mouse_prev_zone) { 238 | next_scroll = scroll_offset - scroll_inc; 239 | if (next_scroll > 0) { 240 | $window['scroll' + capitalize(dir_prop)](next_scroll); 241 | this['scroll_offset_' + axis] -= scroll_inc; 242 | } 243 | } 244 | 245 | return this; 246 | }; 247 | 248 | 249 | fn.manage_scroll = function(data) { 250 | this.scroll_in('x', data); 251 | this.scroll_in('y', data); 252 | }; 253 | 254 | 255 | fn.calculate_dimensions = function(e) { 256 | this.window_height = $window.height(); 257 | this.window_width = $window.width(); 258 | }; 259 | 260 | 261 | fn.drag_handler = function(e) { 262 | var node = e.target.nodeName; 263 | // skip if drag is disabled, or click was not done with the mouse primary button 264 | if (this.disabled || e.which !== 1 && !isTouch) { 265 | return; 266 | } 267 | 268 | if (this.ignore_drag(e)) { 269 | return; 270 | } 271 | 272 | var self = this; 273 | var first = true; 274 | this.$player = $(e.currentTarget); 275 | 276 | this.el_init_pos = this.get_actual_pos(this.$player); 277 | this.mouse_init_pos = this.get_mouse_pos(e); 278 | this.offsetY = this.mouse_init_pos.top - this.el_init_pos.top; 279 | 280 | this.$document.on(this.pointer_events.move, function(mme) { 281 | var mouse_actual_pos = self.get_mouse_pos(mme); 282 | var diff_x = Math.abs( 283 | mouse_actual_pos.left - self.mouse_init_pos.left); 284 | var diff_y = Math.abs( 285 | mouse_actual_pos.top - self.mouse_init_pos.top); 286 | if (!(diff_x > self.options.distance || 287 | diff_y > self.options.distance) 288 | ) { 289 | return false; 290 | } 291 | 292 | if (first) { 293 | first = false; 294 | self.on_dragstart.call(self, mme); 295 | return false; 296 | } 297 | 298 | if (self.is_dragging === true) { 299 | self.on_dragmove.call(self, mme); 300 | } 301 | 302 | return false; 303 | }); 304 | 305 | if (!isTouch) { return false; } 306 | }; 307 | 308 | 309 | fn.on_dragstart = function(e) { 310 | e.preventDefault(); 311 | 312 | if (this.is_dragging) { return this; } 313 | 314 | this.drag_start = this.is_dragging = true; 315 | var offset = this.$container.offset(); 316 | this.baseX = Math.round(offset.left); 317 | this.baseY = Math.round(offset.top); 318 | this.initial_container_width = this.options.container_width || this.$container.width(); 319 | 320 | if (this.options.helper === 'clone') { 321 | this.$helper = this.$player.clone() 322 | .appendTo(this.$container).addClass('helper'); 323 | this.helper = true; 324 | } else { 325 | this.helper = false; 326 | } 327 | 328 | this.win_offset_y = $(window).scrollTop(); 329 | this.win_offset_x = $(window).scrollLeft(); 330 | this.scroll_offset_y = 0; 331 | this.scroll_offset_x = 0; 332 | this.el_init_offset = this.$player.offset(); 333 | this.player_width = this.$player.width(); 334 | this.player_height = this.$player.height(); 335 | 336 | this.set_limits(this.options.container_width); 337 | 338 | if (this.options.start) { 339 | this.options.start.call(this.$player, e, this.get_drag_data(e)); 340 | } 341 | return false; 342 | }; 343 | 344 | 345 | fn.on_dragmove = function(e) { 346 | var data = this.get_drag_data(e); 347 | 348 | this.options.autoscroll && this.manage_scroll(data); 349 | 350 | if (this.options.move_element) { 351 | (this.helper ? this.$helper : this.$player).css({ 352 | 'position': 'absolute', 353 | 'left' : data.position.left, 354 | 'top' : data.position.top 355 | }); 356 | } 357 | 358 | var last_position = this.last_position || data.position; 359 | data.prev_position = last_position; 360 | 361 | if (this.options.drag) { 362 | this.options.drag.call(this.$player, e, data); 363 | } 364 | 365 | this.last_position = data.position; 366 | return false; 367 | }; 368 | 369 | 370 | fn.on_dragstop = function(e) { 371 | var data = this.get_drag_data(e); 372 | this.drag_start = false; 373 | 374 | if (this.options.stop) { 375 | this.options.stop.call(this.$player, e, data); 376 | } 377 | 378 | if (this.helper && this.options.remove_helper) { 379 | this.$helper.remove(); 380 | } 381 | 382 | return false; 383 | }; 384 | 385 | fn.on_select_start = function(e) { 386 | if (this.disabled) { return; } 387 | 388 | if (this.ignore_drag(e)) { 389 | return; 390 | } 391 | 392 | return false; 393 | }; 394 | 395 | fn.enable = function() { 396 | this.disabled = false; 397 | }; 398 | 399 | fn.disable = function() { 400 | this.disabled = true; 401 | }; 402 | 403 | fn.destroy = function() { 404 | this.disable(); 405 | 406 | this.$container.off(this.ns); 407 | this.$document.off(this.ns); 408 | $(window).off(this.ns); 409 | 410 | $.removeData(this.$container, 'drag'); 411 | }; 412 | 413 | fn.ignore_drag = function(event) { 414 | if (this.options.handle) { 415 | return !$(event.target).is(this.options.handle); 416 | } 417 | 418 | if ($.isFunction(this.options.ignore_dragging)) { 419 | return this.options.ignore_dragging(event); 420 | } 421 | 422 | return $(event.target).is(this.options.ignore_dragging.join(', ')); 423 | }; 424 | 425 | //jQuery adapter 426 | $.fn.drag = function ( options ) { 427 | return new Draggable(this, options); 428 | }; 429 | 430 | return Draggable; 431 | 432 | })); 433 | -------------------------------------------------------------------------------- /src/jquery.gridster.js: -------------------------------------------------------------------------------- 1 | /* 2 | * jquery.gridster 3 | * https://github.com/ducksboard/gridster.js 4 | * 5 | * Copyright (c) 2012 ducksboard 6 | * Licensed under the MIT licenses. 7 | */ 8 | 9 | ;(function(root, factory) { 10 | 11 | if (typeof define === 'function' && define.amd) { 12 | define(['jquery', 'gridster-draggable', 'gridster-collision'], factory); 13 | } else { 14 | root.Gridster = factory(root.$ || root.jQuery, root.GridsterDraggable, 15 | root.GridsterCollision); 16 | } 17 | 18 | }(this, function($, Draggable, Collision) { 19 | 20 | var defaults = { 21 | namespace: '', 22 | widget_selector: 'li', 23 | widget_margins: [10, 10], 24 | widget_base_dimensions: [400, 225], 25 | extra_rows: 0, 26 | extra_cols: 0, 27 | min_cols: 1, 28 | max_cols: Infinity, 29 | min_rows: 15, 30 | max_size_x: false, 31 | autogrow_cols: false, 32 | autogenerate_stylesheet: true, 33 | avoid_overlapped_widgets: true, 34 | auto_init: true, 35 | serialize_params: function($w, wgd) { 36 | return { 37 | col: wgd.col, 38 | row: wgd.row, 39 | size_x: wgd.size_x, 40 | size_y: wgd.size_y 41 | }; 42 | }, 43 | collision: {}, 44 | draggable: { 45 | items: '.gs-w', 46 | distance: 4, 47 | ignore_dragging: Draggable.defaults.ignore_dragging.slice(0) 48 | }, 49 | resize: { 50 | enabled: false, 51 | axes: ['both'], 52 | handle_append_to: '', 53 | handle_class: 'gs-resize-handle', 54 | max_size: [Infinity, Infinity], 55 | min_size: [1, 1] 56 | } 57 | }; 58 | 59 | /** 60 | * @class Gridster 61 | * @uses Draggable 62 | * @uses Collision 63 | * @param {HTMLElement} el The HTMLelement that contains all the widgets. 64 | * @param {Object} [options] An Object with all options you want to 65 | * overwrite: 66 | * @param {HTMLElement|String} [options.widget_selector] Define who will 67 | * be the draggable widgets. Can be a CSS Selector String or a 68 | * collection of HTMLElements 69 | * @param {Array} [options.widget_margins] Margin between widgets. 70 | * The first index for the horizontal margin (left, right) and 71 | * the second for the vertical margin (top, bottom). 72 | * @param {Array} [options.widget_base_dimensions] Base widget dimensions 73 | * in pixels. The first index for the width and the second for the 74 | * height. 75 | * @param {Number} [options.extra_cols] Add more columns in addition to 76 | * those that have been calculated. 77 | * @param {Number} [options.extra_rows] Add more rows in addition to 78 | * those that have been calculated. 79 | * @param {Number} [options.min_cols] The minimum required columns. 80 | * @param {Number} [options.max_cols] The maximum columns possible (set to null 81 | * for no maximum). 82 | * @param {Number} [options.min_rows] The minimum required rows. 83 | * @param {Number} [options.max_size_x] The maximum number of columns 84 | * that a widget can span. 85 | * @param {Boolean} [options.autogenerate_stylesheet] If true, all the 86 | * CSS required to position all widgets in their respective columns 87 | * and rows will be generated automatically and injected to the 88 | * `` of the document. You can set this to false, and write 89 | * your own CSS targeting rows and cols via data-attributes like so: 90 | * `[data-col="1"] { left: 10px; }` 91 | * @param {Boolean} [options.avoid_overlapped_widgets] Avoid that widgets loaded 92 | * from the DOM can be overlapped. It is helpful if the positions were 93 | * bad stored in the database or if there was any conflict. 94 | * @param {Boolean} [options.auto_init] Automatically call gridster init 95 | * method or not when the plugin is instantiated. 96 | * @param {Function} [options.serialize_params] Return the data you want 97 | * for each widget in the serialization. Two arguments are passed: 98 | * `$w`: the jQuery wrapped HTMLElement, and `wgd`: the grid 99 | * coords object (`col`, `row`, `size_x`, `size_y`). 100 | * @param {Object} [options.collision] An Object with all options for 101 | * Collision class you want to overwrite. See Collision docs for 102 | * more info. 103 | * @param {Object} [options.draggable] An Object with all options for 104 | * Draggable class you want to overwrite. See Draggable docs for more 105 | * info. 106 | * @param {Object|Function} [options.draggable.ignore_dragging] Note that 107 | * if you use a Function, and resize is enabled, you should ignore the 108 | * resize handlers manually (options.resize.handle_class). 109 | * @param {Object} [options.resize] An Object with resize config options. 110 | * @param {Boolean} [options.resize.enabled] Set to true to enable 111 | * resizing. 112 | * @param {Array} [options.resize.axes] Axes in which widgets can be 113 | * resized. Possible values: ['x', 'y', 'both']. 114 | * @param {String} [options.resize.handle_append_to] Set a valid CSS 115 | * selector to append resize handles to. 116 | * @param {String} [options.resize.handle_class] CSS class name used 117 | * by resize handles. 118 | * @param {Array} [options.resize.max_size] Limit widget dimensions 119 | * when resizing. Array values should be integers: 120 | * `[max_cols_occupied, max_rows_occupied]` 121 | * @param {Array} [options.resize.min_size] Limit widget dimensions 122 | * when resizing. Array values should be integers: 123 | * `[min_cols_occupied, min_rows_occupied]` 124 | * @param {Function} [options.resize.start] Function executed 125 | * when resizing starts. 126 | * @param {Function} [otions.resize.resize] Function executed 127 | * during the resizing. 128 | * @param {Function} [options.resize.stop] Function executed 129 | * when resizing stops. 130 | * 131 | * @constructor 132 | */ 133 | function Gridster(el, options) { 134 | this.options = $.extend(true, {}, defaults, options); 135 | this.$el = $(el); 136 | this.$wrapper = this.$el.parent(); 137 | this.$widgets = this.$el.children( 138 | this.options.widget_selector).addClass('gs-w'); 139 | this.widgets = []; 140 | this.$changed = $([]); 141 | this.wrapper_width = this.$wrapper.width(); 142 | this.min_widget_width = (this.options.widget_margins[0] * 2) + 143 | this.options.widget_base_dimensions[0]; 144 | this.min_widget_height = (this.options.widget_margins[1] * 2) + 145 | this.options.widget_base_dimensions[1]; 146 | 147 | this.generated_stylesheets = []; 148 | this.$style_tags = $([]); 149 | 150 | this.options.auto_init && this.init(); 151 | } 152 | 153 | Gridster.defaults = defaults; 154 | Gridster.generated_stylesheets = []; 155 | 156 | 157 | /** 158 | * Sorts an Array of grid coords objects (representing the grid coords of 159 | * each widget) in ascending way. 160 | * 161 | * @method sort_by_row_asc 162 | * @param {Array} widgets Array of grid coords objects 163 | * @return {Array} Returns the array sorted. 164 | */ 165 | Gridster.sort_by_row_asc = function(widgets) { 166 | widgets = widgets.sort(function(a, b) { 167 | if (!a.row) { 168 | a = $(a).coords().grid; 169 | b = $(b).coords().grid; 170 | } 171 | 172 | if (a.row > b.row) { 173 | return 1; 174 | } 175 | return -1; 176 | }); 177 | 178 | return widgets; 179 | }; 180 | 181 | 182 | /** 183 | * Sorts an Array of grid coords objects (representing the grid coords of 184 | * each widget) placing first the empty cells upper left. 185 | * 186 | * @method sort_by_row_and_col_asc 187 | * @param {Array} widgets Array of grid coords objects 188 | * @return {Array} Returns the array sorted. 189 | */ 190 | Gridster.sort_by_row_and_col_asc = function(widgets) { 191 | widgets = widgets.sort(function(a, b) { 192 | if (a.row > b.row || a.row === b.row && a.col > b.col) { 193 | return 1; 194 | } 195 | return -1; 196 | }); 197 | 198 | return widgets; 199 | }; 200 | 201 | 202 | /** 203 | * Sorts an Array of grid coords objects by column (representing the grid 204 | * coords of each widget) in ascending way. 205 | * 206 | * @method sort_by_col_asc 207 | * @param {Array} widgets Array of grid coords objects 208 | * @return {Array} Returns the array sorted. 209 | */ 210 | Gridster.sort_by_col_asc = function(widgets) { 211 | widgets = widgets.sort(function(a, b) { 212 | if (a.col > b.col) { 213 | return 1; 214 | } 215 | return -1; 216 | }); 217 | 218 | return widgets; 219 | }; 220 | 221 | 222 | /** 223 | * Sorts an Array of grid coords objects (representing the grid coords of 224 | * each widget) in descending way. 225 | * 226 | * @method sort_by_row_desc 227 | * @param {Array} widgets Array of grid coords objects 228 | * @return {Array} Returns the array sorted. 229 | */ 230 | Gridster.sort_by_row_desc = function(widgets) { 231 | widgets = widgets.sort(function(a, b) { 232 | if (a.row + a.size_y < b.row + b.size_y) { 233 | return 1; 234 | } 235 | return -1; 236 | }); 237 | return widgets; 238 | }; 239 | 240 | 241 | 242 | /** Instance Methods **/ 243 | 244 | var fn = Gridster.prototype; 245 | 246 | fn.init = function() { 247 | this.options.resize.enabled && this.setup_resize(); 248 | this.generate_grid_and_stylesheet(); 249 | this.get_widgets_from_DOM(); 250 | this.set_dom_grid_height(); 251 | this.set_dom_grid_width(); 252 | this.$wrapper.addClass('ready'); 253 | this.draggable(); 254 | this.options.resize.enabled && this.resizable(); 255 | 256 | $(window).bind('resize.gridster', throttle( 257 | $.proxy(this.recalculate_faux_grid, this), 200)); 258 | }; 259 | 260 | 261 | /** 262 | * Disables dragging. 263 | * 264 | * @method disable 265 | * @return {Class} Returns the instance of the Gridster Class. 266 | */ 267 | fn.disable = function() { 268 | this.$wrapper.find('.player-revert').removeClass('player-revert'); 269 | this.drag_api.disable(); 270 | return this; 271 | }; 272 | 273 | 274 | /** 275 | * Enables dragging. 276 | * 277 | * @method enable 278 | * @return {Class} Returns the instance of the Gridster Class. 279 | */ 280 | fn.enable = function() { 281 | this.drag_api.enable(); 282 | return this; 283 | }; 284 | 285 | 286 | 287 | /** 288 | * Disables drag-and-drop widget resizing. 289 | * 290 | * @method disable 291 | * @return {Class} Returns instance of gridster Class. 292 | */ 293 | fn.disable_resize = function() { 294 | this.$el.addClass('gs-resize-disabled'); 295 | this.resize_api.disable(); 296 | return this; 297 | }; 298 | 299 | 300 | /** 301 | * Enables drag-and-drop widget resizing. 302 | * 303 | * @method enable 304 | * @return {Class} Returns instance of gridster Class. 305 | */ 306 | fn.enable_resize = function() { 307 | this.$el.removeClass('gs-resize-disabled'); 308 | this.resize_api.enable(); 309 | return this; 310 | }; 311 | 312 | 313 | /** 314 | * Add a new widget to the grid. 315 | * 316 | * @method add_widget 317 | * @param {String|HTMLElement} html The string representing the HTML of the widget 318 | * or the HTMLElement. 319 | * @param {Number} [size_x] The nº of rows the widget occupies horizontally. 320 | * @param {Number} [size_y] The nº of columns the widget occupies vertically. 321 | * @param {Number} [col] The column the widget should start in. 322 | * @param {Number} [row] The row the widget should start in. 323 | * @param {Array} [max_size] max_size Maximun size (in units) for width and height. 324 | * @param {Array} [min_size] min_size Minimum size (in units) for width and height. 325 | * @return {HTMLElement} Returns the jQuery wrapped HTMLElement representing. 326 | * the widget that was just created. 327 | */ 328 | fn.add_widget = function(html, size_x, size_y, col, row, max_size, min_size) { 329 | var pos; 330 | size_x || (size_x = 1); 331 | size_y || (size_y = 1); 332 | 333 | if (!col & !row) { 334 | pos = this.next_position(size_x, size_y); 335 | } else { 336 | pos = { 337 | col: col, 338 | row: row, 339 | size_x: size_x, 340 | size_y: size_y 341 | }; 342 | 343 | this.empty_cells(col, row, size_x, size_y); 344 | } 345 | 346 | var $w = $(html).attr({ 347 | 'data-col': pos.col, 348 | 'data-row': pos.row, 349 | 'data-sizex' : size_x, 350 | 'data-sizey' : size_y 351 | }).addClass('gs-w').appendTo(this.$el); 352 | 353 | this.$widgets = this.$widgets.add($w); 354 | 355 | this.register_widget($w); 356 | 357 | this.add_faux_rows(pos.size_y); 358 | //this.add_faux_cols(pos.size_x); 359 | 360 | if (max_size) { 361 | this.set_widget_max_size($w, max_size); 362 | } 363 | 364 | if (min_size) { 365 | this.set_widget_min_size($w, min_size); 366 | } 367 | 368 | this.set_dom_grid_width(); 369 | this.set_dom_grid_height(); 370 | 371 | this.drag_api.set_limits(this.cols * this.min_widget_width); 372 | 373 | return $w; 374 | }; 375 | 376 | 377 | /** 378 | * Change widget size limits. 379 | * 380 | * @method set_widget_min_size 381 | * @param {HTMLElement|Number} $widget The jQuery wrapped HTMLElement 382 | * representing the widget or an index representing the desired widget. 383 | * @param {Array} min_size Minimum size (in units) for width and height. 384 | * @return {HTMLElement} Returns instance of gridster Class. 385 | */ 386 | fn.set_widget_min_size = function($widget, min_size) { 387 | $widget = typeof $widget === 'number' ? 388 | this.$widgets.eq($widget) : $widget; 389 | 390 | if (!$widget.length) { return this; } 391 | 392 | var wgd = $widget.data('coords').grid; 393 | wgd.min_size_x = min_size[0]; 394 | wgd.min_size_y = min_size[1]; 395 | 396 | return this; 397 | }; 398 | 399 | 400 | /** 401 | * Change widget size limits. 402 | * 403 | * @method set_widget_max_size 404 | * @param {HTMLElement|Number} $widget The jQuery wrapped HTMLElement 405 | * representing the widget or an index representing the desired widget. 406 | * @param {Array} max_size Maximun size (in units) for width and height. 407 | * @return {HTMLElement} Returns instance of gridster Class. 408 | */ 409 | fn.set_widget_max_size = function($widget, max_size) { 410 | $widget = typeof $widget === 'number' ? 411 | this.$widgets.eq($widget) : $widget; 412 | 413 | if (!$widget.length) { return this; } 414 | 415 | var wgd = $widget.data('coords').grid; 416 | wgd.max_size_x = max_size[0]; 417 | wgd.max_size_y = max_size[1]; 418 | 419 | return this; 420 | }; 421 | 422 | 423 | /** 424 | * Append the resize handle into a widget. 425 | * 426 | * @method add_resize_handle 427 | * @param {HTMLElement} $widget The jQuery wrapped HTMLElement 428 | * representing the widget. 429 | * @return {HTMLElement} Returns instance of gridster Class. 430 | */ 431 | fn.add_resize_handle = function($w) { 432 | var append_to = this.options.resize.handle_append_to; 433 | $(this.resize_handle_tpl).appendTo( append_to ? $(append_to, $w) : $w); 434 | 435 | return this; 436 | }; 437 | 438 | 439 | /** 440 | * Change the size of a widget. Width is limited to the current grid width. 441 | * 442 | * @method resize_widget 443 | * @param {HTMLElement} $widget The jQuery wrapped HTMLElement 444 | * representing the widget. 445 | * @param {Number} size_x The number of columns that will occupy the widget. 446 | * By default size_x is limited to the space available from 447 | * the column where the widget begins, until the last column to the right. 448 | * @param {Number} size_y The number of rows that will occupy the widget. 449 | * @param {Function} [callback] Function executed when the widget is removed. 450 | * @return {HTMLElement} Returns $widget. 451 | */ 452 | fn.resize_widget = function($widget, size_x, size_y, callback) { 453 | var wgd = $widget.coords().grid; 454 | var col = wgd.col; 455 | var max_cols = this.options.max_cols; 456 | var old_size_y = wgd.size_y; 457 | var old_col = wgd.col; 458 | var new_col = old_col; 459 | 460 | size_x || (size_x = wgd.size_x); 461 | size_y || (size_y = wgd.size_y); 462 | 463 | if (max_cols !== Infinity) { 464 | size_x = Math.min(size_x, max_cols - col + 1); 465 | } 466 | 467 | if (size_y > old_size_y) { 468 | this.add_faux_rows(Math.max(size_y - old_size_y, 0)); 469 | } 470 | 471 | var player_rcol = (col + size_x - 1); 472 | if (player_rcol > this.cols) { 473 | this.add_faux_cols(player_rcol - this.cols); 474 | } 475 | 476 | var new_grid_data = { 477 | col: new_col, 478 | row: wgd.row, 479 | size_x: size_x, 480 | size_y: size_y 481 | }; 482 | 483 | this.mutate_widget_in_gridmap($widget, wgd, new_grid_data); 484 | 485 | this.set_dom_grid_height(); 486 | this.set_dom_grid_width(); 487 | 488 | if (callback) { 489 | callback.call(this, new_grid_data.size_x, new_grid_data.size_y); 490 | } 491 | 492 | return $widget; 493 | }; 494 | 495 | /** 496 | * Change the dimensions of widgets. 497 | * 498 | * @method resize_widget_dimensions 499 | * @param {Object} [options] An Object with all options you want to 500 | * overwrite: 501 | * @param {Array} [options.widget_margins] Margin between widgets. 502 | * The first index for the horizontal margin (left, right) and 503 | * the second for the vertical margin (top, bottom). 504 | * @param {Array} [options.widget_base_dimensions] Base widget dimensions 505 | * in pixels. The first index for the width and the second for the 506 | * height. 507 | * @return {Class} Returns the instance of the Gridster Class. 508 | */ 509 | fn.resize_widget_dimensions = function(options) { 510 | if (options.widget_margins) { 511 | this.options.widget_margins = options.widget_margins; 512 | } 513 | 514 | if (options.widget_base_dimensions) { 515 | this.options.widget_base_dimensions = options.widget_base_dimensions; 516 | } 517 | 518 | this.min_widget_width = (this.options.widget_margins[0] * 2) + this.options.widget_base_dimensions[0]; 519 | this.min_widget_height = (this.options.widget_margins[1] * 2) + this.options.widget_base_dimensions[1]; 520 | 521 | var serializedGrid = this.serialize(); 522 | this.$widgets.each($.proxy(function(i, widget) { 523 | var $widget = $(widget); 524 | this.resize_widget($widget); 525 | }, this)); 526 | 527 | this.generate_grid_and_stylesheet(); 528 | this.get_widgets_from_DOM(); 529 | this.set_dom_grid_height(); 530 | 531 | return this; 532 | }; 533 | 534 | 535 | 536 | /** 537 | * Mutate widget dimensions and position in the grid map. 538 | * 539 | * @method mutate_widget_in_gridmap 540 | * @param {HTMLElement} $widget The jQuery wrapped HTMLElement 541 | * representing the widget to mutate. 542 | * @param {Object} wgd Current widget grid data (col, row, size_x, size_y). 543 | * @param {Object} new_wgd New widget grid data. 544 | * @return {HTMLElement} Returns instance of gridster Class. 545 | */ 546 | fn.mutate_widget_in_gridmap = function($widget, wgd, new_wgd) { 547 | var old_size_x = wgd.size_x; 548 | var old_size_y = wgd.size_y; 549 | 550 | var old_cells_occupied = this.get_cells_occupied(wgd); 551 | var new_cells_occupied = this.get_cells_occupied(new_wgd); 552 | 553 | var empty_cols = []; 554 | $.each(old_cells_occupied.cols, function(i, col) { 555 | if ($.inArray(col, new_cells_occupied.cols) === -1) { 556 | empty_cols.push(col); 557 | } 558 | }); 559 | 560 | var occupied_cols = []; 561 | $.each(new_cells_occupied.cols, function(i, col) { 562 | if ($.inArray(col, old_cells_occupied.cols) === -1) { 563 | occupied_cols.push(col); 564 | } 565 | }); 566 | 567 | var empty_rows = []; 568 | $.each(old_cells_occupied.rows, function(i, row) { 569 | if ($.inArray(row, new_cells_occupied.rows) === -1) { 570 | empty_rows.push(row); 571 | } 572 | }); 573 | 574 | var occupied_rows = []; 575 | $.each(new_cells_occupied.rows, function(i, row) { 576 | if ($.inArray(row, old_cells_occupied.rows) === -1) { 577 | occupied_rows.push(row); 578 | } 579 | }); 580 | 581 | this.remove_from_gridmap(wgd); 582 | 583 | if (occupied_cols.length) { 584 | var cols_to_empty = [ 585 | new_wgd.col, new_wgd.row, new_wgd.size_x, Math.min(old_size_y, new_wgd.size_y), $widget 586 | ]; 587 | this.empty_cells.apply(this, cols_to_empty); 588 | } 589 | 590 | if (occupied_rows.length) { 591 | var rows_to_empty = [new_wgd.col, new_wgd.row, new_wgd.size_x, new_wgd.size_y, $widget]; 592 | this.empty_cells.apply(this, rows_to_empty); 593 | } 594 | 595 | // not the same that wgd = new_wgd; 596 | wgd.col = new_wgd.col; 597 | wgd.row = new_wgd.row; 598 | wgd.size_x = new_wgd.size_x; 599 | wgd.size_y = new_wgd.size_y; 600 | 601 | this.add_to_gridmap(new_wgd, $widget); 602 | 603 | $widget.removeClass('player-revert'); 604 | 605 | //update coords instance attributes 606 | $widget.data('coords').update({ 607 | width: (new_wgd.size_x * this.options.widget_base_dimensions[0] + 608 | ((new_wgd.size_x - 1) * this.options.widget_margins[0]) * 2), 609 | height: (new_wgd.size_y * this.options.widget_base_dimensions[1] + 610 | ((new_wgd.size_y - 1) * this.options.widget_margins[1]) * 2) 611 | }); 612 | 613 | $widget.attr({ 614 | 'data-col': new_wgd.col, 615 | 'data-row': new_wgd.row, 616 | 'data-sizex': new_wgd.size_x, 617 | 'data-sizey': new_wgd.size_y 618 | }); 619 | 620 | if (empty_cols.length) { 621 | var cols_to_remove_holes = [ 622 | empty_cols[0], new_wgd.row, 623 | empty_cols.length, 624 | Math.min(old_size_y, new_wgd.size_y), 625 | $widget 626 | ]; 627 | 628 | this.remove_empty_cells.apply(this, cols_to_remove_holes); 629 | } 630 | 631 | if (empty_rows.length) { 632 | var rows_to_remove_holes = [ 633 | new_wgd.col, new_wgd.row, new_wgd.size_x, new_wgd.size_y, $widget 634 | ]; 635 | this.remove_empty_cells.apply(this, rows_to_remove_holes); 636 | } 637 | 638 | this.move_widget_up($widget); 639 | 640 | return this; 641 | }; 642 | 643 | 644 | /** 645 | * Move down widgets in cells represented by the arguments col, row, size_x, 646 | * size_y 647 | * 648 | * @method empty_cells 649 | * @param {Number} col The column where the group of cells begin. 650 | * @param {Number} row The row where the group of cells begin. 651 | * @param {Number} size_x The number of columns that the group of cells 652 | * occupy. 653 | * @param {Number} size_y The number of rows that the group of cells 654 | * occupy. 655 | * @param {HTMLElement} $exclude Exclude widgets from being moved. 656 | * @return {Class} Returns the instance of the Gridster Class. 657 | */ 658 | fn.empty_cells = function(col, row, size_x, size_y, $exclude) { 659 | var $nexts = this.widgets_below({ 660 | col: col, 661 | row: row - size_y, 662 | size_x: size_x, 663 | size_y: size_y 664 | }); 665 | 666 | $nexts.not($exclude).each($.proxy(function(i, w) { 667 | var wgd = $(w).coords().grid; 668 | if ( !(wgd.row <= (row + size_y - 1))) { return; } 669 | var diff = (row + size_y) - wgd.row; 670 | this.move_widget_down($(w), diff); 671 | }, this)); 672 | 673 | this.set_dom_grid_height(); 674 | 675 | return this; 676 | }; 677 | 678 | 679 | /** 680 | * Move up widgets below cells represented by the arguments col, row, size_x, 681 | * size_y. 682 | * 683 | * @method remove_empty_cells 684 | * @param {Number} col The column where the group of cells begin. 685 | * @param {Number} row The row where the group of cells begin. 686 | * @param {Number} size_x The number of columns that the group of cells 687 | * occupy. 688 | * @param {Number} size_y The number of rows that the group of cells 689 | * occupy. 690 | * @param {HTMLElement} exclude Exclude widgets from being moved. 691 | * @return {Class} Returns the instance of the Gridster Class. 692 | */ 693 | fn.remove_empty_cells = function(col, row, size_x, size_y, exclude) { 694 | var $nexts = this.widgets_below({ 695 | col: col, 696 | row: row, 697 | size_x: size_x, 698 | size_y: size_y 699 | }); 700 | 701 | $nexts.not(exclude).each($.proxy(function(i, widget) { 702 | this.move_widget_up( $(widget), size_y ); 703 | }, this)); 704 | 705 | this.set_dom_grid_height(); 706 | 707 | return this; 708 | }; 709 | 710 | 711 | /** 712 | * Get the most left column below to add a new widget. 713 | * 714 | * @method next_position 715 | * @param {Number} size_x The nº of rows the widget occupies horizontally. 716 | * @param {Number} size_y The nº of columns the widget occupies vertically. 717 | * @return {Object} Returns a grid coords object representing the future 718 | * widget coords. 719 | */ 720 | fn.next_position = function(size_x, size_y) { 721 | size_x || (size_x = 1); 722 | size_y || (size_y = 1); 723 | var ga = this.gridmap; 724 | var cols_l = ga.length; 725 | var valid_pos = []; 726 | var rows_l; 727 | 728 | for (var c = 1; c < cols_l; c++) { 729 | rows_l = ga[c].length; 730 | for (var r = 1; r <= rows_l; r++) { 731 | var can_move_to = this.can_move_to({ 732 | size_x: size_x, 733 | size_y: size_y 734 | }, c, r); 735 | 736 | if (can_move_to) { 737 | valid_pos.push({ 738 | col: c, 739 | row: r, 740 | size_y: size_y, 741 | size_x: size_x 742 | }); 743 | } 744 | } 745 | } 746 | 747 | if (valid_pos.length) { 748 | return Gridster.sort_by_row_and_col_asc(valid_pos)[0]; 749 | } 750 | return false; 751 | }; 752 | 753 | 754 | /** 755 | * Remove a widget from the grid. 756 | * 757 | * @method remove_widget 758 | * @param {HTMLElement} el The jQuery wrapped HTMLElement you want to remove. 759 | * @param {Boolean|Function} silent If true, widgets below the removed one 760 | * will not move up. If a Function is passed it will be used as callback. 761 | * @param {Function} callback Function executed when the widget is removed. 762 | * @return {Class} Returns the instance of the Gridster Class. 763 | */ 764 | fn.remove_widget = function(el, silent, callback) { 765 | var $el = el instanceof $ ? el : $(el); 766 | var wgd = $el.coords().grid; 767 | 768 | // if silent is a function assume it's a callback 769 | if ($.isFunction(silent)) { 770 | callback = silent; 771 | silent = false; 772 | } 773 | 774 | this.cells_occupied_by_placeholder = {}; 775 | this.$widgets = this.$widgets.not($el); 776 | 777 | var $nexts = this.widgets_below($el); 778 | 779 | this.remove_from_gridmap(wgd); 780 | 781 | $el.fadeOut($.proxy(function() { 782 | $el.remove(); 783 | 784 | if (!silent) { 785 | $nexts.each($.proxy(function(i, widget) { 786 | this.move_widget_up( $(widget), wgd.size_y ); 787 | }, this)); 788 | } 789 | 790 | this.set_dom_grid_height(); 791 | 792 | if (callback) { 793 | callback.call(this, el); 794 | } 795 | }, this)); 796 | 797 | return this; 798 | }; 799 | 800 | 801 | /** 802 | * Remove all widgets from the grid. 803 | * 804 | * @method remove_all_widgets 805 | * @param {Function} callback Function executed for each widget removed. 806 | * @return {Class} Returns the instance of the Gridster Class. 807 | */ 808 | fn.remove_all_widgets = function(callback) { 809 | this.$widgets.each($.proxy(function(i, el){ 810 | this.remove_widget(el, true, callback); 811 | }, this)); 812 | 813 | return this; 814 | }; 815 | 816 | 817 | /** 818 | * Returns a serialized array of the widgets in the grid. 819 | * 820 | * @method serialize 821 | * @param {HTMLElement} [$widgets] The collection of jQuery wrapped 822 | * HTMLElements you want to serialize. If no argument is passed all widgets 823 | * will be serialized. 824 | * @return {Array} Returns an Array of Objects with the data specified in 825 | * the serialize_params option. 826 | */ 827 | fn.serialize = function($widgets) { 828 | $widgets || ($widgets = this.$widgets); 829 | 830 | return $widgets.map($.proxy(function(i, widget) { 831 | var $w = $(widget); 832 | return this.options.serialize_params($w, $w.coords().grid); 833 | }, this)).get(); 834 | }; 835 | 836 | 837 | /** 838 | * Returns a serialized array of the widgets that have changed their 839 | * position. 840 | * 841 | * @method serialize_changed 842 | * @return {Array} Returns an Array of Objects with the data specified in 843 | * the serialize_params option. 844 | */ 845 | fn.serialize_changed = function() { 846 | return this.serialize(this.$changed); 847 | }; 848 | 849 | 850 | /** 851 | * Convert widgets from DOM elements to "widget grid data" Objects. 852 | * 853 | * @method dom_to_coords 854 | * @param {HTMLElement} $widget The widget to be converted. 855 | */ 856 | fn.dom_to_coords = function($widget) { 857 | return { 858 | 'col': parseInt($widget.attr('data-col'), 10), 859 | 'row': parseInt($widget.attr('data-row'), 10), 860 | 'size_x': parseInt($widget.attr('data-sizex'), 10) || 1, 861 | 'size_y': parseInt($widget.attr('data-sizey'), 10) || 1, 862 | 'max_size_x': parseInt($widget.attr('data-max-sizex'), 10) || false, 863 | 'max_size_y': parseInt($widget.attr('data-max-sizey'), 10) || false, 864 | 'min_size_x': parseInt($widget.attr('data-min-sizex'), 10) || false, 865 | 'min_size_y': parseInt($widget.attr('data-min-sizey'), 10) || false, 866 | 'el': $widget 867 | }; 868 | }; 869 | 870 | 871 | /** 872 | * Creates the grid coords object representing the widget an add it to the 873 | * mapped array of positions. 874 | * 875 | * @method register_widget 876 | * @param {HTMLElement|Object} $el jQuery wrapped HTMLElement representing 877 | * the widget, or an "widget grid data" Object with (col, row, el ...). 878 | * @return {Boolean} Returns true if the widget final position is different 879 | * than the original. 880 | */ 881 | fn.register_widget = function($el) { 882 | var isDOM = $el instanceof jQuery; 883 | var wgd = isDOM ? this.dom_to_coords($el) : $el; 884 | var posChanged = false; 885 | isDOM || ($el = wgd.el); 886 | 887 | var empty_upper_row = this.can_go_widget_up(wgd); 888 | if (empty_upper_row) { 889 | wgd.row = empty_upper_row; 890 | $el.attr('data-row', empty_upper_row); 891 | this.$el.trigger('gridster:positionchanged', [wgd]); 892 | posChanged = true; 893 | } 894 | 895 | if (this.options.avoid_overlapped_widgets && 896 | !this.can_move_to( 897 | {size_x: wgd.size_x, size_y: wgd.size_y}, wgd.col, wgd.row) 898 | ) { 899 | $.extend(wgd, this.next_position(wgd.size_x, wgd.size_y)); 900 | $el.attr({ 901 | 'data-col': wgd.col, 902 | 'data-row': wgd.row, 903 | 'data-sizex': wgd.size_x, 904 | 'data-sizey': wgd.size_y 905 | }); 906 | posChanged = true; 907 | } 908 | 909 | // attach Coord object to player data-coord attribute 910 | $el.data('coords', $el.coords()); 911 | // Extend Coord object with grid position info 912 | $el.data('coords').grid = wgd; 913 | 914 | this.add_to_gridmap(wgd, $el); 915 | 916 | this.options.resize.enabled && this.add_resize_handle($el); 917 | 918 | return posChanged; 919 | }; 920 | 921 | 922 | /** 923 | * Update in the mapped array of positions the value of cells represented by 924 | * the grid coords object passed in the `grid_data` param. 925 | * 926 | * @param {Object} grid_data The grid coords object representing the cells 927 | * to update in the mapped array. 928 | * @param {HTMLElement|Boolean} value Pass `false` or the jQuery wrapped 929 | * HTMLElement, depends if you want to delete an existing position or add 930 | * a new one. 931 | * @method update_widget_position 932 | * @return {Class} Returns the instance of the Gridster Class. 933 | */ 934 | fn.update_widget_position = function(grid_data, value) { 935 | this.for_each_cell_occupied(grid_data, function(col, row) { 936 | if (!this.gridmap[col]) { return this; } 937 | this.gridmap[col][row] = value; 938 | }); 939 | return this; 940 | }; 941 | 942 | 943 | /** 944 | * Remove a widget from the mapped array of positions. 945 | * 946 | * @method remove_from_gridmap 947 | * @param {Object} grid_data The grid coords object representing the cells 948 | * to update in the mapped array. 949 | * @return {Class} Returns the instance of the Gridster Class. 950 | */ 951 | fn.remove_from_gridmap = function(grid_data) { 952 | return this.update_widget_position(grid_data, false); 953 | }; 954 | 955 | 956 | /** 957 | * Add a widget to the mapped array of positions. 958 | * 959 | * @method add_to_gridmap 960 | * @param {Object} grid_data The grid coords object representing the cells 961 | * to update in the mapped array. 962 | * @param {HTMLElement|Boolean} value The value to set in the specified 963 | * position . 964 | * @return {Class} Returns the instance of the Gridster Class. 965 | */ 966 | fn.add_to_gridmap = function(grid_data, value) { 967 | this.update_widget_position(grid_data, value || grid_data.el); 968 | 969 | if (grid_data.el) { 970 | var $widgets = this.widgets_below(grid_data.el); 971 | $widgets.each($.proxy(function(i, widget) { 972 | this.move_widget_up( $(widget)); 973 | }, this)); 974 | } 975 | }; 976 | 977 | 978 | /** 979 | * Make widgets draggable. 980 | * 981 | * @uses Draggable 982 | * @method draggable 983 | * @return {Class} Returns the instance of the Gridster Class. 984 | */ 985 | fn.draggable = function() { 986 | var self = this; 987 | var draggable_options = $.extend(true, {}, this.options.draggable, { 988 | offset_left: this.options.widget_margins[0], 989 | offset_top: this.options.widget_margins[1], 990 | container_width: this.cols * this.min_widget_width, 991 | limit: true, 992 | start: function(event, ui) { 993 | self.$widgets.filter('.player-revert') 994 | .removeClass('player-revert'); 995 | 996 | self.$player = $(this); 997 | self.$helper = $(ui.$helper); 998 | 999 | self.helper = !self.$helper.is(self.$player); 1000 | 1001 | self.on_start_drag.call(self, event, ui); 1002 | self.$el.trigger('gridster:dragstart'); 1003 | }, 1004 | stop: function(event, ui) { 1005 | self.on_stop_drag.call(self, event, ui); 1006 | self.$el.trigger('gridster:dragstop'); 1007 | }, 1008 | drag: throttle(function(event, ui) { 1009 | self.on_drag.call(self, event, ui); 1010 | self.$el.trigger('gridster:drag'); 1011 | }, 60) 1012 | }); 1013 | 1014 | this.drag_api = this.$el.drag(draggable_options); 1015 | return this; 1016 | }; 1017 | 1018 | 1019 | /** 1020 | * Bind resize events to get resize working. 1021 | * 1022 | * @method resizable 1023 | * @return {Class} Returns instance of gridster Class. 1024 | */ 1025 | fn.resizable = function() { 1026 | this.resize_api = this.$el.drag({ 1027 | items: '.' + this.options.resize.handle_class, 1028 | offset_left: this.options.widget_margins[0], 1029 | container_width: this.container_width, 1030 | move_element: false, 1031 | resize: true, 1032 | limit: this.options.autogrow_cols ? false : true, 1033 | start: $.proxy(this.on_start_resize, this), 1034 | stop: $.proxy(function(event, ui) { 1035 | delay($.proxy(function() { 1036 | this.on_stop_resize(event, ui); 1037 | }, this), 120); 1038 | }, this), 1039 | drag: throttle($.proxy(this.on_resize, this), 60) 1040 | }); 1041 | 1042 | return this; 1043 | }; 1044 | 1045 | 1046 | /** 1047 | * Setup things required for resizing. Like build templates for drag handles. 1048 | * 1049 | * @method setup_resize 1050 | * @return {Class} Returns instance of gridster Class. 1051 | */ 1052 | fn.setup_resize = function() { 1053 | this.resize_handle_class = this.options.resize.handle_class; 1054 | var axes = this.options.resize.axes; 1055 | var handle_tpl = ''; 1057 | 1058 | this.resize_handle_tpl = $.map(axes, function(type) { 1059 | return handle_tpl.replace('{type}', type); 1060 | }).join(''); 1061 | 1062 | if ($.isArray(this.options.draggable.ignore_dragging)) { 1063 | this.options.draggable.ignore_dragging.push( 1064 | '.' + this.resize_handle_class); 1065 | } 1066 | 1067 | return this; 1068 | }; 1069 | 1070 | 1071 | /** 1072 | * This function is executed when the player begins to be dragged. 1073 | * 1074 | * @method on_start_drag 1075 | * @param {Event} event The original browser event 1076 | * @param {Object} ui A prepared ui object with useful drag-related data 1077 | */ 1078 | fn.on_start_drag = function(event, ui) { 1079 | this.$helper.add(this.$player).add(this.$wrapper).addClass('dragging'); 1080 | 1081 | this.highest_col = this.get_highest_occupied_cell().col; 1082 | 1083 | this.$player.addClass('player'); 1084 | this.player_grid_data = this.$player.coords().grid; 1085 | this.placeholder_grid_data = $.extend({}, this.player_grid_data); 1086 | 1087 | this.set_dom_grid_height(this.$el.height() + 1088 | (this.player_grid_data.size_y * this.min_widget_height)); 1089 | 1090 | this.set_dom_grid_width(this.cols); 1091 | 1092 | var pgd_sizex = this.player_grid_data.size_x; 1093 | var cols_diff = this.cols - this.highest_col; 1094 | 1095 | if (this.options.autogrow_cols && cols_diff <= pgd_sizex) { 1096 | this.add_faux_cols(Math.min(pgd_sizex - cols_diff, 1)); 1097 | } 1098 | 1099 | var colliders = this.faux_grid; 1100 | var coords = this.$player.data('coords').coords; 1101 | 1102 | this.cells_occupied_by_player = this.get_cells_occupied( 1103 | this.player_grid_data); 1104 | this.cells_occupied_by_placeholder = this.get_cells_occupied( 1105 | this.placeholder_grid_data); 1106 | 1107 | this.last_cols = []; 1108 | this.last_rows = []; 1109 | 1110 | // see jquery.collision.js 1111 | this.collision_api = this.$helper.collision( 1112 | colliders, this.options.collision); 1113 | 1114 | this.$preview_holder = $('<' + this.$player.get(0).tagName + ' />', { 1115 | 'class': 'preview-holder', 1116 | 'data-row': this.$player.attr('data-row'), 1117 | 'data-col': this.$player.attr('data-col'), 1118 | css: { 1119 | width: coords.width, 1120 | height: coords.height 1121 | } 1122 | }).appendTo(this.$el); 1123 | 1124 | if (this.options.draggable.start) { 1125 | this.options.draggable.start.call(this, event, ui); 1126 | } 1127 | }; 1128 | 1129 | 1130 | /** 1131 | * This function is executed when the player is being dragged. 1132 | * 1133 | * @method on_drag 1134 | * @param {Event} event The original browser event 1135 | * @param {Object} ui A prepared ui object with useful drag-related data 1136 | */ 1137 | fn.on_drag = function(event, ui) { 1138 | //break if dragstop has been fired 1139 | if (this.$player === null) { 1140 | return false; 1141 | } 1142 | 1143 | var abs_offset = { 1144 | left: ui.position.left + this.baseX, 1145 | top: ui.position.top + this.baseY 1146 | }; 1147 | 1148 | // auto grow cols 1149 | if (this.options.autogrow_cols) { 1150 | var prcol = this.placeholder_grid_data.col + 1151 | this.placeholder_grid_data.size_x - 1; 1152 | 1153 | // "- 1" due to adding at least 1 column in on_start_drag 1154 | if (prcol >= this.cols - 1 && this.options.max_cols >= this.cols + 1) { 1155 | this.add_faux_cols(1); 1156 | this.set_dom_grid_width(this.cols + 1); 1157 | this.drag_api.set_limits(this.container_width); 1158 | } 1159 | 1160 | this.collision_api.set_colliders(this.faux_grid); 1161 | } 1162 | 1163 | this.colliders_data = this.collision_api.get_closest_colliders( 1164 | abs_offset); 1165 | 1166 | this.on_overlapped_column_change( 1167 | this.on_start_overlapping_column, this.on_stop_overlapping_column); 1168 | 1169 | this.on_overlapped_row_change( 1170 | this.on_start_overlapping_row, this.on_stop_overlapping_row); 1171 | 1172 | 1173 | if (this.helper && this.$player) { 1174 | this.$player.css({ 1175 | 'left': ui.position.left, 1176 | 'top': ui.position.top 1177 | }); 1178 | } 1179 | 1180 | if (this.options.draggable.drag) { 1181 | this.options.draggable.drag.call(this, event, ui); 1182 | } 1183 | }; 1184 | 1185 | 1186 | /** 1187 | * This function is executed when the player stops being dragged. 1188 | * 1189 | * @method on_stop_drag 1190 | * @param {Event} event The original browser event 1191 | * @param {Object} ui A prepared ui object with useful drag-related data 1192 | */ 1193 | fn.on_stop_drag = function(event, ui) { 1194 | this.$helper.add(this.$player).add(this.$wrapper) 1195 | .removeClass('dragging'); 1196 | 1197 | ui.position.left = ui.position.left + this.baseX; 1198 | ui.position.top = ui.position.top + this.baseY; 1199 | this.colliders_data = this.collision_api.get_closest_colliders( 1200 | ui.position); 1201 | 1202 | this.on_overlapped_column_change( 1203 | this.on_start_overlapping_column, 1204 | this.on_stop_overlapping_column 1205 | ); 1206 | 1207 | this.on_overlapped_row_change( 1208 | this.on_start_overlapping_row, 1209 | this.on_stop_overlapping_row 1210 | ); 1211 | 1212 | this.$player.addClass('player-revert').removeClass('player') 1213 | .attr({ 1214 | 'data-col': this.placeholder_grid_data.col, 1215 | 'data-row': this.placeholder_grid_data.row 1216 | }).css({ 1217 | 'left': '', 1218 | 'top': '' 1219 | }); 1220 | 1221 | this.$changed = this.$changed.add(this.$player); 1222 | 1223 | this.cells_occupied_by_player = this.get_cells_occupied( 1224 | this.placeholder_grid_data); 1225 | this.set_cells_player_occupies( 1226 | this.placeholder_grid_data.col, this.placeholder_grid_data.row); 1227 | 1228 | this.$player.coords().grid.row = this.placeholder_grid_data.row; 1229 | this.$player.coords().grid.col = this.placeholder_grid_data.col; 1230 | 1231 | if (this.options.draggable.stop) { 1232 | this.options.draggable.stop.call(this, event, ui); 1233 | } 1234 | 1235 | this.$preview_holder.remove(); 1236 | 1237 | this.$player = null; 1238 | this.$helper = null; 1239 | this.placeholder_grid_data = {}; 1240 | this.player_grid_data = {}; 1241 | this.cells_occupied_by_placeholder = {}; 1242 | this.cells_occupied_by_player = {}; 1243 | 1244 | this.set_dom_grid_height(); 1245 | this.set_dom_grid_width(); 1246 | 1247 | if (this.options.autogrow_cols) { 1248 | this.drag_api.set_limits(this.cols * this.min_widget_width); 1249 | } 1250 | }; 1251 | 1252 | 1253 | /** 1254 | * This function is executed every time a widget starts to be resized. 1255 | * 1256 | * @method on_start_resize 1257 | * @param {Event} event The original browser event 1258 | * @param {Object} ui A prepared ui object with useful drag-related data 1259 | */ 1260 | fn.on_start_resize = function(event, ui) { 1261 | this.$resized_widget = ui.$player.closest('.gs-w'); 1262 | this.resize_coords = this.$resized_widget.coords(); 1263 | this.resize_wgd = this.resize_coords.grid; 1264 | this.resize_initial_width = this.resize_coords.coords.width; 1265 | this.resize_initial_height = this.resize_coords.coords.height; 1266 | this.resize_initial_sizex = this.resize_coords.grid.size_x; 1267 | this.resize_initial_sizey = this.resize_coords.grid.size_y; 1268 | this.resize_initial_col = this.resize_coords.grid.col; 1269 | this.resize_last_sizex = this.resize_initial_sizex; 1270 | this.resize_last_sizey = this.resize_initial_sizey; 1271 | 1272 | this.resize_max_size_x = Math.min(this.resize_wgd.max_size_x || 1273 | this.options.resize.max_size[0], 1274 | this.options.max_cols - this.resize_initial_col + 1); 1275 | this.resize_max_size_y = this.resize_wgd.max_size_y || 1276 | this.options.resize.max_size[1]; 1277 | 1278 | this.resize_min_size_x = (this.resize_wgd.min_size_x || 1279 | this.options.resize.min_size[0] || 1); 1280 | this.resize_min_size_y = (this.resize_wgd.min_size_y || 1281 | this.options.resize.min_size[1] || 1); 1282 | 1283 | this.resize_initial_last_col = this.get_highest_occupied_cell().col; 1284 | 1285 | this.set_dom_grid_width(this.cols); 1286 | 1287 | this.resize_dir = { 1288 | right: ui.$player.is('.' + this.resize_handle_class + '-x'), 1289 | bottom: ui.$player.is('.' + this.resize_handle_class + '-y') 1290 | }; 1291 | 1292 | this.$resized_widget.css({ 1293 | 'min-width': this.options.widget_base_dimensions[0], 1294 | 'min-height': this.options.widget_base_dimensions[1] 1295 | }); 1296 | 1297 | var nodeName = this.$resized_widget.get(0).tagName; 1298 | this.$resize_preview_holder = $('<' + nodeName + ' />', { 1299 | 'class': 'preview-holder resize-preview-holder', 1300 | 'data-row': this.$resized_widget.attr('data-row'), 1301 | 'data-col': this.$resized_widget.attr('data-col'), 1302 | 'css': { 1303 | 'width': this.resize_initial_width, 1304 | 'height': this.resize_initial_height 1305 | } 1306 | }).appendTo(this.$el); 1307 | 1308 | this.$resized_widget.addClass('resizing'); 1309 | 1310 | if (this.options.resize.start) { 1311 | this.options.resize.start.call(this, event, ui, this.$resized_widget); 1312 | } 1313 | 1314 | this.$el.trigger('gridster:resizestart'); 1315 | }; 1316 | 1317 | 1318 | /** 1319 | * This function is executed every time a widget stops being resized. 1320 | * 1321 | * @method on_stop_resize 1322 | * @param {Event} event The original browser event 1323 | * @param {Object} ui A prepared ui object with useful drag-related data 1324 | */ 1325 | fn.on_stop_resize = function(event, ui) { 1326 | this.$resized_widget 1327 | .removeClass('resizing') 1328 | .css({ 1329 | 'width': '', 1330 | 'height': '' 1331 | }); 1332 | 1333 | delay($.proxy(function() { 1334 | this.$resize_preview_holder 1335 | .remove() 1336 | .css({ 1337 | 'min-width': '', 1338 | 'min-height': '' 1339 | }); 1340 | 1341 | if (this.options.resize.stop) { 1342 | this.options.resize.stop.call(this, event, ui, this.$resized_widget); 1343 | } 1344 | 1345 | this.$el.trigger('gridster:resizestop'); 1346 | }, this), 300); 1347 | 1348 | this.set_dom_grid_width(); 1349 | 1350 | if (this.options.autogrow_cols) { 1351 | this.drag_api.set_limits(this.cols * this.min_widget_width); 1352 | } 1353 | }; 1354 | 1355 | 1356 | /** 1357 | * This function is executed when a widget is being resized. 1358 | * 1359 | * @method on_resize 1360 | * @param {Event} event The original browser event 1361 | * @param {Object} ui A prepared ui object with useful drag-related data 1362 | */ 1363 | fn.on_resize = function(event, ui) { 1364 | var rel_x = (ui.pointer.diff_left); 1365 | var rel_y = (ui.pointer.diff_top); 1366 | var wbd_x = this.options.widget_base_dimensions[0]; 1367 | var wbd_y = this.options.widget_base_dimensions[1]; 1368 | var margin_x = this.options.widget_margins[0]; 1369 | var margin_y = this.options.widget_margins[1]; 1370 | var max_size_x = this.resize_max_size_x; 1371 | var min_size_x = this.resize_min_size_x; 1372 | var max_size_y = this.resize_max_size_y; 1373 | var min_size_y = this.resize_min_size_y; 1374 | var autogrow = this.options.autogrow_cols; 1375 | var width; 1376 | var max_width = Infinity; 1377 | var max_height = Infinity; 1378 | 1379 | var inc_units_x = Math.ceil((rel_x / (wbd_x + margin_x * 2)) - 0.2); 1380 | var inc_units_y = Math.ceil((rel_y / (wbd_y + margin_y * 2)) - 0.2); 1381 | 1382 | var size_x = Math.max(1, this.resize_initial_sizex + inc_units_x); 1383 | var size_y = Math.max(1, this.resize_initial_sizey + inc_units_y); 1384 | 1385 | var max_cols = (this.container_width / this.min_widget_width) - 1386 | this.resize_initial_col + 1; 1387 | var limit_width = ((max_cols * this.min_widget_width) - margin_x * 2); 1388 | 1389 | size_x = Math.max(Math.min(size_x, max_size_x), min_size_x); 1390 | size_x = Math.min(max_cols, size_x); 1391 | width = (max_size_x * wbd_x) + ((size_x - 1) * margin_x * 2); 1392 | max_width = Math.min(width, limit_width); 1393 | min_width = (min_size_x * wbd_x) + ((size_x - 1) * margin_x * 2); 1394 | 1395 | size_y = Math.max(Math.min(size_y, max_size_y), min_size_y); 1396 | max_height = (max_size_y * wbd_y) + ((size_y - 1) * margin_y * 2); 1397 | min_height = (min_size_y * wbd_y) + ((size_y - 1) * margin_y * 2); 1398 | 1399 | if (this.resize_dir.right) { 1400 | size_y = this.resize_initial_sizey; 1401 | } else if (this.resize_dir.bottom) { 1402 | size_x = this.resize_initial_sizex; 1403 | } 1404 | 1405 | if (autogrow) { 1406 | var last_widget_col = this.resize_initial_col + size_x - 1; 1407 | if (autogrow && this.resize_initial_last_col <= last_widget_col) { 1408 | this.set_dom_grid_width(Math.max(last_widget_col + 1, this.cols)); 1409 | 1410 | if (this.cols < last_widget_col) { 1411 | this.add_faux_cols(last_widget_col - this.cols); 1412 | } 1413 | } 1414 | } 1415 | 1416 | var css_props = {}; 1417 | !this.resize_dir.bottom && (css_props.width = Math.max(Math.min( 1418 | this.resize_initial_width + rel_x, max_width), min_width)); 1419 | !this.resize_dir.right && (css_props.height = Math.max(Math.min( 1420 | this.resize_initial_height + rel_y, max_height), min_height)); 1421 | 1422 | this.$resized_widget.css(css_props); 1423 | 1424 | if (size_x !== this.resize_last_sizex || 1425 | size_y !== this.resize_last_sizey) { 1426 | 1427 | this.resize_widget(this.$resized_widget, size_x, size_y); 1428 | this.set_dom_grid_width(this.cols); 1429 | 1430 | this.$resize_preview_holder.css({ 1431 | 'width': '', 1432 | 'height': '' 1433 | }).attr({ 1434 | 'data-row': this.$resized_widget.attr('data-row'), 1435 | 'data-sizex': size_x, 1436 | 'data-sizey': size_y 1437 | }); 1438 | } 1439 | 1440 | if (this.options.resize.resize) { 1441 | this.options.resize.resize.call(this, event, ui, this.$resized_widget); 1442 | } 1443 | 1444 | this.$el.trigger('gridster:resize'); 1445 | 1446 | this.resize_last_sizex = size_x; 1447 | this.resize_last_sizey = size_y; 1448 | }; 1449 | 1450 | 1451 | /** 1452 | * Executes the callbacks passed as arguments when a column begins to be 1453 | * overlapped or stops being overlapped. 1454 | * 1455 | * @param {Function} start_callback Function executed when a new column 1456 | * begins to be overlapped. The column is passed as first argument. 1457 | * @param {Function} stop_callback Function executed when a column stops 1458 | * being overlapped. The column is passed as first argument. 1459 | * @method on_overlapped_column_change 1460 | * @return {Class} Returns the instance of the Gridster Class. 1461 | */ 1462 | fn.on_overlapped_column_change = function(start_callback, stop_callback) { 1463 | if (!this.colliders_data.length) { 1464 | return this; 1465 | } 1466 | var cols = this.get_targeted_columns( 1467 | this.colliders_data[0].el.data.col); 1468 | 1469 | var last_n_cols = this.last_cols.length; 1470 | var n_cols = cols.length; 1471 | var i; 1472 | 1473 | for (i = 0; i < n_cols; i++) { 1474 | if ($.inArray(cols[i], this.last_cols) === -1) { 1475 | (start_callback || $.noop).call(this, cols[i]); 1476 | } 1477 | } 1478 | 1479 | for (i = 0; i< last_n_cols; i++) { 1480 | if ($.inArray(this.last_cols[i], cols) === -1) { 1481 | (stop_callback || $.noop).call(this, this.last_cols[i]); 1482 | } 1483 | } 1484 | 1485 | this.last_cols = cols; 1486 | 1487 | return this; 1488 | }; 1489 | 1490 | 1491 | /** 1492 | * Executes the callbacks passed as arguments when a row starts to be 1493 | * overlapped or stops being overlapped. 1494 | * 1495 | * @param {Function} start_callback Function executed when a new row begins 1496 | * to be overlapped. The row is passed as first argument. 1497 | * @param {Function} end_callback Function executed when a row stops being 1498 | * overlapped. The row is passed as first argument. 1499 | * @method on_overlapped_row_change 1500 | * @return {Class} Returns the instance of the Gridster Class. 1501 | */ 1502 | fn.on_overlapped_row_change = function(start_callback, end_callback) { 1503 | if (!this.colliders_data.length) { 1504 | return this; 1505 | } 1506 | var rows = this.get_targeted_rows(this.colliders_data[0].el.data.row); 1507 | var last_n_rows = this.last_rows.length; 1508 | var n_rows = rows.length; 1509 | var i; 1510 | 1511 | for (i = 0; i < n_rows; i++) { 1512 | if ($.inArray(rows[i], this.last_rows) === -1) { 1513 | (start_callback || $.noop).call(this, rows[i]); 1514 | } 1515 | } 1516 | 1517 | for (i = 0; i < last_n_rows; i++) { 1518 | if ($.inArray(this.last_rows[i], rows) === -1) { 1519 | (end_callback || $.noop).call(this, this.last_rows[i]); 1520 | } 1521 | } 1522 | 1523 | this.last_rows = rows; 1524 | }; 1525 | 1526 | 1527 | /** 1528 | * Sets the current position of the player 1529 | * 1530 | * @param {Number} col 1531 | * @param {Number} row 1532 | * @param {Boolean} no_player 1533 | * @method set_player 1534 | * @return {object} 1535 | */ 1536 | fn.set_player = function(col, row, no_player) { 1537 | var self = this; 1538 | if (!no_player) { 1539 | this.empty_cells_player_occupies(); 1540 | } 1541 | var cell = !no_player ? self.colliders_data[0].el.data : {col: col}; 1542 | var to_col = cell.col; 1543 | var to_row = row || cell.row; 1544 | 1545 | this.player_grid_data = { 1546 | col: to_col, 1547 | row: to_row, 1548 | size_y : this.player_grid_data.size_y, 1549 | size_x : this.player_grid_data.size_x 1550 | }; 1551 | 1552 | this.cells_occupied_by_player = this.get_cells_occupied( 1553 | this.player_grid_data); 1554 | 1555 | var $overlapped_widgets = this.get_widgets_overlapped( 1556 | this.player_grid_data); 1557 | 1558 | var constraints = this.widgets_constraints($overlapped_widgets); 1559 | 1560 | this.manage_movements(constraints.can_go_up, to_col, to_row); 1561 | this.manage_movements(constraints.can_not_go_up, to_col, to_row); 1562 | 1563 | /* if there is not widgets overlapping in the new player position, 1564 | * update the new placeholder position. */ 1565 | if (!$overlapped_widgets.length) { 1566 | var pp = this.can_go_player_up(this.player_grid_data); 1567 | if (pp !== false) { 1568 | to_row = pp; 1569 | } 1570 | this.set_placeholder(to_col, to_row); 1571 | } 1572 | 1573 | return { 1574 | col: to_col, 1575 | row: to_row 1576 | }; 1577 | }; 1578 | 1579 | 1580 | /** 1581 | * See which of the widgets in the $widgets param collection can go to 1582 | * a upper row and which not. 1583 | * 1584 | * @method widgets_contraints 1585 | * @param {jQuery} $widgets A jQuery wrapped collection of 1586 | * HTMLElements. 1587 | * @return {object} Returns a literal Object with two keys: `can_go_up` & 1588 | * `can_not_go_up`. Each contains a set of HTMLElements. 1589 | */ 1590 | fn.widgets_constraints = function($widgets) { 1591 | var $widgets_can_go_up = $([]); 1592 | var $widgets_can_not_go_up; 1593 | var wgd_can_go_up = []; 1594 | var wgd_can_not_go_up = []; 1595 | 1596 | $widgets.each($.proxy(function(i, w) { 1597 | var $w = $(w); 1598 | var wgd = $w.coords().grid; 1599 | if (this.can_go_widget_up(wgd)) { 1600 | $widgets_can_go_up = $widgets_can_go_up.add($w); 1601 | wgd_can_go_up.push(wgd); 1602 | } else { 1603 | wgd_can_not_go_up.push(wgd); 1604 | } 1605 | }, this)); 1606 | 1607 | $widgets_can_not_go_up = $widgets.not($widgets_can_go_up); 1608 | 1609 | return { 1610 | can_go_up: Gridster.sort_by_row_asc(wgd_can_go_up), 1611 | can_not_go_up: Gridster.sort_by_row_desc(wgd_can_not_go_up) 1612 | }; 1613 | }; 1614 | 1615 | 1616 | /** 1617 | * Sorts an Array of grid coords objects (representing the grid coords of 1618 | * each widget) in descending way. 1619 | * 1620 | * @method manage_movements 1621 | * @param {jQuery} $widgets A jQuery collection of HTMLElements 1622 | * representing the widgets you want to move. 1623 | * @param {Number} to_col The column to which we want to move the widgets. 1624 | * @param {Number} to_row The row to which we want to move the widgets. 1625 | * @return {Class} Returns the instance of the Gridster Class. 1626 | */ 1627 | fn.manage_movements = function($widgets, to_col, to_row) { 1628 | $.each($widgets, $.proxy(function(i, w) { 1629 | var wgd = w; 1630 | var $w = wgd.el; 1631 | 1632 | var can_go_widget_up = this.can_go_widget_up(wgd); 1633 | 1634 | if (can_go_widget_up) { 1635 | //target CAN go up 1636 | //so move widget up 1637 | this.move_widget_to($w, can_go_widget_up); 1638 | this.set_placeholder(to_col, can_go_widget_up + wgd.size_y); 1639 | 1640 | } else { 1641 | //target can't go up 1642 | var can_go_player_up = this.can_go_player_up( 1643 | this.player_grid_data); 1644 | 1645 | if (!can_go_player_up) { 1646 | // target can't go up 1647 | // player cant't go up 1648 | // so we need to move widget down to a position that dont 1649 | // overlaps player 1650 | var y = (to_row + this.player_grid_data.size_y) - wgd.row; 1651 | 1652 | this.move_widget_down($w, y); 1653 | this.set_placeholder(to_col, to_row); 1654 | } 1655 | } 1656 | }, this)); 1657 | 1658 | return this; 1659 | }; 1660 | 1661 | /** 1662 | * Determines if there is a widget in the row and col given. Or if the 1663 | * HTMLElement passed as first argument is the player. 1664 | * 1665 | * @method is_player 1666 | * @param {Number|HTMLElement} col_or_el A jQuery wrapped collection of 1667 | * HTMLElements. 1668 | * @param {Number} [row] The column to which we want to move the widgets. 1669 | * @return {Boolean} Returns true or false. 1670 | */ 1671 | fn.is_player = function(col_or_el, row) { 1672 | if (row && !this.gridmap[col_or_el]) { return false; } 1673 | var $w = row ? this.gridmap[col_or_el][row] : col_or_el; 1674 | return $w && ($w.is(this.$player) || $w.is(this.$helper)); 1675 | }; 1676 | 1677 | 1678 | /** 1679 | * Determines if the widget that is being dragged is currently over the row 1680 | * and col given. 1681 | * 1682 | * @method is_player_in 1683 | * @param {Number} col The column to check. 1684 | * @param {Number} row The row to check. 1685 | * @return {Boolean} Returns true or false. 1686 | */ 1687 | fn.is_player_in = function(col, row) { 1688 | var c = this.cells_occupied_by_player || {}; 1689 | return $.inArray(col, c.cols) >= 0 && $.inArray(row, c.rows) >= 0; 1690 | }; 1691 | 1692 | 1693 | /** 1694 | * Determines if the placeholder is currently over the row and col given. 1695 | * 1696 | * @method is_placeholder_in 1697 | * @param {Number} col The column to check. 1698 | * @param {Number} row The row to check. 1699 | * @return {Boolean} Returns true or false. 1700 | */ 1701 | fn.is_placeholder_in = function(col, row) { 1702 | var c = this.cells_occupied_by_placeholder || {}; 1703 | return this.is_placeholder_in_col(col) && $.inArray(row, c.rows) >= 0; 1704 | }; 1705 | 1706 | 1707 | /** 1708 | * Determines if the placeholder is currently over the column given. 1709 | * 1710 | * @method is_placeholder_in_col 1711 | * @param {Number} col The column to check. 1712 | * @return {Boolean} Returns true or false. 1713 | */ 1714 | fn.is_placeholder_in_col = function(col) { 1715 | var c = this.cells_occupied_by_placeholder || []; 1716 | return $.inArray(col, c.cols) >= 0; 1717 | }; 1718 | 1719 | 1720 | /** 1721 | * Determines if the cell represented by col and row params is empty. 1722 | * 1723 | * @method is_empty 1724 | * @param {Number} col The column to check. 1725 | * @param {Number} row The row to check. 1726 | * @return {Boolean} Returns true or false. 1727 | */ 1728 | fn.is_empty = function(col, row) { 1729 | if (typeof this.gridmap[col] !== 'undefined') { 1730 | if(typeof this.gridmap[col][row] !== 'undefined' && 1731 | this.gridmap[col][row] === false 1732 | ) { 1733 | return true; 1734 | } 1735 | return false; 1736 | } 1737 | return true; 1738 | }; 1739 | 1740 | 1741 | /** 1742 | * Determines if the cell represented by col and row params is occupied. 1743 | * 1744 | * @method is_occupied 1745 | * @param {Number} col The column to check. 1746 | * @param {Number} row The row to check. 1747 | * @return {Boolean} Returns true or false. 1748 | */ 1749 | fn.is_occupied = function(col, row) { 1750 | if (!this.gridmap[col]) { 1751 | return false; 1752 | } 1753 | 1754 | if (this.gridmap[col][row]) { 1755 | return true; 1756 | } 1757 | return false; 1758 | }; 1759 | 1760 | 1761 | /** 1762 | * Determines if there is a widget in the cell represented by col/row params. 1763 | * 1764 | * @method is_widget 1765 | * @param {Number} col The column to check. 1766 | * @param {Number} row The row to check. 1767 | * @return {Boolean|HTMLElement} Returns false if there is no widget, 1768 | * else returns the jQuery HTMLElement 1769 | */ 1770 | fn.is_widget = function(col, row) { 1771 | var cell = this.gridmap[col]; 1772 | if (!cell) { 1773 | return false; 1774 | } 1775 | 1776 | cell = cell[row]; 1777 | 1778 | if (cell) { 1779 | return cell; 1780 | } 1781 | 1782 | return false; 1783 | }; 1784 | 1785 | 1786 | /** 1787 | * Determines if there is a widget in the cell represented by col/row 1788 | * params and if this is under the widget that is being dragged. 1789 | * 1790 | * @method is_widget_under_player 1791 | * @param {Number} col The column to check. 1792 | * @param {Number} row The row to check. 1793 | * @return {Boolean} Returns true or false. 1794 | */ 1795 | fn.is_widget_under_player = function(col, row) { 1796 | if (this.is_widget(col, row)) { 1797 | return this.is_player_in(col, row); 1798 | } 1799 | return false; 1800 | }; 1801 | 1802 | 1803 | /** 1804 | * Get widgets overlapping with the player or with the object passed 1805 | * representing the grid cells. 1806 | * 1807 | * @method get_widgets_under_player 1808 | * @return {HTMLElement} Returns a jQuery collection of HTMLElements 1809 | */ 1810 | fn.get_widgets_under_player = function(cells) { 1811 | cells || (cells = this.cells_occupied_by_player || {cols: [], rows: []}); 1812 | var $widgets = $([]); 1813 | 1814 | $.each(cells.cols, $.proxy(function(i, col) { 1815 | $.each(cells.rows, $.proxy(function(i, row) { 1816 | if(this.is_widget(col, row)) { 1817 | $widgets = $widgets.add(this.gridmap[col][row]); 1818 | } 1819 | }, this)); 1820 | }, this)); 1821 | 1822 | return $widgets; 1823 | }; 1824 | 1825 | 1826 | /** 1827 | * Put placeholder at the row and column specified. 1828 | * 1829 | * @method set_placeholder 1830 | * @param {Number} col The column to which we want to move the 1831 | * placeholder. 1832 | * @param {Number} row The row to which we want to move the 1833 | * placeholder. 1834 | * @return {Class} Returns the instance of the Gridster Class. 1835 | */ 1836 | fn.set_placeholder = function(col, row) { 1837 | var phgd = $.extend({}, this.placeholder_grid_data); 1838 | var $nexts = this.widgets_below({ 1839 | col: phgd.col, 1840 | row: phgd.row, 1841 | size_y: phgd.size_y, 1842 | size_x: phgd.size_x 1843 | }); 1844 | 1845 | // Prevents widgets go out of the grid 1846 | var right_col = (col + phgd.size_x - 1); 1847 | if (right_col > this.cols) { 1848 | col = col - (right_col - col); 1849 | } 1850 | 1851 | var moved_down = this.placeholder_grid_data.row < row; 1852 | var changed_column = this.placeholder_grid_data.col !== col; 1853 | 1854 | this.placeholder_grid_data.col = col; 1855 | this.placeholder_grid_data.row = row; 1856 | 1857 | this.cells_occupied_by_placeholder = this.get_cells_occupied( 1858 | this.placeholder_grid_data); 1859 | 1860 | this.$preview_holder.attr({ 1861 | 'data-row' : row, 1862 | 'data-col' : col 1863 | }); 1864 | 1865 | if (moved_down || changed_column) { 1866 | $nexts.each($.proxy(function(i, widget) { 1867 | this.move_widget_up( 1868 | $(widget), this.placeholder_grid_data.col - col + phgd.size_y); 1869 | }, this)); 1870 | } 1871 | 1872 | var $widgets_under_ph = this.get_widgets_under_player( 1873 | this.cells_occupied_by_placeholder); 1874 | 1875 | if ($widgets_under_ph.length) { 1876 | $widgets_under_ph.each($.proxy(function(i, widget) { 1877 | var $w = $(widget); 1878 | this.move_widget_down( 1879 | $w, row + phgd.size_y - $w.data('coords').grid.row); 1880 | }, this)); 1881 | } 1882 | 1883 | }; 1884 | 1885 | 1886 | /** 1887 | * Determines whether the player can move to a position above. 1888 | * 1889 | * @method can_go_player_up 1890 | * @param {Object} widget_grid_data The actual grid coords object of the 1891 | * player. 1892 | * @return {Number|Boolean} If the player can be moved to an upper row 1893 | * returns the row number, else returns false. 1894 | */ 1895 | fn.can_go_player_up = function(widget_grid_data) { 1896 | var p_bottom_row = widget_grid_data.row + widget_grid_data.size_y - 1; 1897 | var result = true; 1898 | var upper_rows = []; 1899 | var min_row = 10000; 1900 | var $widgets_under_player = this.get_widgets_under_player(); 1901 | 1902 | /* generate an array with columns as index and array with upper rows 1903 | * empty as value */ 1904 | this.for_each_column_occupied(widget_grid_data, function(tcol) { 1905 | var grid_col = this.gridmap[tcol]; 1906 | var r = p_bottom_row + 1; 1907 | upper_rows[tcol] = []; 1908 | 1909 | while (--r > 0) { 1910 | if (this.is_empty(tcol, r) || this.is_player(tcol, r) || 1911 | this.is_widget(tcol, r) && 1912 | grid_col[r].is($widgets_under_player) 1913 | ) { 1914 | upper_rows[tcol].push(r); 1915 | min_row = r < min_row ? r : min_row; 1916 | } else { 1917 | break; 1918 | } 1919 | } 1920 | 1921 | if (upper_rows[tcol].length === 0) { 1922 | result = false; 1923 | return true; //break 1924 | } 1925 | 1926 | upper_rows[tcol].sort(function(a, b) { 1927 | return a - b; 1928 | }); 1929 | }); 1930 | 1931 | if (!result) { return false; } 1932 | 1933 | return this.get_valid_rows(widget_grid_data, upper_rows, min_row); 1934 | }; 1935 | 1936 | 1937 | /** 1938 | * Determines whether a widget can move to a position above. 1939 | * 1940 | * @method can_go_widget_up 1941 | * @param {Object} widget_grid_data The actual grid coords object of the 1942 | * widget we want to check. 1943 | * @return {Number|Boolean} If the widget can be moved to an upper row 1944 | * returns the row number, else returns false. 1945 | */ 1946 | fn.can_go_widget_up = function(widget_grid_data) { 1947 | var p_bottom_row = widget_grid_data.row + widget_grid_data.size_y - 1; 1948 | var result = true; 1949 | var upper_rows = []; 1950 | var min_row = 10000; 1951 | 1952 | /* generate an array with columns as index and array with topmost rows 1953 | * empty as value */ 1954 | this.for_each_column_occupied(widget_grid_data, function(tcol) { 1955 | var grid_col = this.gridmap[tcol]; 1956 | upper_rows[tcol] = []; 1957 | 1958 | var r = p_bottom_row + 1; 1959 | // iterate over each row 1960 | while (--r > 0) { 1961 | if (this.is_widget(tcol, r) && !this.is_player_in(tcol, r)) { 1962 | if (!grid_col[r].is(widget_grid_data.el)) { 1963 | break; 1964 | } 1965 | } 1966 | 1967 | if (!this.is_player(tcol, r) && 1968 | !this.is_placeholder_in(tcol, r) && 1969 | !this.is_player_in(tcol, r)) { 1970 | upper_rows[tcol].push(r); 1971 | } 1972 | 1973 | if (r < min_row) { 1974 | min_row = r; 1975 | } 1976 | } 1977 | 1978 | if (upper_rows[tcol].length === 0) { 1979 | result = false; 1980 | return true; //break 1981 | } 1982 | 1983 | upper_rows[tcol].sort(function(a, b) { 1984 | return a - b; 1985 | }); 1986 | }); 1987 | 1988 | if (!result) { return false; } 1989 | 1990 | return this.get_valid_rows(widget_grid_data, upper_rows, min_row); 1991 | }; 1992 | 1993 | 1994 | /** 1995 | * Search a valid row for the widget represented by `widget_grid_data' in 1996 | * the `upper_rows` array. Iteration starts from row specified in `min_row`. 1997 | * 1998 | * @method get_valid_rows 1999 | * @param {Object} widget_grid_data The actual grid coords object of the 2000 | * player. 2001 | * @param {Array} upper_rows An array with columns as index and arrays 2002 | * of valid rows as values. 2003 | * @param {Number} min_row The upper row from which the iteration will start. 2004 | * @return {Number|Boolean} Returns the upper row valid from the `upper_rows` 2005 | * for the widget in question. 2006 | */ 2007 | fn.get_valid_rows = function(widget_grid_data, upper_rows, min_row) { 2008 | var p_top_row = widget_grid_data.row; 2009 | var p_bottom_row = widget_grid_data.row + widget_grid_data.size_y - 1; 2010 | var size_y = widget_grid_data.size_y; 2011 | var r = min_row - 1; 2012 | var valid_rows = []; 2013 | 2014 | while (++r <= p_bottom_row ) { 2015 | var common = true; 2016 | $.each(upper_rows, function(col, rows) { 2017 | if ($.isArray(rows) && $.inArray(r, rows) === -1) { 2018 | common = false; 2019 | } 2020 | }); 2021 | 2022 | if (common === true) { 2023 | valid_rows.push(r); 2024 | if (valid_rows.length === size_y) { 2025 | break; 2026 | } 2027 | } 2028 | } 2029 | 2030 | var new_row = false; 2031 | if (size_y === 1) { 2032 | if (valid_rows[0] !== p_top_row) { 2033 | new_row = valid_rows[0] || false; 2034 | } 2035 | } else { 2036 | if (valid_rows[0] !== p_top_row) { 2037 | new_row = this.get_consecutive_numbers_index( 2038 | valid_rows, size_y); 2039 | } 2040 | } 2041 | 2042 | return new_row; 2043 | }; 2044 | 2045 | 2046 | fn.get_consecutive_numbers_index = function(arr, size_y) { 2047 | var max = arr.length; 2048 | var result = []; 2049 | var first = true; 2050 | var prev = -1; // or null? 2051 | 2052 | for (var i=0; i < max; i++) { 2053 | if (first || arr[i] === prev + 1) { 2054 | result.push(i); 2055 | if (result.length === size_y) { 2056 | break; 2057 | } 2058 | first = false; 2059 | } else { 2060 | result = []; 2061 | first = true; 2062 | } 2063 | 2064 | prev = arr[i]; 2065 | } 2066 | 2067 | return result.length >= size_y ? arr[result[0]] : false; 2068 | }; 2069 | 2070 | 2071 | /** 2072 | * Get widgets overlapping with the player. 2073 | * 2074 | * @method get_widgets_overlapped 2075 | * @return {jQuery} Returns a jQuery collection of HTMLElements. 2076 | */ 2077 | fn.get_widgets_overlapped = function() { 2078 | var $w; 2079 | var $widgets = $([]); 2080 | var used = []; 2081 | var rows_from_bottom = this.cells_occupied_by_player.rows.slice(0); 2082 | rows_from_bottom.reverse(); 2083 | 2084 | $.each(this.cells_occupied_by_player.cols, $.proxy(function(i, col) { 2085 | $.each(rows_from_bottom, $.proxy(function(i, row) { 2086 | // if there is a widget in the player position 2087 | if (!this.gridmap[col]) { return true; } //next iteration 2088 | var $w = this.gridmap[col][row]; 2089 | if (this.is_occupied(col, row) && !this.is_player($w) && 2090 | $.inArray($w, used) === -1 2091 | ) { 2092 | $widgets = $widgets.add($w); 2093 | used.push($w); 2094 | } 2095 | 2096 | }, this)); 2097 | }, this)); 2098 | 2099 | return $widgets; 2100 | }; 2101 | 2102 | 2103 | /** 2104 | * This callback is executed when the player begins to collide with a column. 2105 | * 2106 | * @method on_start_overlapping_column 2107 | * @param {Number} col The collided column. 2108 | * @return {jQuery} Returns a jQuery collection of HTMLElements. 2109 | */ 2110 | fn.on_start_overlapping_column = function(col) { 2111 | this.set_player(col, false); 2112 | }; 2113 | 2114 | 2115 | /** 2116 | * A callback executed when the player begins to collide with a row. 2117 | * 2118 | * @method on_start_overlapping_row 2119 | * @param {Number} row The collided row. 2120 | * @return {jQuery} Returns a jQuery collection of HTMLElements. 2121 | */ 2122 | fn.on_start_overlapping_row = function(row) { 2123 | this.set_player(false, row); 2124 | }; 2125 | 2126 | 2127 | /** 2128 | * A callback executed when the the player ends to collide with a column. 2129 | * 2130 | * @method on_stop_overlapping_column 2131 | * @param {Number} col The collided row. 2132 | * @return {jQuery} Returns a jQuery collection of HTMLElements. 2133 | */ 2134 | fn.on_stop_overlapping_column = function(col) { 2135 | this.set_player(col, false); 2136 | 2137 | var self = this; 2138 | this.for_each_widget_below(col, this.cells_occupied_by_player.rows[0], 2139 | function(tcol, trow) { 2140 | self.move_widget_up(this, self.player_grid_data.size_y); 2141 | }); 2142 | }; 2143 | 2144 | 2145 | /** 2146 | * This callback is executed when the player ends to collide with a row. 2147 | * 2148 | * @method on_stop_overlapping_row 2149 | * @param {Number} row The collided row. 2150 | * @return {jQuery} Returns a jQuery collection of HTMLElements. 2151 | */ 2152 | fn.on_stop_overlapping_row = function(row) { 2153 | this.set_player(false, row); 2154 | 2155 | var self = this; 2156 | var cols = this.cells_occupied_by_player.cols; 2157 | for (var c = 0, cl = cols.length; c < cl; c++) { 2158 | this.for_each_widget_below(cols[c], row, function(tcol, trow) { 2159 | self.move_widget_up(this, self.player_grid_data.size_y); 2160 | }); 2161 | } 2162 | }; 2163 | 2164 | 2165 | /** 2166 | * Move a widget to a specific row. The cell or cells must be empty. 2167 | * If the widget has widgets below, all of these widgets will be moved also 2168 | * if they can. 2169 | * 2170 | * @method move_widget_to 2171 | * @param {HTMLElement} $widget The jQuery wrapped HTMLElement of the 2172 | * widget is going to be moved. 2173 | * @return {Class} Returns the instance of the Gridster Class. 2174 | */ 2175 | fn.move_widget_to = function($widget, row) { 2176 | var self = this; 2177 | var widget_grid_data = $widget.coords().grid; 2178 | var diff = row - widget_grid_data.row; 2179 | var $next_widgets = this.widgets_below($widget); 2180 | 2181 | var can_move_to_new_cell = this.can_move_to( 2182 | widget_grid_data, widget_grid_data.col, row, $widget); 2183 | 2184 | if (can_move_to_new_cell === false) { 2185 | return false; 2186 | } 2187 | 2188 | this.remove_from_gridmap(widget_grid_data); 2189 | widget_grid_data.row = row; 2190 | this.add_to_gridmap(widget_grid_data); 2191 | $widget.attr('data-row', row); 2192 | this.$changed = this.$changed.add($widget); 2193 | 2194 | 2195 | $next_widgets.each(function(i, widget) { 2196 | var $w = $(widget); 2197 | var wgd = $w.coords().grid; 2198 | var can_go_up = self.can_go_widget_up(wgd); 2199 | if (can_go_up && can_go_up !== wgd.row) { 2200 | self.move_widget_to($w, can_go_up); 2201 | } 2202 | }); 2203 | 2204 | return this; 2205 | }; 2206 | 2207 | 2208 | /** 2209 | * Move up the specified widget and all below it. 2210 | * 2211 | * @method move_widget_up 2212 | * @param {HTMLElement} $widget The widget you want to move. 2213 | * @param {Number} [y_units] The number of cells that the widget has to move. 2214 | * @return {Class} Returns the instance of the Gridster Class. 2215 | */ 2216 | fn.move_widget_up = function($widget, y_units) { 2217 | var el_grid_data = $widget.coords().grid; 2218 | var actual_row = el_grid_data.row; 2219 | var moved = []; 2220 | var can_go_up = true; 2221 | y_units || (y_units = 1); 2222 | 2223 | if (!this.can_go_up($widget)) { return false; } //break; 2224 | 2225 | this.for_each_column_occupied(el_grid_data, function(col) { 2226 | // can_go_up 2227 | if ($.inArray($widget, moved) === -1) { 2228 | var widget_grid_data = $widget.coords().grid; 2229 | var next_row = actual_row - y_units; 2230 | next_row = this.can_go_up_to_row( 2231 | widget_grid_data, col, next_row); 2232 | 2233 | if (!next_row) { 2234 | return true; 2235 | } 2236 | 2237 | var $next_widgets = this.widgets_below($widget); 2238 | 2239 | this.remove_from_gridmap(widget_grid_data); 2240 | widget_grid_data.row = next_row; 2241 | this.add_to_gridmap(widget_grid_data); 2242 | $widget.attr('data-row', widget_grid_data.row); 2243 | this.$changed = this.$changed.add($widget); 2244 | 2245 | moved.push($widget); 2246 | 2247 | $next_widgets.each($.proxy(function(i, widget) { 2248 | this.move_widget_up($(widget), y_units); 2249 | }, this)); 2250 | } 2251 | }); 2252 | 2253 | }; 2254 | 2255 | 2256 | /** 2257 | * Move down the specified widget and all below it. 2258 | * 2259 | * @method move_widget_down 2260 | * @param {jQuery} $widget The jQuery object representing the widget 2261 | * you want to move. 2262 | * @param {Number} y_units The number of cells that the widget has to move. 2263 | * @return {Class} Returns the instance of the Gridster Class. 2264 | */ 2265 | fn.move_widget_down = function($widget, y_units) { 2266 | var el_grid_data, actual_row, moved, y_diff; 2267 | 2268 | if (y_units <= 0) { return false; } 2269 | 2270 | el_grid_data = $widget.coords().grid; 2271 | actual_row = el_grid_data.row; 2272 | moved = []; 2273 | y_diff = y_units; 2274 | 2275 | if (!$widget) { return false; } 2276 | 2277 | if ($.inArray($widget, moved) === -1) { 2278 | 2279 | var widget_grid_data = $widget.coords().grid; 2280 | var next_row = actual_row + y_units; 2281 | var $next_widgets = this.widgets_below($widget); 2282 | 2283 | this.remove_from_gridmap(widget_grid_data); 2284 | 2285 | $next_widgets.each($.proxy(function(i, widget) { 2286 | var $w = $(widget); 2287 | var wd = $w.coords().grid; 2288 | var tmp_y = this.displacement_diff( 2289 | wd, widget_grid_data, y_diff); 2290 | 2291 | if (tmp_y > 0) { 2292 | this.move_widget_down($w, tmp_y); 2293 | } 2294 | }, this)); 2295 | 2296 | widget_grid_data.row = next_row; 2297 | this.update_widget_position(widget_grid_data, $widget); 2298 | $widget.attr('data-row', widget_grid_data.row); 2299 | this.$changed = this.$changed.add($widget); 2300 | 2301 | moved.push($widget); 2302 | } 2303 | }; 2304 | 2305 | 2306 | /** 2307 | * Check if the widget can move to the specified row, else returns the 2308 | * upper row possible. 2309 | * 2310 | * @method can_go_up_to_row 2311 | * @param {Number} widget_grid_data The current grid coords object of the 2312 | * widget. 2313 | * @param {Number} col The target column. 2314 | * @param {Number} row The target row. 2315 | * @return {Boolean|Number} Returns the row number if the widget can move 2316 | * to the target position, else returns false. 2317 | */ 2318 | fn.can_go_up_to_row = function(widget_grid_data, col, row) { 2319 | var ga = this.gridmap; 2320 | var result = true; 2321 | var urc = []; // upper_rows_in_columns 2322 | var actual_row = widget_grid_data.row; 2323 | var r; 2324 | 2325 | /* generate an array with columns as index and array with 2326 | * upper rows empty in the column */ 2327 | this.for_each_column_occupied(widget_grid_data, function(tcol) { 2328 | var grid_col = ga[tcol]; 2329 | urc[tcol] = []; 2330 | 2331 | r = actual_row; 2332 | while (r--) { 2333 | if (this.is_empty(tcol, r) && 2334 | !this.is_placeholder_in(tcol, r) 2335 | ) { 2336 | urc[tcol].push(r); 2337 | } else { 2338 | break; 2339 | } 2340 | } 2341 | 2342 | if (!urc[tcol].length) { 2343 | result = false; 2344 | return true; 2345 | } 2346 | 2347 | }); 2348 | 2349 | if (!result) { return false; } 2350 | 2351 | /* get common rows starting from upper position in all the columns 2352 | * that widget occupies */ 2353 | r = row; 2354 | for (r = 1; r < actual_row; r++) { 2355 | var common = true; 2356 | 2357 | for (var uc = 0, ucl = urc.length; uc < ucl; uc++) { 2358 | if (urc[uc] && $.inArray(r, urc[uc]) === -1) { 2359 | common = false; 2360 | } 2361 | } 2362 | 2363 | if (common === true) { 2364 | result = r; 2365 | break; 2366 | } 2367 | } 2368 | 2369 | return result; 2370 | }; 2371 | 2372 | 2373 | fn.displacement_diff = function(widget_grid_data, parent_bgd, y_units) { 2374 | var actual_row = widget_grid_data.row; 2375 | var diffs = []; 2376 | var parent_max_y = parent_bgd.row + parent_bgd.size_y; 2377 | 2378 | this.for_each_column_occupied(widget_grid_data, function(col) { 2379 | var temp_y_units = 0; 2380 | 2381 | for (var r = parent_max_y; r < actual_row; r++) { 2382 | if (this.is_empty(col, r)) { 2383 | temp_y_units = temp_y_units + 1; 2384 | } 2385 | } 2386 | 2387 | diffs.push(temp_y_units); 2388 | }); 2389 | 2390 | var max_diff = Math.max.apply(Math, diffs); 2391 | y_units = (y_units - max_diff); 2392 | 2393 | return y_units > 0 ? y_units : 0; 2394 | }; 2395 | 2396 | 2397 | /** 2398 | * Get widgets below a widget. 2399 | * 2400 | * @method widgets_below 2401 | * @param {HTMLElement} $el The jQuery wrapped HTMLElement. 2402 | * @return {jQuery} A jQuery collection of HTMLElements. 2403 | */ 2404 | fn.widgets_below = function($el) { 2405 | var el_grid_data = $.isPlainObject($el) ? $el : $el.coords().grid; 2406 | var self = this; 2407 | var ga = this.gridmap; 2408 | var next_row = el_grid_data.row + el_grid_data.size_y - 1; 2409 | var $nexts = $([]); 2410 | 2411 | this.for_each_column_occupied(el_grid_data, function(col) { 2412 | self.for_each_widget_below(col, next_row, function(tcol, trow) { 2413 | if (!self.is_player(this) && $.inArray(this, $nexts) === -1) { 2414 | $nexts = $nexts.add(this); 2415 | return true; // break 2416 | } 2417 | }); 2418 | }); 2419 | 2420 | return Gridster.sort_by_row_asc($nexts); 2421 | }; 2422 | 2423 | 2424 | /** 2425 | * Update the array of mapped positions with the new player position. 2426 | * 2427 | * @method set_cells_player_occupies 2428 | * @param {Number} col The new player col. 2429 | * @param {Number} col The new player row. 2430 | * @return {Class} Returns the instance of the Gridster Class. 2431 | */ 2432 | fn.set_cells_player_occupies = function(col, row) { 2433 | this.remove_from_gridmap(this.placeholder_grid_data); 2434 | this.placeholder_grid_data.col = col; 2435 | this.placeholder_grid_data.row = row; 2436 | this.add_to_gridmap(this.placeholder_grid_data, this.$player); 2437 | return this; 2438 | }; 2439 | 2440 | 2441 | /** 2442 | * Remove from the array of mapped positions the reference to the player. 2443 | * 2444 | * @method empty_cells_player_occupies 2445 | * @return {Class} Returns the instance of the Gridster Class. 2446 | */ 2447 | fn.empty_cells_player_occupies = function() { 2448 | this.remove_from_gridmap(this.placeholder_grid_data); 2449 | return this; 2450 | }; 2451 | 2452 | 2453 | fn.can_go_up = function($el) { 2454 | var el_grid_data = $el.coords().grid; 2455 | var initial_row = el_grid_data.row; 2456 | var prev_row = initial_row - 1; 2457 | var ga = this.gridmap; 2458 | var upper_rows_by_column = []; 2459 | 2460 | var result = true; 2461 | if (initial_row === 1) { return false; } 2462 | 2463 | this.for_each_column_occupied(el_grid_data, function(col) { 2464 | var $w = this.is_widget(col, prev_row); 2465 | 2466 | if (this.is_occupied(col, prev_row) || 2467 | this.is_player(col, prev_row) || 2468 | this.is_placeholder_in(col, prev_row) || 2469 | this.is_player_in(col, prev_row) 2470 | ) { 2471 | result = false; 2472 | return true; //break 2473 | } 2474 | }); 2475 | 2476 | return result; 2477 | }; 2478 | 2479 | 2480 | /** 2481 | * Check if it's possible to move a widget to a specific col/row. It takes 2482 | * into account the dimensions (`size_y` and `size_x` attrs. of the grid 2483 | * coords object) the widget occupies. 2484 | * 2485 | * @method can_move_to 2486 | * @param {Object} widget_grid_data The grid coords object that represents 2487 | * the widget. 2488 | * @param {Object} col The col to check. 2489 | * @param {Object} row The row to check. 2490 | * @param {Number} [max_row] The max row allowed. 2491 | * @return {Boolean} Returns true if all cells are empty, else return false. 2492 | */ 2493 | fn.can_move_to = function(widget_grid_data, col, row, max_row) { 2494 | var ga = this.gridmap; 2495 | var $w = widget_grid_data.el; 2496 | var future_wd = { 2497 | size_y: widget_grid_data.size_y, 2498 | size_x: widget_grid_data.size_x, 2499 | col: col, 2500 | row: row 2501 | }; 2502 | var result = true; 2503 | 2504 | //Prevents widgets go out of the grid 2505 | var right_col = col + widget_grid_data.size_x - 1; 2506 | if (right_col > this.cols) { 2507 | return false; 2508 | } 2509 | 2510 | if (max_row && max_row < row + widget_grid_data.size_y - 1) { 2511 | return false; 2512 | } 2513 | 2514 | this.for_each_cell_occupied(future_wd, function(tcol, trow) { 2515 | var $tw = this.is_widget(tcol, trow); 2516 | if ($tw && (!widget_grid_data.el || $tw.is($w))) { 2517 | result = false; 2518 | } 2519 | }); 2520 | 2521 | return result; 2522 | }; 2523 | 2524 | 2525 | /** 2526 | * Given the leftmost column returns all columns that are overlapping 2527 | * with the player. 2528 | * 2529 | * @method get_targeted_columns 2530 | * @param {Number} [from_col] The leftmost column. 2531 | * @return {Array} Returns an array with column numbers. 2532 | */ 2533 | fn.get_targeted_columns = function(from_col) { 2534 | var max = (from_col || this.player_grid_data.col) + 2535 | (this.player_grid_data.size_x - 1); 2536 | var cols = []; 2537 | for (var col = from_col; col <= max; col++) { 2538 | cols.push(col); 2539 | } 2540 | return cols; 2541 | }; 2542 | 2543 | 2544 | /** 2545 | * Given the upper row returns all rows that are overlapping with the player. 2546 | * 2547 | * @method get_targeted_rows 2548 | * @param {Number} [from_row] The upper row. 2549 | * @return {Array} Returns an array with row numbers. 2550 | */ 2551 | fn.get_targeted_rows = function(from_row) { 2552 | var max = (from_row || this.player_grid_data.row) + 2553 | (this.player_grid_data.size_y - 1); 2554 | var rows = []; 2555 | for (var row = from_row; row <= max; row++) { 2556 | rows.push(row); 2557 | } 2558 | return rows; 2559 | }; 2560 | 2561 | /** 2562 | * Get all columns and rows that a widget occupies. 2563 | * 2564 | * @method get_cells_occupied 2565 | * @param {Object} el_grid_data The grid coords object of the widget. 2566 | * @return {Object} Returns an object like `{ cols: [], rows: []}`. 2567 | */ 2568 | fn.get_cells_occupied = function(el_grid_data) { 2569 | var cells = { cols: [], rows: []}; 2570 | var i; 2571 | if (arguments[1] instanceof $) { 2572 | el_grid_data = arguments[1].coords().grid; 2573 | } 2574 | 2575 | for (i = 0; i < el_grid_data.size_x; i++) { 2576 | var col = el_grid_data.col + i; 2577 | cells.cols.push(col); 2578 | } 2579 | 2580 | for (i = 0; i < el_grid_data.size_y; i++) { 2581 | var row = el_grid_data.row + i; 2582 | cells.rows.push(row); 2583 | } 2584 | 2585 | return cells; 2586 | }; 2587 | 2588 | 2589 | /** 2590 | * Iterate over the cells occupied by a widget executing a function for 2591 | * each one. 2592 | * 2593 | * @method for_each_cell_occupied 2594 | * @param {Object} el_grid_data The grid coords object that represents the 2595 | * widget. 2596 | * @param {Function} callback The function to execute on each column 2597 | * iteration. Column and row are passed as arguments. 2598 | * @return {Class} Returns the instance of the Gridster Class. 2599 | */ 2600 | fn.for_each_cell_occupied = function(grid_data, callback) { 2601 | this.for_each_column_occupied(grid_data, function(col) { 2602 | this.for_each_row_occupied(grid_data, function(row) { 2603 | callback.call(this, col, row); 2604 | }); 2605 | }); 2606 | return this; 2607 | }; 2608 | 2609 | 2610 | /** 2611 | * Iterate over the columns occupied by a widget executing a function for 2612 | * each one. 2613 | * 2614 | * @method for_each_column_occupied 2615 | * @param {Object} el_grid_data The grid coords object that represents 2616 | * the widget. 2617 | * @param {Function} callback The function to execute on each column 2618 | * iteration. The column number is passed as first argument. 2619 | * @return {Class} Returns the instance of the Gridster Class. 2620 | */ 2621 | fn.for_each_column_occupied = function(el_grid_data, callback) { 2622 | for (var i = 0; i < el_grid_data.size_x; i++) { 2623 | var col = el_grid_data.col + i; 2624 | callback.call(this, col, el_grid_data); 2625 | } 2626 | }; 2627 | 2628 | 2629 | /** 2630 | * Iterate over the rows occupied by a widget executing a function for 2631 | * each one. 2632 | * 2633 | * @method for_each_row_occupied 2634 | * @param {Object} el_grid_data The grid coords object that represents 2635 | * the widget. 2636 | * @param {Function} callback The function to execute on each column 2637 | * iteration. The row number is passed as first argument. 2638 | * @return {Class} Returns the instance of the Gridster Class. 2639 | */ 2640 | fn.for_each_row_occupied = function(el_grid_data, callback) { 2641 | for (var i = 0; i < el_grid_data.size_y; i++) { 2642 | var row = el_grid_data.row + i; 2643 | callback.call(this, row, el_grid_data); 2644 | } 2645 | }; 2646 | 2647 | 2648 | 2649 | fn._traversing_widgets = function(type, direction, col, row, callback) { 2650 | var ga = this.gridmap; 2651 | if (!ga[col]) { return; } 2652 | 2653 | var cr, max; 2654 | var action = type + '/' + direction; 2655 | if (arguments[2] instanceof $) { 2656 | var el_grid_data = arguments[2].coords().grid; 2657 | col = el_grid_data.col; 2658 | row = el_grid_data.row; 2659 | callback = arguments[3]; 2660 | } 2661 | var matched = []; 2662 | var trow = row; 2663 | 2664 | 2665 | var methods = { 2666 | 'for_each/above': function() { 2667 | while (trow--) { 2668 | if (trow > 0 && this.is_widget(col, trow) && 2669 | $.inArray(ga[col][trow], matched) === -1 2670 | ) { 2671 | cr = callback.call(ga[col][trow], col, trow); 2672 | matched.push(ga[col][trow]); 2673 | if (cr) { break; } 2674 | } 2675 | } 2676 | }, 2677 | 'for_each/below': function() { 2678 | for (trow = row + 1, max = ga[col].length; trow < max; trow++) { 2679 | if (this.is_widget(col, trow) && 2680 | $.inArray(ga[col][trow], matched) === -1 2681 | ) { 2682 | cr = callback.call(ga[col][trow], col, trow); 2683 | matched.push(ga[col][trow]); 2684 | if (cr) { break; } 2685 | } 2686 | } 2687 | } 2688 | }; 2689 | 2690 | if (methods[action]) { 2691 | methods[action].call(this); 2692 | } 2693 | }; 2694 | 2695 | 2696 | /** 2697 | * Iterate over each widget above the column and row specified. 2698 | * 2699 | * @method for_each_widget_above 2700 | * @param {Number} col The column to start iterating. 2701 | * @param {Number} row The row to start iterating. 2702 | * @param {Function} callback The function to execute on each widget 2703 | * iteration. The value of `this` inside the function is the jQuery 2704 | * wrapped HTMLElement. 2705 | * @return {Class} Returns the instance of the Gridster Class. 2706 | */ 2707 | fn.for_each_widget_above = function(col, row, callback) { 2708 | this._traversing_widgets('for_each', 'above', col, row, callback); 2709 | return this; 2710 | }; 2711 | 2712 | 2713 | /** 2714 | * Iterate over each widget below the column and row specified. 2715 | * 2716 | * @method for_each_widget_below 2717 | * @param {Number} col The column to start iterating. 2718 | * @param {Number} row The row to start iterating. 2719 | * @param {Function} callback The function to execute on each widget 2720 | * iteration. The value of `this` inside the function is the jQuery wrapped 2721 | * HTMLElement. 2722 | * @return {Class} Returns the instance of the Gridster Class. 2723 | */ 2724 | fn.for_each_widget_below = function(col, row, callback) { 2725 | this._traversing_widgets('for_each', 'below', col, row, callback); 2726 | return this; 2727 | }; 2728 | 2729 | 2730 | /** 2731 | * Returns the highest occupied cell in the grid. 2732 | * 2733 | * @method get_highest_occupied_cell 2734 | * @return {Object} Returns an object with `col` and `row` numbers. 2735 | */ 2736 | fn.get_highest_occupied_cell = function() { 2737 | var r; 2738 | var gm = this.gridmap; 2739 | var rl = gm[1].length; 2740 | var rows = [], cols = []; 2741 | var row_in_col = []; 2742 | for (var c = gm.length - 1; c >= 1; c--) { 2743 | for (r = rl - 1; r >= 1; r--) { 2744 | if (this.is_widget(c, r)) { 2745 | rows.push(r); 2746 | cols.push(c); 2747 | break; 2748 | } 2749 | } 2750 | } 2751 | 2752 | return { 2753 | col: Math.max.apply(Math, cols), 2754 | row: Math.max.apply(Math, rows) 2755 | }; 2756 | }; 2757 | 2758 | 2759 | fn.get_widgets_from = function(col, row) { 2760 | var ga = this.gridmap; 2761 | var $widgets = $(); 2762 | 2763 | if (col) { 2764 | $widgets = $widgets.add( 2765 | this.$widgets.filter(function() { 2766 | var tcol = $(this).attr('data-col'); 2767 | return (tcol === col || tcol > col); 2768 | }) 2769 | ); 2770 | } 2771 | 2772 | if (row) { 2773 | $widgets = $widgets.add( 2774 | this.$widgets.filter(function() { 2775 | var trow = $(this).attr('data-row'); 2776 | return (trow === row || trow > row); 2777 | }) 2778 | ); 2779 | } 2780 | 2781 | return $widgets; 2782 | }; 2783 | 2784 | 2785 | /** 2786 | * Set the current height of the parent grid. 2787 | * 2788 | * @method set_dom_grid_height 2789 | * @return {Object} Returns the instance of the Gridster class. 2790 | */ 2791 | fn.set_dom_grid_height = function(height) { 2792 | if (typeof height === 'undefined') { 2793 | var r = this.get_highest_occupied_cell().row; 2794 | height = r * this.min_widget_height; 2795 | } 2796 | 2797 | this.container_height = height; 2798 | this.$el.css('height', this.container_height); 2799 | return this; 2800 | }; 2801 | 2802 | /** 2803 | * Set the current width of the parent grid. 2804 | * 2805 | * @method set_dom_grid_width 2806 | * @return {Object} Returns the instance of the Gridster class. 2807 | */ 2808 | fn.set_dom_grid_width = function(cols) { 2809 | if (typeof cols === 'undefined') { 2810 | cols = this.get_highest_occupied_cell().col; 2811 | } 2812 | 2813 | var max_cols = (this.options.autogrow_cols ? this.options.max_cols : 2814 | this.cols); 2815 | 2816 | cols = Math.min(max_cols, Math.max(cols, this.options.min_cols)); 2817 | this.container_width = cols * this.min_widget_width; 2818 | this.$el.css('width', this.container_width); 2819 | return this; 2820 | }; 2821 | 2822 | 2823 | /** 2824 | * It generates the neccessary styles to position the widgets. 2825 | * 2826 | * @method generate_stylesheet 2827 | * @param {Number} rows Number of columns. 2828 | * @param {Number} cols Number of rows. 2829 | * @return {Object} Returns the instance of the Gridster class. 2830 | */ 2831 | fn.generate_stylesheet = function(opts) { 2832 | var styles = ''; 2833 | var max_size_x = this.options.max_size_x || this.cols; 2834 | var max_rows = 0; 2835 | var max_cols = 0; 2836 | var i; 2837 | var rules; 2838 | 2839 | opts || (opts = {}); 2840 | opts.cols || (opts.cols = this.cols); 2841 | opts.rows || (opts.rows = this.rows); 2842 | opts.namespace || (opts.namespace = this.options.namespace); 2843 | opts.widget_base_dimensions || 2844 | (opts.widget_base_dimensions = this.options.widget_base_dimensions); 2845 | opts.widget_margins || 2846 | (opts.widget_margins = this.options.widget_margins); 2847 | opts.min_widget_width = (opts.widget_margins[0] * 2) + 2848 | opts.widget_base_dimensions[0]; 2849 | opts.min_widget_height = (opts.widget_margins[1] * 2) + 2850 | opts.widget_base_dimensions[1]; 2851 | 2852 | // don't duplicate stylesheets for the same configuration 2853 | var serialized_opts = $.param(opts); 2854 | if ($.inArray(serialized_opts, Gridster.generated_stylesheets) >= 0) { 2855 | return false; 2856 | } 2857 | 2858 | this.generated_stylesheets.push(serialized_opts); 2859 | Gridster.generated_stylesheets.push(serialized_opts); 2860 | 2861 | /* generate CSS styles for cols */ 2862 | for (i = opts.cols; i >= 0; i--) { 2863 | styles += (opts.namespace + ' [data-col="'+ (i + 1) + '"] { left:' + 2864 | ((i * opts.widget_base_dimensions[0]) + 2865 | (i * opts.widget_margins[0]) + 2866 | ((i + 1) * opts.widget_margins[0])) + 'px; }\n'); 2867 | } 2868 | 2869 | /* generate CSS styles for rows */ 2870 | for (i = opts.rows; i >= 0; i--) { 2871 | styles += (opts.namespace + ' [data-row="' + (i + 1) + '"] { top:' + 2872 | ((i * opts.widget_base_dimensions[1]) + 2873 | (i * opts.widget_margins[1]) + 2874 | ((i + 1) * opts.widget_margins[1]) ) + 'px; }\n'); 2875 | } 2876 | 2877 | for (var y = 1; y <= opts.rows; y++) { 2878 | styles += (opts.namespace + ' [data-sizey="' + y + '"] { height:' + 2879 | (y * opts.widget_base_dimensions[1] + 2880 | (y - 1) * (opts.widget_margins[1] * 2)) + 'px; }\n'); 2881 | } 2882 | 2883 | for (var x = 1; x <= max_size_x; x++) { 2884 | styles += (opts.namespace + ' [data-sizex="' + x + '"] { width:' + 2885 | (x * opts.widget_base_dimensions[0] + 2886 | (x - 1) * (opts.widget_margins[0] * 2)) + 'px; }\n'); 2887 | } 2888 | 2889 | this.remove_style_tags(); 2890 | 2891 | return this.add_style_tag(styles); 2892 | }; 2893 | 2894 | 2895 | /** 2896 | * Injects the given CSS as string to the head of the document. 2897 | * 2898 | * @method add_style_tag 2899 | * @param {String} css The styles to apply. 2900 | * @return {Object} Returns the instance of the Gridster class. 2901 | */ 2902 | fn.add_style_tag = function(css) { 2903 | var d = document; 2904 | var tag = d.createElement('style'); 2905 | 2906 | d.getElementsByTagName('head')[0].appendChild(tag); 2907 | tag.setAttribute('type', 'text/css'); 2908 | 2909 | if (tag.styleSheet) { 2910 | tag.styleSheet.cssText = css; 2911 | } 2912 | else{ 2913 | tag.appendChild(document.createTextNode(css)); 2914 | } 2915 | 2916 | this.remove_style_tags(); 2917 | this.$style_tags = this.$style_tags.add(tag); 2918 | 2919 | return this; 2920 | }; 2921 | 2922 | 2923 | /** 2924 | * Remove the style tag with the associated id from the head of the document 2925 | * 2926 | * @method remove_style_tag 2927 | * @return {Object} Returns the instance of the Gridster class. 2928 | */ 2929 | fn.remove_style_tags = function() { 2930 | var all_styles = Gridster.generated_stylesheets; 2931 | var ins_styles = this.generated_stylesheets; 2932 | 2933 | this.$style_tags.remove(); 2934 | 2935 | Gridster.generated_stylesheets = $.map(all_styles, function(s) { 2936 | if ($.inArray(s, ins_styles) === -1) { return s; } 2937 | }); 2938 | }; 2939 | 2940 | 2941 | /** 2942 | * Generates a faux grid to collide with it when a widget is dragged and 2943 | * detect row or column that we want to go. 2944 | * 2945 | * @method generate_faux_grid 2946 | * @param {Number} rows Number of columns. 2947 | * @param {Number} cols Number of rows. 2948 | * @return {Object} Returns the instance of the Gridster class. 2949 | */ 2950 | fn.generate_faux_grid = function(rows, cols) { 2951 | this.faux_grid = []; 2952 | this.gridmap = []; 2953 | var col; 2954 | var row; 2955 | for (col = cols; col > 0; col--) { 2956 | this.gridmap[col] = []; 2957 | for (row = rows; row > 0; row--) { 2958 | this.add_faux_cell(row, col); 2959 | } 2960 | } 2961 | return this; 2962 | }; 2963 | 2964 | 2965 | /** 2966 | * Add cell to the faux grid. 2967 | * 2968 | * @method add_faux_cell 2969 | * @param {Number} row The row for the new faux cell. 2970 | * @param {Number} col The col for the new faux cell. 2971 | * @return {Object} Returns the instance of the Gridster class. 2972 | */ 2973 | fn.add_faux_cell = function(row, col) { 2974 | var coords = $({ 2975 | left: this.baseX + ((col - 1) * this.min_widget_width), 2976 | top: this.baseY + (row -1) * this.min_widget_height, 2977 | width: this.min_widget_width, 2978 | height: this.min_widget_height, 2979 | col: col, 2980 | row: row, 2981 | original_col: col, 2982 | original_row: row 2983 | }).coords(); 2984 | 2985 | if (!$.isArray(this.gridmap[col])) { 2986 | this.gridmap[col] = []; 2987 | } 2988 | 2989 | this.gridmap[col][row] = false; 2990 | this.faux_grid.push(coords); 2991 | 2992 | return this; 2993 | }; 2994 | 2995 | 2996 | /** 2997 | * Add rows to the faux grid. 2998 | * 2999 | * @method add_faux_rows 3000 | * @param {Number} rows The number of rows you want to add to the faux grid. 3001 | * @return {Object} Returns the instance of the Gridster class. 3002 | */ 3003 | fn.add_faux_rows = function(rows) { 3004 | var actual_rows = this.rows; 3005 | var max_rows = actual_rows + (rows || 1); 3006 | 3007 | for (var r = max_rows; r > actual_rows; r--) { 3008 | for (var c = this.cols; c >= 1; c--) { 3009 | this.add_faux_cell(r, c); 3010 | } 3011 | } 3012 | 3013 | this.rows = max_rows; 3014 | 3015 | if (this.options.autogenerate_stylesheet) { 3016 | this.generate_stylesheet(); 3017 | } 3018 | 3019 | return this; 3020 | }; 3021 | 3022 | /** 3023 | * Add cols to the faux grid. 3024 | * 3025 | * @method add_faux_cols 3026 | * @param {Number} cols The number of cols you want to add to the faux grid. 3027 | * @return {Object} Returns the instance of the Gridster class. 3028 | */ 3029 | fn.add_faux_cols = function(cols) { 3030 | var actual_cols = this.cols; 3031 | var max_cols = actual_cols + (cols || 1); 3032 | max_cols = Math.min(max_cols, this.options.max_cols); 3033 | 3034 | for (var c = actual_cols + 1; c <= max_cols; c++) { 3035 | for (var r = this.rows; r >= 1; r--) { 3036 | this.add_faux_cell(r, c); 3037 | } 3038 | } 3039 | 3040 | this.cols = max_cols; 3041 | 3042 | if (this.options.autogenerate_stylesheet) { 3043 | this.generate_stylesheet(); 3044 | } 3045 | 3046 | return this; 3047 | }; 3048 | 3049 | 3050 | /** 3051 | * Recalculates the offsets for the faux grid. You need to use it when 3052 | * the browser is resized. 3053 | * 3054 | * @method recalculate_faux_grid 3055 | * @return {Object} Returns the instance of the Gridster class. 3056 | */ 3057 | fn.recalculate_faux_grid = function() { 3058 | var aw = this.$wrapper.width(); 3059 | this.baseX = ($(window).width() - aw) / 2; 3060 | this.baseY = this.$wrapper.offset().top; 3061 | 3062 | $.each(this.faux_grid, $.proxy(function(i, coords) { 3063 | this.faux_grid[i] = coords.update({ 3064 | left: this.baseX + (coords.data.col -1) * this.min_widget_width, 3065 | top: this.baseY + (coords.data.row -1) * this.min_widget_height 3066 | }); 3067 | }, this)); 3068 | 3069 | return this; 3070 | }; 3071 | 3072 | 3073 | /** 3074 | * Get all widgets in the DOM and register them. 3075 | * 3076 | * @method get_widgets_from_DOM 3077 | * @return {Object} Returns the instance of the Gridster class. 3078 | */ 3079 | fn.get_widgets_from_DOM = function() { 3080 | var widgets_coords = this.$widgets.map($.proxy(function(i, widget) { 3081 | var $w = $(widget); 3082 | return this.dom_to_coords($w); 3083 | }, this)); 3084 | 3085 | widgets_coords = Gridster.sort_by_row_and_col_asc(widgets_coords); 3086 | 3087 | var changes = $(widgets_coords).map($.proxy(function(i, wgd) { 3088 | return this.register_widget(wgd) || null; 3089 | }, this)); 3090 | 3091 | if (changes.length) { 3092 | this.$el.trigger('gridster:positionschanged'); 3093 | } 3094 | 3095 | return this; 3096 | }; 3097 | 3098 | 3099 | /** 3100 | * Calculate columns and rows to be set based on the configuration 3101 | * parameters, grid dimensions, etc ... 3102 | * 3103 | * @method generate_grid_and_stylesheet 3104 | * @return {Object} Returns the instance of the Gridster class. 3105 | */ 3106 | fn.generate_grid_and_stylesheet = function() { 3107 | var aw = this.$wrapper.width(); 3108 | var max_cols = this.options.max_cols; 3109 | 3110 | var cols = Math.floor(aw / this.min_widget_width) + 3111 | this.options.extra_cols; 3112 | 3113 | var actual_cols = this.$widgets.map(function() { 3114 | return $(this).attr('data-col'); 3115 | }).get(); 3116 | 3117 | //needed to pass tests with phantomjs 3118 | actual_cols.length || (actual_cols = [0]); 3119 | 3120 | var min_cols = Math.max.apply(Math, actual_cols); 3121 | 3122 | this.cols = Math.max(min_cols, cols, this.options.min_cols); 3123 | 3124 | if (max_cols !== Infinity && max_cols >= min_cols && max_cols < this.cols) { 3125 | this.cols = max_cols; 3126 | } 3127 | 3128 | // get all rows that could be occupied by the current widgets 3129 | var max_rows = this.options.extra_rows; 3130 | this.$widgets.each(function(i, w) { 3131 | max_rows += (+$(w).attr('data-sizey')); 3132 | }); 3133 | 3134 | this.rows = Math.max(max_rows, this.options.min_rows); 3135 | 3136 | this.baseX = ($(window).width() - aw) / 2; 3137 | this.baseY = this.$wrapper.offset().top; 3138 | 3139 | if (this.options.autogenerate_stylesheet) { 3140 | this.generate_stylesheet(); 3141 | } 3142 | 3143 | return this.generate_faux_grid(this.rows, this.cols); 3144 | }; 3145 | 3146 | /** 3147 | * Destroy this gridster by removing any sign of its presence, making it easy to avoid memory leaks 3148 | * 3149 | * @method destroy 3150 | * @param {Boolean} remove If true, remove gridster from DOM. 3151 | * @return {Object} Returns the instance of the Gridster class. 3152 | */ 3153 | fn.destroy = function(remove) { 3154 | this.$el.removeData('gridster'); 3155 | 3156 | // remove bound callback on window resize 3157 | $(window).unbind('.gridster'); 3158 | 3159 | if (this.drag_api) { 3160 | this.drag_api.destroy(); 3161 | } 3162 | 3163 | this.remove_style_tags(); 3164 | 3165 | remove && this.$el.remove(); 3166 | 3167 | return this; 3168 | }; 3169 | 3170 | 3171 | //jQuery adapter 3172 | $.fn.gridster = function(options) { 3173 | return this.each(function() { 3174 | if (! $(this).data('gridster')) { 3175 | $(this).data('gridster', new Gridster( this, options )); 3176 | } 3177 | }); 3178 | }; 3179 | 3180 | return Gridster; 3181 | 3182 | })); 3183 | --------------------------------------------------------------------------------