├── .bowerrc ├── .editorconfig ├── .gitattributes ├── .gitignore ├── .jsbeautifyrc ├── .jshintrc ├── .travis.yml ├── Gruntfile.js ├── LICENSE ├── README.md ├── bower.json ├── demo ├── common │ └── style.css ├── dashboard │ ├── script.js │ ├── style.css │ ├── view.html │ └── widget_settings.html └── main │ ├── script.js │ ├── style.css │ └── view.html ├── dist ├── angular-gridster.css ├── angular-gridster.min.css └── angular-gridster.min.js ├── index.html ├── karma.conf.js ├── package.json ├── ptor.conf.js ├── src ├── angular-gridster.js └── angular-gridster.less ├── test.html └── test ├── e2e └── gridster.js └── spec ├── gridster-directive.js ├── gridster-item.js └── gridster.js /.bowerrc: -------------------------------------------------------------------------------- 1 | { 2 | "directory": "bower_components" 3 | } 4 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig helps developers define and maintain consistent 2 | # coding styles between different editors and IDEs 3 | # editorconfig.org 4 | 5 | root = true 6 | 7 | 8 | [*] 9 | 10 | # Change these settings to your own preference 11 | indent_style = tab 12 | indent_size = 4 13 | 14 | # We recommend you to keep these unchanged 15 | end_of_line = lf 16 | charset = utf-8 17 | trim_trailing_whitespace = true 18 | insert_final_newline = true 19 | 20 | [*.md] 21 | trim_trailing_whitespace = false 22 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | bower_components 3 | .tmp 4 | .sass-cache 5 | nbproject 6 | coverage 7 | .idea/ 8 | -------------------------------------------------------------------------------- /.jsbeautifyrc: -------------------------------------------------------------------------------- 1 | { 2 | "css": { 3 | "indentSize": 4, 4 | "indentWithTabs": true 5 | }, 6 | "js": { 7 | "indentSize": 4, 8 | "indentWithTabs": true 9 | }, 10 | "html": { 11 | "indentSize": 4, 12 | "indentWithTabs": true 13 | } 14 | } 15 | 16 | -------------------------------------------------------------------------------- /.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "eqnull": true, 3 | "jasmine": true, 4 | "immed": true, 5 | "white": false, 6 | "unused": true, 7 | "node": true, 8 | "browser": true, 9 | "esnext": true, 10 | "bitwise": true, 11 | "camelcase": true, 12 | "curly": true, 13 | "eqeqeq": true, 14 | "immed": true, 15 | "indent": 2, 16 | "latedef": true, 17 | "newcap": true, 18 | "noarg": true, 19 | "quotmark": "single", 20 | "regexp": true, 21 | "undef": true, 22 | "unused": true, 23 | "strict": true, 24 | "trailing": true, 25 | "globals": { 26 | "after": false, 27 | "afterEach": false, 28 | "angular": false, 29 | "before": false, 30 | "beforeEach": false, 31 | "browser": false, 32 | "jquery": false, 33 | "$": false, 34 | "describe": false, 35 | "expect": false, 36 | "inject": false, 37 | "it": false, 38 | "by": false, 39 | "protractor": false, 40 | "spyOn": false 41 | } 42 | } 43 | 44 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | 3 | node_js: 4 | - 4.2.1 5 | 6 | before_script: 7 | - export DISPLAY=:99.0 8 | - sh -e /etc/init.d/xvfb start 9 | - npm install --quiet -g grunt-cli karma bower protractor 10 | - npm install 11 | - bower install 12 | - node ./node_modules/grunt-protractor-runner/node_modules/protractor/bin/webdriver-manager update --standalone 13 | - node ./node_modules/grunt-protractor-runner/node_modules/protractor/bin/webdriver-manager start > /dev/null & 14 | - sleep 5 15 | 16 | script: grunt test 17 | -------------------------------------------------------------------------------- /Gruntfile.js: -------------------------------------------------------------------------------- 1 | 'use_strict'; 2 | 3 | module.exports = function(grunt) { 4 | 5 | require('matchdep').filterDev('grunt-*').forEach(grunt.loadNpmTasks); 6 | require('time-grunt')(grunt); 7 | 8 | grunt.initConfig({ 9 | pkg: grunt.file.readJSON('package.json'), 10 | bump: { 11 | options: { 12 | files: ['package.json', 'bower.json'], 13 | updateConfigs: [], 14 | commit: false, 15 | push: false, 16 | commitMessage: 'Release v%VERSION%', 17 | commitFiles: ['package.json', 'bower.json'] 18 | } 19 | }, 20 | connect: { 21 | options: { 22 | port: 9000, 23 | hostname: 'localhost' 24 | }, 25 | dev: { 26 | options: { 27 | open: true, 28 | livereload: 35729 29 | } 30 | }, 31 | cli: { 32 | options: {} 33 | } 34 | }, 35 | jsbeautifier: { 36 | options: { 37 | config: '.jsbeautifyrc' 38 | }, 39 | files: [ 40 | 'demo/**/*.js', 41 | 'src/**/*.js', 42 | 'test/**/*.js', 43 | 'Gruntfile.js', 44 | 'karma.conf.js', 45 | 'bower.json', 46 | 'index.html', 47 | 'ptor.conf.js' 48 | ] 49 | }, 50 | jshint: { 51 | options: { 52 | jshintrc: '.jshintrc' 53 | }, 54 | files: ['src/*.js', 'test/**/*.js'] 55 | }, 56 | karma: { 57 | unit: { 58 | configFile: 'karma.conf.js', 59 | background: true, 60 | singleRun: false 61 | }, 62 | singleRun: { 63 | configFile: 'karma.conf.js', 64 | singleRun: true 65 | } 66 | }, 67 | less: { 68 | dist: { 69 | options: { 70 | compress: true 71 | }, 72 | files: { 73 | "dist/angular-gridster.min.css": "src/angular-gridster.less" 74 | } 75 | }, 76 | min: { 77 | files: { 78 | "dist/angular-gridster.css": "src/angular-gridster.less" 79 | } 80 | } 81 | }, 82 | protractor: { 83 | e2e: { 84 | options: { 85 | configFile: "ptor.conf.js", 86 | args: {} 87 | } 88 | } 89 | }, 90 | uglify: { 91 | dist: { 92 | options: { 93 | banner: ['/*', 94 | ' * <%= pkg.name %>', 95 | ' * <%= pkg.homepage %>', 96 | ' *', 97 | ' * @version: <%= pkg.version %>', 98 | ' * @license: <%= pkg.license %>', 99 | ' */\n' 100 | ].join('\n') 101 | }, 102 | files: { 103 | 'dist/angular-gridster.min.js': ['src/angular-gridster.js'] 104 | } 105 | } 106 | }, 107 | watch: { 108 | dev: { 109 | files: ['Gruntfile.js', 'karma.conf.js', 'ptor.conf.js', 'src/*', 'test/**/*.js'], 110 | tasks: ['jsbeautifier', 'jshint', 'uglify', 'less', 'karma:unit:run'], 111 | options: { 112 | reload: true, 113 | livereload: true, 114 | port: 35729 115 | } 116 | }, 117 | e2e: { // separate e2e so livereload doesn't have to wait for e2e tests 118 | files: ['src/*', 'test/**/*.js'], 119 | tasks: ['jsbeautifier', 'jshint', 'uglify', 'protractor'] 120 | } 121 | } 122 | }); 123 | 124 | grunt.registerTask('default', ['jsbeautifier', 'jshint', 'uglify', 'less']); 125 | 126 | grunt.registerTask('dev', ['connect:dev', 'karma:unit:start', 'watch:dev']); 127 | grunt.registerTask('e2e', ['connect:cli', 'protractor', 'watch:e2e']); 128 | grunt.registerTask('test', ['connect:cli', 'karma:singleRun', 'protractor']); 129 | 130 | }; 131 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2013 Manifest Web Design 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | angular-gridster 2 | ================ 3 | [![Build Status](https://travis-ci.org/ManifestWebDesign/angular-gridster.svg)](https://travis-ci.org/ManifestWebDesign/angular-gridster) 4 | 5 | An implementation of gridster-like widgets for Angular JS. This is not a wrapper on the original gridster jQuery plugin (http://gridster.net/). It is instead completely rewritten as Angular directives. Rewriting allowed for some additional features and better use of Angular data binding. Even more importantly, the original plugin had unpredictable behavior and crashed when wrapped with an Angular directive in my initial tests. 6 | 7 | ## Demo 8 | 9 | See Live Demo 10 | 11 | ## Installation 12 | 13 | ```bash 14 | bower install angular-gridster 15 | ``` 16 | 17 | Then, import the following in your HTML alongside `jQuery` and `angular`: 18 | ```html 19 | 20 | 21 | 22 | ``` 23 | 24 | `jquery.resize` is a jQuery plugin needed to check for changes in the gridster size. 25 | 26 | ## Usage 27 | 28 | 29 | ```js 30 | 31 | // load the gridster module 32 | angular.module('myModule', ['gridster']); 33 | 34 | ``` 35 | 36 | Default usage: 37 | ```HTML 38 |
39 | 42 |
43 | ``` 44 | Which expects a scope setup like the following: 45 | ``` JavaScript 46 | // IMPORTANT: Items should be placed in the grid in the order in which they should appear. 47 | // In most cases the sorting should be by row ASC, col ASC 48 | 49 | // these map directly to gridsterItem directive options 50 | $scope.standardItems = [ 51 | { sizeX: 2, sizeY: 1, row: 0, col: 0 }, 52 | { sizeX: 2, sizeY: 2, row: 0, col: 2 }, 53 | { sizeX: 1, sizeY: 1, row: 0, col: 4 }, 54 | { sizeX: 1, sizeY: 1, row: 0, col: 5 }, 55 | { sizeX: 2, sizeY: 1, row: 1, col: 0 }, 56 | { sizeX: 1, sizeY: 1, row: 1, col: 4 }, 57 | { sizeX: 1, sizeY: 2, row: 1, col: 5 }, 58 | { sizeX: 1, sizeY: 1, row: 2, col: 0 }, 59 | { sizeX: 2, sizeY: 1, row: 2, col: 1 }, 60 | { sizeX: 1, sizeY: 1, row: 2, col: 3 }, 61 | { sizeX: 1, sizeY: 1, row: 2, col: 4 } 62 | ]; 63 | ``` 64 | Alternatively, you can use the html attributes, similar to the original gridster plugin, but with two-way data binding: 65 | ```HTML 66 |
67 | 70 |
71 | ``` 72 | or: 73 | ```HTML 74 |
75 | 78 |
79 | ``` 80 | This allows the items to provide their own structure for row, col, and size: 81 | ```JavaScript 82 | $scope.customItems = [ 83 | { size: { x: 2, y: 1 }, position: [0, 0] }, 84 | { size: { x: 2, y: 2 }, position: [0, 2] }, 85 | { size: { x: 1, y: 1 }, position: [0, 4] }, 86 | { size: { x: 1, y: 1 }, position: [0, 5] }, 87 | { size: { x: 2, y: 1 }, position: [1, 0] }, 88 | { size: { x: 1, y: 1 }, position: [1, 4] }, 89 | { size: { x: 1, y: 2 }, position: [1, 5] }, 90 | { size: { x: 1, y: 1 }, position: [2, 0] }, 91 | { size: { x: 2, y: 1 }, position: [2, 1] }, 92 | { size: { x: 1, y: 1 }, position: [2, 3] }, 93 | { size: { x: 1, y: 1 }, position: [2, 4] } 94 | ]; 95 | ``` 96 | Instead of using attributes for row, col, and size, you can also just use a mapping object for the gridster-item directive: 97 | ```HTML 98 |
99 | 102 |
103 | ``` 104 | This expects a scope similar to the previous example, but with customItemMap also defined in the scope: 105 | ```JavaScript 106 | // maps the item from customItems in the scope to the gridsterItem options 107 | $scope.customItemMap = { 108 | sizeX: 'item.size.x', 109 | sizeY: 'item.size.y', 110 | row: 'item.position[0]', 111 | col: 'item.position[1]', 112 | minSizeY: 'item.minSizeY', 113 | maxSizeY: 'item.maxSizeY' 114 | }; 115 | ``` 116 | The gridsterItem directive can be configured like this: 117 | ```HTML 118 |
119 | 122 |
123 | ``` 124 | 125 | ## Configuration 126 | 127 | #### Via Scope 128 | Simply pass your desired options to the gridster directive 129 | 130 | ```JavaScript 131 | $scope.gridsterOpts = { 132 | columns: 6, // the width of the grid, in columns 133 | pushing: true, // whether to push other items out of the way on move or resize 134 | floating: true, // whether to automatically float items up so they stack (you can temporarily disable if you are adding unsorted items with ng-repeat) 135 | swapping: false, // whether or not to have items of the same size switch places instead of pushing down if they are the same size 136 | width: 'auto', // can be an integer or 'auto'. 'auto' scales gridster to be the full width of its containing element 137 | colWidth: 'auto', // can be an integer or 'auto'. 'auto' uses the pixel width of the element divided by 'columns' 138 | rowHeight: 'match', // can be an integer or 'match'. Match uses the colWidth, giving you square widgets. 139 | margins: [10, 10], // the pixel distance between each widget 140 | outerMargin: true, // whether margins apply to outer edges of the grid 141 | sparse: false, // "true" can increase performance of dragging and resizing for big grid (e.g. 20x50) 142 | isMobile: false, // stacks the grid items if true 143 | mobileBreakPoint: 600, // if the screen is not wider that this, remove the grid layout and stack the items 144 | mobileModeEnabled: true, // whether or not to toggle mobile mode when screen width is less than mobileBreakPoint 145 | minColumns: 1, // the minimum columns the grid must have 146 | minRows: 2, // the minimum height of the grid, in rows 147 | maxRows: 100, 148 | defaultSizeX: 2, // the default width of a gridster item, if not specifed 149 | defaultSizeY: 1, // the default height of a gridster item, if not specified 150 | minSizeX: 1, // minimum column width of an item 151 | maxSizeX: null, // maximum column width of an item 152 | minSizeY: 1, // minumum row height of an item 153 | maxSizeY: null, // maximum row height of an item 154 | resizable: { 155 | enabled: true, 156 | handles: ['n', 'e', 's', 'w', 'ne', 'se', 'sw', 'nw'], 157 | start: function(event, $element, widget) {}, // optional callback fired when resize is started, 158 | resize: function(event, $element, widget) {}, // optional callback fired when item is resized, 159 | stop: function(event, $element, widget) {} // optional callback fired when item is finished resizing 160 | }, 161 | draggable: { 162 | enabled: true, // whether dragging items is supported 163 | handle: '.my-class', // optional selector for drag handle 164 | start: function(event, $element, widget) {}, // optional callback fired when drag is started, 165 | drag: function(event, $element, widget) {}, // optional callback fired when item is moved, 166 | stop: function(event, $element, widget) {} // optional callback fired when item is finished dragging 167 | } 168 | }; 169 | ``` 170 | 171 | 172 | #### Via Constant 173 | You can also override the default configuration site wide by modifying the ```gridsterConfig``` constant 174 | 175 | ```js 176 | angular.module('yourApp').run(['gridsterConfig', function(gridsterConfig) { 177 | gridsterConfig.width = 1000; 178 | }]); 179 | ``` 180 | 181 | ## Controller Access 182 | 183 | The gridster and gridsterItem directive controller objects can be accessed within their scopes as 'gridster' and 'gridsterItem'. 184 | 185 | These controllers are internal APIs that are subject to change. 186 | 187 | ```html 188 |
189 | 194 |
195 | ``` 196 | 197 | 198 | ## Gridster Events 199 | 200 | #### gridster-mobile-changed 201 | When the gridster goes in or out of mobile mode, a 'gridster-mobile-changed' event is broadcast on rootScope: 202 | 203 | ```js 204 | scope.$on('gridster-mobile-changed', function(gridster) { 205 | }) 206 | ``` 207 | 208 | #### gridster-draggable-changed 209 | When the gridster draggable properties change, a 'gridster-draggable-changed' event is broadcast on rootScope: 210 | 211 | ```js 212 | scope.$on('gridster-draggable-changed', function(gridster) { 213 | }) 214 | ``` 215 | 216 | #### gridster-resizable-changed 217 | When the gridster resizable properties change, a 'gridster-resizable-changed' event is broadcast on rootScope: 218 | 219 | ```js 220 | scope.$on('gridster-resizable-changed', function(gridster) { 221 | }) 222 | ``` 223 | 224 | #### gridster-resized 225 | When the gridster element's size changes, a 'gridster-resized' event is broadcast on rootScope: 226 | 227 | ```js 228 | scope.$on('gridster-resized', function(sizes, gridster) { 229 | // sizes[0] = width 230 | // sizes[1] = height 231 | // gridster. 232 | }) 233 | ``` 234 | 235 | ## Gridster Item Events 236 | 237 | #### gridster-item-transition-end 238 | Gridster items have CSS transitions by default. Gridster items listen for css transition-end across different browsers and broadcast the event 'gridster-item-transition-end'. You can listen for it like this from within the gridster-item directive: 239 | 240 | ```js 241 | scope.$on('gridster-item-transition-end', function(item) { 242 | // item.$element 243 | // item.gridster 244 | // item.row 245 | // item.col 246 | // item.sizeX 247 | // item.sizeY 248 | // item.minSizeX 249 | // item.minSizeY 250 | // item.maxSizeX 251 | // item.maxSizeY 252 | }) 253 | ``` 254 | 255 | #### gridster-item-initialized 256 | After a gridster item's controller has finished with setup, it broadcasts an event 'gridster-item-initialized' on its own scope. You can listen for it like this from within the gridster-item directive: 257 | 258 | ```js 259 | scope.$on('gridster-item-initialized', function(item) { 260 | // item.$element 261 | // item.gridster 262 | // item.row 263 | // item.col 264 | // item.sizeX 265 | // item.sizeY 266 | // item.minSizeX 267 | // item.minSizeY 268 | // item.maxSizeX 269 | // item.maxSizeY 270 | }) 271 | ``` 272 | 273 | #### gridster-item-resized 274 | After a gridster item's size changes (rows or columns), it broadcasts an event 'gridster-item-resized' on its own scope. You can listen for it like this from within the gridster-item directive: 275 | 276 | ```js 277 | scope.$on('gridster-item-resized', function(item) { 278 | // item.$element 279 | // item.gridster 280 | // item.row 281 | // item.col 282 | // item.sizeX 283 | // item.sizeY 284 | // item.minSizeX 285 | // item.minSizeY 286 | // item.maxSizeX 287 | // item.maxSizeY 288 | }) 289 | ``` 290 | 291 | ## Watching item changes of size and position 292 | 293 | The typical Angular way would be to do a $scope.$watch on your item or items in the scope. Example: 294 | 295 | ```JavaScript 296 | // two objects, converted to gridster items in the view via ng-repeat 297 | $scope.items = [{},{}]; 298 | 299 | $scope.$watch('items', function(items){ 300 | // one of the items changed 301 | }, true); 302 | ``` 303 | 304 | or 305 | 306 | ```JavaScript 307 | $scope.$watch('items[0]', function(){ 308 | // item0 changed 309 | }, true); 310 | ``` 311 | 312 | or 313 | 314 | ```JavaScript 315 | $scope.$watch('items[0].sizeX', function(){ 316 | // item0 sizeX changed 317 | }, true); 318 | ``` 319 | 320 | The third argument, true, is to make the watch based on the value of the object, rather than just matching the reference to the object. 321 | 322 | 323 | ## Note 324 | This directive/plugin does not generate style tags, like the jQuery plugin. It also uses standard camelCase for variables and object properties, while the original plugin used lower\_case\_with_underscores. These options have not and may never be implemented: 325 | 326 | * widget_class - not necessary since directives already whatever classes and attributes you want to add 327 | * widget_margins - replaced by 'margins' 328 | * widget\_base\_dimensions - replaced by 'defaultSizeX' and 'defaultSizeY' 329 | * min_cols - currently, only 'columns' is used to defined the maximum width 330 | * max_cols - currently, only 'columns' is used to defined the maximum width 331 | * min_rows - replaced by 'minRows' 332 | * max_rows - replaced by 'maxRows' 333 | * max\_size\_x 334 | * max\_size\_y 335 | * extra_cols 336 | * extra_rows 337 | * autogenerate_stylesheet 338 | * avoid\_overlapped\_widgets 339 | * resize.axes 340 | * resize.handle_class - replaced by 'resize.handle', which doesn't need to be a class 341 | * resize.handle\_append\_to 342 | * resize.max_size 343 | * collision.on\_overlap\_start 344 | * collision.on_overlap 345 | * collision.on\_overlap\_stop 346 | 347 | ## Contributing 348 | 349 | #### Install project dependencies 350 | ```bash 351 | npm install 352 | bower install 353 | ``` 354 | 355 | #### Style Guide 356 | Please respect the formatting specified in .editorconfig 357 | 358 | #### Grunt Tasks 359 | ```grunt default``` Runs jshint & compiles project 360 | 361 | ```grunt dev``` Opens demo page, starts karma test runner, runs unit tests on src & test folder changes 362 | 363 | ```grunt e2e``` Watch src folder and run e2e tests on changes 364 | 365 | ```grunt test``` Runs the unit & e2e tests 366 | -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "angular-gridster", 3 | "version": "0.13.14", 4 | "main": [ 5 | "src/angular-gridster.js", 6 | "dist/angular-gridster.css" 7 | ], 8 | "dependencies": { 9 | "angular": ">= 1.2.0", 10 | "javascript-detect-element-resize": "~0.5.1" 11 | }, 12 | "devDependencies": { 13 | "angular-mocks": ">= 1.2.0", 14 | "angular-animate": ">= 1.2.0", 15 | "jquery": "*", 16 | "jquery-simulate": "*" 17 | }, 18 | "ignore": [ 19 | "test", 20 | ".bowerrc", 21 | ".editorconfig", 22 | ".gitattributes", 23 | ".gitignore", 24 | ".jshintrc", 25 | ".travis.yml", 26 | "Gruntfile.js", 27 | "karma.conf.js", 28 | "ptor.conf.js", 29 | "package.json" 30 | ] 31 | } 32 | -------------------------------------------------------------------------------- /demo/common/style.css: -------------------------------------------------------------------------------- 1 | .navbar { 2 | padding: 0 20px; 3 | } 4 | a:hover { 5 | cursor: pointer; 6 | } 7 | input { 8 | font-family: 'Helvetica Neue', Arial, sans-serif; 9 | font-size: 14px; 10 | padding: 4px; 11 | } 12 | .container { 13 | margin: auto; 14 | max-width: 1000px; 15 | } 16 | .code-coverage { 17 | color: #fff; 18 | font-weight: bold; 19 | float: right; 20 | padding: 10px; 21 | } -------------------------------------------------------------------------------- /demo/dashboard/script.js: -------------------------------------------------------------------------------- 1 | angular.module('app') 2 | 3 | .controller('DashboardCtrl', ['$scope', '$timeout', 4 | function($scope, $timeout) { 5 | $scope.gridsterOptions = { 6 | margins: [20, 20], 7 | columns: 4, 8 | draggable: { 9 | handle: 'h3' 10 | } 11 | }; 12 | 13 | $scope.dashboards = { 14 | '1': { 15 | id: '1', 16 | name: 'Home', 17 | widgets: [{ 18 | col: 0, 19 | row: 0, 20 | sizeY: 1, 21 | sizeX: 1, 22 | name: "Widget 1" 23 | }, { 24 | col: 2, 25 | row: 1, 26 | sizeY: 1, 27 | sizeX: 1, 28 | name: "Widget 2" 29 | }] 30 | }, 31 | '2': { 32 | id: '2', 33 | name: 'Other', 34 | widgets: [{ 35 | col: 1, 36 | row: 1, 37 | sizeY: 1, 38 | sizeX: 2, 39 | name: "Other Widget 1" 40 | }, { 41 | col: 1, 42 | row: 3, 43 | sizeY: 1, 44 | sizeX: 1, 45 | name: "Other Widget 2" 46 | }] 47 | } 48 | }; 49 | 50 | $scope.clear = function() { 51 | $scope.dashboard.widgets = []; 52 | }; 53 | 54 | $scope.addWidget = function() { 55 | $scope.dashboard.widgets.push({ 56 | name: "New Widget", 57 | sizeX: 1, 58 | sizeY: 1 59 | }); 60 | }; 61 | 62 | $scope.$watch('selectedDashboardId', function(newVal, oldVal) { 63 | if (newVal !== oldVal) { 64 | $scope.dashboard = $scope.dashboards[newVal]; 65 | } else { 66 | $scope.dashboard = $scope.dashboards[1]; 67 | } 68 | }); 69 | 70 | // init dashboard 71 | $scope.selectedDashboardId = '1'; 72 | 73 | } 74 | ]) 75 | 76 | .controller('CustomWidgetCtrl', ['$scope', '$modal', 77 | function($scope, $modal) { 78 | 79 | $scope.remove = function(widget) { 80 | $scope.dashboard.widgets.splice($scope.dashboard.widgets.indexOf(widget), 1); 81 | }; 82 | 83 | $scope.openSettings = function(widget) { 84 | $modal.open({ 85 | scope: $scope, 86 | templateUrl: 'demo/dashboard/widget_settings.html', 87 | controller: 'WidgetSettingsCtrl', 88 | resolve: { 89 | widget: function() { 90 | return widget; 91 | } 92 | } 93 | }); 94 | }; 95 | 96 | } 97 | ]) 98 | 99 | .controller('WidgetSettingsCtrl', ['$scope', '$timeout', '$rootScope', '$modalInstance', 'widget', 100 | function($scope, $timeout, $rootScope, $modalInstance, widget) { 101 | $scope.widget = widget; 102 | 103 | $scope.form = { 104 | name: widget.name, 105 | sizeX: widget.sizeX, 106 | sizeY: widget.sizeY, 107 | col: widget.col, 108 | row: widget.row 109 | }; 110 | 111 | $scope.sizeOptions = [{ 112 | id: '1', 113 | name: '1' 114 | }, { 115 | id: '2', 116 | name: '2' 117 | }, { 118 | id: '3', 119 | name: '3' 120 | }, { 121 | id: '4', 122 | name: '4' 123 | }]; 124 | 125 | $scope.dismiss = function() { 126 | $modalInstance.dismiss(); 127 | }; 128 | 129 | $scope.remove = function() { 130 | $scope.dashboard.widgets.splice($scope.dashboard.widgets.indexOf(widget), 1); 131 | $modalInstance.close(); 132 | }; 133 | 134 | $scope.submit = function() { 135 | angular.extend(widget, $scope.form); 136 | 137 | $modalInstance.close(widget); 138 | }; 139 | 140 | } 141 | ]) 142 | 143 | // helper code 144 | .filter('object2Array', function() { 145 | return function(input) { 146 | var out = []; 147 | for (i in input) { 148 | out.push(input[i]); 149 | } 150 | return out; 151 | } 152 | }); 153 | -------------------------------------------------------------------------------- /demo/dashboard/style.css: -------------------------------------------------------------------------------- 1 | 2 | .controls { 3 | margin-bottom: 20px; 4 | } 5 | .page-header { 6 | margin-top: 20px; 7 | } 8 | ul { 9 | list-style: none; 10 | } 11 | .box { 12 | height: 100%; 13 | border: 1px solid #ccc; 14 | background-color: #fff; 15 | } 16 | .box-header { 17 | background-color: #eee; 18 | padding: 0 30px 0 10px; 19 | border-bottom: 1px solid #ccc; 20 | cursor: move; 21 | position: relative; 22 | } 23 | .box-header h3 { 24 | margin-top: 10px; 25 | display: inline-block; 26 | } 27 | .box-content { 28 | padding: 10px; 29 | } 30 | .box-header-btns { 31 | top: 15px; 32 | right: 10px; 33 | cursor: pointer; 34 | position: absolute; 35 | } 36 | a { 37 | color: #ccc; 38 | } 39 | form { 40 | margin-bottom: 0; 41 | } 42 | .gridster { 43 | border: 1px solid #ccc; 44 | } -------------------------------------------------------------------------------- /demo/dashboard/view.html: -------------------------------------------------------------------------------- 1 | 9 |
10 | 25 |
-------------------------------------------------------------------------------- /demo/dashboard/widget_settings.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | 7 | 8 | 52 | 53 | 58 |
59 |
60 | -------------------------------------------------------------------------------- /demo/main/script.js: -------------------------------------------------------------------------------- 1 | angular.module('app') 2 | 3 | .directive('integer', function() { 4 | return { 5 | require: 'ngModel', 6 | link: function(scope, ele, attr, ctrl) { 7 | ctrl.$parsers.unshift(function(viewValue) { 8 | if (viewValue === '' || viewValue === null || typeof viewValue === 'undefined') { 9 | return null; 10 | } 11 | return parseInt(viewValue, 10); 12 | }); 13 | } 14 | }; 15 | }) 16 | 17 | .controller('MainCtrl', function($scope) { 18 | 19 | $scope.gridsterOpts = { 20 | margins: [20, 20], 21 | outerMargin: false, 22 | pushing: true, 23 | floating: true, 24 | draggable: { 25 | enabled: false 26 | }, 27 | resizable: { 28 | enabled: false, 29 | handles: ['n', 'e', 's', 'w', 'se', 'sw'] 30 | } 31 | }; 32 | 33 | // these map directly to gridsterItem options 34 | $scope.standardItems = [{ 35 | sizeX: 2, 36 | sizeY: 1, 37 | row: 0, 38 | col: 0 39 | }, { 40 | sizeX: 2, 41 | sizeY: 2, 42 | row: 0, 43 | col: 2 44 | }, { 45 | sizeX: 2, 46 | sizeY: 1, 47 | row: 2, 48 | col: 1 49 | }, { 50 | sizeX: 1, 51 | sizeY: 1, 52 | row: 2, 53 | col: 3 54 | }, { 55 | sizeX: 1, 56 | sizeY: 1, 57 | row: 2, 58 | col: 4 59 | }, { 60 | sizeX: 1, 61 | sizeY: 1, 62 | row: 0, 63 | col: 4 64 | }, { 65 | sizeX: 1, 66 | sizeY: 1, 67 | row: 0, 68 | col: 5 69 | }, { 70 | sizeX: 2, 71 | sizeY: 1, 72 | row: 1, 73 | col: 0 74 | }, { 75 | sizeX: 1, 76 | sizeY: 1, 77 | row: 1, 78 | col: 4 79 | }, { 80 | sizeX: 1, 81 | sizeY: 2, 82 | row: 1, 83 | col: 5 84 | }, { 85 | sizeX: 1, 86 | sizeY: 1, 87 | row: 2, 88 | col: 0 89 | }]; 90 | 91 | // these are non-standard, so they require mapping options 92 | $scope.customItems = [{ 93 | size: { 94 | x: 2, 95 | y: 1 96 | }, 97 | position: [0, 0] 98 | }, { 99 | size: { 100 | x: 2, 101 | y: 2 102 | }, 103 | position: [0, 2] 104 | }, { 105 | size: { 106 | x: 1, 107 | y: 1 108 | }, 109 | position: [1, 4] 110 | }, { 111 | size: { 112 | x: 1, 113 | y: 2 114 | }, 115 | position: [1, 5] 116 | }, { 117 | size: { 118 | x: 1, 119 | y: 1 120 | }, 121 | position: [2, 0] 122 | }, { 123 | size: { 124 | x: 2, 125 | y: 1 126 | }, 127 | position: [2, 1] 128 | }, { 129 | size: { 130 | x: 1, 131 | y: 1 132 | }, 133 | position: [2, 3] 134 | }, { 135 | size: { 136 | x: 1, 137 | y: 1 138 | }, 139 | position: [0, 4] 140 | }, { 141 | size: { 142 | x: 1, 143 | y: 1 144 | }, 145 | position: [0, 5] 146 | }, { 147 | size: { 148 | x: 2, 149 | y: 1 150 | }, 151 | position: [1, 0] 152 | }, { 153 | size: { 154 | x: 1, 155 | y: 1 156 | }, 157 | position: [2, 4] 158 | }]; 159 | 160 | $scope.emptyItems = [{ 161 | name: 'Item1' 162 | }, { 163 | name: 'Item2' 164 | }, { 165 | name: 'Item3' 166 | }, { 167 | name: 'Item4' 168 | }]; 169 | 170 | // map the gridsterItem to the custom item structure 171 | $scope.customItemMap = { 172 | sizeX: 'item.size.x', 173 | sizeY: 'item.size.y', 174 | row: 'item.position[0]', 175 | col: 'item.position[1]' 176 | }; 177 | 178 | }); 179 | -------------------------------------------------------------------------------- /demo/main/style.css: -------------------------------------------------------------------------------- 1 | body { 2 | font-family: 'Helvetica Neue', Arial, sans-serif; 3 | background: #004756; 4 | color: #fff; 5 | } 6 | .gridster .gridster-item { 7 | -webkit-box-shadow: 0 0 5px rgba(0, 0, 0, 0.3); 8 | -moz-box-shadow: 0 0 5px rgba(0, 0, 0, 0.3); 9 | box-shadow: 0 0 5px rgba(0, 0, 0, 0.3); 10 | color: #004756; 11 | background: #ffffff; 12 | padding: 10px; 13 | } -------------------------------------------------------------------------------- /demo/main/view.html: -------------------------------------------------------------------------------- 1 |
2 |

Standard Items

3 | 4 | 5 | 6 | 7 | 8 | 9 | Margins: 10 | 11 | x 12 | 13 | 14 |

15 | Each item provides its own dimensions and position using the standard fields: { row: row, col: col, sizeX: sizeX, sizeY: sizeY }. 16 |

17 |
18 | 27 |
28 | 29 |

Custom Items

30 |

31 | Each item provides its own dimensions but with custom fields defined using customItemMap: { position: [row, col], size: { x: sizeX, y: sizeY }} 32 |

33 |
34 | 43 |
44 | 45 |

Custom Items2

46 |

47 | Each item provides its own dimensions but with custom fields indicated using html attributes: row, col, sizex, sizey. Size can also be in the form of data-size-x or data-sizex. 48 |

49 |
50 | 59 |
60 | 61 |

Empty Items

62 |

63 | Each item stores the standard options as an object within itself: { grid: {row: row, col: col, sizeX: sizeX, sizeY: sizeY }} 64 |

65 |
66 | 75 |
76 | 77 |

No Configuration or Binding

78 |

79 | No data binding or configuration provided. 80 |

81 |
82 | 87 |
88 |
-------------------------------------------------------------------------------- /dist/angular-gridster.css: -------------------------------------------------------------------------------- 1 | /** 2 | * gridster.js - v0.2.1 - 2013-10-28 * http://gridster.net 3 | * Copyright (c) 2013 ducksboard; Licensed MIT 4 | */ 5 | .gridster { 6 | position: relative; 7 | margin: auto; 8 | height: 0; 9 | } 10 | .gridster > ul { 11 | margin: 0; 12 | list-style: none; 13 | padding: 0; 14 | } 15 | .gridster-item { 16 | -webkit-box-sizing: border-box; 17 | -moz-box-sizing: border-box; 18 | box-sizing: border-box; 19 | list-style: none; 20 | z-index: 2; 21 | position: absolute; 22 | display: none; 23 | } 24 | .gridster-loaded { 25 | -webkit-transition: height .3s; 26 | -moz-transition: height .3s; 27 | -o-transition: height .3s; 28 | transition: height .3s; 29 | } 30 | .gridster-loaded .gridster-item { 31 | display: block; 32 | position: absolute; 33 | -webkit-transition: opacity .3s, left .3s, top .3s, width .3s, height .3s; 34 | -moz-transition: opacity .3s, left .3s, top .3s, width .3s, height .3s; 35 | -o-transition: opacity .3s, left .3s, top .3s, width .3s, height .3s; 36 | transition: opacity .3s, left .3s, top .3s, width .3s, height .3s; 37 | -webkit-transition-delay: 50ms; 38 | -moz-transition-delay: 50ms; 39 | -o-transition-delay: 50ms; 40 | transition-delay: 50ms; 41 | } 42 | .gridster-loaded .gridster-preview-holder { 43 | display: none; 44 | z-index: 1; 45 | position: absolute; 46 | background-color: #ddd; 47 | border-color: #fff; 48 | opacity: 0.2; 49 | } 50 | .gridster-loaded .gridster-item.gridster-item-moving, 51 | .gridster-loaded .gridster-preview-holder { 52 | -webkit-transition: none; 53 | -moz-transition: none; 54 | -o-transition: none; 55 | transition: none; 56 | } 57 | .gridster-mobile { 58 | height: auto !important; 59 | } 60 | .gridster-mobile .gridster-item { 61 | height: auto; 62 | position: static; 63 | float: none; 64 | } 65 | .gridster-item.ng-leave.ng-leave-active { 66 | opacity: 0; 67 | } 68 | .gridster-item.ng-enter { 69 | opacity: 1; 70 | } 71 | .gridster-item-moving { 72 | z-index: 3; 73 | } 74 | /* RESIZE */ 75 | .gridster-item-resizable-handler { 76 | position: absolute; 77 | font-size: 1px; 78 | display: block; 79 | z-index: 5; 80 | } 81 | .handle-se { 82 | cursor: se-resize; 83 | width: 0; 84 | height: 0; 85 | right: 1px; 86 | bottom: 1px; 87 | border-style: solid; 88 | border-width: 0 0 12px 12px; 89 | border-color: transparent; 90 | } 91 | .handle-ne { 92 | cursor: ne-resize; 93 | width: 12px; 94 | height: 12px; 95 | right: 1px; 96 | top: 1px; 97 | } 98 | .handle-nw { 99 | cursor: nw-resize; 100 | width: 12px; 101 | height: 12px; 102 | left: 1px; 103 | top: 1px; 104 | } 105 | .handle-sw { 106 | cursor: sw-resize; 107 | width: 12px; 108 | height: 12px; 109 | left: 1px; 110 | bottom: 1px; 111 | } 112 | .handle-e { 113 | cursor: e-resize; 114 | width: 12px; 115 | bottom: 0; 116 | right: 1px; 117 | top: 0; 118 | } 119 | .handle-s { 120 | cursor: s-resize; 121 | height: 12px; 122 | right: 0; 123 | bottom: 1px; 124 | left: 0; 125 | } 126 | .handle-n { 127 | cursor: n-resize; 128 | height: 12px; 129 | right: 0; 130 | top: 1px; 131 | left: 0; 132 | } 133 | .handle-w { 134 | cursor: w-resize; 135 | width: 12px; 136 | left: 1px; 137 | top: 0; 138 | bottom: 0; 139 | } 140 | .gridster .gridster-item:hover .gridster-box { 141 | border: 1.5px solid #B3B2B3; 142 | } 143 | .gridster .gridster-item:hover .handle-se { 144 | border-color: transparent transparent #ccc; 145 | } 146 | -------------------------------------------------------------------------------- /dist/angular-gridster.min.css: -------------------------------------------------------------------------------- 1 | .gridster{position:relative;margin:auto;height:0}.gridster>ul{margin:0;list-style:none;padding:0}.gridster-item{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box;list-style:none;z-index:2;position:absolute;display:none}.gridster-loaded{-webkit-transition:height .3s;-moz-transition:height .3s;-o-transition:height .3s;transition:height .3s}.gridster-loaded .gridster-item{display:block;position:absolute;-webkit-transition:opacity .3s,left .3s,top .3s,width .3s,height .3s;-moz-transition:opacity .3s,left .3s,top .3s,width .3s,height .3s;-o-transition:opacity .3s,left .3s,top .3s,width .3s,height .3s;transition:opacity .3s,left .3s,top .3s,width .3s,height .3s;-webkit-transition-delay:50ms;-moz-transition-delay:50ms;-o-transition-delay:50ms;transition-delay:50ms}.gridster-loaded .gridster-preview-holder{display:none;z-index:1;position:absolute;background-color:#ddd;border-color:#fff;opacity:.2}.gridster-loaded .gridster-item.gridster-item-moving,.gridster-loaded .gridster-preview-holder{-webkit-transition:none;-moz-transition:none;-o-transition:none;transition:none}.gridster-mobile{height:auto !important}.gridster-mobile .gridster-item{height:auto;position:static;float:none}.gridster-item.ng-leave.ng-leave-active{opacity:0}.gridster-item.ng-enter{opacity:1}.gridster-item-moving{z-index:3}.gridster-item-resizable-handler{position:absolute;font-size:1px;display:block;z-index:5}.handle-se{cursor:se-resize;width:0;height:0;right:1px;bottom:1px;border-style:solid;border-width:0 0 12px 12px;border-color:transparent}.handle-ne{cursor:ne-resize;width:12px;height:12px;right:1px;top:1px}.handle-nw{cursor:nw-resize;width:12px;height:12px;left:1px;top:1px}.handle-sw{cursor:sw-resize;width:12px;height:12px;left:1px;bottom:1px}.handle-e{cursor:e-resize;width:12px;bottom:0;right:1px;top:0}.handle-s{cursor:s-resize;height:12px;right:0;bottom:1px;left:0}.handle-n{cursor:n-resize;height:12px;right:0;top:1px;left:0}.handle-w{cursor:w-resize;width:12px;left:1px;top:0;bottom:0}.gridster .gridster-item:hover .gridster-box{border:1.5px solid #B3B2B3}.gridster .gridster-item:hover .handle-se{border-color:transparent transparent #ccc} -------------------------------------------------------------------------------- /dist/angular-gridster.min.js: -------------------------------------------------------------------------------- 1 | /* 2 | * angular-gridster 3 | * http://manifestwebdesign.github.io/angular-gridster 4 | * 5 | * @version: 0.13.14 6 | * @license: MIT 7 | */ 8 | !function(a,b){"use strict";"function"==typeof define&&define.amd?define(["angular"],b):"object"==typeof exports?module.exports=b(require("angular")):b(a.angular)}(this,function(a){"use strict";return a.module("gridster",[]).constant("gridsterConfig",{columns:6,pushing:!0,floating:!0,swapping:!1,width:"auto",colWidth:"auto",rowHeight:"match",margins:[10,10],outerMargin:!0,sparse:!1,isMobile:!1,mobileBreakPoint:600,mobileModeEnabled:!0,minColumns:1,minRows:1,maxRows:100,defaultSizeX:2,defaultSizeY:1,minSizeX:1,maxSizeX:null,minSizeY:1,maxSizeY:null,saveGridItemCalculatedHeightInMobile:!1,resizable:{enabled:!0,handles:["s","e","n","w","se","ne","sw","nw"]},draggable:{enabled:!0,scrollSensitivity:20,scrollSpeed:15}}).controller("GridsterCtrl",["gridsterConfig","$timeout",function(b,c){var d=this;a.extend(this,b),this.resizable=a.extend({},b.resizable||{}),this.draggable=a.extend({},b.draggable||{});var e=!1;this.layoutChanged=function(){e||(e=!0,c(function(){e=!1,d.loaded&&d.floatItemsUp(),d.updateHeight(d.movingItem?d.movingItem.sizeY:0)},30))},this.grid=[],this.allItems=[],this.destroy=function(){this.grid&&(this.grid=[]),this.$element=null,this.allItems&&(this.allItems.length=0,this.allItems=null)},this.setOptions=function(b){if(b)if(b=a.extend({},b),b.draggable&&(a.extend(this.draggable,b.draggable),delete b.draggable),b.resizable&&(a.extend(this.resizable,b.resizable),delete b.resizable),a.extend(this,b),this.margins&&2===this.margins.length)for(var c=0,d=this.margins.length;c-1&&c>-1&&a.sizeX+c<=this.columns&&a.sizeY+b<=this.maxRows},this.autoSetItemPosition=function(a){for(var b=0;b=a.col&&d<=a.row+a.sizeY-1&&e>=a.row},this.removeItem=function(a){for(var b,c=0,d=this.grid.length;c-1;){for(var e=1,f=b;f>-1;){var g=this.grid[a];if(g){var h=g[f];if(h&&(!c||c.indexOf(h)===-1)&&h.sizeX>=e&&h.sizeY>=d)return h}++e,--f}--a,++d}return null},this.putItems=function(a){for(var b=0,c=a.length;b=b)){for(;a.row-1;){var h=this.getItems(g,b,d,c,a);if(0!==h.length)break;e=g,f=b,--g}null!==e&&this.putItem(a,e,f)}},this.updateHeight=function(a){var b=this.minRows;a=a||0;for(var c=this.grid.length;c>=0;--c){var d=this.grid[c];if(d)for(var e=0,f=d.length;e0?Math.min(this.maxRows,b):Math.max(this.maxRows,b)},this.pixelsToRows=function(a,b){return this.outerMargin||(a+=this.margins[0]/2),b===!0?Math.ceil(a/this.curRowHeight):b===!1?Math.floor(a/this.curRowHeight):Math.round(a/this.curRowHeight)},this.pixelsToColumns=function(a,b){return this.outerMargin||(a+=this.margins[1]/2),b===!0?Math.ceil(a/this.curColWidth):b===!1?Math.floor(a/this.curColWidth):Math.round(a/this.curColWidth)}}]).directive("gridsterPreview",function(){return{replace:!0,scope:!0,require:"^gridster",template:'
',link:function(a,b,c,d){a.previewStyle=function(){return d.movingItem?{display:"block",height:d.movingItem.sizeY*d.curRowHeight-d.margins[0]+"px",width:d.movingItem.sizeX*d.curColWidth-d.margins[1]+"px",top:d.movingItem.row*d.curRowHeight+(d.outerMargin?d.margins[0]:0)+"px",left:d.movingItem.col*d.curColWidth+(d.outerMargin?d.margins[1]:0)+"px"}:{display:"none"}}}}}).directive("gridster",["$timeout","$window","$rootScope","gridsterDebounce",function(b,c,d,e){return{scope:!0,restrict:"EAC",controller:"GridsterCtrl",controllerAs:"gridster",compile:function(f){return f.prepend('
'),function(f,g,h,i){function j(){g.css("height",i.gridHeight*i.curRowHeight+(i.outerMargin?i.margins[0]:-i.margins[0])+"px")}function k(a){if(i.setOptions(a),l(g[0])){"auto"===i.width?i.curWidth=g[0].offsetWidth||parseInt(g.css("width"),10):i.curWidth=i.width,"auto"===i.colWidth?i.curColWidth=(i.curWidth+(i.outerMargin?-i.margins[1]:i.margins[1]))/i.columns:i.curColWidth=i.colWidth,i.curRowHeight=i.rowHeight,"string"==typeof i.rowHeight&&("match"===i.rowHeight?i.curRowHeight=Math.round(i.curColWidth):i.rowHeight.indexOf("*")!==-1?i.curRowHeight=Math.round(i.curColWidth*i.rowHeight.replace("*","").replace(" ","")):i.rowHeight.indexOf("/")!==-1&&(i.curRowHeight=Math.round(i.curColWidth/i.rowHeight.replace("/","").replace(" ","")))),i.isMobile=i.mobileModeEnabled&&i.curWidth<=i.mobileBreakPoint;for(var b=0,c=i.grid.length;bb&&(d=b-p-r,z=h-d),q+ic&&(f=c-q-s,A=i-f),p+=d,q+=f,e.css({top:q+"px",left:p+"px"}),k(a),!0}function o(a){return!(!e.hasClass("gridster-item-moving")||e.hasClass("gridster-item-resizing"))&&(z=A=0,l(a),!0)}var p,q,r,s,t,u,v=0,w=0,x=0,y=0,z=0,A=0,B=0,C=0,D=b[0],E=["select","option","input","textarea","button"],F=null,G=null;this.enable=function(){if(F!==!0){if(F=!0,G)return void G.enable();G=new d(e[0],m,n,o),G.enable()}},this.disable=function(){F!==!1&&(F=!1,G&&G.disable())},this.toggle=function(a){a?this.enable():this.disable()},this.destroy=function(){this.disable()}}return e}]).factory("GridsterResizable",["GridsterTouch",function(b){function c(c,d,e,f,g){function h(h){function i(a){c.addClass("gridster-item-moving"),c.addClass("gridster-item-resizing"),e.movingItem=f,f.setElementSizeX(),f.setElementSizeY(),f.setElementPosition(),e.updateHeight(1),d.$apply(function(){e.resizable&&e.resizable.start&&e.resizable.start(a,c,g,f)})}function j(a){var b=f.row,i=f.col,j=f.sizeX,k=f.sizeY,l=e.resizable&&e.resizable.resize,m=f.col;["w","nw","sw"].indexOf(h)!==-1&&(m=e.pixelsToColumns(o,!1));var n=f.row;["n","ne","nw"].indexOf(h)!==-1&&(n=e.pixelsToRows(p,!1));var s=f.sizeX;["n","s"].indexOf(h)===-1&&(s=e.pixelsToColumns(q,!0));var t=f.sizeY;["e","w"].indexOf(h)===-1&&(t=e.pixelsToRows(r,!0));var u=n>-1&&m>-1&&s+m<=e.columns&&t+n<=e.maxRows;!u||e.pushing===!1&&0!==e.getItems(n,m,s,t,f).length||(f.row=n,f.col=m,f.sizeX=s,f.sizeY=t);var v=f.row!==b||f.col!==i||f.sizeX!==j||f.sizeY!==k;(l||v)&&d.$apply(function(){l&&e.resizable.resize(a,c,g,f)})}function k(a){c.removeClass("gridster-item-moving"),c.removeClass("gridster-item-resizing"),e.movingItem=null,f.setPosition(f.row,f.col),f.setSizeY(f.sizeY),f.setSizeX(f.sizeX),d.$apply(function(){e.resizable&&e.resizable.stop&&e.resizable.stop(a,c,g,f)})}function l(a){switch(a.which){case 1:break;case 2:case 3:return}return u=e.draggable.enabled,u&&(e.draggable.enabled=!1,d.$broadcast("gridster-draggable-changed",e)),z=a.pageX,A=a.pageY,o=parseInt(c.css("left"),10),p=parseInt(c.css("top"),10),q=c[0].offsetWidth,r=c[0].offsetHeight,s=f.sizeX,t=f.sizeY,i(a),!0}function m(a){var b=e.curWidth-1;x=a.pageX,y=a.pageY;var d=x-z+B,f=y-A+C;B=C=0,z=x,A=y;var g=f,h=d;return w.indexOf("n")>=0&&(r-g=0&&(r+gE&&(f=E-p-r,C=g-f),r+=f),w.indexOf("w")>=0&&(q-h=0&&(q+hb&&(d=b-o-q,B=h-d),q+=d),c.css({top:p+"px",left:o+"px",width:q+"px",height:r+"px"}),j(a),!0}function n(a){return e.draggable.enabled!==u&&(e.draggable.enabled=u,d.$broadcast("gridster-draggable-changed",e)),B=C=0,k(a),!0}var o,p,q,r,s,t,u,v,w=h,x=0,y=0,z=0,A=0,B=0,C=0,D=0,E=9999,F=0,G=function(){return(f.minSizeY?f.minSizeY:1)*e.curRowHeight-e.margins[0]},H=function(){return(f.minSizeX?f.minSizeX:1)*e.curColWidth-e.margins[1]},I=null;this.enable=function(){I||(I=a.element('
'),c.append(I)),v=new b(I[0],l,m,n),v.enable()},this.disable=function(){I&&(I.remove(),I=null),v.disable(),v=void 0},this.destroy=function(){this.disable()}}var i=[],j=e.resizable.handles;"string"==typeof j&&(j=e.resizable.handles.split(","));for(var k=!1,l=0,m=j.length;l 2 | 3 | 4 | 5 | 6 | 7 | Angular Gridster 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 48 | 49 | 50 | 51 | 52 | 53 | 54 |
55 | 56 | 57 | 73 | 74 |
75 | 76 |
77 | 78 | 79 | -------------------------------------------------------------------------------- /karma.conf.js: -------------------------------------------------------------------------------- 1 | // Karma configuration 2 | // http://karma-runner.github.io/0.10/config/configuration-file.html 3 | 4 | module.exports = function(config) { 5 | config.set({ 6 | // base path, that will be used to resolve files and exclude 7 | basePath: '', 8 | 9 | reporters: ['progress', 'coverage'], 10 | 11 | // testing framework to use (jasmine/mocha/qunit/...) 12 | frameworks: ['jasmine'], 13 | 14 | // list of files / patterns to load in the browser 15 | files: [ 16 | 'bower_components/jquery/dist/jquery.js', 17 | 'bower_components/jquery-simulate/jquery.simulate.js', 18 | 'bower_components/javascript-detect-element-resize/jquery.resize.js', 19 | 'bower_components/angular/angular.js', 20 | 'bower_components/angular-mocks/angular-mocks.js', 21 | 'src/angular-gridster.js', 22 | 'test/spec/*.js' 23 | ], 24 | 25 | preprocessors: { 26 | 'src/*.js': ['coverage'] 27 | }, 28 | 29 | coverageReporter: { 30 | type: 'html', 31 | dir: 'coverage/' 32 | }, 33 | 34 | background: false, 35 | 36 | // list of files / patterns to exclude 37 | exclude: [], 38 | 39 | // web server port 40 | port: 9898, 41 | 42 | // level of logging 43 | // possible values: LOG_DISABLE || LOG_ERROR || LOG_WARN || LOG_INFO || LOG_DEBUG 44 | logLevel: config.LOG_ERROR, 45 | 46 | 47 | // enable / disable watching file and executing tests whenever any file changes 48 | autoWatch: false, 49 | 50 | 51 | // Start these browsers, currently available: 52 | // - Chrome 53 | // - ChromeCanary 54 | // - Firefox 55 | // - Opera 56 | // - Safari (only Mac) 57 | // - PhantomJS 58 | // - IE (only Windows) 59 | browsers: ['PhantomJS'], 60 | // Continuous Integration mode 61 | // if true, it capture browsers, run tests and exit 62 | singleRun: false 63 | }); 64 | }; 65 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "angular-gridster", 3 | "version": "0.13.14", 4 | "description": "This directive gives you gridster behavior", 5 | "license": "MIT", 6 | "homepage": "http://manifestwebdesign.github.io/angular-gridster", 7 | "authors": "https://github.com/ManifestWebDesign/angular-gridster/graphs/contributors", 8 | "devDependencies": { 9 | "grunt": "^1.0.1", 10 | "grunt-bump": "0.8.0", 11 | "grunt-contrib-concat": "~1.0.1", 12 | "grunt-contrib-connect": "~1.0.2", 13 | "grunt-contrib-jshint": "~1.0.0", 14 | "grunt-contrib-less": "~1.3.0", 15 | "grunt-contrib-uglify": "~1.0.1", 16 | "grunt-contrib-watch": "~1.0.0", 17 | "grunt-jsbeautifier": "^0.2.13", 18 | "grunt-karma": "~2.0.0", 19 | "grunt-protractor-runner": "~3.2.0", 20 | "jasmine-core": "~2.4.1", 21 | "karma": "~1.1.1", 22 | "karma-chrome-launcher": "~1.0.1", 23 | "karma-coverage": "~1.1.0", 24 | "karma-firefox-launcher": "~1.0.0", 25 | "karma-jasmine": "~1.0.2", 26 | "karma-mocha": "~1.1.1", 27 | "karma-phantomjs-launcher": "^1.0.0", 28 | "karma-script-launcher": "~1.0.0", 29 | "lodash": "~4.13.1", 30 | "matchdep": "~1.0.1", 31 | "mocha": "~2.5.3", 32 | "time-grunt": "~1.3.0" 33 | }, 34 | "engines": { 35 | "node": ">=4.2.1" 36 | }, 37 | "scripts": { 38 | "test": "grunt test", 39 | "pretest": "node ./node_modules/grunt-protractor-runner/node_modules/protractor/bin/webdriver-manager update" 40 | }, 41 | "repository": { 42 | "type": "git", 43 | "url": "git@github.com:ManifestWebDesign/angular-gridster.git" 44 | }, 45 | "main": "dist/angular-gridster.min.js" 46 | } 47 | -------------------------------------------------------------------------------- /ptor.conf.js: -------------------------------------------------------------------------------- 1 | // An example configuration file. 2 | exports.config = { 3 | // The address of a running selenium server. 4 | seleniumAddress: 'http://localhost:4444/wd/hub', 5 | baseUrl: 'http://localhost:9000', 6 | 7 | // Capabilities to be passed to the webdriver instance. 8 | capabilities: { 9 | 'browserName': 'phantomjs' 10 | }, 11 | 12 | // Spec patterns are relative to the location of the spec file. They may 13 | // include glob patterns. 14 | specs: ['test/e2e/*.js'], 15 | 16 | // Options to be passed to Jasmine-node. 17 | jasmineNodeOpts: { 18 | showColors: true // Use colors in the command line report. 19 | } 20 | }; 21 | -------------------------------------------------------------------------------- /src/angular-gridster.js: -------------------------------------------------------------------------------- 1 | /*global define:true*/ 2 | (function(root, factory) { 3 | 4 | 'use strict'; 5 | 6 | if (typeof define === 'function' && define.amd) { 7 | // AMD 8 | define(['angular'], factory); 9 | } else if (typeof exports === 'object') { 10 | // CommonJS 11 | module.exports = factory(require('angular')); 12 | } else { 13 | // Browser, nothing "exported". Only registered as a module with angular. 14 | factory(root.angular); 15 | } 16 | }(this, function(angular) { 17 | 18 | 'use strict'; 19 | 20 | // This returned angular module 'gridster' is what is exported. 21 | return angular.module('gridster', []) 22 | 23 | .constant('gridsterConfig', { 24 | columns: 6, // number of columns in the grid 25 | pushing: true, // whether to push other items out of the way 26 | floating: true, // whether to automatically float items up so they stack 27 | swapping: false, // whether or not to have items switch places instead of push down if they are the same size 28 | width: 'auto', // width of the grid. "auto" will expand the grid to its parent container 29 | colWidth: 'auto', // width of grid columns. "auto" will divide the width of the grid evenly among the columns 30 | rowHeight: 'match', // height of grid rows. 'match' will make it the same as the column width, a numeric value will be interpreted as pixels, '/2' is half the column width, '*5' is five times the column width, etc. 31 | margins: [10, 10], // margins in between grid items 32 | outerMargin: true, 33 | sparse: false, // "true" can increase performance of dragging and resizing for big grid (e.g. 20x50) 34 | isMobile: false, // toggle mobile view 35 | mobileBreakPoint: 600, // width threshold to toggle mobile mode 36 | mobileModeEnabled: true, // whether or not to toggle mobile mode when screen width is less than mobileBreakPoint 37 | minColumns: 1, // minimum amount of columns the grid can scale down to 38 | minRows: 1, // minimum amount of rows to show if the grid is empty 39 | maxRows: 100, // maximum amount of rows in the grid 40 | defaultSizeX: 2, // default width of an item in columns 41 | defaultSizeY: 1, // default height of an item in rows 42 | minSizeX: 1, // minimum column width of an item 43 | maxSizeX: null, // maximum column width of an item 44 | minSizeY: 1, // minumum row height of an item 45 | maxSizeY: null, // maximum row height of an item 46 | saveGridItemCalculatedHeightInMobile: false, // grid item height in mobile display. true- to use the calculated height by sizeY given 47 | resizable: { // options to pass to resizable handler 48 | enabled: true, 49 | handles: ['s', 'e', 'n', 'w', 'se', 'ne', 'sw', 'nw'] 50 | }, 51 | draggable: { // options to pass to draggable handler 52 | enabled: true, 53 | scrollSensitivity: 20, // Distance in pixels from the edge of the viewport after which the viewport should scroll, relative to pointer 54 | scrollSpeed: 15 // Speed at which the window should scroll once the mouse pointer gets within scrollSensitivity distance 55 | } 56 | }) 57 | 58 | .controller('GridsterCtrl', ['gridsterConfig', '$timeout', 59 | function(gridsterConfig, $timeout) { 60 | 61 | var gridster = this; 62 | 63 | /** 64 | * Create options from gridsterConfig constant 65 | */ 66 | angular.extend(this, gridsterConfig); 67 | 68 | this.resizable = angular.extend({}, gridsterConfig.resizable || {}); 69 | this.draggable = angular.extend({}, gridsterConfig.draggable || {}); 70 | 71 | var flag = false; 72 | this.layoutChanged = function() { 73 | if (flag) { 74 | return; 75 | } 76 | flag = true; 77 | $timeout(function() { 78 | flag = false; 79 | if (gridster.loaded) { 80 | gridster.floatItemsUp(); 81 | } 82 | gridster.updateHeight(gridster.movingItem ? gridster.movingItem.sizeY : 0); 83 | }, 30); 84 | }; 85 | 86 | /** 87 | * A positional array of the items in the grid 88 | */ 89 | this.grid = []; 90 | this.allItems = []; 91 | 92 | /** 93 | * Clean up after yourself 94 | */ 95 | this.destroy = function() { 96 | // empty the grid to cut back on the possibility 97 | // of circular references 98 | if (this.grid) { 99 | this.grid = []; 100 | } 101 | this.$element = null; 102 | 103 | if (this.allItems) { 104 | this.allItems.length = 0; 105 | this.allItems = null; 106 | } 107 | }; 108 | 109 | /** 110 | * Overrides default options 111 | * 112 | * @param {Object} options The options to override 113 | */ 114 | this.setOptions = function(options) { 115 | if (!options) { 116 | return; 117 | } 118 | 119 | options = angular.extend({}, options); 120 | 121 | // all this to avoid using jQuery... 122 | if (options.draggable) { 123 | angular.extend(this.draggable, options.draggable); 124 | delete(options.draggable); 125 | } 126 | if (options.resizable) { 127 | angular.extend(this.resizable, options.resizable); 128 | delete(options.resizable); 129 | } 130 | 131 | angular.extend(this, options); 132 | 133 | if (!this.margins || this.margins.length !== 2) { 134 | this.margins = [0, 0]; 135 | } else { 136 | for (var x = 0, l = this.margins.length; x < l; ++x) { 137 | this.margins[x] = parseInt(this.margins[x], 10); 138 | if (isNaN(this.margins[x])) { 139 | this.margins[x] = 0; 140 | } 141 | } 142 | } 143 | }; 144 | 145 | /** 146 | * Check if item can occupy a specified position in the grid 147 | * 148 | * @param {Object} item The item in question 149 | * @param {Number} row The row index 150 | * @param {Number} column The column index 151 | * @returns {Boolean} True if if item fits 152 | */ 153 | this.canItemOccupy = function(item, row, column) { 154 | return row > -1 && column > -1 && item.sizeX + column <= this.columns && item.sizeY + row <= this.maxRows; 155 | }; 156 | 157 | /** 158 | * Set the item in the first suitable position 159 | * 160 | * @param {Object} item The item to insert 161 | */ 162 | this.autoSetItemPosition = function(item) { 163 | // walk through each row and column looking for a place it will fit 164 | for (var rowIndex = 0; rowIndex < this.maxRows; ++rowIndex) { 165 | for (var colIndex = 0; colIndex < this.columns; ++colIndex) { 166 | // only insert if position is not already taken and it can fit 167 | var items = this.getItems(rowIndex, colIndex, item.sizeX, item.sizeY, item); 168 | if (items.length === 0 && this.canItemOccupy(item, rowIndex, colIndex)) { 169 | this.putItem(item, rowIndex, colIndex); 170 | return; 171 | } 172 | } 173 | } 174 | throw new Error('Unable to place item!'); 175 | }; 176 | 177 | /** 178 | * Gets items at a specific coordinate 179 | * 180 | * @param {Number} row 181 | * @param {Number} column 182 | * @param {Number} sizeX 183 | * @param {Number} sizeY 184 | * @param {Array} excludeItems An array of items to exclude from selection 185 | * @returns {Array} Items that match the criteria 186 | */ 187 | this.getItems = function(row, column, sizeX, sizeY, excludeItems) { 188 | var items = []; 189 | if (!sizeX || !sizeY) { 190 | sizeX = sizeY = 1; 191 | } 192 | if (excludeItems && !(excludeItems instanceof Array)) { 193 | excludeItems = [excludeItems]; 194 | } 195 | var item; 196 | if (this.sparse === false) { // check all cells 197 | for (var h = 0; h < sizeY; ++h) { 198 | for (var w = 0; w < sizeX; ++w) { 199 | item = this.getItem(row + h, column + w, excludeItems); 200 | if (item && (!excludeItems || excludeItems.indexOf(item) === -1) && items.indexOf(item) === -1) { 201 | items.push(item); 202 | } 203 | } 204 | } 205 | } else { // check intersection with all items 206 | var bottom = row + sizeY - 1; 207 | var right = column + sizeX - 1; 208 | for (var i = 0; i < this.allItems.length; ++i) { 209 | item = this.allItems[i]; 210 | if (item && (!excludeItems || excludeItems.indexOf(item) === -1) && items.indexOf(item) === -1 && this.intersect(item, column, right, row, bottom)) { 211 | items.push(item); 212 | } 213 | } 214 | } 215 | return items; 216 | }; 217 | 218 | /** 219 | * @param {Array} items 220 | * @returns {Object} An item that represents the bounding box of the items 221 | */ 222 | this.getBoundingBox = function(items) { 223 | 224 | if (items.length === 0) { 225 | return null; 226 | } 227 | if (items.length === 1) { 228 | return { 229 | row: items[0].row, 230 | col: items[0].col, 231 | sizeY: items[0].sizeY, 232 | sizeX: items[0].sizeX 233 | }; 234 | } 235 | 236 | var maxRow = 0; 237 | var maxCol = 0; 238 | var minRow = 9999; 239 | var minCol = 9999; 240 | 241 | for (var i = 0, l = items.length; i < l; ++i) { 242 | var item = items[i]; 243 | minRow = Math.min(item.row, minRow); 244 | minCol = Math.min(item.col, minCol); 245 | maxRow = Math.max(item.row + item.sizeY, maxRow); 246 | maxCol = Math.max(item.col + item.sizeX, maxCol); 247 | } 248 | 249 | return { 250 | row: minRow, 251 | col: minCol, 252 | sizeY: maxRow - minRow, 253 | sizeX: maxCol - minCol 254 | }; 255 | }; 256 | 257 | /** 258 | * Checks if item intersects specified box 259 | * 260 | * @param {object} item 261 | * @param {number} left 262 | * @param {number} right 263 | * @param {number} top 264 | * @param {number} bottom 265 | */ 266 | 267 | this.intersect = function(item, left, right, top, bottom) { 268 | return (left <= item.col + item.sizeX - 1 && 269 | right >= item.col && 270 | top <= item.row + item.sizeY - 1 && 271 | bottom >= item.row); 272 | }; 273 | 274 | 275 | /** 276 | * Removes an item from the grid 277 | * 278 | * @param {Object} item 279 | */ 280 | this.removeItem = function(item) { 281 | var index; 282 | for (var rowIndex = 0, l = this.grid.length; rowIndex < l; ++rowIndex) { 283 | var columns = this.grid[rowIndex]; 284 | if (!columns) { 285 | continue; 286 | } 287 | index = columns.indexOf(item); 288 | if (index !== -1) { 289 | columns[index] = null; 290 | break; 291 | } 292 | } 293 | if (this.sparse) { 294 | index = this.allItems.indexOf(item); 295 | if (index !== -1) { 296 | this.allItems.splice(index, 1); 297 | } 298 | } 299 | this.layoutChanged(); 300 | }; 301 | 302 | /** 303 | * Returns the item at a specified coordinate 304 | * 305 | * @param {Number} row 306 | * @param {Number} column 307 | * @param {Array} excludeItems Items to exclude from selection 308 | * @returns {Object} The matched item or null 309 | */ 310 | this.getItem = function(row, column, excludeItems) { 311 | if (excludeItems && !(excludeItems instanceof Array)) { 312 | excludeItems = [excludeItems]; 313 | } 314 | var sizeY = 1; 315 | while (row > -1) { 316 | var sizeX = 1, 317 | col = column; 318 | while (col > -1) { 319 | var items = this.grid[row]; 320 | if (items) { 321 | var item = items[col]; 322 | if (item && (!excludeItems || excludeItems.indexOf(item) === -1) && item.sizeX >= sizeX && item.sizeY >= sizeY) { 323 | return item; 324 | } 325 | } 326 | ++sizeX; 327 | --col; 328 | } 329 | --row; 330 | ++sizeY; 331 | } 332 | return null; 333 | }; 334 | 335 | /** 336 | * Insert an array of items into the grid 337 | * 338 | * @param {Array} items An array of items to insert 339 | */ 340 | this.putItems = function(items) { 341 | for (var i = 0, l = items.length; i < l; ++i) { 342 | this.putItem(items[i]); 343 | } 344 | }; 345 | 346 | /** 347 | * Insert a single item into the grid 348 | * 349 | * @param {Object} item The item to insert 350 | * @param {Number} row (Optional) Specifies the items row index 351 | * @param {Number} column (Optional) Specifies the items column index 352 | * @param {Array} ignoreItems 353 | */ 354 | this.putItem = function(item, row, column, ignoreItems) { 355 | // auto place item if no row specified 356 | if (typeof row === 'undefined' || row === null) { 357 | row = item.row; 358 | column = item.col; 359 | if (typeof row === 'undefined' || row === null) { 360 | this.autoSetItemPosition(item); 361 | return; 362 | } 363 | } 364 | 365 | // keep item within allowed bounds 366 | if (!this.canItemOccupy(item, row, column)) { 367 | column = Math.min(this.columns - item.sizeX, Math.max(0, column)); 368 | row = Math.min(this.maxRows - item.sizeY, Math.max(0, row)); 369 | } 370 | 371 | // check if item is already in grid 372 | if (item.oldRow !== null && typeof item.oldRow !== 'undefined') { 373 | var samePosition = item.oldRow === row && item.oldColumn === column; 374 | var inGrid = this.grid[row] && this.grid[row][column] === item; 375 | if (samePosition && inGrid) { 376 | item.row = row; 377 | item.col = column; 378 | return; 379 | } else { 380 | // remove from old position 381 | var oldRow = this.grid[item.oldRow]; 382 | if (oldRow && oldRow[item.oldColumn] === item) { 383 | delete oldRow[item.oldColumn]; 384 | } 385 | } 386 | } 387 | 388 | item.oldRow = item.row = row; 389 | item.oldColumn = item.col = column; 390 | 391 | this.moveOverlappingItems(item, ignoreItems); 392 | 393 | if (!this.grid[row]) { 394 | this.grid[row] = []; 395 | } 396 | this.grid[row][column] = item; 397 | 398 | if (this.sparse && this.allItems.indexOf(item) === -1) { 399 | this.allItems.push(item); 400 | } 401 | 402 | if (this.movingItem === item) { 403 | this.floatItemUp(item); 404 | } 405 | this.layoutChanged(); 406 | }; 407 | 408 | /** 409 | * Trade row and column if item1 with item2 410 | * 411 | * @param {Object} item1 412 | * @param {Object} item2 413 | */ 414 | this.swapItems = function(item1, item2) { 415 | this.grid[item1.row][item1.col] = item2; 416 | this.grid[item2.row][item2.col] = item1; 417 | 418 | var item1Row = item1.row; 419 | var item1Col = item1.col; 420 | item1.row = item2.row; 421 | item1.col = item2.col; 422 | item2.row = item1Row; 423 | item2.col = item1Col; 424 | }; 425 | 426 | /** 427 | * Prevents items from being overlapped 428 | * 429 | * @param {Object} item The item that should remain 430 | * @param {Array} ignoreItems 431 | */ 432 | this.moveOverlappingItems = function(item, ignoreItems) { 433 | // don't move item, so ignore it 434 | if (!ignoreItems) { 435 | ignoreItems = [item]; 436 | } else if (ignoreItems.indexOf(item) === -1) { 437 | ignoreItems = ignoreItems.slice(0); 438 | ignoreItems.push(item); 439 | } 440 | 441 | // get the items in the space occupied by the item's coordinates 442 | var overlappingItems = this.getItems( 443 | item.row, 444 | item.col, 445 | item.sizeX, 446 | item.sizeY, 447 | ignoreItems 448 | ); 449 | this.moveItemsDown(overlappingItems, item.row + item.sizeY, ignoreItems); 450 | }; 451 | 452 | /** 453 | * Moves an array of items to a specified row 454 | * 455 | * @param {Array} items The items to move 456 | * @param {Number} newRow The target row 457 | * @param {Array} ignoreItems 458 | */ 459 | this.moveItemsDown = function(items, newRow, ignoreItems) { 460 | if (!items || items.length === 0) { 461 | return; 462 | } 463 | items.sort(function(a, b) { 464 | return a.row - b.row; 465 | }); 466 | 467 | ignoreItems = ignoreItems ? ignoreItems.slice(0) : []; 468 | var topRows = {}, 469 | item, i, l; 470 | 471 | // calculate the top rows in each column 472 | for (i = 0, l = items.length; i < l; ++i) { 473 | item = items[i]; 474 | var topRow = topRows[item.col]; 475 | if (typeof topRow === 'undefined' || item.row < topRow) { 476 | topRows[item.col] = item.row; 477 | } 478 | } 479 | 480 | // move each item down from the top row in its column to the row 481 | for (i = 0, l = items.length; i < l; ++i) { 482 | item = items[i]; 483 | var rowsToMove = newRow - topRows[item.col]; 484 | this.moveItemDown(item, item.row + rowsToMove, ignoreItems); 485 | ignoreItems.push(item); 486 | } 487 | }; 488 | 489 | /** 490 | * Moves an item down to a specified row 491 | * 492 | * @param {Object} item The item to move 493 | * @param {Number} newRow The target row 494 | * @param {Array} ignoreItems 495 | */ 496 | this.moveItemDown = function(item, newRow, ignoreItems) { 497 | if (item.row >= newRow) { 498 | return; 499 | } 500 | while (item.row < newRow) { 501 | ++item.row; 502 | this.moveOverlappingItems(item, ignoreItems); 503 | } 504 | this.putItem(item, item.row, item.col, ignoreItems); 505 | }; 506 | 507 | /** 508 | * Moves all items up as much as possible 509 | */ 510 | this.floatItemsUp = function() { 511 | if (this.floating === false) { 512 | return; 513 | } 514 | for (var rowIndex = 0, l = this.grid.length; rowIndex < l; ++rowIndex) { 515 | var columns = this.grid[rowIndex]; 516 | if (!columns) { 517 | continue; 518 | } 519 | for (var colIndex = 0, len = columns.length; colIndex < len; ++colIndex) { 520 | var item = columns[colIndex]; 521 | if (item) { 522 | this.floatItemUp(item); 523 | } 524 | } 525 | } 526 | }; 527 | 528 | /** 529 | * Float an item up to the most suitable row 530 | * 531 | * @param {Object} item The item to move 532 | */ 533 | this.floatItemUp = function(item) { 534 | if (this.floating === false) { 535 | return; 536 | } 537 | var colIndex = item.col, 538 | sizeY = item.sizeY, 539 | sizeX = item.sizeX, 540 | bestRow = null, 541 | bestColumn = null, 542 | rowIndex = item.row - 1; 543 | 544 | while (rowIndex > -1) { 545 | var items = this.getItems(rowIndex, colIndex, sizeX, sizeY, item); 546 | if (items.length !== 0) { 547 | break; 548 | } 549 | bestRow = rowIndex; 550 | bestColumn = colIndex; 551 | --rowIndex; 552 | } 553 | if (bestRow !== null) { 554 | this.putItem(item, bestRow, bestColumn); 555 | } 556 | }; 557 | 558 | /** 559 | * Update gridsters height 560 | * 561 | * @param {Number} plus (Optional) Additional height to add 562 | */ 563 | this.updateHeight = function(plus) { 564 | var maxHeight = this.minRows; 565 | plus = plus || 0; 566 | for (var rowIndex = this.grid.length; rowIndex >= 0; --rowIndex) { 567 | var columns = this.grid[rowIndex]; 568 | if (!columns) { 569 | continue; 570 | } 571 | for (var colIndex = 0, len = columns.length; colIndex < len; ++colIndex) { 572 | if (columns[colIndex]) { 573 | maxHeight = Math.max(maxHeight, rowIndex + plus + columns[colIndex].sizeY); 574 | } 575 | } 576 | } 577 | this.gridHeight = this.maxRows - maxHeight > 0 ? Math.min(this.maxRows, maxHeight) : Math.max(this.maxRows, maxHeight); 578 | }; 579 | 580 | /** 581 | * Returns the number of rows that will fit in given amount of pixels 582 | * 583 | * @param {Number} pixels 584 | * @param {Boolean} ceilOrFloor (Optional) Determines rounding method 585 | */ 586 | this.pixelsToRows = function(pixels, ceilOrFloor) { 587 | if (!this.outerMargin) { 588 | pixels += this.margins[0] / 2; 589 | } 590 | 591 | if (ceilOrFloor === true) { 592 | return Math.ceil(pixels / this.curRowHeight); 593 | } else if (ceilOrFloor === false) { 594 | return Math.floor(pixels / this.curRowHeight); 595 | } 596 | 597 | return Math.round(pixels / this.curRowHeight); 598 | }; 599 | 600 | /** 601 | * Returns the number of columns that will fit in a given amount of pixels 602 | * 603 | * @param {Number} pixels 604 | * @param {Boolean} ceilOrFloor (Optional) Determines rounding method 605 | * @returns {Number} The number of columns 606 | */ 607 | this.pixelsToColumns = function(pixels, ceilOrFloor) { 608 | if (!this.outerMargin) { 609 | pixels += this.margins[1] / 2; 610 | } 611 | 612 | if (ceilOrFloor === true) { 613 | return Math.ceil(pixels / this.curColWidth); 614 | } else if (ceilOrFloor === false) { 615 | return Math.floor(pixels / this.curColWidth); 616 | } 617 | 618 | return Math.round(pixels / this.curColWidth); 619 | }; 620 | } 621 | ]) 622 | 623 | .directive('gridsterPreview', function() { 624 | return { 625 | replace: true, 626 | scope: true, 627 | require: '^gridster', 628 | template: '
', 629 | link: function(scope, $el, attrs, gridster) { 630 | 631 | /** 632 | * @returns {Object} style object for preview element 633 | */ 634 | scope.previewStyle = function() { 635 | if (!gridster.movingItem) { 636 | return { 637 | display: 'none' 638 | }; 639 | } 640 | 641 | return { 642 | display: 'block', 643 | height: (gridster.movingItem.sizeY * gridster.curRowHeight - gridster.margins[0]) + 'px', 644 | width: (gridster.movingItem.sizeX * gridster.curColWidth - gridster.margins[1]) + 'px', 645 | top: (gridster.movingItem.row * gridster.curRowHeight + (gridster.outerMargin ? gridster.margins[0] : 0)) + 'px', 646 | left: (gridster.movingItem.col * gridster.curColWidth + (gridster.outerMargin ? gridster.margins[1] : 0)) + 'px' 647 | }; 648 | }; 649 | } 650 | }; 651 | }) 652 | 653 | /** 654 | * The gridster directive 655 | * 656 | * @param {Function} $timeout 657 | * @param {Object} $window 658 | * @param {Object} $rootScope 659 | * @param {Function} gridsterDebounce 660 | */ 661 | .directive('gridster', ['$timeout', '$window', '$rootScope', 'gridsterDebounce', 662 | function($timeout, $window, $rootScope, gridsterDebounce) { 663 | return { 664 | scope: true, 665 | restrict: 'EAC', 666 | controller: 'GridsterCtrl', 667 | controllerAs: 'gridster', 668 | compile: function($tplElem) { 669 | 670 | $tplElem.prepend('
'); 671 | 672 | return function(scope, $elem, attrs, gridster) { 673 | gridster.loaded = false; 674 | 675 | gridster.$element = $elem; 676 | 677 | scope.gridster = gridster; 678 | 679 | $elem.addClass('gridster'); 680 | 681 | var isVisible = function(ele) { 682 | return ele.style.visibility !== 'hidden' && ele.style.display !== 'none'; 683 | }; 684 | 685 | function updateHeight() { 686 | $elem.css('height', (gridster.gridHeight * gridster.curRowHeight) + (gridster.outerMargin ? gridster.margins[0] : -gridster.margins[0]) + 'px'); 687 | } 688 | 689 | scope.$watch(function() { 690 | return gridster.gridHeight; 691 | }, updateHeight); 692 | 693 | scope.$watch(function() { 694 | return gridster.movingItem; 695 | }, function() { 696 | gridster.updateHeight(gridster.movingItem ? gridster.movingItem.sizeY : 0); 697 | }); 698 | 699 | function refresh(config) { 700 | gridster.setOptions(config); 701 | 702 | if (!isVisible($elem[0])) { 703 | return; 704 | } 705 | 706 | // resolve "auto" & "match" values 707 | if (gridster.width === 'auto') { 708 | gridster.curWidth = $elem[0].offsetWidth || parseInt($elem.css('width'), 10); 709 | } else { 710 | gridster.curWidth = gridster.width; 711 | } 712 | 713 | if (gridster.colWidth === 'auto') { 714 | gridster.curColWidth = (gridster.curWidth + (gridster.outerMargin ? -gridster.margins[1] : gridster.margins[1])) / gridster.columns; 715 | } else { 716 | gridster.curColWidth = gridster.colWidth; 717 | } 718 | 719 | gridster.curRowHeight = gridster.rowHeight; 720 | if (typeof gridster.rowHeight === 'string') { 721 | if (gridster.rowHeight === 'match') { 722 | gridster.curRowHeight = Math.round(gridster.curColWidth); 723 | } else if (gridster.rowHeight.indexOf('*') !== -1) { 724 | gridster.curRowHeight = Math.round(gridster.curColWidth * gridster.rowHeight.replace('*', '').replace(' ', '')); 725 | } else if (gridster.rowHeight.indexOf('/') !== -1) { 726 | gridster.curRowHeight = Math.round(gridster.curColWidth / gridster.rowHeight.replace('/', '').replace(' ', '')); 727 | } 728 | } 729 | 730 | gridster.isMobile = gridster.mobileModeEnabled && gridster.curWidth <= gridster.mobileBreakPoint; 731 | 732 | // loop through all items and reset their CSS 733 | for (var rowIndex = 0, l = gridster.grid.length; rowIndex < l; ++rowIndex) { 734 | var columns = gridster.grid[rowIndex]; 735 | if (!columns) { 736 | continue; 737 | } 738 | 739 | for (var colIndex = 0, len = columns.length; colIndex < len; ++colIndex) { 740 | if (columns[colIndex]) { 741 | var item = columns[colIndex]; 742 | item.setElementPosition(); 743 | item.setElementSizeY(); 744 | item.setElementSizeX(); 745 | } 746 | } 747 | } 748 | 749 | updateHeight(); 750 | } 751 | 752 | var optionsKey = attrs.gridster; 753 | if (optionsKey) { 754 | scope.$parent.$watch(optionsKey, function(newConfig) { 755 | refresh(newConfig); 756 | }, true); 757 | } else { 758 | refresh({}); 759 | } 760 | 761 | scope.$watch(function() { 762 | return gridster.loaded; 763 | }, function() { 764 | if (gridster.loaded) { 765 | $elem.addClass('gridster-loaded'); 766 | $rootScope.$broadcast('gridster-loaded', gridster); 767 | } else { 768 | $elem.removeClass('gridster-loaded'); 769 | } 770 | }); 771 | 772 | scope.$watch(function() { 773 | return gridster.isMobile; 774 | }, function() { 775 | if (gridster.isMobile) { 776 | $elem.addClass('gridster-mobile').removeClass('gridster-desktop'); 777 | } else { 778 | $elem.removeClass('gridster-mobile').addClass('gridster-desktop'); 779 | } 780 | $rootScope.$broadcast('gridster-mobile-changed', gridster); 781 | }); 782 | 783 | scope.$watch(function() { 784 | return gridster.draggable; 785 | }, function() { 786 | $rootScope.$broadcast('gridster-draggable-changed', gridster); 787 | }, true); 788 | 789 | scope.$watch(function() { 790 | return gridster.resizable; 791 | }, function() { 792 | $rootScope.$broadcast('gridster-resizable-changed', gridster); 793 | }, true); 794 | 795 | var prevWidth = $elem[0].offsetWidth || parseInt($elem.css('width'), 10); 796 | 797 | var resize = function() { 798 | var width = $elem[0].offsetWidth || parseInt($elem.css('width'), 10); 799 | 800 | if (!width || width === prevWidth || gridster.movingItem) { 801 | return; 802 | } 803 | prevWidth = width; 804 | 805 | if (gridster.loaded) { 806 | $elem.removeClass('gridster-loaded'); 807 | } 808 | 809 | refresh(); 810 | 811 | if (gridster.loaded) { 812 | $elem.addClass('gridster-loaded'); 813 | } 814 | 815 | $rootScope.$broadcast('gridster-resized', [width, $elem[0].offsetHeight], gridster); 816 | }; 817 | 818 | // track element width changes any way we can 819 | var onResize = gridsterDebounce(function onResize() { 820 | resize(); 821 | $timeout(function() { 822 | scope.$apply(); 823 | }); 824 | }, 100); 825 | 826 | scope.$watch(function() { 827 | return isVisible($elem[0]); 828 | }, onResize); 829 | 830 | // see https://github.com/sdecima/javascript-detect-element-resize 831 | if (typeof window.addResizeListener === 'function') { 832 | window.addResizeListener($elem[0], onResize); 833 | } else { 834 | scope.$watch(function() { 835 | return $elem[0].offsetWidth || parseInt($elem.css('width'), 10); 836 | }, resize); 837 | } 838 | var $win = angular.element($window); 839 | $win.on('resize', onResize); 840 | 841 | // be sure to cleanup 842 | scope.$on('$destroy', function() { 843 | gridster.destroy(); 844 | $win.off('resize', onResize); 845 | if (typeof window.removeResizeListener === 'function') { 846 | window.removeResizeListener($elem[0], onResize); 847 | } 848 | }); 849 | 850 | // allow a little time to place items before floating up 851 | $timeout(function() { 852 | scope.$watch('gridster.floating', function() { 853 | gridster.floatItemsUp(); 854 | }); 855 | gridster.loaded = true; 856 | }, 100); 857 | }; 858 | } 859 | }; 860 | } 861 | ]) 862 | 863 | .controller('GridsterItemCtrl', function() { 864 | this.$element = null; 865 | this.gridster = null; 866 | this.row = null; 867 | this.col = null; 868 | this.sizeX = null; 869 | this.sizeY = null; 870 | this.minSizeX = 0; 871 | this.minSizeY = 0; 872 | this.maxSizeX = null; 873 | this.maxSizeY = null; 874 | 875 | this.init = function($element, gridster) { 876 | this.$element = $element; 877 | this.gridster = gridster; 878 | this.sizeX = gridster.defaultSizeX; 879 | this.sizeY = gridster.defaultSizeY; 880 | }; 881 | 882 | this.destroy = function() { 883 | // set these to null to avoid the possibility of circular references 884 | this.gridster = null; 885 | this.$element = null; 886 | }; 887 | 888 | /** 889 | * Returns the items most important attributes 890 | */ 891 | this.toJSON = function() { 892 | return { 893 | row: this.row, 894 | col: this.col, 895 | sizeY: this.sizeY, 896 | sizeX: this.sizeX 897 | }; 898 | }; 899 | 900 | this.isMoving = function() { 901 | return this.gridster.movingItem === this; 902 | }; 903 | 904 | /** 905 | * Set the items position 906 | * 907 | * @param {Number} row 908 | * @param {Number} column 909 | */ 910 | this.setPosition = function(row, column) { 911 | this.gridster.putItem(this, row, column); 912 | 913 | if (!this.isMoving()) { 914 | this.setElementPosition(); 915 | } 916 | }; 917 | 918 | /** 919 | * Sets a specified size property 920 | * 921 | * @param {String} key Can be either "x" or "y" 922 | * @param {Number} value The size amount 923 | * @param {Boolean} preventMove 924 | */ 925 | this.setSize = function(key, value, preventMove) { 926 | key = key.toUpperCase(); 927 | var camelCase = 'size' + key, 928 | titleCase = 'Size' + key; 929 | if (value === '') { 930 | return; 931 | } 932 | value = parseInt(value, 10); 933 | if (isNaN(value) || value === 0) { 934 | value = this.gridster['default' + titleCase]; 935 | } 936 | var max = key === 'X' ? this.gridster.columns : this.gridster.maxRows; 937 | if (this['max' + titleCase]) { 938 | max = Math.min(this['max' + titleCase], max); 939 | } 940 | if (this.gridster['max' + titleCase]) { 941 | max = Math.min(this.gridster['max' + titleCase], max); 942 | } 943 | if (key === 'X' && this.cols) { 944 | max -= this.cols; 945 | } else if (key === 'Y' && this.rows) { 946 | max -= this.rows; 947 | } 948 | 949 | var min = 0; 950 | if (this['min' + titleCase]) { 951 | min = Math.max(this['min' + titleCase], min); 952 | } 953 | if (this.gridster['min' + titleCase]) { 954 | min = Math.max(this.gridster['min' + titleCase], min); 955 | } 956 | 957 | value = Math.max(Math.min(value, max), min); 958 | 959 | var changed = (this[camelCase] !== value || (this['old' + titleCase] && this['old' + titleCase] !== value)); 960 | this['old' + titleCase] = this[camelCase] = value; 961 | 962 | if (!this.isMoving()) { 963 | this['setElement' + titleCase](); 964 | } 965 | if (!preventMove && changed) { 966 | this.gridster.moveOverlappingItems(this); 967 | this.gridster.layoutChanged(); 968 | } 969 | 970 | return changed; 971 | }; 972 | 973 | /** 974 | * Sets the items sizeY property 975 | * 976 | * @param {Number} rows 977 | * @param {Boolean} preventMove 978 | */ 979 | this.setSizeY = function(rows, preventMove) { 980 | return this.setSize('Y', rows, preventMove); 981 | }; 982 | 983 | /** 984 | * Sets the items sizeX property 985 | * 986 | * @param {Number} columns 987 | * @param {Boolean} preventMove 988 | */ 989 | this.setSizeX = function(columns, preventMove) { 990 | return this.setSize('X', columns, preventMove); 991 | }; 992 | 993 | /** 994 | * Sets an elements position on the page 995 | */ 996 | this.setElementPosition = function() { 997 | if (this.gridster.isMobile) { 998 | this.$element.css({ 999 | marginLeft: this.gridster.margins[0] + 'px', 1000 | marginRight: this.gridster.margins[0] + 'px', 1001 | marginTop: this.gridster.margins[1] + 'px', 1002 | marginBottom: this.gridster.margins[1] + 'px', 1003 | top: '', 1004 | left: '' 1005 | }); 1006 | } else { 1007 | this.$element.css({ 1008 | margin: 0, 1009 | top: (this.row * this.gridster.curRowHeight + (this.gridster.outerMargin ? this.gridster.margins[0] : 0)) + 'px', 1010 | left: (this.col * this.gridster.curColWidth + (this.gridster.outerMargin ? this.gridster.margins[1] : 0)) + 'px' 1011 | }); 1012 | } 1013 | }; 1014 | 1015 | /** 1016 | * Sets an elements height 1017 | */ 1018 | this.setElementSizeY = function() { 1019 | if (this.gridster.isMobile && !this.gridster.saveGridItemCalculatedHeightInMobile) { 1020 | this.$element.css('height', ''); 1021 | } else { 1022 | this.$element.css('height', (this.sizeY * this.gridster.curRowHeight - this.gridster.margins[0]) + 'px'); 1023 | } 1024 | }; 1025 | 1026 | /** 1027 | * Sets an elements width 1028 | */ 1029 | this.setElementSizeX = function() { 1030 | if (this.gridster.isMobile) { 1031 | this.$element.css('width', ''); 1032 | } else { 1033 | this.$element.css('width', (this.sizeX * this.gridster.curColWidth - this.gridster.margins[1]) + 'px'); 1034 | } 1035 | }; 1036 | 1037 | /** 1038 | * Gets an element's width 1039 | */ 1040 | this.getElementSizeX = function() { 1041 | return (this.sizeX * this.gridster.curColWidth - this.gridster.margins[1]); 1042 | }; 1043 | 1044 | /** 1045 | * Gets an element's height 1046 | */ 1047 | this.getElementSizeY = function() { 1048 | return (this.sizeY * this.gridster.curRowHeight - this.gridster.margins[0]); 1049 | }; 1050 | 1051 | }) 1052 | 1053 | .factory('GridsterTouch', [function() { 1054 | return function GridsterTouch(target, startEvent, moveEvent, endEvent) { 1055 | var lastXYById = {}; 1056 | 1057 | // Opera doesn't have Object.keys so we use this wrapper 1058 | var numberOfKeys = function(theObject) { 1059 | if (Object.keys) { 1060 | return Object.keys(theObject).length; 1061 | } 1062 | 1063 | var n = 0, 1064 | key; 1065 | for (key in theObject) { 1066 | ++n; 1067 | } 1068 | 1069 | return n; 1070 | }; 1071 | 1072 | // this calculates the delta needed to convert pageX/Y to offsetX/Y because offsetX/Y don't exist in the TouchEvent object or in Firefox's MouseEvent object 1073 | var computeDocumentToElementDelta = function(theElement) { 1074 | var elementLeft = 0; 1075 | var elementTop = 0; 1076 | var oldIEUserAgent = navigator.userAgent.match(/\bMSIE\b/); 1077 | 1078 | for (var offsetElement = theElement; offsetElement != null; offsetElement = offsetElement.offsetParent) { 1079 | // the following is a major hack for versions of IE less than 8 to avoid an apparent problem on the IEBlog with double-counting the offsets 1080 | // this may not be a general solution to IE7's problem with offsetLeft/offsetParent 1081 | if (oldIEUserAgent && 1082 | (!document.documentMode || document.documentMode < 8) && 1083 | offsetElement.currentStyle.position === 'relative' && offsetElement.offsetParent && offsetElement.offsetParent.currentStyle.position === 'relative' && offsetElement.offsetLeft === offsetElement.offsetParent.offsetLeft) { 1084 | // add only the top 1085 | elementTop += offsetElement.offsetTop; 1086 | } else { 1087 | elementLeft += offsetElement.offsetLeft; 1088 | elementTop += offsetElement.offsetTop; 1089 | } 1090 | } 1091 | 1092 | return { 1093 | x: elementLeft, 1094 | y: elementTop 1095 | }; 1096 | }; 1097 | 1098 | // cache the delta from the document to our event target (reinitialized each mousedown/MSPointerDown/touchstart) 1099 | var documentToTargetDelta = computeDocumentToElementDelta(target); 1100 | var useSetReleaseCapture = false; 1101 | 1102 | // common event handler for the mouse/pointer/touch models and their down/start, move, up/end, and cancel events 1103 | var doEvent = function(theEvtObj) { 1104 | 1105 | if (theEvtObj.type === 'mousemove' && numberOfKeys(lastXYById) === 0) { 1106 | return; 1107 | } 1108 | 1109 | var prevent = true; 1110 | 1111 | var pointerList = theEvtObj.changedTouches ? theEvtObj.changedTouches : [theEvtObj]; 1112 | for (var i = 0; i < pointerList.length; ++i) { 1113 | var pointerObj = pointerList[i]; 1114 | var pointerId = (typeof pointerObj.identifier !== 'undefined') ? pointerObj.identifier : (typeof pointerObj.pointerId !== 'undefined') ? pointerObj.pointerId : 1; 1115 | 1116 | // use the pageX/Y coordinates to compute target-relative coordinates when we have them (in ie < 9, we need to do a little work to put them there) 1117 | if (typeof pointerObj.pageX === 'undefined') { 1118 | // initialize assuming our source element is our target 1119 | pointerObj.pageX = pointerObj.offsetX + documentToTargetDelta.x; 1120 | pointerObj.pageY = pointerObj.offsetY + documentToTargetDelta.y; 1121 | 1122 | if (pointerObj.srcElement.offsetParent === target && document.documentMode && document.documentMode === 8 && pointerObj.type === 'mousedown') { 1123 | // source element is a child piece of VML, we're in IE8, and we've not called setCapture yet - add the origin of the source element 1124 | pointerObj.pageX += pointerObj.srcElement.offsetLeft; 1125 | pointerObj.pageY += pointerObj.srcElement.offsetTop; 1126 | } else if (pointerObj.srcElement !== target && !document.documentMode || document.documentMode < 8) { 1127 | // source element isn't the target (most likely it's a child piece of VML) and we're in a version of IE before IE8 - 1128 | // the offsetX/Y values are unpredictable so use the clientX/Y values and adjust by the scroll offsets of its parents 1129 | // to get the document-relative coordinates (the same as pageX/Y) 1130 | var sx = -2, 1131 | sy = -2; // adjust for old IE's 2-pixel border 1132 | for (var scrollElement = pointerObj.srcElement; scrollElement !== null; scrollElement = scrollElement.parentNode) { 1133 | sx += scrollElement.scrollLeft ? scrollElement.scrollLeft : 0; 1134 | sy += scrollElement.scrollTop ? scrollElement.scrollTop : 0; 1135 | } 1136 | 1137 | pointerObj.pageX = pointerObj.clientX + sx; 1138 | pointerObj.pageY = pointerObj.clientY + sy; 1139 | } 1140 | } 1141 | 1142 | 1143 | var pageX = pointerObj.pageX; 1144 | var pageY = pointerObj.pageY; 1145 | 1146 | if (theEvtObj.type.match(/(start|down)$/i)) { 1147 | // clause for processing MSPointerDown, touchstart, and mousedown 1148 | 1149 | // refresh the document-to-target delta on start in case the target has moved relative to document 1150 | documentToTargetDelta = computeDocumentToElementDelta(target); 1151 | 1152 | // protect against failing to get an up or end on this pointerId 1153 | if (lastXYById[pointerId]) { 1154 | if (endEvent) { 1155 | endEvent({ 1156 | target: theEvtObj.target, 1157 | which: theEvtObj.which, 1158 | pointerId: pointerId, 1159 | pageX: pageX, 1160 | pageY: pageY 1161 | }); 1162 | } 1163 | 1164 | delete lastXYById[pointerId]; 1165 | } 1166 | 1167 | if (startEvent) { 1168 | if (prevent) { 1169 | prevent = startEvent({ 1170 | target: theEvtObj.target, 1171 | which: theEvtObj.which, 1172 | pointerId: pointerId, 1173 | pageX: pageX, 1174 | pageY: pageY 1175 | }); 1176 | } 1177 | } 1178 | 1179 | // init last page positions for this pointer 1180 | lastXYById[pointerId] = { 1181 | x: pageX, 1182 | y: pageY 1183 | }; 1184 | 1185 | // IE pointer model 1186 | if (target.msSetPointerCapture && prevent) { 1187 | target.msSetPointerCapture(pointerId); 1188 | } else if (theEvtObj.type === 'mousedown' && numberOfKeys(lastXYById) === 1) { 1189 | if (useSetReleaseCapture) { 1190 | target.setCapture(true); 1191 | } else { 1192 | document.addEventListener('mousemove', doEvent, false); 1193 | document.addEventListener('mouseup', doEvent, false); 1194 | document.addEventListener('mouseleave', doEvent, false); 1195 | } 1196 | } 1197 | } else if (theEvtObj.type.match(/move$/i)) { 1198 | // clause handles mousemove, MSPointerMove, and touchmove 1199 | 1200 | if (lastXYById[pointerId] && !(lastXYById[pointerId].x === pageX && lastXYById[pointerId].y === pageY)) { 1201 | // only extend if the pointer is down and it's not the same as the last point 1202 | 1203 | if (moveEvent && prevent) { 1204 | prevent = moveEvent({ 1205 | target: theEvtObj.target, 1206 | which: theEvtObj.which, 1207 | pointerId: pointerId, 1208 | pageX: pageX, 1209 | pageY: pageY 1210 | }); 1211 | } 1212 | 1213 | // update last page positions for this pointer 1214 | lastXYById[pointerId].x = pageX; 1215 | lastXYById[pointerId].y = pageY; 1216 | } 1217 | } else if (lastXYById[pointerId] && theEvtObj.type.match(/(up|end|cancel|leave)$/i)) { 1218 | // clause handles up/end/cancel 1219 | 1220 | if (endEvent && prevent) { 1221 | prevent = endEvent({ 1222 | target: theEvtObj.target, 1223 | which: theEvtObj.which, 1224 | pointerId: pointerId, 1225 | pageX: pageX, 1226 | pageY: pageY 1227 | }); 1228 | } 1229 | 1230 | // delete last page positions for this pointer 1231 | delete lastXYById[pointerId]; 1232 | 1233 | // in the Microsoft pointer model, release the capture for this pointer 1234 | // in the mouse model, release the capture or remove document-level event handlers if there are no down points 1235 | // nothing is required for the iOS touch model because capture is implied on touchstart 1236 | if (target.msReleasePointerCapture) { 1237 | target.msReleasePointerCapture(pointerId); 1238 | } else if (theEvtObj.type === 'mouseup' && numberOfKeys(lastXYById) === 0) { 1239 | if (useSetReleaseCapture) { 1240 | target.releaseCapture(); 1241 | } else { 1242 | document.removeEventListener('mousemove', doEvent, false); 1243 | document.removeEventListener('mouseup', doEvent, false); 1244 | document.removeEventListener('mouseleave', doEvent, false); 1245 | } 1246 | } 1247 | } 1248 | } 1249 | 1250 | if (prevent) { 1251 | if (theEvtObj.preventDefault) { 1252 | theEvtObj.preventDefault(); 1253 | } 1254 | 1255 | if (theEvtObj.preventManipulation) { 1256 | theEvtObj.preventManipulation(); 1257 | } 1258 | 1259 | if (theEvtObj.preventMouseEvent) { 1260 | theEvtObj.preventMouseEvent(); 1261 | } 1262 | } 1263 | }; 1264 | 1265 | // saving the settings for contentZooming and touchaction before activation 1266 | var contentZooming, msTouchAction; 1267 | 1268 | this.enable = function() { 1269 | 1270 | if (window.navigator.msPointerEnabled) { 1271 | // Microsoft pointer model 1272 | target.addEventListener('MSPointerDown', doEvent, false); 1273 | target.addEventListener('MSPointerMove', doEvent, false); 1274 | target.addEventListener('MSPointerUp', doEvent, false); 1275 | target.addEventListener('MSPointerCancel', doEvent, false); 1276 | 1277 | // css way to prevent panning in our target area 1278 | if (typeof target.style.msContentZooming !== 'undefined') { 1279 | contentZooming = target.style.msContentZooming; 1280 | target.style.msContentZooming = 'none'; 1281 | } 1282 | 1283 | // new in Windows Consumer Preview: css way to prevent all built-in touch actions on our target 1284 | // without this, you cannot touch draw on the element because IE will intercept the touch events 1285 | if (typeof target.style.msTouchAction !== 'undefined') { 1286 | msTouchAction = target.style.msTouchAction; 1287 | target.style.msTouchAction = 'none'; 1288 | } 1289 | } else if (target.addEventListener) { 1290 | // iOS touch model 1291 | target.addEventListener('touchstart', doEvent, false); 1292 | target.addEventListener('touchmove', doEvent, false); 1293 | target.addEventListener('touchend', doEvent, false); 1294 | target.addEventListener('touchcancel', doEvent, false); 1295 | 1296 | // mouse model 1297 | target.addEventListener('mousedown', doEvent, false); 1298 | 1299 | // mouse model with capture 1300 | // rejecting gecko because, unlike ie, firefox does not send events to target when the mouse is outside target 1301 | if (target.setCapture && !window.navigator.userAgent.match(/\bGecko\b/)) { 1302 | useSetReleaseCapture = true; 1303 | 1304 | target.addEventListener('mousemove', doEvent, false); 1305 | target.addEventListener('mouseup', doEvent, false); 1306 | } 1307 | } else if (target.attachEvent && target.setCapture) { 1308 | // legacy IE mode - mouse with capture 1309 | useSetReleaseCapture = true; 1310 | target.attachEvent('onmousedown', function() { 1311 | doEvent(window.event); 1312 | window.event.returnValue = false; 1313 | return false; 1314 | }); 1315 | target.attachEvent('onmousemove', function() { 1316 | doEvent(window.event); 1317 | window.event.returnValue = false; 1318 | return false; 1319 | }); 1320 | target.attachEvent('onmouseup', function() { 1321 | doEvent(window.event); 1322 | window.event.returnValue = false; 1323 | return false; 1324 | }); 1325 | } 1326 | }; 1327 | 1328 | this.disable = function() { 1329 | if (window.navigator.msPointerEnabled) { 1330 | // Microsoft pointer model 1331 | target.removeEventListener('MSPointerDown', doEvent, false); 1332 | target.removeEventListener('MSPointerMove', doEvent, false); 1333 | target.removeEventListener('MSPointerUp', doEvent, false); 1334 | target.removeEventListener('MSPointerCancel', doEvent, false); 1335 | 1336 | // reset zooming to saved value 1337 | if (contentZooming) { 1338 | target.style.msContentZooming = contentZooming; 1339 | } 1340 | 1341 | // reset touch action setting 1342 | if (msTouchAction) { 1343 | target.style.msTouchAction = msTouchAction; 1344 | } 1345 | } else if (target.removeEventListener) { 1346 | // iOS touch model 1347 | target.removeEventListener('touchstart', doEvent, false); 1348 | target.removeEventListener('touchmove', doEvent, false); 1349 | target.removeEventListener('touchend', doEvent, false); 1350 | target.removeEventListener('touchcancel', doEvent, false); 1351 | 1352 | // mouse model 1353 | target.removeEventListener('mousedown', doEvent, false); 1354 | 1355 | // mouse model with capture 1356 | // rejecting gecko because, unlike ie, firefox does not send events to target when the mouse is outside target 1357 | if (target.setCapture && !window.navigator.userAgent.match(/\bGecko\b/)) { 1358 | useSetReleaseCapture = true; 1359 | 1360 | target.removeEventListener('mousemove', doEvent, false); 1361 | target.removeEventListener('mouseup', doEvent, false); 1362 | } 1363 | } else if (target.detachEvent && target.setCapture) { 1364 | // legacy IE mode - mouse with capture 1365 | useSetReleaseCapture = true; 1366 | target.detachEvent('onmousedown'); 1367 | target.detachEvent('onmousemove'); 1368 | target.detachEvent('onmouseup'); 1369 | } 1370 | }; 1371 | 1372 | return this; 1373 | }; 1374 | }]) 1375 | 1376 | .factory('GridsterDraggable', ['$document', '$window', 'GridsterTouch', 1377 | function($document, $window, GridsterTouch) { 1378 | function GridsterDraggable($el, scope, gridster, item, itemOptions) { 1379 | 1380 | var elmX, elmY, elmW, elmH, 1381 | 1382 | mouseX = 0, 1383 | mouseY = 0, 1384 | lastMouseX = 0, 1385 | lastMouseY = 0, 1386 | mOffX = 0, 1387 | mOffY = 0, 1388 | 1389 | minTop = 0, 1390 | minLeft = 0, 1391 | realdocument = $document[0]; 1392 | 1393 | var originalCol, originalRow; 1394 | var inputTags = ['select', 'option', 'input', 'textarea', 'button']; 1395 | 1396 | function dragStart(event) { 1397 | $el.addClass('gridster-item-moving'); 1398 | gridster.movingItem = item; 1399 | 1400 | gridster.updateHeight(item.sizeY); 1401 | scope.$apply(function() { 1402 | if (gridster.draggable && gridster.draggable.start) { 1403 | gridster.draggable.start(event, $el, itemOptions, item); 1404 | } 1405 | }); 1406 | } 1407 | 1408 | function drag(event) { 1409 | var oldRow = item.row, 1410 | oldCol = item.col, 1411 | hasCallback = gridster.draggable && gridster.draggable.drag, 1412 | scrollSensitivity = gridster.draggable.scrollSensitivity, 1413 | scrollSpeed = gridster.draggable.scrollSpeed; 1414 | 1415 | var row = Math.min(gridster.pixelsToRows(elmY), gridster.maxRows - 1); 1416 | var col = Math.min(gridster.pixelsToColumns(elmX), gridster.columns - 1); 1417 | 1418 | var itemsInTheWay = gridster.getItems(row, col, item.sizeX, item.sizeY, item); 1419 | var hasItemsInTheWay = itemsInTheWay.length !== 0; 1420 | 1421 | if (gridster.swapping === true && hasItemsInTheWay) { 1422 | var boundingBoxItem = gridster.getBoundingBox(itemsInTheWay), 1423 | sameSize = boundingBoxItem.sizeX === item.sizeX && boundingBoxItem.sizeY === item.sizeY, 1424 | sameRow = boundingBoxItem.row === oldRow, 1425 | sameCol = boundingBoxItem.col === oldCol, 1426 | samePosition = boundingBoxItem.row === row && boundingBoxItem.col === col, 1427 | inline = sameRow || sameCol; 1428 | 1429 | if (sameSize && itemsInTheWay.length === 1) { 1430 | if (samePosition) { 1431 | gridster.swapItems(item, itemsInTheWay[0]); 1432 | } else if (inline) { 1433 | return; 1434 | } 1435 | } else if (boundingBoxItem.sizeX <= item.sizeX && boundingBoxItem.sizeY <= item.sizeY && inline) { 1436 | var emptyRow = item.row <= row ? item.row : row + item.sizeY, 1437 | emptyCol = item.col <= col ? item.col : col + item.sizeX, 1438 | rowOffset = emptyRow - boundingBoxItem.row, 1439 | colOffset = emptyCol - boundingBoxItem.col; 1440 | 1441 | for (var i = 0, l = itemsInTheWay.length; i < l; ++i) { 1442 | var itemInTheWay = itemsInTheWay[i]; 1443 | 1444 | var itemsInFreeSpace = gridster.getItems( 1445 | itemInTheWay.row + rowOffset, 1446 | itemInTheWay.col + colOffset, 1447 | itemInTheWay.sizeX, 1448 | itemInTheWay.sizeY, 1449 | item 1450 | ); 1451 | 1452 | if (itemsInFreeSpace.length === 0) { 1453 | gridster.putItem(itemInTheWay, itemInTheWay.row + rowOffset, itemInTheWay.col + colOffset); 1454 | } 1455 | } 1456 | } 1457 | } 1458 | 1459 | if (gridster.pushing !== false || !hasItemsInTheWay) { 1460 | item.row = row; 1461 | item.col = col; 1462 | } 1463 | 1464 | if (event.pageY - realdocument.body.scrollTop < scrollSensitivity) { 1465 | realdocument.body.scrollTop = realdocument.body.scrollTop - scrollSpeed; 1466 | } else if ($window.innerHeight - (event.pageY - realdocument.body.scrollTop) < scrollSensitivity) { 1467 | realdocument.body.scrollTop = realdocument.body.scrollTop + scrollSpeed; 1468 | } 1469 | 1470 | if (event.pageX - realdocument.body.scrollLeft < scrollSensitivity) { 1471 | realdocument.body.scrollLeft = realdocument.body.scrollLeft - scrollSpeed; 1472 | } else if ($window.innerWidth - (event.pageX - realdocument.body.scrollLeft) < scrollSensitivity) { 1473 | realdocument.body.scrollLeft = realdocument.body.scrollLeft + scrollSpeed; 1474 | } 1475 | 1476 | if (hasCallback || oldRow !== item.row || oldCol !== item.col) { 1477 | scope.$apply(function() { 1478 | if (hasCallback) { 1479 | gridster.draggable.drag(event, $el, itemOptions, item); 1480 | } 1481 | }); 1482 | } 1483 | } 1484 | 1485 | function dragStop(event) { 1486 | $el.removeClass('gridster-item-moving'); 1487 | var row = Math.min(gridster.pixelsToRows(elmY), gridster.maxRows - 1); 1488 | var col = Math.min(gridster.pixelsToColumns(elmX), gridster.columns - 1); 1489 | if (gridster.pushing !== false || gridster.getItems(row, col, item.sizeX, item.sizeY, item).length === 0) { 1490 | item.row = row; 1491 | item.col = col; 1492 | } 1493 | gridster.movingItem = null; 1494 | item.setPosition(item.row, item.col); 1495 | 1496 | scope.$apply(function() { 1497 | if (gridster.draggable && gridster.draggable.stop) { 1498 | gridster.draggable.stop(event, $el, itemOptions, item); 1499 | } 1500 | }); 1501 | } 1502 | 1503 | function mouseDown(e) { 1504 | if (inputTags.indexOf(e.target.nodeName.toLowerCase()) !== -1) { 1505 | return false; 1506 | } 1507 | 1508 | var $target = angular.element(e.target); 1509 | 1510 | // exit, if a resize handle was hit 1511 | if ($target.hasClass('gridster-item-resizable-handler')) { 1512 | return false; 1513 | } 1514 | 1515 | // exit, if the target has it's own click event 1516 | if ($target.attr('onclick') || $target.attr('ng-click')) { 1517 | return false; 1518 | } 1519 | 1520 | // only works if you have jQuery 1521 | if ($target.closest && $target.closest('.gridster-no-drag').length) { 1522 | return false; 1523 | } 1524 | 1525 | // apply drag handle filter 1526 | if (gridster.draggable && gridster.draggable.handle) { 1527 | var $dragHandles = angular.element($el[0].querySelectorAll(gridster.draggable.handle)); 1528 | var match = false; 1529 | outerloop: 1530 | for (var h = 0, hl = $dragHandles.length; h < hl; ++h) { 1531 | var handle = $dragHandles[h]; 1532 | if (handle === e.target) { 1533 | match = true; 1534 | break; 1535 | } 1536 | var target = e.target; 1537 | for (var p = 0; p < 20; ++p) { 1538 | var parent = target.parentNode; 1539 | if (parent === $el[0] || !parent) { 1540 | break; 1541 | } 1542 | if (parent === handle) { 1543 | match = true; 1544 | break outerloop; 1545 | } 1546 | target = parent; 1547 | } 1548 | } 1549 | if (!match) { 1550 | return false; 1551 | } 1552 | } 1553 | 1554 | switch (e.which) { 1555 | case 1: 1556 | // left mouse button 1557 | break; 1558 | case 2: 1559 | case 3: 1560 | // right or middle mouse button 1561 | return; 1562 | } 1563 | 1564 | lastMouseX = e.pageX; 1565 | lastMouseY = e.pageY; 1566 | 1567 | elmX = parseInt($el.css('left'), 10); 1568 | elmY = parseInt($el.css('top'), 10); 1569 | elmW = $el[0].offsetWidth; 1570 | elmH = $el[0].offsetHeight; 1571 | 1572 | originalCol = item.col; 1573 | originalRow = item.row; 1574 | 1575 | dragStart(e); 1576 | 1577 | return true; 1578 | } 1579 | 1580 | function mouseMove(e) { 1581 | if (!$el.hasClass('gridster-item-moving') || $el.hasClass('gridster-item-resizing')) { 1582 | return false; 1583 | } 1584 | 1585 | var maxLeft = gridster.curWidth - 1; 1586 | var maxTop = gridster.curRowHeight * gridster.maxRows - 1; 1587 | 1588 | // Get the current mouse position. 1589 | mouseX = e.pageX; 1590 | mouseY = e.pageY; 1591 | 1592 | // Get the deltas 1593 | var diffX = mouseX - lastMouseX + mOffX; 1594 | var diffY = mouseY - lastMouseY + mOffY; 1595 | mOffX = mOffY = 0; 1596 | 1597 | // Update last processed mouse positions. 1598 | lastMouseX = mouseX; 1599 | lastMouseY = mouseY; 1600 | 1601 | var dX = diffX, 1602 | dY = diffY; 1603 | if (elmX + dX < minLeft) { 1604 | diffX = minLeft - elmX; 1605 | mOffX = dX - diffX; 1606 | } else if (elmX + elmW + dX > maxLeft) { 1607 | diffX = maxLeft - elmX - elmW; 1608 | mOffX = dX - diffX; 1609 | } 1610 | 1611 | if (elmY + dY < minTop) { 1612 | diffY = minTop - elmY; 1613 | mOffY = dY - diffY; 1614 | } else if (elmY + elmH + dY > maxTop) { 1615 | diffY = maxTop - elmY - elmH; 1616 | mOffY = dY - diffY; 1617 | } 1618 | elmX += diffX; 1619 | elmY += diffY; 1620 | 1621 | // set new position 1622 | $el.css({ 1623 | 'top': elmY + 'px', 1624 | 'left': elmX + 'px' 1625 | }); 1626 | 1627 | drag(e); 1628 | 1629 | return true; 1630 | } 1631 | 1632 | function mouseUp(e) { 1633 | if (!$el.hasClass('gridster-item-moving') || $el.hasClass('gridster-item-resizing')) { 1634 | return false; 1635 | } 1636 | 1637 | mOffX = mOffY = 0; 1638 | 1639 | dragStop(e); 1640 | 1641 | return true; 1642 | } 1643 | 1644 | var enabled = null; 1645 | var gridsterTouch = null; 1646 | 1647 | this.enable = function() { 1648 | if (enabled === true) { 1649 | return; 1650 | } 1651 | enabled = true; 1652 | 1653 | if (gridsterTouch) { 1654 | gridsterTouch.enable(); 1655 | return; 1656 | } 1657 | 1658 | gridsterTouch = new GridsterTouch($el[0], mouseDown, mouseMove, mouseUp); 1659 | gridsterTouch.enable(); 1660 | }; 1661 | 1662 | this.disable = function() { 1663 | if (enabled === false) { 1664 | return; 1665 | } 1666 | 1667 | enabled = false; 1668 | if (gridsterTouch) { 1669 | gridsterTouch.disable(); 1670 | } 1671 | }; 1672 | 1673 | this.toggle = function(enabled) { 1674 | if (enabled) { 1675 | this.enable(); 1676 | } else { 1677 | this.disable(); 1678 | } 1679 | }; 1680 | 1681 | this.destroy = function() { 1682 | this.disable(); 1683 | }; 1684 | } 1685 | 1686 | return GridsterDraggable; 1687 | } 1688 | ]) 1689 | 1690 | .factory('GridsterResizable', ['GridsterTouch', function(GridsterTouch) { 1691 | function GridsterResizable($el, scope, gridster, item, itemOptions) { 1692 | 1693 | function ResizeHandle(handleClass) { 1694 | 1695 | var hClass = handleClass; 1696 | 1697 | var elmX, elmY, elmW, elmH, 1698 | 1699 | mouseX = 0, 1700 | mouseY = 0, 1701 | lastMouseX = 0, 1702 | lastMouseY = 0, 1703 | mOffX = 0, 1704 | mOffY = 0, 1705 | 1706 | minTop = 0, 1707 | maxTop = 9999, 1708 | minLeft = 0; 1709 | 1710 | var getMinHeight = function() { 1711 | return (item.minSizeY ? item.minSizeY : 1) * gridster.curRowHeight - gridster.margins[0]; 1712 | }; 1713 | var getMinWidth = function() { 1714 | return (item.minSizeX ? item.minSizeX : 1) * gridster.curColWidth - gridster.margins[1]; 1715 | }; 1716 | 1717 | var originalWidth, originalHeight; 1718 | var savedDraggable; 1719 | 1720 | function resizeStart(e) { 1721 | $el.addClass('gridster-item-moving'); 1722 | $el.addClass('gridster-item-resizing'); 1723 | 1724 | gridster.movingItem = item; 1725 | 1726 | item.setElementSizeX(); 1727 | item.setElementSizeY(); 1728 | item.setElementPosition(); 1729 | gridster.updateHeight(1); 1730 | 1731 | scope.$apply(function() { 1732 | // callback 1733 | if (gridster.resizable && gridster.resizable.start) { 1734 | gridster.resizable.start(e, $el, itemOptions, item); // options is the item model 1735 | } 1736 | }); 1737 | } 1738 | 1739 | function resize(e) { 1740 | var oldRow = item.row, 1741 | oldCol = item.col, 1742 | oldSizeX = item.sizeX, 1743 | oldSizeY = item.sizeY, 1744 | hasCallback = gridster.resizable && gridster.resizable.resize; 1745 | 1746 | var col = item.col; 1747 | // only change column if grabbing left edge 1748 | if (['w', 'nw', 'sw'].indexOf(handleClass) !== -1) { 1749 | col = gridster.pixelsToColumns(elmX, false); 1750 | } 1751 | 1752 | var row = item.row; 1753 | // only change row if grabbing top edge 1754 | if (['n', 'ne', 'nw'].indexOf(handleClass) !== -1) { 1755 | row = gridster.pixelsToRows(elmY, false); 1756 | } 1757 | 1758 | var sizeX = item.sizeX; 1759 | // only change row if grabbing left or right edge 1760 | if (['n', 's'].indexOf(handleClass) === -1) { 1761 | sizeX = gridster.pixelsToColumns(elmW, true); 1762 | } 1763 | 1764 | var sizeY = item.sizeY; 1765 | // only change row if grabbing top or bottom edge 1766 | if (['e', 'w'].indexOf(handleClass) === -1) { 1767 | sizeY = gridster.pixelsToRows(elmH, true); 1768 | } 1769 | 1770 | 1771 | var canOccupy = row > -1 && col > -1 && sizeX + col <= gridster.columns && sizeY + row <= gridster.maxRows; 1772 | if (canOccupy && (gridster.pushing !== false || gridster.getItems(row, col, sizeX, sizeY, item).length === 0)) { 1773 | item.row = row; 1774 | item.col = col; 1775 | item.sizeX = sizeX; 1776 | item.sizeY = sizeY; 1777 | } 1778 | var isChanged = item.row !== oldRow || item.col !== oldCol || item.sizeX !== oldSizeX || item.sizeY !== oldSizeY; 1779 | 1780 | if (hasCallback || isChanged) { 1781 | scope.$apply(function() { 1782 | if (hasCallback) { 1783 | gridster.resizable.resize(e, $el, itemOptions, item); // options is the item model 1784 | } 1785 | }); 1786 | } 1787 | } 1788 | 1789 | function resizeStop(e) { 1790 | $el.removeClass('gridster-item-moving'); 1791 | $el.removeClass('gridster-item-resizing'); 1792 | 1793 | gridster.movingItem = null; 1794 | 1795 | item.setPosition(item.row, item.col); 1796 | item.setSizeY(item.sizeY); 1797 | item.setSizeX(item.sizeX); 1798 | 1799 | scope.$apply(function() { 1800 | if (gridster.resizable && gridster.resizable.stop) { 1801 | gridster.resizable.stop(e, $el, itemOptions, item); // options is the item model 1802 | } 1803 | }); 1804 | } 1805 | 1806 | function mouseDown(e) { 1807 | switch (e.which) { 1808 | case 1: 1809 | // left mouse button 1810 | break; 1811 | case 2: 1812 | case 3: 1813 | // right or middle mouse button 1814 | return; 1815 | } 1816 | 1817 | // save the draggable setting to restore after resize 1818 | savedDraggable = gridster.draggable.enabled; 1819 | if (savedDraggable) { 1820 | gridster.draggable.enabled = false; 1821 | scope.$broadcast('gridster-draggable-changed', gridster); 1822 | } 1823 | 1824 | // Get the current mouse position. 1825 | lastMouseX = e.pageX; 1826 | lastMouseY = e.pageY; 1827 | 1828 | // Record current widget dimensions 1829 | elmX = parseInt($el.css('left'), 10); 1830 | elmY = parseInt($el.css('top'), 10); 1831 | elmW = $el[0].offsetWidth; 1832 | elmH = $el[0].offsetHeight; 1833 | 1834 | originalWidth = item.sizeX; 1835 | originalHeight = item.sizeY; 1836 | 1837 | resizeStart(e); 1838 | 1839 | return true; 1840 | } 1841 | 1842 | function mouseMove(e) { 1843 | var maxLeft = gridster.curWidth - 1; 1844 | 1845 | // Get the current mouse position. 1846 | mouseX = e.pageX; 1847 | mouseY = e.pageY; 1848 | 1849 | // Get the deltas 1850 | var diffX = mouseX - lastMouseX + mOffX; 1851 | var diffY = mouseY - lastMouseY + mOffY; 1852 | mOffX = mOffY = 0; 1853 | 1854 | // Update last processed mouse positions. 1855 | lastMouseX = mouseX; 1856 | lastMouseY = mouseY; 1857 | 1858 | var dY = diffY, 1859 | dX = diffX; 1860 | 1861 | if (hClass.indexOf('n') >= 0) { 1862 | if (elmH - dY < getMinHeight()) { 1863 | diffY = elmH - getMinHeight(); 1864 | mOffY = dY - diffY; 1865 | } else if (elmY + dY < minTop) { 1866 | diffY = minTop - elmY; 1867 | mOffY = dY - diffY; 1868 | } 1869 | elmY += diffY; 1870 | elmH -= diffY; 1871 | } 1872 | if (hClass.indexOf('s') >= 0) { 1873 | if (elmH + dY < getMinHeight()) { 1874 | diffY = getMinHeight() - elmH; 1875 | mOffY = dY - diffY; 1876 | } else if (elmY + elmH + dY > maxTop) { 1877 | diffY = maxTop - elmY - elmH; 1878 | mOffY = dY - diffY; 1879 | } 1880 | elmH += diffY; 1881 | } 1882 | if (hClass.indexOf('w') >= 0) { 1883 | if (elmW - dX < getMinWidth()) { 1884 | diffX = elmW - getMinWidth(); 1885 | mOffX = dX - diffX; 1886 | } else if (elmX + dX < minLeft) { 1887 | diffX = minLeft - elmX; 1888 | mOffX = dX - diffX; 1889 | } 1890 | elmX += diffX; 1891 | elmW -= diffX; 1892 | } 1893 | if (hClass.indexOf('e') >= 0) { 1894 | if (elmW + dX < getMinWidth()) { 1895 | diffX = getMinWidth() - elmW; 1896 | mOffX = dX - diffX; 1897 | } else if (elmX + elmW + dX > maxLeft) { 1898 | diffX = maxLeft - elmX - elmW; 1899 | mOffX = dX - diffX; 1900 | } 1901 | elmW += diffX; 1902 | } 1903 | 1904 | // set new position 1905 | $el.css({ 1906 | 'top': elmY + 'px', 1907 | 'left': elmX + 'px', 1908 | 'width': elmW + 'px', 1909 | 'height': elmH + 'px' 1910 | }); 1911 | 1912 | resize(e); 1913 | 1914 | return true; 1915 | } 1916 | 1917 | function mouseUp(e) { 1918 | // restore draggable setting to its original state 1919 | if (gridster.draggable.enabled !== savedDraggable) { 1920 | gridster.draggable.enabled = savedDraggable; 1921 | scope.$broadcast('gridster-draggable-changed', gridster); 1922 | } 1923 | 1924 | mOffX = mOffY = 0; 1925 | 1926 | resizeStop(e); 1927 | 1928 | return true; 1929 | } 1930 | 1931 | var $dragHandle = null; 1932 | var unifiedInput; 1933 | 1934 | this.enable = function() { 1935 | if (!$dragHandle) { 1936 | $dragHandle = angular.element('
'); 1937 | $el.append($dragHandle); 1938 | } 1939 | 1940 | unifiedInput = new GridsterTouch($dragHandle[0], mouseDown, mouseMove, mouseUp); 1941 | unifiedInput.enable(); 1942 | }; 1943 | 1944 | this.disable = function() { 1945 | if ($dragHandle) { 1946 | $dragHandle.remove(); 1947 | $dragHandle = null; 1948 | } 1949 | 1950 | unifiedInput.disable(); 1951 | unifiedInput = undefined; 1952 | }; 1953 | 1954 | this.destroy = function() { 1955 | this.disable(); 1956 | }; 1957 | } 1958 | 1959 | var handles = []; 1960 | var handlesOpts = gridster.resizable.handles; 1961 | if (typeof handlesOpts === 'string') { 1962 | handlesOpts = gridster.resizable.handles.split(','); 1963 | } 1964 | var enabled = false; 1965 | 1966 | for (var c = 0, l = handlesOpts.length; c < l; c++) { 1967 | handles.push(new ResizeHandle(handlesOpts[c])); 1968 | } 1969 | 1970 | this.enable = function() { 1971 | if (enabled) { 1972 | return; 1973 | } 1974 | for (var c = 0, l = handles.length; c < l; c++) { 1975 | handles[c].enable(); 1976 | } 1977 | enabled = true; 1978 | }; 1979 | 1980 | this.disable = function() { 1981 | if (!enabled) { 1982 | return; 1983 | } 1984 | for (var c = 0, l = handles.length; c < l; c++) { 1985 | handles[c].disable(); 1986 | } 1987 | enabled = false; 1988 | }; 1989 | 1990 | this.toggle = function(enabled) { 1991 | if (enabled) { 1992 | this.enable(); 1993 | } else { 1994 | this.disable(); 1995 | } 1996 | }; 1997 | 1998 | this.destroy = function() { 1999 | for (var c = 0, l = handles.length; c < l; c++) { 2000 | handles[c].destroy(); 2001 | } 2002 | }; 2003 | } 2004 | return GridsterResizable; 2005 | }]) 2006 | 2007 | .factory('gridsterDebounce', function() { 2008 | return function gridsterDebounce(func, wait, immediate) { 2009 | var timeout; 2010 | return function() { 2011 | var context = this, 2012 | args = arguments; 2013 | var later = function() { 2014 | timeout = null; 2015 | if (!immediate) { 2016 | func.apply(context, args); 2017 | } 2018 | }; 2019 | var callNow = immediate && !timeout; 2020 | clearTimeout(timeout); 2021 | timeout = setTimeout(later, wait); 2022 | if (callNow) { 2023 | func.apply(context, args); 2024 | } 2025 | }; 2026 | }; 2027 | }) 2028 | 2029 | /** 2030 | * GridsterItem directive 2031 | * @param $parse 2032 | * @param GridsterDraggable 2033 | * @param GridsterResizable 2034 | * @param gridsterDebounce 2035 | */ 2036 | .directive('gridsterItem', ['$parse', 'GridsterDraggable', 'GridsterResizable', 'gridsterDebounce', 2037 | function($parse, GridsterDraggable, GridsterResizable, gridsterDebounce) { 2038 | return { 2039 | scope: true, 2040 | restrict: 'EA', 2041 | controller: 'GridsterItemCtrl', 2042 | controllerAs: 'gridsterItem', 2043 | require: ['^gridster', 'gridsterItem'], 2044 | link: function(scope, $el, attrs, controllers) { 2045 | var optionsKey = attrs.gridsterItem, 2046 | options; 2047 | 2048 | var gridster = controllers[0], 2049 | item = controllers[1]; 2050 | 2051 | scope.gridster = gridster; 2052 | 2053 | // bind the item's position properties 2054 | // options can be an object specified by gridster-item="object" 2055 | // or the options can be the element html attributes object 2056 | if (optionsKey) { 2057 | var $optionsGetter = $parse(optionsKey); 2058 | options = $optionsGetter(scope) || {}; 2059 | if (!options && $optionsGetter.assign) { 2060 | options = { 2061 | row: item.row, 2062 | col: item.col, 2063 | sizeX: item.sizeX, 2064 | sizeY: item.sizeY, 2065 | minSizeX: 0, 2066 | minSizeY: 0, 2067 | maxSizeX: null, 2068 | maxSizeY: null 2069 | }; 2070 | $optionsGetter.assign(scope, options); 2071 | } 2072 | } else { 2073 | options = attrs; 2074 | } 2075 | 2076 | item.init($el, gridster); 2077 | 2078 | $el.addClass('gridster-item'); 2079 | 2080 | var aspects = ['minSizeX', 'maxSizeX', 'minSizeY', 'maxSizeY', 'sizeX', 'sizeY', 'row', 'col'], 2081 | $getters = {}; 2082 | 2083 | var expressions = []; 2084 | var aspectFn = function(aspect) { 2085 | var expression; 2086 | if (typeof options[aspect] === 'string') { 2087 | // watch the expression in the scope 2088 | expression = options[aspect]; 2089 | } else if (typeof options[aspect.toLowerCase()] === 'string') { 2090 | // watch the expression in the scope 2091 | expression = options[aspect.toLowerCase()]; 2092 | } else if (optionsKey) { 2093 | // watch the expression on the options object in the scope 2094 | expression = optionsKey + '.' + aspect; 2095 | } else { 2096 | return; 2097 | } 2098 | expressions.push('"' + aspect + '":' + expression); 2099 | $getters[aspect] = $parse(expression); 2100 | 2101 | // initial set 2102 | var val = $getters[aspect](scope); 2103 | if (typeof val === 'number') { 2104 | item[aspect] = val; 2105 | } 2106 | }; 2107 | 2108 | for (var i = 0, l = aspects.length; i < l; ++i) { 2109 | aspectFn(aspects[i]); 2110 | } 2111 | 2112 | var watchExpressions = '{' + expressions.join(',') + '}'; 2113 | // when the value changes externally, update the internal item object 2114 | scope.$watchCollection(watchExpressions, function(newVals, oldVals) { 2115 | for (var aspect in newVals) { 2116 | var newVal = newVals[aspect]; 2117 | var oldVal = oldVals[aspect]; 2118 | if (oldVal === newVal) { 2119 | continue; 2120 | } 2121 | newVal = parseInt(newVal, 10); 2122 | if (!isNaN(newVal)) { 2123 | item[aspect] = newVal; 2124 | } 2125 | } 2126 | }); 2127 | 2128 | function positionChanged() { 2129 | // call setPosition so the element and gridster controller are updated 2130 | item.setPosition(item.row, item.col); 2131 | 2132 | // when internal item position changes, update externally bound values 2133 | if ($getters.row && $getters.row.assign) { 2134 | $getters.row.assign(scope, item.row); 2135 | } 2136 | if ($getters.col && $getters.col.assign) { 2137 | $getters.col.assign(scope, item.col); 2138 | } 2139 | } 2140 | scope.$watch(function() { 2141 | return item.row + ',' + item.col; 2142 | }, positionChanged); 2143 | 2144 | function sizeChanged() { 2145 | var changedX = item.setSizeX(item.sizeX, true); 2146 | if (changedX && $getters.sizeX && $getters.sizeX.assign) { 2147 | $getters.sizeX.assign(scope, item.sizeX); 2148 | } 2149 | var changedY = item.setSizeY(item.sizeY, true); 2150 | if (changedY && $getters.sizeY && $getters.sizeY.assign) { 2151 | $getters.sizeY.assign(scope, item.sizeY); 2152 | } 2153 | 2154 | if (changedX || changedY) { 2155 | item.gridster.moveOverlappingItems(item); 2156 | gridster.layoutChanged(); 2157 | scope.$broadcast('gridster-item-resized', item); 2158 | } 2159 | } 2160 | 2161 | scope.$watch(function() { 2162 | return item.sizeY + ',' + item.sizeX + ',' + item.minSizeX + ',' + item.maxSizeX + ',' + item.minSizeY + ',' + item.maxSizeY; 2163 | }, sizeChanged); 2164 | 2165 | var draggable = new GridsterDraggable($el, scope, gridster, item, options); 2166 | var resizable = new GridsterResizable($el, scope, gridster, item, options); 2167 | 2168 | var updateResizable = function() { 2169 | resizable.toggle(!gridster.isMobile && gridster.resizable && gridster.resizable.enabled); 2170 | }; 2171 | updateResizable(); 2172 | 2173 | var updateDraggable = function() { 2174 | draggable.toggle(!gridster.isMobile && gridster.draggable && gridster.draggable.enabled); 2175 | }; 2176 | updateDraggable(); 2177 | 2178 | scope.$on('gridster-draggable-changed', updateDraggable); 2179 | scope.$on('gridster-resizable-changed', updateResizable); 2180 | scope.$on('gridster-resized', updateResizable); 2181 | scope.$on('gridster-mobile-changed', function() { 2182 | updateResizable(); 2183 | updateDraggable(); 2184 | }); 2185 | 2186 | function whichTransitionEvent() { 2187 | var el = document.createElement('div'); 2188 | var transitions = { 2189 | 'transition': 'transitionend', 2190 | 'OTransition': 'oTransitionEnd', 2191 | 'MozTransition': 'transitionend', 2192 | 'WebkitTransition': 'webkitTransitionEnd' 2193 | }; 2194 | for (var t in transitions) { 2195 | if (el.style[t] !== undefined) { 2196 | return transitions[t]; 2197 | } 2198 | } 2199 | } 2200 | 2201 | var debouncedTransitionEndPublisher = gridsterDebounce(function() { 2202 | scope.$apply(function() { 2203 | scope.$broadcast('gridster-item-transition-end', item); 2204 | }); 2205 | }, 50); 2206 | 2207 | $el.on(whichTransitionEvent(), debouncedTransitionEndPublisher); 2208 | 2209 | scope.$broadcast('gridster-item-initialized', item); 2210 | 2211 | return scope.$on('$destroy', function() { 2212 | try { 2213 | resizable.destroy(); 2214 | draggable.destroy(); 2215 | } catch (e) {} 2216 | 2217 | try { 2218 | gridster.removeItem(item); 2219 | } catch (e) {} 2220 | 2221 | try { 2222 | item.destroy(); 2223 | } catch (e) {} 2224 | }); 2225 | } 2226 | }; 2227 | } 2228 | ]) 2229 | 2230 | .directive('gridsterNoDrag', function() { 2231 | return { 2232 | restrict: 'A', 2233 | link: function(scope, $element) { 2234 | $element.addClass('gridster-no-drag'); 2235 | } 2236 | }; 2237 | }) 2238 | 2239 | ; 2240 | 2241 | })); 2242 | -------------------------------------------------------------------------------- /src/angular-gridster.less: -------------------------------------------------------------------------------- 1 | /** 2 | * gridster.js - v0.2.1 - 2013-10-28 * http://gridster.net 3 | * Copyright (c) 2013 ducksboard; Licensed MIT 4 | */ 5 | 6 | .gridster { 7 | position: relative; 8 | margin: auto; 9 | height: 0; 10 | } 11 | 12 | .gridster > ul { 13 | margin: 0; 14 | list-style: none; 15 | padding: 0; 16 | } 17 | 18 | .gridster-item { 19 | -webkit-box-sizing: border-box; 20 | -moz-box-sizing: border-box; 21 | box-sizing: border-box; 22 | list-style: none; 23 | z-index: 2; 24 | position: absolute; 25 | display: none; 26 | } 27 | 28 | .gridster-loaded { 29 | -webkit-transition: height .3s; 30 | -moz-transition: height .3s; 31 | -o-transition: height .3s; 32 | transition: height .3s; 33 | 34 | .gridster-item { 35 | display: block; 36 | position: absolute; 37 | -webkit-transition: opacity .3s, left .3s, top .3s, width .3s, height .3s; 38 | -moz-transition: opacity .3s, left .3s, top .3s, width .3s, height .3s; 39 | -o-transition: opacity .3s, left .3s, top .3s, width .3s, height .3s; 40 | transition: opacity .3s, left .3s, top .3s, width .3s, height .3s; 41 | -webkit-transition-delay: 50ms; 42 | -moz-transition-delay: 50ms; 43 | -o-transition-delay: 50ms; 44 | transition-delay: 50ms; 45 | } 46 | 47 | .gridster-preview-holder { 48 | display: none; 49 | z-index: 1; 50 | position: absolute; 51 | background-color: #ddd; 52 | border-color: #fff; 53 | opacity: 0.2; 54 | } 55 | 56 | .gridster-item.gridster-item-moving, 57 | .gridster-preview-holder { 58 | -webkit-transition: none; 59 | -moz-transition: none; 60 | -o-transition: none; 61 | transition: none; 62 | } 63 | } 64 | 65 | .gridster-mobile { 66 | height: auto !important; 67 | 68 | .gridster-item { 69 | height: auto; 70 | position: static; 71 | float: none; 72 | } 73 | } 74 | 75 | .gridster-item.ng-leave.ng-leave-active { 76 | opacity: 0; 77 | } 78 | .gridster-item.ng-enter { 79 | opacity: 1; 80 | } 81 | 82 | .gridster-item-moving { 83 | z-index: 3; 84 | } 85 | 86 | /* RESIZE */ 87 | .gridster-item-resizable-handler { 88 | position: absolute; 89 | font-size: 1px; 90 | display: block; 91 | z-index: 5; 92 | } 93 | 94 | .handle-se { 95 | cursor: se-resize; 96 | width: 0; 97 | height: 0; 98 | right: 1px; 99 | bottom: 1px; 100 | border-style: solid; 101 | border-width: 0 0 12px 12px; 102 | border-color: transparent; 103 | } 104 | 105 | .handle-ne { 106 | cursor: ne-resize; 107 | width: 12px; 108 | height: 12px; 109 | right: 1px; 110 | top: 1px; 111 | } 112 | 113 | .handle-nw { 114 | cursor: nw-resize; 115 | width: 12px; 116 | height: 12px; 117 | left: 1px; 118 | top: 1px; 119 | } 120 | 121 | .handle-sw { 122 | cursor: sw-resize; 123 | width: 12px; 124 | height: 12px; 125 | left: 1px; 126 | bottom: 1px; 127 | } 128 | 129 | .handle-e { 130 | cursor: e-resize; 131 | width: 12px; 132 | bottom: 0; 133 | right: 1px; 134 | top: 0; 135 | } 136 | 137 | .handle-s { 138 | cursor: s-resize; 139 | height: 12px; 140 | right: 0; 141 | bottom: 1px; 142 | left: 0; 143 | } 144 | 145 | .handle-n { 146 | cursor: n-resize; 147 | height: 12px; 148 | right: 0; 149 | top: 1px; 150 | left: 0; 151 | } 152 | 153 | .handle-w { 154 | cursor: w-resize; 155 | width: 12px; 156 | left: 1px; 157 | top: 0; 158 | bottom: 0; 159 | } 160 | 161 | .gridster .gridster-item:hover .gridster-box { 162 | border: 1.5px solid #B3B2B3; 163 | } 164 | 165 | .gridster .gridster-item:hover .handle-se { 166 | border-color: transparent transparent #ccc; 167 | } -------------------------------------------------------------------------------- /test.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Angular Gridster 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 19 | 20 | 21 | 22 | 23 | 28 | 29 | 30 | 31 | 32 | 33 |
34 |
35 |
36 |

Standard Items

37 |

38 | Each item provides its own dimensions and position using the standard fields: { row: row, col: col, sizeX: sizeX, sizeY: sizeY }. 39 |

40 |
41 |
    42 |
  • 43 |
    x
    44 | , 45 | 46 |
    47 | x 48 | 49 |
  • 50 |
51 |
52 | 53 |

Custom Items

54 |

55 | Each item provides its own dimensions but with custom fields defined using customItemMap: { position: [row, col], size: { x: sizeX, y: sizeY }} 56 |

57 |
58 |
    59 |
  • 60 | , 61 | 62 |
    63 | x 64 | 65 |
  • 66 |
67 |
68 | 69 |

Custom Items2

70 |

71 | Each item provides its own dimensions but with custom fields indicated using html attributes: row, col, sizex, sizey. Size can also be in the form of data-size-x or data-sizex. 72 |

73 |
74 |
    75 |
  • 76 | , 77 | 78 |
    79 | x 80 | 81 |
  • 82 |
83 |
84 | 85 |

Empty Items

86 |

87 | Each item stores the standard options as an object within itself: { grid: {row: row, col: col, sizeX: sizeX, sizeY: sizeY }} 88 |

89 |
90 |
    91 |
  • 92 | , 93 | 94 |
    95 | x 96 | 97 |
  • 98 |
99 |
100 | 101 |

No Configuration or Binding

102 |

103 | No data binding or configuration provided. 104 |

105 |
106 |
    107 |
  • 108 |
109 |
110 |
111 |
112 | 113 |
114 | 115 | 116 | 117 | -------------------------------------------------------------------------------- /test/e2e/gridster.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /* global browser */ 4 | /* global element */ 5 | /* global by */ 6 | 7 | describe('Controller: GridsterCtrl', function() { 8 | var items, 9 | firstItem; 10 | 11 | beforeEach(function() { 12 | browser.get('test.html'); 13 | browser.driver.manage() 14 | .window() 15 | .setSize(1000, 1000); 16 | items = element.all(by.css('[gridster-item]')); 17 | firstItem = items.get(0); 18 | }); 19 | 20 | it('should have a page with elements', function() { 21 | element.all(by.repeater('item in standardItems')).then(function(items) { 22 | expect(items.length).toEqual(11); 23 | }); 24 | 25 | browser.findElement(by.css('h2:first-child')).then(function(el) { 26 | return el.getText().then(function(text) { 27 | expect(text).toBe('Standard Items'); 28 | }); 29 | }); 30 | }); 31 | 32 | it('should allow the user to enter a size', function() { 33 | var width = 0; 34 | 35 | firstItem.getSize().then(function(size) { 36 | expect(size.width).toBeGreaterThan(0); 37 | width = size.width; 38 | }).then(function() { 39 | return firstItem.element(by.model('item.sizeX')); 40 | }).then(function(input) { 41 | return input.sendKeys('2').then(function() { 42 | input.sendKeys(protractor.Key.TAB); 43 | }); 44 | }).then(function() { 45 | return firstItem.getSize(); 46 | }).then(function(size) { 47 | expect(size.width).toBeGreaterThan(width); 48 | }); 49 | }); 50 | 51 | it('should resize the row widths and heights', function() { 52 | var initialSize; 53 | 54 | browser.driver.manage().window().setSize(1200, 1200); 55 | firstItem.getSize() 56 | .then(function setInitialSize(size) { 57 | initialSize = size; 58 | }) 59 | .then(function() { 60 | browser.driver.manage().window().setSize(1000, 1000); 61 | firstItem.getSize().then(function(newSize) { 62 | expect(newSize.width).toBeLessThan(initialSize.width); 63 | expect(newSize.height).toBeLessThan(initialSize.height); 64 | }); 65 | }); 66 | }); 67 | }); 68 | -------------------------------------------------------------------------------- /test/spec/gridster-directive.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | describe('gridster directive', function() { 4 | 5 | beforeEach(module('gridster')); 6 | 7 | var $scope; 8 | var GridsterCtrl; 9 | var $el; 10 | var startCount; 11 | var resizeCount; 12 | var stopCount; 13 | var broadcastOnRootScope; 14 | 15 | var dragHelper = function(el, dx, dy) { 16 | el.simulate('mouseover').simulate('drag', { 17 | moves: 1, 18 | dx: dx, 19 | dy: dy 20 | }); 21 | }; 22 | 23 | beforeEach(inject(function($rootScope, $compile) { 24 | broadcastOnRootScope = spyOn($rootScope, '$broadcast').and.callThrough(); 25 | 26 | $scope = $rootScope.$new(); 27 | startCount = resizeCount = stopCount = 0; 28 | 29 | $scope.opts = { 30 | minRows: 3, 31 | resizable: { 32 | enabled: true, 33 | handles: ['n', 'e', 's', 'w', 'se', 'sw'], 34 | start: function() { 35 | startCount++; 36 | }, 37 | resize: function() { 38 | resizeCount++; 39 | }, 40 | stop: function() { 41 | stopCount++; 42 | } 43 | } 44 | }; 45 | 46 | $scope.dashboard = { 47 | widgets: [{ 48 | id: 1, 49 | row: 0, 50 | col: 0, 51 | sizeX: 1, 52 | sizeY: 1 53 | }, { 54 | id: 2, 55 | row: 0, 56 | col: 3, 57 | sizeX: 2, 58 | sizeY: 1 59 | }, { 60 | id: 3, 61 | row: 1, 62 | col: 3, 63 | sizeX: 2, 64 | sizeY: 2 65 | }] 66 | }; 67 | 68 | $el = angular.element('
' + 69 | '
  • ' + 70 | '
'); 71 | 72 | $el.appendTo(document.body); // append to body so jquery-simulate works 73 | 74 | $compile($el)($scope); 75 | $scope.$digest(); 76 | 77 | GridsterCtrl = $el.controller('gridster'); 78 | })); 79 | 80 | 81 | it('should add a class of gridster', function() { 82 | expect($el.hasClass('gridster')).toBe(true); 83 | }); 84 | 85 | it('should override options', function() { 86 | expect(GridsterCtrl.minRows).toBe($scope.opts.minRows); 87 | }); 88 | 89 | it('should add widgets to DOM', function() { 90 | expect($el.find('li').length).toBe($scope.dashboard.widgets.length); 91 | }); 92 | 93 | it('should initialize resizable', function() { 94 | var $widget = $el.find('li:first-child'); 95 | 96 | expect($widget.find('.handle-s').length).toBe(1); 97 | }); 98 | 99 | it('should update widget dimensions on resize & trigger custom resize events', function() { 100 | var $widget = $el.find('li:first-child'); 101 | var handle = $widget.find('.handle-e'); 102 | 103 | expect($widget.width()).toBe(155); 104 | expect($scope.dashboard.widgets[0].sizeX).toBe(1); 105 | expect(startCount).toBe(0); 106 | expect(resizeCount).toBe(0); 107 | expect(stopCount).toBe(0); 108 | 109 | dragHelper(handle, 50); // should resize to next width step 110 | 111 | expect($widget.width()).toBe(320); 112 | expect($scope.dashboard.widgets[0].sizeX).toBe(2); 113 | expect(startCount).toBe(1); 114 | expect(resizeCount).toBe(1); 115 | expect(stopCount).toBe(1); 116 | }); 117 | 118 | it('should broadcast "gridster-item-resized" event on resize', function() { 119 | // arrange 120 | var eHandle = $el.find('li:first-child').find('.handle-e'); 121 | var sHandle = $el.find('li:first-child').find('.handle-s'); 122 | broadcastOnRootScope.calls.reset(); 123 | 124 | // act 125 | dragHelper(eHandle, 50); 126 | 127 | // assert 128 | expect(broadcastOnRootScope).toHaveBeenCalledWith('gridster-item-resized', jasmine.objectContaining({ 129 | sizeX: 2, 130 | sizeY: 1 131 | })); 132 | 133 | // arrange 134 | broadcastOnRootScope.calls.reset(); 135 | 136 | // act 137 | dragHelper(sHandle, 0, 50); 138 | 139 | // assert 140 | expect(broadcastOnRootScope).toHaveBeenCalledWith('gridster-item-resized', jasmine.objectContaining({ 141 | sizeX: 2, 142 | sizeY: 2 143 | })); 144 | }); 145 | 146 | }); 147 | -------------------------------------------------------------------------------- /test/spec/gridster-item.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | describe('Controller: GridsterItemCtrl', function() { 4 | // load the controller's module 5 | beforeEach(module('gridster')); 6 | 7 | var gridster, 8 | config, 9 | scope, 10 | item1x1, 11 | item2x1, 12 | item1x2, 13 | item2x2, 14 | gridsterItem; 15 | 16 | // Initialize the controller and a mock scope 17 | beforeEach(inject(function($controller, $rootScope) { 18 | scope = $rootScope.$new(); 19 | 20 | config = { 21 | colWidth: 100, 22 | rowHeight: 100, 23 | columns: 6, 24 | margins: [10, 10], 25 | defaultHeight: 1, 26 | defaultWidth: 2, 27 | minRows: 2, 28 | maxRows: 100, 29 | mobileBreakPoint: 600, 30 | defaultSizeX: 3, 31 | defaultSizeY: 4 32 | }; 33 | 34 | gridster = $controller('GridsterCtrl'); 35 | gridsterItem = $controller('GridsterItemCtrl'); 36 | 37 | item1x1 = { 38 | sizeX: 1, 39 | sizeY: 1, 40 | id: '1x1' 41 | }; 42 | item2x1 = { 43 | sizeX: 2, 44 | sizeY: 1, 45 | id: '2x1' 46 | }; 47 | item2x2 = { 48 | sizeX: 2, 49 | sizeY: 2, 50 | id: '2x2' 51 | }; 52 | item1x2 = { 53 | sizeX: 1, 54 | sizeY: 2, 55 | id: '1x2' 56 | }; 57 | 58 | gridster.setOptions(config); 59 | gridsterItem.init(null, gridster); 60 | })); 61 | 62 | it('should get defaults from gridster', function() { 63 | expect(gridsterItem.sizeX).toBe(config.defaultSizeX); 64 | expect(gridsterItem.sizeY).toBe(config.defaultSizeY); 65 | }); 66 | 67 | }); 68 | -------------------------------------------------------------------------------- /test/spec/gridster.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | describe('GridsterCtrl', function() { 4 | 5 | // load the controller's module 6 | beforeEach(module('gridster')); 7 | 8 | var GridsterCtrl, 9 | item1x1, 10 | item2x1, 11 | item1x2, 12 | item2x2; 13 | 14 | // Initialize the controller 15 | beforeEach(inject(function($controller) { 16 | item1x1 = { 17 | sizeX: 1, 18 | sizeY: 1, 19 | id: '1x1' 20 | }; 21 | item2x1 = { 22 | sizeX: 2, 23 | sizeY: 1, 24 | id: '2x1' 25 | }; 26 | item2x2 = { 27 | sizeX: 2, 28 | sizeY: 2, 29 | id: '2x2' 30 | }; 31 | item1x2 = { 32 | sizeX: 1, 33 | sizeY: 2, 34 | id: '1x2' 35 | }; 36 | 37 | var config = [item1x1, item2x1, item2x2, item1x2]; 38 | 39 | GridsterCtrl = $controller('GridsterCtrl'); 40 | GridsterCtrl.setOptions(config); 41 | })); 42 | 43 | it('should have a grid Array', function() { 44 | expect(GridsterCtrl.grid.constructor).toBe(Array); 45 | }); 46 | 47 | describe('options', function() { 48 | it('should set default options', function() { 49 | expect(GridsterCtrl.columns).toBe(6); 50 | expect(GridsterCtrl.width).toBe('auto'); 51 | expect(GridsterCtrl.colWidth).toBe('auto'); 52 | expect(GridsterCtrl.rowHeight).toBe('match'); 53 | expect(GridsterCtrl.margins).toEqual([10, 10]); 54 | expect(GridsterCtrl.isMobile).toBe(false); 55 | expect(GridsterCtrl.minColumns).toEqual(1); 56 | expect(GridsterCtrl.minRows).toBe(1); 57 | expect(GridsterCtrl.maxRows).toBe(100); 58 | expect(GridsterCtrl.defaultSizeX).toBe(2); 59 | expect(GridsterCtrl.defaultSizeY).toBe(1); 60 | expect(GridsterCtrl.mobileBreakPoint).toBe(600); 61 | expect(GridsterCtrl.resizable.enabled).toBe(true); 62 | expect(GridsterCtrl.draggable.enabled).toBe(true); 63 | }); 64 | 65 | // todo: move these to e2e test 66 | // it('should resolve smart options', function() { 67 | // expect(GridsterCtrl.curWidth).toBe(400); // inherit element width 68 | // expect(GridsterCtrl.curColWidth).toBe(65); // (400 - 10) / 6 69 | // expect(GridsterCtrl.curRowHeight).toBe(65); // match curColWidth 70 | // }); 71 | 72 | it('should update options', function() { 73 | GridsterCtrl.setOptions({ 74 | width: 1200, 75 | colWidth: 120, 76 | rowHeight: 140, 77 | columns: 7, 78 | margins: [15, 15] 79 | }); 80 | 81 | expect(GridsterCtrl.width).toBe(1200); 82 | expect(GridsterCtrl.colWidth).toBe(120); 83 | expect(GridsterCtrl.rowHeight).toBe(140); 84 | expect(GridsterCtrl.columns).toBe(7); 85 | expect(GridsterCtrl.margins).toEqual([15, 15]); 86 | 87 | // todo: move these to e2e test 88 | // expect(GridsterCtrl.curColWidth).toBe(120); 89 | // expect(GridsterCtrl.curRowHeight).toBe(140); 90 | }); 91 | }); 92 | 93 | describe('autoSetItemPosition', function() { 94 | it('should place an item in the first available space', function() { 95 | GridsterCtrl.putItem(item2x1, 0, 1); 96 | GridsterCtrl.autoSetItemPosition(item1x1); 97 | expect(GridsterCtrl.getItem(0, 0)).toBe(item1x1); 98 | 99 | GridsterCtrl.autoSetItemPosition(item2x2); 100 | expect(GridsterCtrl.getItem(0, 3)).toBe(item2x2); 101 | }); 102 | 103 | it('should respect item size', function() { 104 | GridsterCtrl.putItem(item2x1, 0, 1); 105 | 106 | GridsterCtrl.autoSetItemPosition(item2x2); 107 | expect(GridsterCtrl.getItem(0, 3)).toBe(item2x2); 108 | }); 109 | }); 110 | 111 | describe('putItem', function() { 112 | it('should be able to place an item with coordinates', function() { 113 | GridsterCtrl.putItem(item1x1, 2, 3); 114 | expect(GridsterCtrl.getItem(2, 3)).toBe(item1x1); 115 | }); 116 | 117 | it('should place an item without coordinates into empty grid', function() { 118 | GridsterCtrl.putItem(item1x1); 119 | expect(GridsterCtrl.getItem(0, 0)).toBe(item1x1); 120 | }); 121 | 122 | it('should place item into without coordinates into the next available position', function() { 123 | // place 1x1 at 0x0 124 | GridsterCtrl.putItem(item1x1); 125 | expect(GridsterCtrl.getItem(0, 0)).toBe(item1x1); 126 | 127 | // place 2x1 at 0x2 128 | item2x1.row = 0; 129 | item2x1.col = 2; 130 | GridsterCtrl.putItem(item2x1); 131 | expect(GridsterCtrl.getItem(0, 2)).toBe(item2x1); 132 | 133 | // place 1x2 in without coordinates 134 | GridsterCtrl.putItem(item1x2); 135 | expect(GridsterCtrl.getItem(0, 1)).toBe(item1x2); // should stick it at 0x1 136 | 137 | // place 2x2 without coordinates 138 | GridsterCtrl.putItem(item2x2); 139 | expect(GridsterCtrl.getItem(0, 4)).toBe(item2x2); // should stick it at 0x4 140 | }); 141 | 142 | it('should not allow items to be placed with negative indices', function() { 143 | GridsterCtrl.putItem(item1x1, -1, -1); 144 | expect(GridsterCtrl.getItem(0, 0)).toBe(item1x1); 145 | expect(item1x1.row).toBe(0); 146 | expect(item1x1.col).toBe(0); 147 | }); 148 | 149 | it('should not float items until told to', function() { 150 | GridsterCtrl.putItem(item1x1, 3, 0); 151 | expect(GridsterCtrl.getItem(0, 0)).toBe(null); 152 | expect(GridsterCtrl.getItem(3, 0)).toBe(item1x1); 153 | }); 154 | 155 | it('should not create two references to the same item', function() { 156 | GridsterCtrl.putItem(item1x1, 0, 0); 157 | expect(GridsterCtrl.getItem(0, 0)).toBe(item1x1); 158 | GridsterCtrl.putItem(item1x1, 0, 4); 159 | expect(GridsterCtrl.getItem(0, 4)).toBe(item1x1); 160 | expect(GridsterCtrl.getItem(0, 0)).toBe(null); 161 | }); 162 | }); 163 | 164 | describe('getItem', function() { 165 | it('should match any column of a multi-column item', function() { 166 | GridsterCtrl.putItem(item2x2, 0, 2); 167 | 168 | // all 4 corners should return the same item 169 | expect(GridsterCtrl.getItem(0, 2)).toBe(item2x2); 170 | expect(GridsterCtrl.getItem(1, 2)).toBe(item2x2); 171 | expect(GridsterCtrl.getItem(0, 3)).toBe(item2x2); 172 | expect(GridsterCtrl.getItem(1, 3)).toBe(item2x2); 173 | }); 174 | }); 175 | 176 | describe('getItems', function() { 177 | it('should get items within an area', function() { 178 | GridsterCtrl.putItem(item2x2, 0, 1); 179 | GridsterCtrl.putItem(item2x1, 2, 0); 180 | 181 | // verify they are still where we put them 182 | expect(GridsterCtrl.getItem(0, 1)).toBe(item2x2); 183 | expect(GridsterCtrl.getItem(2, 0)).toBe(item2x1); 184 | 185 | var items = GridsterCtrl.getItems(1, 0, 2, 1); 186 | expect(items.length).toBe(1); 187 | expect(items[0]).toBe(item2x2); 188 | }); 189 | }); 190 | 191 | describe('floatItemsUp', function() { 192 | it('should float an item up', function() { 193 | GridsterCtrl.putItem(item1x1, 3, 0); 194 | GridsterCtrl.floatItemsUp(); 195 | expect(GridsterCtrl.getItem(0, 0)).toBe(item1x1); 196 | }); 197 | 198 | it('should stack items when they float up', function() { 199 | GridsterCtrl.putItem(item1x1, 3, 0); 200 | GridsterCtrl.floatItemsUp(); 201 | expect(GridsterCtrl.getItem(0, 0)).toBe(item1x1); 202 | 203 | GridsterCtrl.putItem(item2x1, 3, 0); 204 | GridsterCtrl.floatItemsUp(); 205 | expect(GridsterCtrl.getItem(1, 0)).toBe(item2x1); 206 | 207 | GridsterCtrl.putItem(item1x1, 3, 1); 208 | GridsterCtrl.floatItemsUp(); 209 | expect(GridsterCtrl.getItem(1, 1)).toBe(item1x1); 210 | }); 211 | 212 | it('should correctly stack multi-column items when their primary coordinates do not stack', function() { 213 | GridsterCtrl.putItem(item2x2, 0, 2); 214 | GridsterCtrl.putItem(item2x1, 2, 1); 215 | 216 | // verify they are still where we put them 217 | expect(GridsterCtrl.getItem(0, 2)).toBe(item2x2); 218 | expect(GridsterCtrl.getItem(2, 1)).toBe(item2x1); 219 | 220 | // allow them to float up 221 | GridsterCtrl.floatItemsUp(); 222 | 223 | // verify they are still where we put them 224 | expect(GridsterCtrl.getItem(0, 2)).toBe(item2x2); 225 | expect(GridsterCtrl.getItem(2, 1)).toBe(item2x1); 226 | }); 227 | }); 228 | 229 | describe('moveOverlappingItems', function() { 230 | it('should correctly stack items on resize when their primary coordinates do not stack', function() { 231 | GridsterCtrl.putItem(item1x1, 0, 0); 232 | GridsterCtrl.putItem(item2x2, 0, 2); 233 | GridsterCtrl.putItem(item2x1, 1, 0); 234 | 235 | // verify they are still where we put them 236 | expect(GridsterCtrl.getItem(0, 0)).toBe(item1x1); 237 | expect(GridsterCtrl.getItem(0, 2)).toBe(item2x2); 238 | expect(GridsterCtrl.getItem(1, 0)).toBe(item2x1); 239 | 240 | item2x1.sizeX = 3; 241 | GridsterCtrl.moveOverlappingItems(item2x1); 242 | expect(GridsterCtrl.getItem(1, 2)).toBe(item2x1); 243 | 244 | expect(item2x2.row).toBe(2); 245 | }); 246 | 247 | it('should correctly push items down', function() { 248 | GridsterCtrl.putItem(item2x2, 0, 0); 249 | GridsterCtrl.putItem(item1x1, 2, 0); 250 | GridsterCtrl.putItem(item1x2, 1, 1); 251 | GridsterCtrl.floatItemsUp(); 252 | 253 | expect(item2x2.row).toBe(2); 254 | expect(item2x2.col).toBe(0); 255 | 256 | expect(GridsterCtrl.getItem(4, 0)).toBe(item1x1); 257 | 258 | expect(item1x2.row).toBe(0); 259 | expect(item1x2.col).toBe(1); 260 | }); 261 | 262 | it('should correctly push items down', function() { 263 | GridsterCtrl.putItem(item1x2, 0, 0); 264 | GridsterCtrl.putItem(item2x1, 0, 1); 265 | GridsterCtrl.putItem(item1x1, 1, 2); 266 | 267 | item1x2.sizeX = 2; 268 | GridsterCtrl.moveOverlappingItems(item1x2); 269 | 270 | expect(GridsterCtrl.getItem(0, 0)).toBe(item1x2); 271 | expect(GridsterCtrl.getItem(2, 1)).toBe(item2x1); 272 | expect(GridsterCtrl.getItem(3, 2)).toBe(item1x1); 273 | }); 274 | }); 275 | }); 276 | --------------------------------------------------------------------------------