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 |
120 |
121 |
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 |
190 |
191 | {{ gridsterItem.isMoving() }}
192 |
193 |
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 |
15 | Each item provides its own dimensions and position using the standard fields: { row: row, col: col, sizeX: sizeX, sizeY: sizeY }.
16 |
17 |
18 |
19 |
20 | ,
21 |
22 |
23 | x
24 |
25 |
26 |
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 |
35 |
36 | ,
37 |
38 |
39 | x
40 |
41 |
42 |
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 |
51 |
52 | ,
53 |
54 |
55 | x
56 |
57 |
58 |
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 |
67 |
68 | ,
69 |
70 |
71 | x
72 |
73 |
74 |
75 |
76 |
77 |
No Configuration or Binding
78 |
79 | No data binding or configuration provided.
80 |
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 |