5fa8499f4b5288fb771562b32b5b223b3f276873
[portal/sdk.git] /
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: '<div ng-style="previewStyle()" class="gridster-item gridster-preview-holder"></div>',
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('<div ng-if="gridster.movingItem" gridster-preview></div>');
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                                                         }
1195                                                 }
1196                                         } else if (theEvtObj.type.match(/move$/i)) {
1197                                                 //  clause handles mousemove, MSPointerMove, and touchmove
1198
1199                                                 if (lastXYById[pointerId] && !(lastXYById[pointerId].x === pageX && lastXYById[pointerId].y === pageY)) {
1200                                                         //  only extend if the pointer is down and it's not the same as the last point
1201
1202                                                         if (moveEvent && prevent) {
1203                                                                 prevent = moveEvent({
1204                                                                         target: theEvtObj.target,
1205                                                                         which: theEvtObj.which,
1206                                                                         pointerId: pointerId,
1207                                                                         pageX: pageX,
1208                                                                         pageY: pageY
1209                                                                 });
1210                                                         }
1211
1212                                                         //  update last page positions for this pointer
1213                                                         lastXYById[pointerId].x = pageX;
1214                                                         lastXYById[pointerId].y = pageY;
1215                                                 }
1216                                         } else if (lastXYById[pointerId] && theEvtObj.type.match(/(up|end|cancel)$/i)) {
1217                                                 //  clause handles up/end/cancel
1218
1219                                                 if (endEvent && prevent) {
1220                                                         prevent = endEvent({
1221                                                                 target: theEvtObj.target,
1222                                                                 which: theEvtObj.which,
1223                                                                 pointerId: pointerId,
1224                                                                 pageX: pageX,
1225                                                                 pageY: pageY
1226                                                         });
1227                                                 }
1228
1229                                                 //  delete last page positions for this pointer
1230                                                 delete lastXYById[pointerId];
1231
1232                                                 //  in the Microsoft pointer model, release the capture for this pointer
1233                                                 //  in the mouse model, release the capture or remove document-level event handlers if there are no down points
1234                                                 //  nothing is required for the iOS touch model because capture is implied on touchstart
1235                                                 if (target.msReleasePointerCapture) {
1236                                                         target.msReleasePointerCapture(pointerId);
1237                                                 } else if (theEvtObj.type === 'mouseup' && numberOfKeys(lastXYById) === 0) {
1238                                                         if (useSetReleaseCapture) {
1239                                                                 target.releaseCapture();
1240                                                         } else {
1241                                                                 document.removeEventListener('mousemove', doEvent, false);
1242                                                                 document.removeEventListener('mouseup', doEvent, false);
1243                                                         }
1244                                                 }
1245                                         }
1246                                 }
1247
1248                                 if (prevent) {
1249                                         if (theEvtObj.preventDefault) {
1250                                                 theEvtObj.preventDefault();
1251                                         }
1252
1253                                         if (theEvtObj.preventManipulation) {
1254                                                 theEvtObj.preventManipulation();
1255                                         }
1256
1257                                         if (theEvtObj.preventMouseEvent) {
1258                                                 theEvtObj.preventMouseEvent();
1259                                         }
1260                                 }
1261                         };
1262
1263                         // saving the settings for contentZooming and touchaction before activation
1264                         var contentZooming, msTouchAction;
1265
1266                         this.enable = function() {
1267
1268                                 if (window.navigator.msPointerEnabled) {
1269                                         //  Microsoft pointer model
1270                                         target.addEventListener('MSPointerDown', doEvent, false);
1271                                         target.addEventListener('MSPointerMove', doEvent, false);
1272                                         target.addEventListener('MSPointerUp', doEvent, false);
1273                                         target.addEventListener('MSPointerCancel', doEvent, false);
1274
1275                                         //  css way to prevent panning in our target area
1276                                         if (typeof target.style.msContentZooming !== 'undefined') {
1277                                                 contentZooming = target.style.msContentZooming;
1278                                                 target.style.msContentZooming = 'none';
1279                                         }
1280
1281                                         //  new in Windows Consumer Preview: css way to prevent all built-in touch actions on our target
1282                                         //  without this, you cannot touch draw on the element because IE will intercept the touch events
1283                                         if (typeof target.style.msTouchAction !== 'undefined') {
1284                                                 msTouchAction = target.style.msTouchAction;
1285                                                 target.style.msTouchAction = 'none';
1286                                         }
1287                                 } else if (target.addEventListener) {
1288                                         //  iOS touch model
1289                                         target.addEventListener('touchstart', doEvent, false);
1290                                         target.addEventListener('touchmove', doEvent, false);
1291                                         target.addEventListener('touchend', doEvent, false);
1292                                         target.addEventListener('touchcancel', doEvent, false);
1293
1294                                         //  mouse model
1295                                         target.addEventListener('mousedown', doEvent, false);
1296
1297                                         //  mouse model with capture
1298                                         //  rejecting gecko because, unlike ie, firefox does not send events to target when the mouse is outside target
1299                                         if (target.setCapture && !window.navigator.userAgent.match(/\bGecko\b/)) {
1300                                                 useSetReleaseCapture = true;
1301
1302                                                 target.addEventListener('mousemove', doEvent, false);
1303                                                 target.addEventListener('mouseup', doEvent, false);
1304                                         }
1305                                 } else if (target.attachEvent && target.setCapture) {
1306                                         //  legacy IE mode - mouse with capture
1307                                         useSetReleaseCapture = true;
1308                                         target.attachEvent('onmousedown', function() {
1309                                                 doEvent(window.event);
1310                                                 window.event.returnValue = false;
1311                                                 return false;
1312                                         });
1313                                         target.attachEvent('onmousemove', function() {
1314                                                 doEvent(window.event);
1315                                                 window.event.returnValue = false;
1316                                                 return false;
1317                                         });
1318                                         target.attachEvent('onmouseup', function() {
1319                                                 doEvent(window.event);
1320                                                 window.event.returnValue = false;
1321                                                 return false;
1322                                         });
1323                                 }
1324                         };
1325
1326                         this.disable = function() {
1327                                 if (window.navigator.msPointerEnabled) {
1328                                         //  Microsoft pointer model
1329                                         target.removeEventListener('MSPointerDown', doEvent, false);
1330                                         target.removeEventListener('MSPointerMove', doEvent, false);
1331                                         target.removeEventListener('MSPointerUp', doEvent, false);
1332                                         target.removeEventListener('MSPointerCancel', doEvent, false);
1333
1334                                         //  reset zooming to saved value
1335                                         if (contentZooming) {
1336                                                 target.style.msContentZooming = contentZooming;
1337                                         }
1338
1339                                         // reset touch action setting
1340                                         if (msTouchAction) {
1341                                                 target.style.msTouchAction = msTouchAction;
1342                                         }
1343                                 } else if (target.removeEventListener) {
1344                                         //  iOS touch model
1345                                         target.removeEventListener('touchstart', doEvent, false);
1346                                         target.removeEventListener('touchmove', doEvent, false);
1347                                         target.removeEventListener('touchend', doEvent, false);
1348                                         target.removeEventListener('touchcancel', doEvent, false);
1349
1350                                         //  mouse model
1351                                         target.removeEventListener('mousedown', doEvent, false);
1352
1353                                         //  mouse model with capture
1354                                         //  rejecting gecko because, unlike ie, firefox does not send events to target when the mouse is outside target
1355                                         if (target.setCapture && !window.navigator.userAgent.match(/\bGecko\b/)) {
1356                                                 useSetReleaseCapture = true;
1357
1358                                                 target.removeEventListener('mousemove', doEvent, false);
1359                                                 target.removeEventListener('mouseup', doEvent, false);
1360                                         }
1361                                 } else if (target.detachEvent && target.setCapture) {
1362                                         //  legacy IE mode - mouse with capture
1363                                         useSetReleaseCapture = true;
1364                                         target.detachEvent('onmousedown');
1365                                         target.detachEvent('onmousemove');
1366                                         target.detachEvent('onmouseup');
1367                                 }
1368                         };
1369
1370                         return this;
1371                 };
1372         }])
1373
1374         .factory('GridsterDraggable', ['$document', '$window', 'GridsterTouch',
1375                 function($document, $window, GridsterTouch) {
1376                         function GridsterDraggable($el, scope, gridster, item, itemOptions) {
1377
1378                                 var elmX, elmY, elmW, elmH,
1379
1380                                         mouseX = 0,
1381                                         mouseY = 0,
1382                                         lastMouseX = 0,
1383                                         lastMouseY = 0,
1384                                         mOffX = 0,
1385                                         mOffY = 0,
1386
1387                                         minTop = 0,
1388                                         minLeft = 0,
1389                                         realdocument = $document[0];
1390
1391                                 var originalCol, originalRow;
1392                                 var inputTags = ['select', 'option', 'input', 'textarea', 'button'];
1393
1394                                 function dragStart(event) {
1395                                         $el.addClass('gridster-item-moving');
1396                                         gridster.movingItem = item;
1397
1398                                         gridster.updateHeight(item.sizeY);
1399                                         scope.$apply(function() {
1400                                                 if (gridster.draggable && gridster.draggable.start) {
1401                                                         gridster.draggable.start(event, $el, itemOptions, item);
1402                                                 }
1403                                         });
1404                                 }
1405
1406                                 function drag(event) {
1407                                         var oldRow = item.row,
1408                                                 oldCol = item.col,
1409                                                 hasCallback = gridster.draggable && gridster.draggable.drag,
1410                                                 scrollSensitivity = gridster.draggable.scrollSensitivity,
1411                                                 scrollSpeed = gridster.draggable.scrollSpeed;
1412
1413                                         var row = Math.min(gridster.pixelsToRows(elmY), gridster.maxRows - 1);
1414                                         var col = Math.min(gridster.pixelsToColumns(elmX), gridster.columns - 1);
1415
1416                                         var itemsInTheWay = gridster.getItems(row, col, item.sizeX, item.sizeY, item);
1417                                         var hasItemsInTheWay = itemsInTheWay.length !== 0;
1418
1419                                         if (gridster.swapping === true && hasItemsInTheWay) {
1420                                                 var boundingBoxItem = gridster.getBoundingBox(itemsInTheWay),
1421                                                         sameSize = boundingBoxItem.sizeX === item.sizeX && boundingBoxItem.sizeY === item.sizeY,
1422                                                         sameRow = boundingBoxItem.row === oldRow,
1423                                                         sameCol = boundingBoxItem.col === oldCol,
1424                                                         samePosition = boundingBoxItem.row === row && boundingBoxItem.col === col,
1425                                                         inline = sameRow || sameCol;
1426
1427                                                 if (sameSize && itemsInTheWay.length === 1) {
1428                                                         if (samePosition) {
1429                                                                 gridster.swapItems(item, itemsInTheWay[0]);
1430                                                         } else if (inline) {
1431                                                                 return;
1432                                                         }
1433                                                 } else if (boundingBoxItem.sizeX <= item.sizeX && boundingBoxItem.sizeY <= item.sizeY && inline) {
1434                                                         var emptyRow = item.row <= row ? item.row : row + item.sizeY,
1435                                                                 emptyCol = item.col <= col ? item.col : col + item.sizeX,
1436                                                                 rowOffset = emptyRow - boundingBoxItem.row,
1437                                                                 colOffset = emptyCol - boundingBoxItem.col;
1438
1439                                                         for (var i = 0, l = itemsInTheWay.length; i < l; ++i) {
1440                                                                 var itemInTheWay = itemsInTheWay[i];
1441
1442                                                                 var itemsInFreeSpace = gridster.getItems(
1443                                                                         itemInTheWay.row + rowOffset,
1444                                                                         itemInTheWay.col + colOffset,
1445                                                                         itemInTheWay.sizeX,
1446                                                                         itemInTheWay.sizeY,
1447                                                                         item
1448                                                                 );
1449
1450                                                                 if (itemsInFreeSpace.length === 0) {
1451                                                                         gridster.putItem(itemInTheWay, itemInTheWay.row + rowOffset, itemInTheWay.col + colOffset);
1452                                                                 }
1453                                                         }
1454                                                 }
1455                                         }
1456
1457                                         if (gridster.pushing !== false || !hasItemsInTheWay) {
1458                                                 item.row = row;
1459                                                 item.col = col;
1460                                         }
1461
1462                                         if (event.pageY - realdocument.body.scrollTop < scrollSensitivity) {
1463                                                 realdocument.body.scrollTop = realdocument.body.scrollTop - scrollSpeed;
1464                                         } else if ($window.innerHeight - (event.pageY - realdocument.body.scrollTop) < scrollSensitivity) {
1465                                                 realdocument.body.scrollTop = realdocument.body.scrollTop + scrollSpeed;
1466                                         }
1467
1468                                         if (event.pageX - realdocument.body.scrollLeft < scrollSensitivity) {
1469                                                 realdocument.body.scrollLeft = realdocument.body.scrollLeft - scrollSpeed;
1470                                         } else if ($window.innerWidth - (event.pageX - realdocument.body.scrollLeft) < scrollSensitivity) {
1471                                                 realdocument.body.scrollLeft = realdocument.body.scrollLeft + scrollSpeed;
1472                                         }
1473
1474                                         if (hasCallback || oldRow !== item.row || oldCol !== item.col) {
1475                                                 scope.$apply(function() {
1476                                                         if (hasCallback) {
1477                                                                 gridster.draggable.drag(event, $el, itemOptions, item);
1478                                                         }
1479                                                 });
1480                                         }
1481                                 }
1482
1483                                 function dragStop(event) {
1484                                         $el.removeClass('gridster-item-moving');
1485                                         var row = Math.min(gridster.pixelsToRows(elmY), gridster.maxRows - 1);
1486                                         var col = Math.min(gridster.pixelsToColumns(elmX), gridster.columns - 1);
1487                                         if (gridster.pushing !== false || gridster.getItems(row, col, item.sizeX, item.sizeY, item).length === 0) {
1488                                                 item.row = row;
1489                                                 item.col = col;
1490                                         }
1491                                         gridster.movingItem = null;
1492                                         item.setPosition(item.row, item.col);
1493
1494                                         scope.$apply(function() {
1495                                                 if (gridster.draggable && gridster.draggable.stop) {
1496                                                         gridster.draggable.stop(event, $el, itemOptions, item);
1497                                                 }
1498                                         });
1499                                 }
1500
1501                                 function mouseDown(e) {
1502                                         if (inputTags.indexOf(e.target.nodeName.toLowerCase()) !== -1) {
1503                                                 return false;
1504                                         }
1505
1506                                         var $target = angular.element(e.target);
1507
1508                                         // exit, if a resize handle was hit
1509                                         if ($target.hasClass('gridster-item-resizable-handler')) {
1510                                                 return false;
1511                                         }
1512
1513                                         // exit, if the target has it's own click event
1514                                         if ($target.attr('onclick') || $target.attr('ng-click')) {
1515                                                 return false;
1516                                         }
1517
1518                                         // only works if you have jQuery
1519                                         if ($target.closest && $target.closest('.gridster-no-drag').length) {
1520                                                 return false;
1521                                         }
1522
1523                                         // apply drag handle filter
1524                                         if (gridster.draggable && gridster.draggable.handle) {
1525                                                 var $dragHandles = angular.element($el[0].querySelectorAll(gridster.draggable.handle));
1526                                                 var match = false;
1527                                                 outerloop:
1528                                                         for (var h = 0, hl = $dragHandles.length; h < hl; ++h) {
1529                                                                 var handle = $dragHandles[h];
1530                                                                 if (handle === e.target) {
1531                                                                         match = true;
1532                                                                         break;
1533                                                                 }
1534                                                                 var target = e.target;
1535                                                                 for (var p = 0; p < 20; ++p) {
1536                                                                         var parent = target.parentNode;
1537                                                                         if (parent === $el[0] || !parent) {
1538                                                                                 break;
1539                                                                         }
1540                                                                         if (parent === handle) {
1541                                                                                 match = true;
1542                                                                                 break outerloop;
1543                                                                         }
1544                                                                         target = parent;
1545                                                                 }
1546                                                         }
1547                                                 if (!match) {
1548                                                         return false;
1549                                                 }
1550                                         }
1551
1552                                         switch (e.which) {
1553                                                 case 1:
1554                                                         // left mouse button
1555                                                         break;
1556                                                 case 2:
1557                                                 case 3:
1558                                                         // right or middle mouse button
1559                                                         return;
1560                                         }
1561
1562                                         lastMouseX = e.pageX;
1563                                         lastMouseY = e.pageY;
1564
1565                                         elmX = parseInt($el.css('left'), 10);
1566                                         elmY = parseInt($el.css('top'), 10);
1567                                         elmW = $el[0].offsetWidth;
1568                                         elmH = $el[0].offsetHeight;
1569
1570                                         originalCol = item.col;
1571                                         originalRow = item.row;
1572
1573                                         dragStart(e);
1574
1575                                         return true;
1576                                 }
1577
1578                                 function mouseMove(e) {
1579                                         if (!$el.hasClass('gridster-item-moving') || $el.hasClass('gridster-item-resizing')) {
1580                                                 return false;
1581                                         }
1582
1583                                         var maxLeft = gridster.curWidth - 1;
1584                                         var maxTop = gridster.curRowHeight * gridster.maxRows - 1;
1585
1586                                         // Get the current mouse position.
1587                                         mouseX = e.pageX;
1588                                         mouseY = e.pageY;
1589
1590                                         // Get the deltas
1591                                         var diffX = mouseX - lastMouseX + mOffX;
1592                                         var diffY = mouseY - lastMouseY + mOffY;
1593                                         mOffX = mOffY = 0;
1594
1595                                         // Update last processed mouse positions.
1596                                         lastMouseX = mouseX;
1597                                         lastMouseY = mouseY;
1598
1599                                         var dX = diffX,
1600                                                 dY = diffY;
1601                                         if (elmX + dX < minLeft) {
1602                                                 diffX = minLeft - elmX;
1603                                                 mOffX = dX - diffX;
1604                                         } else if (elmX + elmW + dX > maxLeft) {
1605                                                 diffX = maxLeft - elmX - elmW;
1606                                                 mOffX = dX - diffX;
1607                                         }
1608
1609                                         if (elmY + dY < minTop) {
1610                                                 diffY = minTop - elmY;
1611                                                 mOffY = dY - diffY;
1612                                         } else if (elmY + elmH + dY > maxTop) {
1613                                                 diffY = maxTop - elmY - elmH;
1614                                                 mOffY = dY - diffY;
1615                                         }
1616                                         elmX += diffX;
1617                                         elmY += diffY;
1618
1619                                         // set new position
1620                                         $el.css({
1621                                                 'top': elmY + 'px',
1622                                                 'left': elmX + 'px'
1623                                         });
1624
1625                                         drag(e);
1626
1627                                         return true;
1628                                 }
1629
1630                                 function mouseUp(e) {
1631                                         if (!$el.hasClass('gridster-item-moving') || $el.hasClass('gridster-item-resizing')) {
1632                                                 return false;
1633                                         }
1634
1635                                         mOffX = mOffY = 0;
1636
1637                                         dragStop(e);
1638
1639                                         return true;
1640                                 }
1641
1642                                 var enabled = null;
1643                                 var gridsterTouch = null;
1644
1645                                 this.enable = function() {
1646                                         if (enabled === true) {
1647                                                 return;
1648                                         }
1649                                         enabled = true;
1650
1651                                         if (gridsterTouch) {
1652                                                 gridsterTouch.enable();
1653                                                 return;
1654                                         }
1655
1656                                         gridsterTouch = new GridsterTouch($el[0], mouseDown, mouseMove, mouseUp);
1657                                         gridsterTouch.enable();
1658                                 };
1659
1660                                 this.disable = function() {
1661                                         if (enabled === false) {
1662                                                 return;
1663                                         }
1664
1665                                         enabled = false;
1666                                         if (gridsterTouch) {
1667                                                 gridsterTouch.disable();
1668                                         }
1669                                 };
1670
1671                                 this.toggle = function(enabled) {
1672                                         if (enabled) {
1673                                                 this.enable();
1674                                         } else {
1675                                                 this.disable();
1676                                         }
1677                                 };
1678
1679                                 this.destroy = function() {
1680                                         this.disable();
1681                                 };
1682                         }
1683
1684                         return GridsterDraggable;
1685                 }
1686         ])
1687
1688         .factory('GridsterResizable', ['GridsterTouch', function(GridsterTouch) {
1689                 function GridsterResizable($el, scope, gridster, item, itemOptions) {
1690
1691                         function ResizeHandle(handleClass) {
1692
1693                                 var hClass = handleClass;
1694
1695                                 var elmX, elmY, elmW, elmH,
1696
1697                                         mouseX = 0,
1698                                         mouseY = 0,
1699                                         lastMouseX = 0,
1700                                         lastMouseY = 0,
1701                                         mOffX = 0,
1702                                         mOffY = 0,
1703
1704                                         minTop = 0,
1705                                         maxTop = 9999,
1706                                         minLeft = 0;
1707
1708                                 var getMinHeight = function() {
1709                                         return (item.minSizeY ? item.minSizeY : 1) * gridster.curRowHeight - gridster.margins[0];
1710                                 };
1711                                 var getMinWidth = function() {
1712                                         return (item.minSizeX ? item.minSizeX : 1) * gridster.curColWidth - gridster.margins[1];
1713                                 };
1714
1715                                 var originalWidth, originalHeight;
1716                                 var savedDraggable;
1717
1718                                 function resizeStart(e) {
1719                                         $el.addClass('gridster-item-moving');
1720                                         $el.addClass('gridster-item-resizing');
1721
1722                                         gridster.movingItem = item;
1723
1724                                         item.setElementSizeX();
1725                                         item.setElementSizeY();
1726                                         item.setElementPosition();
1727                                         gridster.updateHeight(1);
1728
1729                                         scope.$apply(function() {
1730                                                 // callback
1731                                                 if (gridster.resizable && gridster.resizable.start) {
1732                                                         gridster.resizable.start(e, $el, itemOptions, item); // options is the item model
1733                                                 }
1734                                         });
1735                                 }
1736
1737                                 function resize(e) {
1738                                         var oldRow = item.row,
1739                                                 oldCol = item.col,
1740                                                 oldSizeX = item.sizeX,
1741                                                 oldSizeY = item.sizeY,
1742                                                 hasCallback = gridster.resizable && gridster.resizable.resize;
1743
1744                                         var col = item.col;
1745                                         // only change column if grabbing left edge
1746                                         if (['w', 'nw', 'sw'].indexOf(handleClass) !== -1) {
1747                                                 col = gridster.pixelsToColumns(elmX, false);
1748                                         }
1749
1750                                         var row = item.row;
1751                                         // only change row if grabbing top edge
1752                                         if (['n', 'ne', 'nw'].indexOf(handleClass) !== -1) {
1753                                                 row = gridster.pixelsToRows(elmY, false);
1754                                         }
1755
1756                                         var sizeX = item.sizeX;
1757                                         // only change row if grabbing left or right edge
1758                                         if (['n', 's'].indexOf(handleClass) === -1) {
1759                                                 sizeX = gridster.pixelsToColumns(elmW, true);
1760                                         }
1761
1762                                         var sizeY = item.sizeY;
1763                                         // only change row if grabbing top or bottom edge
1764                                         if (['e', 'w'].indexOf(handleClass) === -1) {
1765                                                 sizeY = gridster.pixelsToRows(elmH, true);
1766                                         }
1767
1768
1769                                         var canOccupy = row > -1 && col > -1 && sizeX + col <= gridster.columns && sizeY + row <= gridster.maxRows;
1770                                         if (canOccupy && (gridster.pushing !== false || gridster.getItems(row, col, sizeX, sizeY, item).length === 0)) {
1771                                                 item.row = row;
1772                                                 item.col = col;
1773                                                 item.sizeX = sizeX;
1774                                                 item.sizeY = sizeY;
1775                                         }
1776                                         var isChanged = item.row !== oldRow || item.col !== oldCol || item.sizeX !== oldSizeX || item.sizeY !== oldSizeY;
1777
1778                                         if (hasCallback || isChanged) {
1779                                                 scope.$apply(function() {
1780                                                         if (hasCallback) {
1781                                                                 gridster.resizable.resize(e, $el, itemOptions, item); // options is the item model
1782                                                         }
1783                                                 });
1784                                         }
1785                                 }
1786
1787                                 function resizeStop(e) {
1788                                         $el.removeClass('gridster-item-moving');
1789                                         $el.removeClass('gridster-item-resizing');
1790
1791                                         gridster.movingItem = null;
1792
1793                                         item.setPosition(item.row, item.col);
1794                                         item.setSizeY(item.sizeY);
1795                                         item.setSizeX(item.sizeX);
1796
1797                                         scope.$apply(function() {
1798                                                 if (gridster.resizable && gridster.resizable.stop) {
1799                                                         gridster.resizable.stop(e, $el, itemOptions, item); // options is the item model
1800                                                 }
1801                                         });
1802                                 }
1803
1804                                 function mouseDown(e) {
1805                                         switch (e.which) {
1806                                                 case 1:
1807                                                         // left mouse button
1808                                                         break;
1809                                                 case 2:
1810                                                 case 3:
1811                                                         // right or middle mouse button
1812                                                         return;
1813                                         }
1814
1815                                         // save the draggable setting to restore after resize
1816                                         savedDraggable = gridster.draggable.enabled;
1817                                         if (savedDraggable) {
1818                                                 gridster.draggable.enabled = false;
1819                                                 scope.$broadcast('gridster-draggable-changed', gridster);
1820                                         }
1821
1822                                         // Get the current mouse position.
1823                                         lastMouseX = e.pageX;
1824                                         lastMouseY = e.pageY;
1825
1826                                         // Record current widget dimensions
1827                                         elmX = parseInt($el.css('left'), 10);
1828                                         elmY = parseInt($el.css('top'), 10);
1829                                         elmW = $el[0].offsetWidth;
1830                                         elmH = $el[0].offsetHeight;
1831
1832                                         originalWidth = item.sizeX;
1833                                         originalHeight = item.sizeY;
1834
1835                                         resizeStart(e);
1836
1837                                         return true;
1838                                 }
1839
1840                                 function mouseMove(e) {
1841                                         var maxLeft = gridster.curWidth - 1;
1842
1843                                         // Get the current mouse position.
1844                                         mouseX = e.pageX;
1845                                         mouseY = e.pageY;
1846
1847                                         // Get the deltas
1848                                         var diffX = mouseX - lastMouseX + mOffX;
1849                                         var diffY = mouseY - lastMouseY + mOffY;
1850                                         mOffX = mOffY = 0;
1851
1852                                         // Update last processed mouse positions.
1853                                         lastMouseX = mouseX;
1854                                         lastMouseY = mouseY;
1855
1856                                         var dY = diffY,
1857                                                 dX = diffX;
1858
1859                                         if (hClass.indexOf('n') >= 0) {
1860                                                 if (elmH - dY < getMinHeight()) {
1861                                                         diffY = elmH - getMinHeight();
1862                                                         mOffY = dY - diffY;
1863                                                 } else if (elmY + dY < minTop) {
1864                                                         diffY = minTop - elmY;
1865                                                         mOffY = dY - diffY;
1866                                                 }
1867                                                 elmY += diffY;
1868                                                 elmH -= diffY;
1869                                         }
1870                                         if (hClass.indexOf('s') >= 0) {
1871                                                 if (elmH + dY < getMinHeight()) {
1872                                                         diffY = getMinHeight() - elmH;
1873                                                         mOffY = dY - diffY;
1874                                                 } else if (elmY + elmH + dY > maxTop) {
1875                                                         diffY = maxTop - elmY - elmH;
1876                                                         mOffY = dY - diffY;
1877                                                 }
1878                                                 elmH += diffY;
1879                                         }
1880                                         if (hClass.indexOf('w') >= 0) {
1881                                                 if (elmW - dX < getMinWidth()) {
1882                                                         diffX = elmW - getMinWidth();
1883                                                         mOffX = dX - diffX;
1884                                                 } else if (elmX + dX < minLeft) {
1885                                                         diffX = minLeft - elmX;
1886                                                         mOffX = dX - diffX;
1887                                                 }
1888                                                 elmX += diffX;
1889                                                 elmW -= diffX;
1890                                         }
1891                                         if (hClass.indexOf('e') >= 0) {
1892                                                 if (elmW + dX < getMinWidth()) {
1893                                                         diffX = getMinWidth() - elmW;
1894                                                         mOffX = dX - diffX;
1895                                                 } else if (elmX + elmW + dX > maxLeft) {
1896                                                         diffX = maxLeft - elmX - elmW;
1897                                                         mOffX = dX - diffX;
1898                                                 }
1899                                                 elmW += diffX;
1900                                         }
1901
1902                                         // set new position
1903                                         $el.css({
1904                                                 'top': elmY + 'px',
1905                                                 'left': elmX + 'px',
1906                                                 'width': elmW + 'px',
1907                                                 'height': elmH + 'px'
1908                                         });
1909
1910                                         resize(e);
1911
1912                                         return true;
1913                                 }
1914
1915                                 function mouseUp(e) {
1916                                         // restore draggable setting to its original state
1917                                         if (gridster.draggable.enabled !== savedDraggable) {
1918                                                 gridster.draggable.enabled = savedDraggable;
1919                                                 scope.$broadcast('gridster-draggable-changed', gridster);
1920                                         }
1921
1922                                         mOffX = mOffY = 0;
1923
1924                                         resizeStop(e);
1925
1926                                         return true;
1927                                 }
1928
1929                                 var $dragHandle = null;
1930                                 var unifiedInput;
1931
1932                                 this.enable = function() {
1933                                         if (!$dragHandle) {
1934                                                 $dragHandle = angular.element('<div class="gridster-item-resizable-handler handle-' + hClass + '"></div>');
1935                                                 $el.append($dragHandle);
1936                                         }
1937
1938                                         unifiedInput = new GridsterTouch($dragHandle[0], mouseDown, mouseMove, mouseUp);
1939                                         unifiedInput.enable();
1940                                 };
1941
1942                                 this.disable = function() {
1943                                         if ($dragHandle) {
1944                                                 $dragHandle.remove();
1945                                                 $dragHandle = null;
1946                                         }
1947
1948                                         unifiedInput.disable();
1949                                         unifiedInput = undefined;
1950                                 };
1951
1952                                 this.destroy = function() {
1953                                         this.disable();
1954                                 };
1955                         }
1956
1957                         var handles = [];
1958                         var handlesOpts = gridster.resizable.handles;
1959                         if (typeof handlesOpts === 'string') {
1960                                 handlesOpts = gridster.resizable.handles.split(',');
1961                         }
1962                         var enabled = false;
1963
1964                         for (var c = 0, l = handlesOpts.length; c < l; c++) {
1965                                 handles.push(new ResizeHandle(handlesOpts[c]));
1966                         }
1967
1968                         this.enable = function() {
1969                                 if (enabled) {
1970                                         return;
1971                                 }
1972                                 for (var c = 0, l = handles.length; c < l; c++) {
1973                                         handles[c].enable();
1974                                 }
1975                                 enabled = true;
1976                         };
1977
1978                         this.disable = function() {
1979                                 if (!enabled) {
1980                                         return;
1981                                 }
1982                                 for (var c = 0, l = handles.length; c < l; c++) {
1983                                         handles[c].disable();
1984                                 }
1985                                 enabled = false;
1986                         };
1987
1988                         this.toggle = function(enabled) {
1989                                 if (enabled) {
1990                                         this.enable();
1991                                 } else {
1992                                         this.disable();
1993                                 }
1994                         };
1995
1996                         this.destroy = function() {
1997                                 for (var c = 0, l = handles.length; c < l; c++) {
1998                                         handles[c].destroy();
1999                                 }
2000                         };
2001                 }
2002                 return GridsterResizable;
2003         }])
2004
2005         .factory('gridsterDebounce', function() {
2006                 return function gridsterDebounce(func, wait, immediate) {
2007                         var timeout;
2008                         return function() {
2009                                 var context = this,
2010                                         args = arguments;
2011                                 var later = function() {
2012                                         timeout = null;
2013                                         if (!immediate) {
2014                                                 func.apply(context, args);
2015                                         }
2016                                 };
2017                                 var callNow = immediate && !timeout;
2018                                 clearTimeout(timeout);
2019                                 timeout = setTimeout(later, wait);
2020                                 if (callNow) {
2021                                         func.apply(context, args);
2022                                 }
2023                         };
2024                 };
2025         })
2026
2027         /**
2028          * GridsterItem directive
2029          * @param $parse
2030          * @param GridsterDraggable
2031          * @param GridsterResizable
2032          * @param gridsterDebounce
2033          */
2034         .directive('gridsterItem', ['$parse', 'GridsterDraggable', 'GridsterResizable', 'gridsterDebounce',
2035                 function($parse, GridsterDraggable, GridsterResizable, gridsterDebounce) {
2036                         return {
2037                                 scope: true,
2038                                 restrict: 'EA',
2039                                 controller: 'GridsterItemCtrl',
2040                                 controllerAs: 'gridsterItem',
2041                                 require: ['^gridster', 'gridsterItem'],
2042                                 link: function(scope, $el, attrs, controllers) {
2043                                         var optionsKey = attrs.gridsterItem,
2044                                                 options;
2045
2046                                         var gridster = controllers[0],
2047                                                 item = controllers[1];
2048
2049                                         scope.gridster = gridster;
2050
2051                                         // bind the item's position properties
2052                                         // options can be an object specified by gridster-item="object"
2053                                         // or the options can be the element html attributes object
2054                                         if (optionsKey) {
2055                                                 var $optionsGetter = $parse(optionsKey);
2056                                                 options = $optionsGetter(scope) || {};
2057                                                 if (!options && $optionsGetter.assign) {
2058                                                         options = {
2059                                                                 row: item.row,
2060                                                                 col: item.col,
2061                                                                 sizeX: item.sizeX,
2062                                                                 sizeY: item.sizeY,
2063                                                                 minSizeX: 0,
2064                                                                 minSizeY: 0,
2065                                                                 maxSizeX: null,
2066                                                                 maxSizeY: null
2067                                                         };
2068                                                         $optionsGetter.assign(scope, options);
2069                                                 }
2070                                         } else {
2071                                                 options = attrs;
2072                                         }
2073
2074                                         item.init($el, gridster);
2075
2076                                         $el.addClass('gridster-item');
2077
2078                                         var aspects = ['minSizeX', 'maxSizeX', 'minSizeY', 'maxSizeY', 'sizeX', 'sizeY', 'row', 'col'],
2079                                                 $getters = {};
2080
2081                                         var expressions = [];
2082                                         var aspectFn = function(aspect) {
2083                                                 var expression;
2084                                                 if (typeof options[aspect] === 'string') {
2085                                                         // watch the expression in the scope
2086                                                         expression = options[aspect];
2087                                                 } else if (typeof options[aspect.toLowerCase()] === 'string') {
2088                                                         // watch the expression in the scope
2089                                                         expression = options[aspect.toLowerCase()];
2090                                                 } else if (optionsKey) {
2091                                                         // watch the expression on the options object in the scope
2092                                                         expression = optionsKey + '.' + aspect;
2093                                                 } else {
2094                                                         return;
2095                                                 }
2096                                                 expressions.push('"' + aspect + '":' + expression);
2097                                                 $getters[aspect] = $parse(expression);
2098
2099                                                 // initial set
2100                                                 var val = $getters[aspect](scope);
2101                                                 if (typeof val === 'number') {
2102                                                         item[aspect] = val;
2103                                                 }
2104                                         };
2105
2106                                         for (var i = 0, l = aspects.length; i < l; ++i) {
2107                                                 aspectFn(aspects[i]);
2108                                         }
2109
2110                                         var watchExpressions = '{' + expressions.join(',') + '}';
2111                                         // when the value changes externally, update the internal item object
2112                                         scope.$watchCollection(watchExpressions, function(newVals, oldVals) {
2113                                                 for (var aspect in newVals) {
2114                                                         var newVal = newVals[aspect];
2115                                                         var oldVal = oldVals[aspect];
2116                                                         if (oldVal === newVal) {
2117                                                                 continue;
2118                                                         }
2119                                                         newVal = parseInt(newVal, 10);
2120                                                         if (!isNaN(newVal)) {
2121                                                                 item[aspect] = newVal;
2122                                                         }
2123                                                 }
2124                                         });
2125
2126                                         function positionChanged() {
2127                                                 // call setPosition so the element and gridster controller are updated
2128                                                 item.setPosition(item.row, item.col);
2129
2130                                                 // when internal item position changes, update externally bound values
2131                                                 if ($getters.row && $getters.row.assign) {
2132                                                         $getters.row.assign(scope, item.row);
2133                                                 }
2134                                                 if ($getters.col && $getters.col.assign) {
2135                                                         $getters.col.assign(scope, item.col);
2136                                                 }
2137                                         }
2138                                         scope.$watch(function() {
2139                                                 return item.row + ',' + item.col;
2140                                         }, positionChanged);
2141
2142                                         function sizeChanged() {
2143                                                 var changedX = item.setSizeX(item.sizeX, true);
2144                                                 if (changedX && $getters.sizeX && $getters.sizeX.assign) {
2145                                                         $getters.sizeX.assign(scope, item.sizeX);
2146                                                 }
2147                                                 var changedY = item.setSizeY(item.sizeY, true);
2148                                                 if (changedY && $getters.sizeY && $getters.sizeY.assign) {
2149                                                         $getters.sizeY.assign(scope, item.sizeY);
2150                                                 }
2151
2152                                                 if (changedX || changedY) {
2153                                                         item.gridster.moveOverlappingItems(item);
2154                                                         gridster.layoutChanged();
2155                                                         scope.$broadcast('gridster-item-resized', item);
2156                                                 }
2157                                         }
2158
2159                                         scope.$watch(function() {
2160                                                 return item.sizeY + ',' + item.sizeX + ',' + item.minSizeX + ',' + item.maxSizeX + ',' + item.minSizeY + ',' + item.maxSizeY;
2161                                         }, sizeChanged);
2162
2163                                         var draggable = new GridsterDraggable($el, scope, gridster, item, options);
2164                                         var resizable = new GridsterResizable($el, scope, gridster, item, options);
2165
2166                                         var updateResizable = function() {
2167                                                 resizable.toggle(!gridster.isMobile && gridster.resizable && gridster.resizable.enabled);
2168                                         };
2169                                         updateResizable();
2170
2171                                         var updateDraggable = function() {
2172                                                 draggable.toggle(!gridster.isMobile && gridster.draggable && gridster.draggable.enabled);
2173                                         };
2174                                         updateDraggable();
2175
2176                                         scope.$on('gridster-draggable-changed', updateDraggable);
2177                                         scope.$on('gridster-resizable-changed', updateResizable);
2178                                         scope.$on('gridster-resized', updateResizable);
2179                                         scope.$on('gridster-mobile-changed', function() {
2180                                                 updateResizable();
2181                                                 updateDraggable();
2182                                         });
2183
2184                                         function whichTransitionEvent() {
2185                                                 var el = document.createElement('div');
2186                                                 var transitions = {
2187                                                         'transition': 'transitionend',
2188                                                         'OTransition': 'oTransitionEnd',
2189                                                         'MozTransition': 'transitionend',
2190                                                         'WebkitTransition': 'webkitTransitionEnd'
2191                                                 };
2192                                                 for (var t in transitions) {
2193                                                         if (el.style[t] !== undefined) {
2194                                                                 return transitions[t];
2195                                                         }
2196                                                 }
2197                                         }
2198
2199                                         var debouncedTransitionEndPublisher = gridsterDebounce(function() {
2200                                                 scope.$apply(function() {
2201                                                         scope.$broadcast('gridster-item-transition-end', item);
2202                                                 });
2203                                         }, 50);
2204
2205                                         $el.on(whichTransitionEvent(), debouncedTransitionEndPublisher);
2206
2207                                         scope.$broadcast('gridster-item-initialized', item);
2208
2209                                         return scope.$on('$destroy', function() {
2210                                                 try {
2211                                                         resizable.destroy();
2212                                                         draggable.destroy();
2213                                                 } catch (e) {}
2214
2215                                                 try {
2216                                                         gridster.removeItem(item);
2217                                                 } catch (e) {}
2218
2219                                                 try {
2220                                                         item.destroy();
2221                                                 } catch (e) {}
2222                                         });
2223                                 }
2224                         };
2225                 }
2226         ])
2227
2228         .directive('gridsterNoDrag', function() {
2229                 return {
2230                         restrict: 'A',
2231                         link: function(scope, $element) {
2232                                 $element.addClass('gridster-no-drag');
2233                         }
2234                 };
2235         })
2236
2237         ;
2238
2239 }));