2 (function(root, factory) {
6 if (typeof define === 'function' && define.amd) {
8 define(['angular'], factory);
9 } else if (typeof exports === 'object') {
11 module.exports = factory(require('angular'));
13 // Browser, nothing "exported". Only registered as a module with angular.
14 factory(root.angular);
16 }(this, function(angular) {
22 var getInternetExplorerVersion = function ()
23 // Returns the version of Internet Explorer >4 or
24 // undefined(indicating the use of another browser).
26 var isIE10 = (eval("/*@cc_on!@*/false") && document.documentMode === 10);
31 div = document.createElement('div'),
32 all = div.getElementsByTagName('i');
34 div.innerHTML = '<!--[if gt IE ' + (++v) + ']><i></i><![endif]-->';
36 return v > 4 ? v : undefined;
39 var browserVersion = getInternetExplorerVersion();
41 if (browserVersion && browserVersion < 9) {
45 // This returned angular module 'gridster' is what is exported.
46 return angular.module('attGridsterLib', [])
48 .constant('gridsterConfig', {
49 columns: 6, // number of columns in the grid
50 pushing: true, // whether to push other items out of the way
51 floating: true, // whether to automatically float items up so they stack
52 swapping: true, // whether or not to have items switch places instead of push down if they are the same size
53 width: 'auto', // width of the grid. "auto" will expand the grid to its parent container
54 colWidth: 'auto', // width of grid columns. "auto" will divide the width of the grid evenly among the columns
55 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.
56 margins: [10, 10], // margins in between grid items
58 isMobile: false, // toggle mobile view
59 mobileBreakPoint: 100, // width threshold to toggle mobile mode
60 mobileModeEnabled: true, // whether or not to toggle mobile mode when screen width is less than mobileBreakPoint
61 minColumns: 1, // minimum amount of columns the grid can scale down to
62 minRows: 1, // minimum amount of rows to show if the grid is empty
63 maxRows: 100, // maximum amount of rows in the grid
64 defaultSizeX: 1, // default width of an item in columns
65 defaultSizeY: 1, // default height of an item in rows
66 minSizeX: 1, // minimum column width of an item
67 maxSizeX: null, // maximum column width of an item
68 minSizeY: 1, // minumum row height of an item
69 maxSizeY: null, // maximum row height of an item
70 saveGridItemCalculatedHeightInMobile: false, // grid item height in mobile display. true- to use the calculated height by sizeY given
71 resizable: { // options to pass to resizable handler
73 handles: ['s', 'e', 'n', 'w', 'se', 'ne', 'sw', 'nw']
75 draggable: { // options to pass to draggable handler
77 scrollSensitivity: 20, // Distance in pixels from the edge of the viewport after which the viewport should scroll, relative to pointer
78 scrollSpeed: 15 // Speed at which the window should scroll once the mouse pointer gets within scrollSensitivity distance
82 .controller('GridsterCtrl', ['gridsterConfig', '$timeout',
83 function(gridsterConfig, $timeout) {
88 * Create options from gridsterConfig constant
90 angular.extend(this, gridsterConfig);
92 this.resizable = angular.extend({}, gridsterConfig.resizable || {});
93 this.draggable = angular.extend({}, gridsterConfig.draggable || {});
96 this.layoutChanged = function() {
101 $timeout(function() {
103 if (gridster.loaded) {
104 gridster.floatItemsUp();
106 gridster.updateHeight(gridster.movingItem ? gridster.movingItem.sizeY : 0);
111 * A positional array of the items in the grid
116 * Clean up after yourself
118 this.destroy = function() {
119 // empty the grid to cut back on the possibility
120 // of circular references
124 this.$element = null;
128 * Overrides default options
130 * @param {Object} options The options to override
132 this.setOptions = function(options) {
137 options = angular.extend({}, options);
139 // all this to avoid using jQuery...
140 if (options.draggable) {
141 angular.extend(this.draggable, options.draggable);
142 delete(options.draggable);
144 if (options.resizable) {
145 angular.extend(this.resizable, options.resizable);
146 delete(options.resizable);
149 angular.extend(this, options);
151 if (!this.margins || this.margins.length !== 2) {
152 this.margins = [0, 0];
154 for (var x = 0, l = this.margins.length; x < l; ++x) {
155 this.margins[x] = parseInt(this.margins[x], 10);
156 if (isNaN(this.margins[x])) {
164 * Check if item can occupy a specified position in the grid
166 * @param {Object} item The item in question
167 * @param {Number} row The row index
168 * @param {Number} column The column index
169 * @returns {Boolean} True if if item fits
171 this.canItemOccupy = function(item, row, column) {
172 return row > -1 && column > -1 && item.sizeX + column <= this.columns && item.sizeY + row <= this.maxRows;
176 * Set the item in the first suitable position
178 * @param {Object} item The item to insert
180 this.autoSetItemPosition = function(item) {
181 // walk through each row and column looking for a place it will fit
182 for (var rowIndex = 0; rowIndex < this.maxRows; ++rowIndex) {
183 for (var colIndex = 0; colIndex < this.columns; ++colIndex) {
184 // only insert if position is not already taken and it can fit
185 var items = this.getItems(rowIndex, colIndex, item.sizeX, item.sizeY, item);
186 if (items.length === 0 && this.canItemOccupy(item, rowIndex, colIndex)) {
187 this.putItem(item, rowIndex, colIndex);
192 throw new Error('Unable to place item!');
196 * Gets items at a specific coordinate
198 * @param {Number} row
199 * @param {Number} column
200 * @param {Number} sizeX
201 * @param {Number} sizeY
202 * @param {Array} excludeItems An array of items to exclude from selection
203 * @returns {Array} Items that match the criteria
205 this.getItems = function(row, column, sizeX, sizeY, excludeItems) {
207 if (!sizeX || !sizeY) {
210 if (excludeItems && !(excludeItems instanceof Array)) {
211 excludeItems = [excludeItems];
213 for (var h = 0; h < sizeY; ++h) {
214 for (var w = 0; w < sizeX; ++w) {
215 var item = this.getItem(row + h, column + w, excludeItems);
216 if (item && (!excludeItems || excludeItems.indexOf(item) === -1) && items.indexOf(item) === -1) {
225 * @param {Array} items
226 * @returns {Object} An item that represents the bounding box of the items
228 this.getBoundingBox = function(items) {
230 if (items.length === 0) {
233 if (items.length === 1) {
237 sizeY: items[0].sizeY,
238 sizeX: items[0].sizeX
247 for (var i = 0, l = items.length; i < l; ++i) {
249 minRow = Math.min(item.row, minRow);
250 minCol = Math.min(item.col, minCol);
251 maxRow = Math.max(item.row + item.sizeY, maxRow);
252 maxCol = Math.max(item.col + item.sizeX, maxCol);
258 sizeY: maxRow - minRow,
259 sizeX: maxCol - minCol
265 * Removes an item from the grid
267 * @param {Object} item
269 this.removeItem = function(item) {
270 for (var rowIndex = 0, l = this.grid.length; rowIndex < l; ++rowIndex) {
271 var columns = this.grid[rowIndex];
275 var index = columns.indexOf(item);
277 columns[index] = null;
281 this.layoutChanged();
285 * Returns the item at a specified coordinate
287 * @param {Number} row
288 * @param {Number} column
289 * @param {Array} excludeItems Items to exclude from selection
290 * @returns {Object} The matched item or null
292 this.getItem = function(row, column, excludeItems) {
293 if (excludeItems && !(excludeItems instanceof Array)) {
294 excludeItems = [excludeItems];
301 var items = this.grid[row];
303 var item = items[col];
304 if (item && (!excludeItems || excludeItems.indexOf(item) === -1) && item.sizeX >= sizeX && item.sizeY >= sizeY) {
318 * Insert an array of items into the grid
320 * @param {Array} items An array of items to insert
322 this.putItems = function(items) {
323 for (var i = 0, l = items.length; i < l; ++i) {
324 this.putItem(items[i]);
329 * Insert a single item into the grid
331 * @param {Object} item The item to insert
332 * @param {Number} row (Optional) Specifies the items row index
333 * @param {Number} column (Optional) Specifies the items column index
334 * @param {Array} ignoreItems
336 this.putItem = function(item, row, column, ignoreItems) {
337 // auto place item if no row specified
338 if (typeof row === 'undefined' || row === null) {
341 if (typeof row === 'undefined' || row === null) {
342 this.autoSetItemPosition(item);
347 // keep item within allowed bounds
348 if (!this.canItemOccupy(item, row, column)) {
349 column = Math.min(this.columns - item.sizeX, Math.max(0, column));
350 row = Math.min(this.maxRows - item.sizeY, Math.max(0, row));
353 // check if item is already in grid
354 if (item.oldRow !== null && typeof item.oldRow !== 'undefined') {
355 var samePosition = item.oldRow === row && item.oldColumn === column;
356 var inGrid = this.grid[row] && this.grid[row][column] === item;
357 if (samePosition && inGrid) {
362 // remove from old position
363 var oldRow = this.grid[item.oldRow];
364 if (oldRow && oldRow[item.oldColumn] === item) {
365 delete oldRow[item.oldColumn];
370 item.oldRow = item.row = row;
371 item.oldColumn = item.col = column;
373 this.moveOverlappingItems(item, ignoreItems);
375 if (!this.grid[row]) {
378 this.grid[row][column] = item;
380 if (this.movingItem === item) {
381 this.floatItemUp(item);
383 this.layoutChanged();
387 * Trade row and column if item1 with item2
389 * @param {Object} item1
390 * @param {Object} item2
392 this.swapItems = function(item1, item2) {
393 this.grid[item1.row][item1.col] = item2;
394 this.grid[item2.row][item2.col] = item1;
396 var item1Row = item1.row;
397 var item1Col = item1.col;
398 item1.row = item2.row;
399 item1.col = item2.col;
400 item2.row = item1Row;
401 item2.col = item1Col;
405 * Prevents items from being overlapped
407 * @param {Object} item The item that should remain
408 * @param {Array} ignoreItems
410 this.moveOverlappingItems = function(item, ignoreItems) {
411 // don't move item, so ignore it
413 ignoreItems = [item];
414 } else if (ignoreItems.indexOf(item) === -1) {
415 ignoreItems = ignoreItems.slice(0);
416 ignoreItems.push(item);
419 // get the items in the space occupied by the item's coordinates
420 var overlappingItems = this.getItems(
427 this.moveItemsDown(overlappingItems, item.row + item.sizeY, ignoreItems);
431 * Moves an array of items to a specified row
433 * @param {Array} items The items to move
434 * @param {Number} newRow The target row
435 * @param {Array} ignoreItems
437 this.moveItemsDown = function(items, newRow, ignoreItems) {
438 if (!items || items.length === 0) {
441 items.sort(function(a, b) {
442 return a.row - b.row;
445 ignoreItems = ignoreItems ? ignoreItems.slice(0) : [];
449 // calculate the top rows in each column
450 for (i = 0, l = items.length; i < l; ++i) {
452 var topRow = topRows[item.col];
453 if (typeof topRow === 'undefined' || item.row < topRow) {
454 topRows[item.col] = item.row;
458 // move each item down from the top row in its column to the row
459 for (i = 0, l = items.length; i < l; ++i) {
461 var rowsToMove = newRow - topRows[item.col];
462 this.moveItemDown(item, item.row + rowsToMove, ignoreItems);
463 ignoreItems.push(item);
468 * Moves an item down to a specified row
470 * @param {Object} item The item to move
471 * @param {Number} newRow The target row
472 * @param {Array} ignoreItems
474 this.moveItemDown = function(item, newRow, ignoreItems) {
475 if (item.row >= newRow) {
478 while (item.row < newRow) {
480 this.moveOverlappingItems(item, ignoreItems);
482 this.putItem(item, item.row, item.col, ignoreItems);
486 * Moves all items up as much as possible
488 this.floatItemsUp = function() {
489 if (this.floating === false) {
492 for (var rowIndex = 0, l = this.grid.length; rowIndex < l; ++rowIndex) {
493 var columns = this.grid[rowIndex];
497 for (var colIndex = 0, len = columns.length; colIndex < len; ++colIndex) {
498 var item = columns[colIndex];
500 this.floatItemUp(item);
507 * Float an item up to the most suitable row
509 * @param {Object} item The item to move
511 this.floatItemUp = function(item) {
512 if (this.floating === false) {
515 var colIndex = item.col,
520 rowIndex = item.row - 1;
522 while (rowIndex > -1) {
523 var items = this.getItems(rowIndex, colIndex, sizeX, sizeY, item);
524 if (items.length !== 0) {
528 bestColumn = colIndex;
531 if (bestRow !== null) {
532 this.putItem(item, bestRow, bestColumn);
537 * Update gridsters height
539 * @param {Number} plus (Optional) Additional height to add
541 this.updateHeight = function(plus) {
542 var maxHeight = this.minRows;
544 for (var rowIndex = this.grid.length; rowIndex >= 0; --rowIndex) {
545 var columns = this.grid[rowIndex];
549 for (var colIndex = 0, len = columns.length; colIndex < len; ++colIndex) {
550 if (columns[colIndex]) {
551 maxHeight = Math.max(maxHeight, rowIndex + plus + columns[colIndex].sizeY);
555 this.gridHeight = this.maxRows - maxHeight > 0 ? Math.min(this.maxRows, maxHeight) : Math.max(this.maxRows, maxHeight);
559 * Returns the number of rows that will fit in given amount of pixels
561 * @param {Number} pixels
562 * @param {Boolean} ceilOrFloor (Optional) Determines rounding method
564 this.pixelsToRows = function(pixels, ceilOrFloor) {
565 if (ceilOrFloor === true) {
566 return Math.ceil(pixels / this.curRowHeight);
567 } else if (ceilOrFloor === false) {
568 return Math.floor(pixels / this.curRowHeight);
571 return Math.round(pixels / this.curRowHeight);
575 * Returns the number of columns that will fit in a given amount of pixels
577 * @param {Number} pixels
578 * @param {Boolean} ceilOrFloor (Optional) Determines rounding method
579 * @returns {Number} The number of columns
581 this.pixelsToColumns = function(pixels, ceilOrFloor) {
582 if (ceilOrFloor === true) {
583 return Math.ceil(pixels / this.curColWidth);
584 } else if (ceilOrFloor === false) {
585 return Math.floor(pixels / this.curColWidth);
588 return Math.round(pixels / this.curColWidth);
593 .directive('gridsterPreview', function() {
597 require: '^gridster',
598 template: '<div ng-style="previewStyle()" class="gridster-item gridster-preview-holder"></div>',
599 link: function(scope, $el, attrs, gridster) {
602 * @returns {Object} style object for preview element
604 scope.previewStyle = function() {
606 if (!gridster.movingItem) {
614 height: (gridster.movingItem.sizeY * gridster.curRowHeight - gridster.margins[0]) + 'px',
615 width: (gridster.movingItem.sizeX * gridster.curColWidth - gridster.margins[1]) + 'px',
616 top: (gridster.movingItem.row * gridster.curRowHeight + (gridster.outerMargin ? gridster.margins[0] : 0)) + 'px',
617 left: (gridster.movingItem.col * gridster.curColWidth + (gridster.outerMargin ? gridster.margins[1] : 0)) + 'px'
625 * The gridster directive
627 * @param {Function} $timeout
628 * @param {Object} $window
629 * @param {Object} $rootScope
630 * @param {Function} gridsterDebounce
632 .directive('gridster', ['$timeout', '$window', '$rootScope', 'gridsterDebounce',
633 function($timeout, $window, $rootScope, gridsterDebounce) {
637 controller: 'GridsterCtrl',
638 controllerAs: 'gridster',
639 compile: function($tplElem) {
641 $tplElem.prepend('<div ng-if="gridster.movingItem" gridster-preview></div>');
643 return function(scope, $elem, attrs, gridster) {
644 gridster.loaded = false;
646 gridster.$element = $elem;
648 scope.gridster = gridster;
650 $elem.addClass('gridster');
652 var isVisible = function(ele) {
653 return ele.style.visibility !== 'hidden' && ele.style.display !== 'none';
656 function refresh(config) {
657 gridster.setOptions(config);
659 if (!isVisible($elem[0])) {
663 // resolve "auto" & "match" values
664 if (gridster.width === 'auto') {
665 gridster.curWidth = $elem[0].offsetWidth || parseInt($elem.css('width'), 10);
667 gridster.curWidth = gridster.width;
670 if (gridster.colWidth === 'auto') {
671 gridster.curColWidth = (gridster.curWidth + (gridster.outerMargin ? -gridster.margins[1] : gridster.margins[1])) / gridster.columns;
673 gridster.curColWidth = gridster.colWidth;
676 gridster.curRowHeight = gridster.rowHeight;
677 if (typeof gridster.rowHeight === 'string') {
678 if (gridster.rowHeight === 'match') {
679 gridster.curRowHeight = Math.round(gridster.curColWidth);
680 } else if (gridster.rowHeight.indexOf('*') !== -1) {
681 gridster.curRowHeight = Math.round(gridster.curColWidth * gridster.rowHeight.replace('*', '').replace(' ', ''));
682 } else if (gridster.rowHeight.indexOf('/') !== -1) {
683 gridster.curRowHeight = Math.round(gridster.curColWidth / gridster.rowHeight.replace('/', '').replace(' ', ''));
687 gridster.isMobile = gridster.mobileModeEnabled && gridster.curWidth <= gridster.mobileBreakPoint;
689 // loop through all items and reset their CSS
690 for (var rowIndex = 0, l = gridster.grid.length; rowIndex < l; ++rowIndex) {
691 var columns = gridster.grid[rowIndex];
696 for (var colIndex = 0, len = columns.length; colIndex < len; ++colIndex) {
697 if (columns[colIndex]) {
698 var item = columns[colIndex];
699 item.setElementPosition();
700 item.setElementSizeY();
701 item.setElementSizeX();
709 var optionsKey = attrs.gridster;
711 scope.$parent.$watch(optionsKey, function(newConfig) {
718 scope.$watch(function() {
719 return gridster.loaded;
721 if (gridster.loaded) {
722 $elem.addClass('gridster-loaded');
724 $elem.removeClass('gridster-loaded');
728 scope.$watch(function() {
729 return gridster.isMobile;
731 if (gridster.isMobile) {
732 $elem.addClass('gridster-mobile').removeClass('gridster-desktop');
734 $elem.removeClass('gridster-mobile').addClass('gridster-desktop');
736 $rootScope.$broadcast('gridster-mobile-changed', gridster);
739 scope.$watch(function() {
740 return gridster.draggable;
742 $rootScope.$broadcast('gridster-draggable-changed', gridster);
745 scope.$watch(function() {
746 return gridster.resizable;
748 $rootScope.$broadcast('gridster-resizable-changed', gridster);
751 function updateHeight() {
752 if(gridster.gridHeight){ //need to put this check, otherwise fail in IE8
753 $elem.css('height', (gridster.gridHeight * gridster.curRowHeight) + (gridster.outerMargin ? gridster.margins[0] : -gridster.margins[0]) + 'px');
757 scope.$watch(function() {
758 return gridster.gridHeight;
761 scope.$watch(function() {
762 return gridster.movingItem;
764 gridster.updateHeight(gridster.movingItem ? gridster.movingItem.sizeY : 0);
767 var prevWidth = $elem[0].offsetWidth || parseInt($elem.css('width'), 10);
769 var resize = function() {
770 var width = $elem[0].offsetWidth || parseInt($elem.css('width'), 10);
772 if (!width || width === prevWidth || gridster.movingItem) {
777 if (gridster.loaded) {
778 $elem.removeClass('gridster-loaded');
783 if (gridster.loaded) {
784 $elem.addClass('gridster-loaded');
787 $rootScope.$broadcast('gridster-resized', [width, $elem[0].offsetHeight], gridster);
790 // track element width changes any way we can
791 var onResize = gridsterDebounce(function onResize() {
793 $timeout(function() {
798 scope.$watch(function() {
799 return isVisible($elem[0]);
802 // see https://github.com/sdecima/javascript-detect-element-resize
803 if (typeof window.addResizeListener === 'function') {
804 window.addResizeListener($elem[0], onResize);
806 scope.$watch(function() {
807 return $elem[0].offsetWidth || parseInt($elem.css('width'), 10);
810 var $win = angular.element($window);
811 $win.on('resize', onResize);
813 // be sure to cleanup
814 scope.$on('$destroy', function() {
816 $win.off('resize', onResize);
817 if (typeof window.removeResizeListener === 'function') {
818 window.removeResizeListener($elem[0], onResize);
822 // allow a little time to place items before floating up
823 $timeout(function() {
824 scope.$watch('gridster.floating', function() {
825 gridster.floatItemsUp();
827 gridster.loaded = true;
835 .controller('GridsterItemCtrl', function() {
836 this.$element = null;
837 this.gridster = null;
844 this.maxSizeX = null;
845 this.maxSizeY = null;
847 this.init = function($element, gridster) {
848 this.$element = $element;
849 this.gridster = gridster;
850 this.sizeX = gridster.defaultSizeX;
851 this.sizeY = gridster.defaultSizeY;
854 this.destroy = function() {
855 // set these to null to avoid the possibility of circular references
856 this.gridster = null;
857 this.$element = null;
861 * Returns the items most important attributes
863 this.toJSON = function() {
872 this.isMoving = function() {
873 return this.gridster.movingItem === this;
877 * Set the items position
879 * @param {Number} row
880 * @param {Number} column
882 this.setPosition = function(row, column) {
883 this.gridster.putItem(this, row, column);
885 if (!this.isMoving()) {
886 this.setElementPosition();
891 * Sets a specified size property
893 * @param {String} key Can be either "x" or "y"
894 * @param {Number} value The size amount
895 * @param {Boolean} preventMove
897 this.setSize = function(key, value, preventMove) {
898 key = key.toUpperCase();
899 var camelCase = 'size' + key,
900 titleCase = 'Size' + key;
904 value = parseInt(value, 10);
905 if (isNaN(value) || value === 0) {
906 value = this.gridster['default' + titleCase];
908 var max = key === 'X' ? this.gridster.columns : this.gridster.maxRows;
909 if (this['max' + titleCase]) {
910 max = Math.min(this['max' + titleCase], max);
912 if (this.gridster['max' + titleCase]) {
913 max = Math.min(this.gridster['max' + titleCase], max);
915 if (key === 'X' && this.cols) {
917 } else if (key === 'Y' && this.rows) {
922 if (this['min' + titleCase]) {
923 min = Math.max(this['min' + titleCase], min);
925 if (this.gridster['min' + titleCase]) {
926 min = Math.max(this.gridster['min' + titleCase], min);
929 value = Math.max(Math.min(value, max), min);
931 var changed = (this[camelCase] !== value || (this['old' + titleCase] && this['old' + titleCase] !== value));
932 this['old' + titleCase] = this[camelCase] = value;
934 if (!this.isMoving()) {
935 this['setElement' + titleCase]();
937 if (!preventMove && changed) {
938 this.gridster.moveOverlappingItems(this);
939 this.gridster.layoutChanged();
946 * Sets the items sizeY property
948 * @param {Number} rows
949 * @param {Boolean} preventMove
951 this.setSizeY = function(rows, preventMove) {
952 return this.setSize('Y', rows, preventMove);
956 * Sets the items sizeX property
958 * @param {Number} columns
959 * @param {Boolean} preventMove
961 this.setSizeX = function(columns, preventMove) {
962 return this.setSize('X', columns, preventMove);
966 * Sets an elements position on the page
968 this.setElementPosition = function() {
969 if (this.gridster.isMobile) {
971 marginLeft: this.gridster.margins[0] + 'px',
972 marginRight: this.gridster.margins[0] + 'px',
973 marginTop: this.gridster.margins[1] + 'px',
974 marginBottom: this.gridster.margins[1] + 'px',
981 top: (this.row * this.gridster.curRowHeight + (this.gridster.outerMargin ? this.gridster.margins[0] : 0)) + 'px',
982 left: (this.col * this.gridster.curColWidth + (this.gridster.outerMargin ? this.gridster.margins[1] : 0)) + 'px'
988 * Sets an elements height
990 this.setElementSizeY = function() {
991 if (this.gridster.isMobile && !this.gridster.saveGridItemCalculatedHeightInMobile) {
992 this.$element.css('height', '');
994 var computedHeight = (this.sizeY * this.gridster.curRowHeight - this.gridster.margins[0]) + 'px';
995 //this.$element.css('height', computedHeight);
996 this.$element.attr('style', this.$element.attr('style') + '; ' + 'height: '+computedHeight+' !important;');
1001 * Sets an elements width
1003 this.setElementSizeX = function() {
1004 if (this.gridster.isMobile) {
1005 this.$element.css('width', '');
1007 this.$element.css('width', (this.sizeX * this.gridster.curColWidth - this.gridster.margins[1]) + 'px');
1012 * Gets an element's width
1014 this.getElementSizeX = function() {
1015 return (this.sizeX * this.gridster.curColWidth - this.gridster.margins[1]);
1019 * Gets an element's height
1021 this.getElementSizeY = function() {
1022 return (this.sizeY * this.gridster.curRowHeight - this.gridster.margins[0]);
1027 .factory('GridsterTouch', [function() {
1028 return function GridsterTouch(target, startEvent, moveEvent, endEvent) {
1029 var lastXYById = {};
1031 // Opera doesn't have Object.keys so we use this wrapper
1032 var numberOfKeys = function(theObject) {
1034 return Object.keys(theObject).length;
1039 for (key in theObject) {
1046 // 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
1047 var computeDocumentToElementDelta = function(theElement) {
1048 var elementLeft = 0;
1050 var oldIEUserAgent = navigator.userAgent.match(/\bMSIE\b/);
1052 for (var offsetElement = theElement; offsetElement != null; offsetElement = offsetElement.offsetParent) {
1053 // 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
1054 // this may not be a general solution to IE7's problem with offsetLeft/offsetParent
1055 if (oldIEUserAgent &&
1056 (!document.documentMode || document.documentMode < 8) &&
1057 offsetElement.currentStyle.position === 'relative' && offsetElement.offsetParent && offsetElement.offsetParent.currentStyle.position === 'relative' && offsetElement.offsetLeft === offsetElement.offsetParent.offsetLeft) {
1059 elementTop += offsetElement.offsetTop;
1061 elementLeft += offsetElement.offsetLeft;
1062 elementTop += offsetElement.offsetTop;
1072 // cache the delta from the document to our event target (reinitialized each mousedown/MSPointerDown/touchstart)
1073 var documentToTargetDelta = computeDocumentToElementDelta(target);
1075 // common event handler for the mouse/pointer/touch models and their down/start, move, up/end, and cancel events
1076 var doEvent = function(theEvtObj) {
1078 if (theEvtObj.type === 'mousemove' && numberOfKeys(lastXYById) === 0) {
1084 var pointerList = theEvtObj.changedTouches ? theEvtObj.changedTouches : [theEvtObj];
1086 for (var i = 0; i < pointerList.length; ++i) {
1087 var pointerObj = pointerList[i];
1088 var pointerId = (typeof pointerObj.identifier !== 'undefined') ? pointerObj.identifier : (typeof pointerObj.pointerId !== 'undefined') ? pointerObj.pointerId : 1;
1090 // 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)
1091 if (typeof pointerObj.pageX === 'undefined') {
1093 // initialize assuming our source element is our target
1095 pointerObj.pageX = pointerObj.offsetX + documentToTargetDelta.x;
1096 pointerObj.pageY = pointerObj.offsetY + documentToTargetDelta.y;
1099 pointerObj.pageX = pointerObj.clientX;
1100 pointerObj.pageY = pointerObj.clientY;
1103 if (pointerObj.srcElement.offsetParent === target && document.documentMode && document.documentMode === 8 && pointerObj.type === 'mousedown') {
1104 // 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
1105 pointerObj.pageX += pointerObj.srcElement.offsetLeft;
1106 pointerObj.pageY += pointerObj.srcElement.offsetTop;
1107 } else if (pointerObj.srcElement !== target && !document.documentMode || document.documentMode < 8) {
1108 // 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 -
1109 // the offsetX/Y values are unpredictable so use the clientX/Y values and adjust by the scroll offsets of its parents
1110 // to get the document-relative coordinates (the same as pageX/Y)
1112 sy = -2; // adjust for old IE's 2-pixel border
1113 for (var scrollElement = pointerObj.srcElement; scrollElement !== null; scrollElement = scrollElement.parentNode) {
1114 sx += scrollElement.scrollLeft ? scrollElement.scrollLeft : 0;
1115 sy += scrollElement.scrollTop ? scrollElement.scrollTop : 0;
1118 pointerObj.pageX = pointerObj.clientX + sx;
1119 pointerObj.pageY = pointerObj.clientY + sy;
1124 var pageX = pointerObj.pageX;
1125 var pageY = pointerObj.pageY;
1127 if (theEvtObj.type.match(/(start|down)$/i)) {
1128 // clause for processing MSPointerDown, touchstart, and mousedown
1130 // refresh the document-to-target delta on start in case the target has moved relative to document
1131 documentToTargetDelta = computeDocumentToElementDelta(target);
1133 // protect against failing to get an up or end on this pointerId
1134 if (lastXYById[pointerId]) {
1137 target: theEvtObj.target,
1138 which: theEvtObj.which,
1139 pointerId: pointerId,
1145 delete lastXYById[pointerId];
1150 prevent = startEvent({
1151 target: theEvtObj.target,
1152 which: theEvtObj.which,
1153 pointerId: pointerId,
1160 // init last page positions for this pointer
1161 lastXYById[pointerId] = {
1167 if (target.msSetPointerCapture) {
1168 target.msSetPointerCapture(pointerId);
1169 } else if (theEvtObj.type === 'mousedown' && numberOfKeys(lastXYById) === 1) {
1170 if (useSetReleaseCapture) {
1171 target.setCapture(true);
1173 document.addEventListener('mousemove', doEvent, false);
1174 document.addEventListener('mouseup', doEvent, false);
1177 } else if (theEvtObj.type.match(/move$/i)) {
1178 // clause handles mousemove, MSPointerMove, and touchmove
1180 if (lastXYById[pointerId] && !(lastXYById[pointerId].x === pageX && lastXYById[pointerId].y === pageY)) {
1181 // only extend if the pointer is down and it's not the same as the last point
1183 if (moveEvent && prevent) {
1184 prevent = moveEvent({
1185 target: theEvtObj.target,
1186 which: theEvtObj.which,
1187 pointerId: pointerId,
1193 // update last page positions for this pointer
1194 lastXYById[pointerId].x = pageX;
1195 lastXYById[pointerId].y = pageY;
1197 } else if (lastXYById[pointerId] && theEvtObj.type.match(/(up|end|cancel)$/i)) {
1198 // clause handles up/end/cancel
1200 if (endEvent && prevent) {
1201 prevent = endEvent({
1202 target: theEvtObj.target,
1203 which: theEvtObj.which,
1204 pointerId: pointerId,
1210 // delete last page positions for this pointer
1211 delete lastXYById[pointerId];
1213 // in the Microsoft pointer model, release the capture for this pointer
1214 // in the mouse model, release the capture or remove document-level event handlers if there are no down points
1215 // nothing is required for the iOS touch model because capture is implied on touchstart
1216 if (target.msReleasePointerCapture) {
1217 target.msReleasePointerCapture(pointerId);
1218 } else if (theEvtObj.type === 'mouseup' && numberOfKeys(lastXYById) === 0) {
1219 if (useSetReleaseCapture) {
1220 target.releaseCapture();
1222 document.removeEventListener('mousemove', doEvent, false);
1223 document.removeEventListener('mouseup', doEvent, false);
1230 if (theEvtObj.preventDefault) {
1231 theEvtObj.preventDefault();
1234 if (theEvtObj.preventManipulation) {
1235 theEvtObj.preventManipulation();
1238 if (theEvtObj.preventMouseEvent) {
1239 theEvtObj.preventMouseEvent();
1244 var useSetReleaseCapture = false;
1245 // saving the settings for contentZooming and touchaction before activation
1246 var contentZooming, msTouchAction;
1248 this.enable = function() {
1250 if (window.navigator.msPointerEnabled) {
1251 // Microsoft pointer model
1252 target.addEventListener('MSPointerDown', doEvent, false);
1253 target.addEventListener('MSPointerMove', doEvent, false);
1254 target.addEventListener('MSPointerUp', doEvent, false);
1255 target.addEventListener('MSPointerCancel', doEvent, false);
1257 // css way to prevent panning in our target area
1258 if (typeof target.style.msContentZooming !== 'undefined') {
1259 contentZooming = target.style.msContentZooming;
1260 target.style.msContentZooming = 'none';
1263 // new in Windows Consumer Preview: css way to prevent all built-in touch actions on our target
1264 // without this, you cannot touch draw on the element because IE will intercept the touch events
1265 if (typeof target.style.msTouchAction !== 'undefined') {
1266 msTouchAction = target.style.msTouchAction;
1267 target.style.msTouchAction = 'none';
1269 } else if (target.addEventListener) {
1271 target.addEventListener('touchstart', doEvent, false);
1272 target.addEventListener('touchmove', doEvent, false);
1273 target.addEventListener('touchend', doEvent, false);
1274 target.addEventListener('touchcancel', doEvent, false);
1277 target.addEventListener('mousedown', doEvent, false);
1279 // mouse model with capture
1280 // rejecting gecko because, unlike ie, firefox does not send events to target when the mouse is outside target
1281 if (target.setCapture && !window.navigator.userAgent.match(/\bGecko\b/)) {
1282 useSetReleaseCapture = true;
1284 target.addEventListener('mousemove', doEvent, false);
1285 target.addEventListener('mouseup', doEvent, false);
1287 } else if (target.attachEvent && target.setCapture) {
1288 // legacy IE mode - mouse with capture
1289 useSetReleaseCapture = true;
1290 target.attachEvent('onmousedown', function() {
1291 doEvent(window.event);
1292 window.event.returnValue = false;
1295 target.attachEvent('onmousemove', function() {
1296 doEvent(window.event);
1297 window.event.returnValue = false;
1300 target.attachEvent('onmouseup', function() {
1301 doEvent(window.event);
1302 window.event.returnValue = false;
1308 this.disable = function() {
1309 if (window.navigator.msPointerEnabled) {
1310 // Microsoft pointer model
1311 target.removeEventListener('MSPointerDown', doEvent, false);
1312 target.removeEventListener('MSPointerMove', doEvent, false);
1313 target.removeEventListener('MSPointerUp', doEvent, false);
1314 target.removeEventListener('MSPointerCancel', doEvent, false);
1316 // reset zooming to saved value
1317 if (contentZooming) {
1318 target.style.msContentZooming = contentZooming;
1321 // reset touch action setting
1322 if (msTouchAction) {
1323 target.style.msTouchAction = msTouchAction;
1325 } else if (target.removeEventListener) {
1327 target.removeEventListener('touchstart', doEvent, false);
1328 target.removeEventListener('touchmove', doEvent, false);
1329 target.removeEventListener('touchend', doEvent, false);
1330 target.removeEventListener('touchcancel', doEvent, false);
1333 target.removeEventListener('mousedown', doEvent, false);
1335 // mouse model with capture
1336 // rejecting gecko because, unlike ie, firefox does not send events to target when the mouse is outside target
1337 if (target.setCapture && !window.navigator.userAgent.match(/\bGecko\b/)) {
1338 useSetReleaseCapture = true;
1340 target.removeEventListener('mousemove', doEvent, false);
1341 target.removeEventListener('mouseup', doEvent, false);
1343 } else if (target.detachEvent && target.setCapture) {
1344 // legacy IE mode - mouse with capture
1345 useSetReleaseCapture = true;
1346 target.detachEvent('onmousedown');
1347 target.detachEvent('onmousemove');
1348 target.detachEvent('onmouseup');
1356 .factory('GridsterDraggable', ['$document', '$timeout', '$window', 'GridsterTouch',
1357 function($document, $timeout, $window, GridsterTouch) {
1358 function GridsterDraggable($el, scope, gridster, item, itemOptions) {
1360 var elmX, elmY, elmW, elmH,
1372 realdocument = $document[0];
1374 var originalCol, originalRow;
1375 var inputTags = ['select', 'input', 'textarea', 'button'];
1377 var gridsterItemDragElement = $el[0].querySelector('[gridster-item-drag]');
1378 //console.log(gridsterItemDragElement);
1379 var isDraggableAreaDefined = gridsterItemDragElement?true:false;
1380 //console.log(isDraggableAreaDefined);
1382 function mouseDown(e) {
1385 e.target = window.event.srcElement;
1386 e.which = window.event.button;
1389 if(isDraggableAreaDefined && (!gridsterItemDragElement.contains(e.target))){
1393 if (inputTags.indexOf(e.target.nodeName.toLowerCase()) !== -1) {
1397 var $target = angular.element(e.target);
1399 // exit, if a resize handle was hit
1400 if ($target.hasClass('gridster-item-resizable-handler')) {
1404 // exit, if the target has it's own click event
1405 if ($target.attr('onclick') || $target.attr('ng-click')) {
1409 // only works if you have jQuery
1410 if ($target.closest && $target.closest('.gridster-no-drag').length) {
1416 // left mouse button
1420 // right or middle mouse button
1424 lastMouseX = e.pageX;
1425 lastMouseY = e.pageY;
1427 elmX = parseInt($el.css('left'), 10);
1428 elmY = parseInt($el.css('top'), 10);
1429 elmW = $el[0].offsetWidth;
1430 elmH = $el[0].offsetHeight;
1432 originalCol = item.col;
1433 originalRow = item.row;
1440 function mouseMove(e) {
1441 if (!$el.hasClass('gridster-item-moving') || $el.hasClass('gridster-item-resizing')) {
1445 var maxLeft = gridster.curWidth - 1;
1447 // Get the current mouse position.
1452 var diffX = mouseX - lastMouseX + mOffX;
1453 var diffY = mouseY - lastMouseY + mOffY;
1456 // Update last processed mouse positions.
1457 lastMouseX = mouseX;
1458 lastMouseY = mouseY;
1462 if (elmX + dX < minLeft) {
1463 diffX = minLeft - elmX;
1465 } else if (elmX + elmW + dX > maxLeft) {
1466 diffX = maxLeft - elmX - elmW;
1470 if (elmY + dY < minTop) {
1471 diffY = minTop - elmY;
1473 } else if (elmY + elmH + dY > maxTop) {
1474 diffY = maxTop - elmY - elmH;
1491 function mouseUp(e) {
1492 if (!$el.hasClass('gridster-item-moving') || $el.hasClass('gridster-item-resizing')) {
1503 function dragStart(event) {
1504 $el.addClass('gridster-item-moving');
1505 gridster.movingItem = item;
1507 gridster.updateHeight(item.sizeY);
1508 scope.$apply(function() {
1509 if (gridster.draggable && gridster.draggable.start) {
1510 gridster.draggable.start(event, $el, itemOptions);
1515 function drag(event) {
1516 var oldRow = item.row,
1518 hasCallback = gridster.draggable && gridster.draggable.drag,
1519 scrollSensitivity = gridster.draggable.scrollSensitivity,
1520 scrollSpeed = gridster.draggable.scrollSpeed;
1522 var row = gridster.pixelsToRows(elmY);
1523 var col = gridster.pixelsToColumns(elmX);
1525 var itemsInTheWay = gridster.getItems(row, col, item.sizeX, item.sizeY, item);
1526 var hasItemsInTheWay = itemsInTheWay.length !== 0;
1528 if (gridster.swapping === true && hasItemsInTheWay) {
1529 var boundingBoxItem = gridster.getBoundingBox(itemsInTheWay),
1530 sameSize = boundingBoxItem.sizeX === item.sizeX && boundingBoxItem.sizeY === item.sizeY,
1531 sameRow = boundingBoxItem.row === oldRow,
1532 sameCol = boundingBoxItem.col === oldCol,
1533 samePosition = boundingBoxItem.row === row && boundingBoxItem.col === col,
1534 inline = sameRow || sameCol;
1536 if (sameSize && itemsInTheWay.length === 1) {
1538 gridster.swapItems(item, itemsInTheWay[0]);
1539 } else if (inline) {
1542 } else if (boundingBoxItem.sizeX <= item.sizeX && boundingBoxItem.sizeY <= item.sizeY && inline) {
1543 var emptyRow = item.row <= row ? item.row : row + item.sizeY,
1544 emptyCol = item.col <= col ? item.col : col + item.sizeX,
1545 rowOffset = emptyRow - boundingBoxItem.row,
1546 colOffset = emptyCol - boundingBoxItem.col;
1548 for (var i = 0, l = itemsInTheWay.length; i < l; ++i) {
1549 var itemInTheWay = itemsInTheWay[i];
1551 var itemsInFreeSpace = gridster.getItems(
1552 itemInTheWay.row + rowOffset,
1553 itemInTheWay.col + colOffset,
1559 if (itemsInFreeSpace.length === 0) {
1560 gridster.putItem(itemInTheWay, itemInTheWay.row + rowOffset, itemInTheWay.col + colOffset);
1566 if (gridster.pushing !== false || !hasItemsInTheWay) {
1571 if(($window.navigator.appName === 'Microsoft Internet Explorer' && !ie8) || $window.navigator.userAgent.indexOf("Firefox")!==-1){
1572 if (event.pageY - realdocument.documentElement.scrollTop < scrollSensitivity) {
1573 realdocument.documentElement.scrollTop = realdocument.documentElement.scrollTop - scrollSpeed;
1574 } else if ($window.innerHeight - (event.pageY - realdocument.documentElement.scrollTop) < scrollSensitivity) {
1575 realdocument.documentElement.scrollTop = realdocument.documentElement.scrollTop + scrollSpeed;
1579 if (event.pageY - realdocument.body.scrollTop < scrollSensitivity) {
1580 realdocument.body.scrollTop = realdocument.body.scrollTop - scrollSpeed;
1581 } else if ($window.innerHeight - (event.pageY - realdocument.body.scrollTop) < scrollSensitivity) {
1582 realdocument.body.scrollTop = realdocument.body.scrollTop + scrollSpeed;
1588 if (event.pageX - realdocument.body.scrollLeft < scrollSensitivity) {
1589 realdocument.body.scrollLeft = realdocument.body.scrollLeft - scrollSpeed;
1590 } else if ($window.innerWidth - (event.pageX - realdocument.body.scrollLeft) < scrollSensitivity) {
1591 realdocument.body.scrollLeft = realdocument.body.scrollLeft + scrollSpeed;
1594 if (hasCallback || oldRow !== item.row || oldCol !== item.col) {
1595 scope.$apply(function() {
1597 gridster.draggable.drag(event, $el, itemOptions);
1603 function dragStop(event) {
1604 $el.removeClass('gridster-item-moving');
1605 var row = gridster.pixelsToRows(elmY);
1606 var col = gridster.pixelsToColumns(elmX);
1607 if (gridster.pushing !== false || gridster.getItems(row, col, item.sizeX, item.sizeY, item).length === 0) {
1611 gridster.movingItem = null;
1612 item.setPosition(item.row, item.col);
1614 scope.$apply(function() {
1615 if (gridster.draggable && gridster.draggable.stop) {
1616 gridster.draggable.stop(event, $el, itemOptions);
1622 var $dragHandles = null;
1623 var unifiedInputs = [];
1625 this.enable = function() {
1626 if (enabled === true) {
1630 // disable and timeout required for some template rendering
1631 $timeout(function() {
1632 // disable any existing draghandles
1633 for (var u = 0, ul = unifiedInputs.length; u < ul; ++u) {
1634 unifiedInputs[u].disable();
1638 if (gridster.draggable && gridster.draggable.handle) {
1639 $dragHandles = angular.element($el[0].querySelectorAll(gridster.draggable.handle));
1640 if ($dragHandles.length === 0) {
1641 // fall back to element if handle not found...
1648 for (var h = 0, hl = $dragHandles.length; h < hl; ++h) {
1649 unifiedInputs[h] = new GridsterTouch($dragHandles[h], mouseDown, mouseMove, mouseUp);
1650 unifiedInputs[h].enable();
1657 this.disable = function() {
1658 if (enabled === false) {
1662 // timeout to avoid race contition with the enable timeout
1663 $timeout(function() {
1665 for (var u = 0, ul = unifiedInputs.length; u < ul; ++u) {
1666 unifiedInputs[u].disable();
1674 this.toggle = function(enabled) {
1682 this.destroy = function() {
1687 return GridsterDraggable;
1691 .factory('GridsterResizable', ['GridsterTouch', function(GridsterTouch) {
1692 function GridsterResizable($el, scope, gridster, item, itemOptions) {
1694 function ResizeHandle(handleClass) {
1696 var hClass = handleClass;
1698 var elmX, elmY, elmW, elmH,
1711 var getMinHeight = function() {
1712 return (item.minSizeY ? item.minSizeY : 1) * gridster.curRowHeight - gridster.margins[0];
1714 var getMinWidth = function() {
1715 return (item.minSizeX ? item.minSizeX : 1) * gridster.curColWidth - gridster.margins[1];
1718 var originalWidth, originalHeight;
1721 function mouseDown(e) {
1724 // left mouse button
1728 // right or middle mouse button
1732 // save the draggable setting to restore after resize
1733 savedDraggable = gridster.draggable.enabled;
1734 if (savedDraggable) {
1735 gridster.draggable.enabled = false;
1736 scope.$broadcast('gridster-draggable-changed', gridster);
1739 // Get the current mouse position.
1740 lastMouseX = e.pageX;
1741 lastMouseY = e.pageY;
1743 // Record current widget dimensions
1744 elmX = parseInt($el.css('left'), 10);
1745 elmY = parseInt($el.css('top'), 10);
1746 elmW = $el[0].offsetWidth;
1747 elmH = $el[0].offsetHeight;
1749 originalWidth = item.sizeX;
1750 originalHeight = item.sizeY;
1757 function resizeStart(e) {
1758 $el.addClass('gridster-item-moving');
1759 $el.addClass('gridster-item-resizing');
1761 gridster.movingItem = item;
1763 item.setElementSizeX();
1764 item.setElementSizeY();
1765 item.setElementPosition();
1766 gridster.updateHeight(1);
1768 scope.$apply(function() {
1770 if (gridster.resizable && gridster.resizable.start) {
1771 gridster.resizable.start(e, $el, itemOptions); // options is the item model
1776 function mouseMove(e) {
1777 var maxLeft = gridster.curWidth - 1;
1779 // Get the current mouse position.
1784 var diffX = mouseX - lastMouseX + mOffX;
1785 var diffY = mouseY - lastMouseY + mOffY;
1788 // Update last processed mouse positions.
1789 lastMouseX = mouseX;
1790 lastMouseY = mouseY;
1795 if (hClass.indexOf('n') >= 0) {
1796 if (elmH - dY < getMinHeight()) {
1797 diffY = elmH - getMinHeight();
1799 } else if (elmY + dY < minTop) {
1800 diffY = minTop - elmY;
1806 if (hClass.indexOf('s') >= 0) {
1807 if (elmH + dY < getMinHeight()) {
1808 diffY = getMinHeight() - elmH;
1810 } else if (elmY + elmH + dY > maxTop) {
1811 diffY = maxTop - elmY - elmH;
1816 if (hClass.indexOf('w') >= 0) {
1817 if (elmW - dX < getMinWidth()) {
1818 diffX = elmW - getMinWidth();
1820 } else if (elmX + dX < minLeft) {
1821 diffX = minLeft - elmX;
1827 if (hClass.indexOf('e') >= 0) {
1828 if (elmW + dX < getMinWidth()) {
1829 diffX = getMinWidth() - elmW;
1831 } else if (elmX + elmW + dX > maxLeft) {
1832 diffX = maxLeft - elmX - elmW;
1841 'left': elmX + 'px',
1842 'width': elmW + 'px',
1843 'height': elmH + 'px'
1851 function mouseUp(e) {
1852 // restore draggable setting to its original state
1853 if (gridster.draggable.enabled !== savedDraggable) {
1854 gridster.draggable.enabled = savedDraggable;
1855 scope.$broadcast('gridster-draggable-changed', gridster);
1865 function resize(e) {
1866 var oldRow = item.row,
1868 oldSizeX = item.sizeX,
1869 oldSizeY = item.sizeY,
1870 hasCallback = gridster.resizable && gridster.resizable.resize;
1873 // only change column if grabbing left edge
1874 if (['w', 'nw', 'sw'].indexOf(handleClass) !== -1) {
1875 col = gridster.pixelsToColumns(elmX, false);
1879 // only change row if grabbing top edge
1880 if (['n', 'ne', 'nw'].indexOf(handleClass) !== -1) {
1881 row = gridster.pixelsToRows(elmY, false);
1884 var sizeX = item.sizeX;
1885 // only change row if grabbing left or right edge
1886 if (['n', 's'].indexOf(handleClass) === -1) {
1887 sizeX = gridster.pixelsToColumns(elmW, true);
1890 var sizeY = item.sizeY;
1891 // only change row if grabbing top or bottom edge
1892 if (['e', 'w'].indexOf(handleClass) === -1) {
1893 sizeY = gridster.pixelsToRows(elmH, true);
1896 if (gridster.pushing !== false || gridster.getItems(row, col, sizeX, sizeY, item).length === 0) {
1902 var isChanged = item.row !== oldRow || item.col !== oldCol || item.sizeX !== oldSizeX || item.sizeY !== oldSizeY;
1904 if (hasCallback || isChanged) {
1905 scope.$apply(function() {
1907 gridster.resizable.resize(e, $el, itemOptions); // options is the item model
1913 function resizeStop(e) {
1914 $el.removeClass('gridster-item-moving');
1915 $el.removeClass('gridster-item-resizing');
1917 gridster.movingItem = null;
1919 item.setPosition(item.row, item.col);
1920 item.setSizeY(item.sizeY);
1921 item.setSizeX(item.sizeX);
1923 scope.$apply(function() {
1924 if (gridster.resizable && gridster.resizable.stop) {
1925 gridster.resizable.stop(e, $el, itemOptions); // options is the item model
1930 var $dragHandle = null;
1933 this.enable = function() {
1935 $dragHandle = angular.element('<div class="gridster-item-resizable-handler handle-' + hClass + '"></div>');
1936 $el.append($dragHandle);
1939 unifiedInput = new GridsterTouch($dragHandle[0], mouseDown, mouseMove, mouseUp);
1940 unifiedInput.enable();
1943 this.disable = function() {
1945 $dragHandle.remove();
1949 unifiedInput.disable();
1950 unifiedInput = undefined;
1953 this.destroy = function() {
1959 var handlesOpts = gridster.resizable.handles;
1960 if (typeof handlesOpts === 'string') {
1961 handlesOpts = gridster.resizable.handles.split(',');
1963 var enabled = false;
1965 for (var c = 0, l = handlesOpts.length; c < l; c++) {
1966 handles.push(new ResizeHandle(handlesOpts[c]));
1969 this.enable = function() {
1973 for (var c = 0, l = handles.length; c < l; c++) {
1974 handles[c].enable();
1979 this.disable = function() {
1983 for (var c = 0, l = handles.length; c < l; c++) {
1984 handles[c].disable();
1989 this.toggle = function(enabled) {
1997 this.destroy = function() {
1998 for (var c = 0, l = handles.length; c < l; c++) {
1999 handles[c].destroy();
2003 return GridsterResizable;
2006 .factory('gridsterDebounce', function() {
2007 return function gridsterDebounce(func, wait, immediate) {
2012 var later = function() {
2015 func.apply(context, args);
2018 var callNow = immediate && !timeout;
2019 clearTimeout(timeout);
2020 timeout = setTimeout(later, wait);
2022 func.apply(context, args);
2029 * GridsterItem directive
2031 * @param GridsterDraggable
2032 * @param GridsterResizable
2033 * @param gridsterDebounce
2035 .directive('gridsterItem', ['$parse', 'GridsterDraggable', 'GridsterResizable', 'gridsterDebounce',
2036 function($parse, GridsterDraggable, GridsterResizable, gridsterDebounce) {
2040 controller: 'GridsterItemCtrl',
2041 controllerAs: 'gridsterItem',
2042 require: ['^gridster', 'gridsterItem'],
2043 link: function(scope, $el, attrs, controllers) {
2044 var optionsKey = attrs.gridsterItem,
2047 var gridster = controllers[0],
2048 item = controllers[1];
2050 scope.gridster = gridster;
2053 // bind the item's position properties
2054 // options can be an object specified by gridster-item="object"
2055 // or the options can be the element html attributes object
2057 var $optionsGetter = $parse(optionsKey);
2058 options = $optionsGetter(scope) || {};
2059 if (!options && $optionsGetter.assign) {
2070 $optionsGetter.assign(scope, options);
2076 item.init($el, gridster);
2078 $el.addClass('gridster-item');
2080 var aspects = ['minSizeX', 'maxSizeX', 'minSizeY', 'maxSizeY', 'sizeX', 'sizeY', 'row', 'col'],
2083 var expressions = [];
2084 var aspectFn = function(aspect) {
2086 if (typeof options[aspect] === 'string') {
2087 // watch the expression in the scope
2088 expression = options[aspect];
2089 } else if (typeof options[aspect.toLowerCase()] === 'string') {
2090 // watch the expression in the scope
2091 expression = options[aspect.toLowerCase()];
2092 } else if (optionsKey) {
2093 // watch the expression on the options object in the scope
2094 expression = optionsKey + '.' + aspect;
2098 expressions.push('"' + aspect + '":' + expression);
2099 $getters[aspect] = $parse(expression);
2102 var val = $getters[aspect](scope);
2103 if (typeof val === 'number') {
2108 for (var i = 0, l = aspects.length; i < l; ++i) {
2109 aspectFn(aspects[i]);
2112 var watchExpressions = '{' + expressions.join(',') + '}';
2114 // when the value changes externally, update the internal item object
2115 scope.$watchCollection(watchExpressions, function(newVals, oldVals) {
2116 for (var aspect in newVals) {
2117 var newVal = newVals[aspect];
2118 var oldVal = oldVals[aspect];
2119 if (oldVal === newVal) {
2122 newVal = parseInt(newVal, 10);
2123 if (!isNaN(newVal)) {
2124 item[aspect] = newVal;
2129 function positionChanged() {
2130 // call setPosition so the element and gridster controller are updated
2131 item.setPosition(item.row, item.col);
2133 // when internal item position changes, update externally bound values
2134 if ($getters.row && $getters.row.assign) {
2135 $getters.row.assign(scope, item.row);
2137 if ($getters.col && $getters.col.assign) {
2138 $getters.col.assign(scope, item.col);
2141 scope.$watch(function() {
2142 return item.row + ',' + item.col;
2143 }, positionChanged);
2145 function sizeChanged() {
2146 var changedX = item.setSizeX(item.sizeX, true);
2147 if (changedX && $getters.sizeX && $getters.sizeX.assign) {
2148 $getters.sizeX.assign(scope, item.sizeX);
2150 var changedY = item.setSizeY(item.sizeY, true);
2151 if (changedY && $getters.sizeY && $getters.sizeY.assign) {
2152 $getters.sizeY.assign(scope, item.sizeY);
2155 if (changedX || changedY) {
2156 item.gridster.moveOverlappingItems(item);
2157 gridster.layoutChanged();
2158 scope.$broadcast('gridster-item-resized', item);
2162 scope.$watch(function() {
2163 return item.sizeY + ',' + item.sizeX + ',' + item.minSizeX + ',' + item.maxSizeX + ',' + item.minSizeY + ',' + item.maxSizeY;
2166 var draggable = new GridsterDraggable($el, scope, gridster, item, options);
2167 var resizable = new GridsterResizable($el, scope, gridster, item, options);
2169 var updateResizable = function() {
2170 resizable.toggle(!gridster.isMobile && gridster.resizable && gridster.resizable.enabled);
2174 var updateDraggable = function() {
2175 draggable.toggle(!gridster.isMobile && gridster.draggable && gridster.draggable.enabled);
2179 scope.$on('gridster-draggable-changed', updateDraggable);
2180 scope.$on('gridster-resizable-changed', updateResizable);
2181 scope.$on('gridster-resized', updateResizable);
2182 scope.$on('gridster-mobile-changed', function() {
2187 function whichTransitionEvent() {
2188 var el = document.createElement('div');
2190 'transition': 'transitionend',
2191 'OTransition': 'oTransitionEnd',
2192 'MozTransition': 'transitionend',
2193 'WebkitTransition': 'webkitTransitionEnd'
2195 for (var t in transitions) {
2196 if (el.style[t] !== undefined) {
2197 return transitions[t];
2202 var debouncedTransitionEndPublisher = gridsterDebounce(function() {
2203 scope.$apply(function() {
2204 scope.$broadcast('gridster-item-transition-end', item);
2208 if(whichTransitionEvent()){ //check for IE8, as it evaluates to null
2209 $el.on(whichTransitionEvent(), debouncedTransitionEndPublisher);
2212 scope.$broadcast('gridster-item-initialized', item);
2214 return scope.$on('$destroy', function() {
2216 resizable.destroy();
2217 draggable.destroy();
2221 gridster.removeItem(item);
2233 .directive('gridsterNoDrag', function() {
2236 link: function(scope, $element) {
2237 $element.addClass('gridster-no-drag');