2 (function(root, factory) {
6 if (typeof define === 'function' && define.amd) {
8 define(['../angular/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 or a -1
24 // (indicating the use of another browser).
26 var rv = -1; // Return value assumes failure.
27 if (navigator.appName == 'Microsoft Internet Explorer')
29 var ua = navigator.userAgent;
30 var re = new RegExp("MSIE ([0-9]{1,}[\.0-9]{0,})");
31 if (re.exec(ua) != null)
32 rv = parseFloat(RegExp.$1);
37 var ver = getInternetExplorerVersion();
39 if ( ver > -1 && ver===8.0){
43 // This returned angular module 'gridster' is what is exported.
44 return angular.module('attGridsterLib', [])
46 .constant('gridsterConfig', {
47 columns: 6, // number of columns in the grid
48 pushing: true, // whether to push other items out of the way
49 floating: true, // whether to automatically float items up so they stack
50 swapping: true, // whether or not to have items switch places instead of push down if they are the same size
51 width: 'auto', // width of the grid. "auto" will expand the grid to its parent container
52 colWidth: 'auto', // width of grid columns. "auto" will divide the width of the grid evenly among the columns
53 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.
54 margins: [10, 10], // margins in between grid items
56 isMobile: false, // toggle mobile view
57 mobileBreakPoint: 100, // width threshold to toggle mobile mode
58 mobileModeEnabled: true, // whether or not to toggle mobile mode when screen width is less than mobileBreakPoint
59 minColumns: 1, // minimum amount of columns the grid can scale down to
60 minRows: 1, // minimum amount of rows to show if the grid is empty
61 maxRows: 100, // maximum amount of rows in the grid
62 defaultSizeX: 1, // default width of an item in columns
63 defaultSizeY: 1, // default height of an item in rows
64 minSizeX: 1, // minimum column width of an item
65 maxSizeX: null, // maximum column width of an item
66 minSizeY: 1, // minumum row height of an item
67 maxSizeY: null, // maximum row height of an item
68 saveGridItemCalculatedHeightInMobile: false, // grid item height in mobile display. true- to use the calculated height by sizeY given
69 resizable: { // options to pass to resizable handler
71 handles: ['s', 'e', 'n', 'w', 'se', 'ne', 'sw', 'nw']
73 draggable: { // options to pass to draggable handler
75 scrollSensitivity: 20, // Distance in pixels from the edge of the viewport after which the viewport should scroll, relative to pointer
76 scrollSpeed: 15 // Speed at which the window should scroll once the mouse pointer gets within scrollSensitivity distance
80 .controller('GridsterCtrl', ['gridsterConfig', '$timeout',
81 function(gridsterConfig, $timeout) {
86 * Create options from gridsterConfig constant
88 angular.extend(this, gridsterConfig);
90 this.resizable = angular.extend({}, gridsterConfig.resizable || {});
91 this.draggable = angular.extend({}, gridsterConfig.draggable || {});
94 this.layoutChanged = function() {
101 if (gridster.loaded) {
102 gridster.floatItemsUp();
104 gridster.updateHeight(gridster.movingItem ? gridster.movingItem.sizeY : 0);
109 * A positional array of the items in the grid
114 * Clean up after yourself
116 this.destroy = function() {
117 // empty the grid to cut back on the possibility
118 // of circular references
122 this.$element = null;
126 * Overrides default options
128 * @param {Object} options The options to override
130 this.setOptions = function(options) {
135 options = angular.extend({}, options);
137 // all this to avoid using jQuery...
138 if (options.draggable) {
139 angular.extend(this.draggable, options.draggable);
140 delete(options.draggable);
142 if (options.resizable) {
143 angular.extend(this.resizable, options.resizable);
144 delete(options.resizable);
147 angular.extend(this, options);
149 if (!this.margins || this.margins.length !== 2) {
150 this.margins = [0, 0];
152 for (var x = 0, l = this.margins.length; x < l; ++x) {
153 this.margins[x] = parseInt(this.margins[x], 10);
154 if (isNaN(this.margins[x])) {
162 * Check if item can occupy a specified position in the grid
164 * @param {Object} item The item in question
165 * @param {Number} row The row index
166 * @param {Number} column The column index
167 * @returns {Boolean} True if if item fits
169 this.canItemOccupy = function(item, row, column) {
170 return row > -1 && column > -1 && item.sizeX + column <= this.columns && item.sizeY + row <= this.maxRows;
174 * Set the item in the first suitable position
176 * @param {Object} item The item to insert
178 this.autoSetItemPosition = function(item) {
179 // walk through each row and column looking for a place it will fit
180 for (var rowIndex = 0; rowIndex < this.maxRows; ++rowIndex) {
181 for (var colIndex = 0; colIndex < this.columns; ++colIndex) {
182 // only insert if position is not already taken and it can fit
183 var items = this.getItems(rowIndex, colIndex, item.sizeX, item.sizeY, item);
184 if (items.length === 0 && this.canItemOccupy(item, rowIndex, colIndex)) {
185 this.putItem(item, rowIndex, colIndex);
190 throw new Error('Unable to place item!');
194 * Gets items at a specific coordinate
196 * @param {Number} row
197 * @param {Number} column
198 * @param {Number} sizeX
199 * @param {Number} sizeY
200 * @param {Array} excludeItems An array of items to exclude from selection
201 * @returns {Array} Items that match the criteria
203 this.getItems = function(row, column, sizeX, sizeY, excludeItems) {
205 if (!sizeX || !sizeY) {
208 if (excludeItems && !(excludeItems instanceof Array)) {
209 excludeItems = [excludeItems];
211 for (var h = 0; h < sizeY; ++h) {
212 for (var w = 0; w < sizeX; ++w) {
213 var item = this.getItem(row + h, column + w, excludeItems);
214 if (item && (!excludeItems || excludeItems.indexOf(item) === -1) && items.indexOf(item) === -1) {
223 * @param {Array} items
224 * @returns {Object} An item that represents the bounding box of the items
226 this.getBoundingBox = function(items) {
228 if (items.length === 0) {
231 if (items.length === 1) {
235 sizeY: items[0].sizeY,
236 sizeX: items[0].sizeX
245 for (var i = 0, l = items.length; i < l; ++i) {
247 minRow = Math.min(item.row, minRow);
248 minCol = Math.min(item.col, minCol);
249 maxRow = Math.max(item.row + item.sizeY, maxRow);
250 maxCol = Math.max(item.col + item.sizeX, maxCol);
256 sizeY: maxRow - minRow,
257 sizeX: maxCol - minCol
263 * Removes an item from the grid
265 * @param {Object} item
267 this.removeItem = function(item) {
268 for (var rowIndex = 0, l = this.grid.length; rowIndex < l; ++rowIndex) {
269 var columns = this.grid[rowIndex];
273 var index = columns.indexOf(item);
275 columns[index] = null;
279 this.layoutChanged();
283 * Returns the item at a specified coordinate
285 * @param {Number} row
286 * @param {Number} column
287 * @param {Array} excludeItems Items to exclude from selection
288 * @returns {Object} The matched item or null
290 this.getItem = function(row, column, excludeItems) {
291 if (excludeItems && !(excludeItems instanceof Array)) {
292 excludeItems = [excludeItems];
299 var items = this.grid[row];
301 var item = items[col];
302 if (item && (!excludeItems || excludeItems.indexOf(item) === -1) && item.sizeX >= sizeX && item.sizeY >= sizeY) {
316 * Insert an array of items into the grid
318 * @param {Array} items An array of items to insert
320 this.putItems = function(items) {
321 for (var i = 0, l = items.length; i < l; ++i) {
322 this.putItem(items[i]);
327 * Insert a single item into the grid
329 * @param {Object} item The item to insert
330 * @param {Number} row (Optional) Specifies the items row index
331 * @param {Number} column (Optional) Specifies the items column index
332 * @param {Array} ignoreItems
334 this.putItem = function(item, row, column, ignoreItems) {
335 // auto place item if no row specified
336 if (typeof row === 'undefined' || row === null) {
339 if (typeof row === 'undefined' || row === null) {
340 this.autoSetItemPosition(item);
345 // keep item within allowed bounds
346 if (!this.canItemOccupy(item, row, column)) {
347 column = Math.min(this.columns - item.sizeX, Math.max(0, column));
348 row = Math.min(this.maxRows - item.sizeY, Math.max(0, row));
351 // check if item is already in grid
352 if (item.oldRow !== null && typeof item.oldRow !== 'undefined') {
353 var samePosition = item.oldRow === row && item.oldColumn === column;
354 var inGrid = this.grid[row] && this.grid[row][column] === item;
355 if (samePosition && inGrid) {
360 // remove from old position
361 var oldRow = this.grid[item.oldRow];
362 if (oldRow && oldRow[item.oldColumn] === item) {
363 delete oldRow[item.oldColumn];
368 item.oldRow = item.row = row;
369 item.oldColumn = item.col = column;
371 this.moveOverlappingItems(item, ignoreItems);
373 if (!this.grid[row]) {
376 this.grid[row][column] = item;
378 if (this.movingItem === item) {
379 this.floatItemUp(item);
381 this.layoutChanged();
385 * Trade row and column if item1 with item2
387 * @param {Object} item1
388 * @param {Object} item2
390 this.swapItems = function(item1, item2) {
391 this.grid[item1.row][item1.col] = item2;
392 this.grid[item2.row][item2.col] = item1;
394 var item1Row = item1.row;
395 var item1Col = item1.col;
396 item1.row = item2.row;
397 item1.col = item2.col;
398 item2.row = item1Row;
399 item2.col = item1Col;
403 * Prevents items from being overlapped
405 * @param {Object} item The item that should remain
406 * @param {Array} ignoreItems
408 this.moveOverlappingItems = function(item, ignoreItems) {
409 // don't move item, so ignore it
411 ignoreItems = [item];
412 } else if (ignoreItems.indexOf(item) === -1) {
413 ignoreItems = ignoreItems.slice(0);
414 ignoreItems.push(item);
417 // get the items in the space occupied by the item's coordinates
418 var overlappingItems = this.getItems(
425 this.moveItemsDown(overlappingItems, item.row + item.sizeY, ignoreItems);
429 * Moves an array of items to a specified row
431 * @param {Array} items The items to move
432 * @param {Number} newRow The target row
433 * @param {Array} ignoreItems
435 this.moveItemsDown = function(items, newRow, ignoreItems) {
436 if (!items || items.length === 0) {
439 items.sort(function(a, b) {
440 return a.row - b.row;
443 ignoreItems = ignoreItems ? ignoreItems.slice(0) : [];
447 // calculate the top rows in each column
448 for (i = 0, l = items.length; i < l; ++i) {
450 var topRow = topRows[item.col];
451 if (typeof topRow === 'undefined' || item.row < topRow) {
452 topRows[item.col] = item.row;
456 // move each item down from the top row in its column to the row
457 for (i = 0, l = items.length; i < l; ++i) {
459 var rowsToMove = newRow - topRows[item.col];
460 this.moveItemDown(item, item.row + rowsToMove, ignoreItems);
461 ignoreItems.push(item);
466 * Moves an item down to a specified row
468 * @param {Object} item The item to move
469 * @param {Number} newRow The target row
470 * @param {Array} ignoreItems
472 this.moveItemDown = function(item, newRow, ignoreItems) {
473 if (item.row >= newRow) {
476 while (item.row < newRow) {
478 this.moveOverlappingItems(item, ignoreItems);
480 this.putItem(item, item.row, item.col, ignoreItems);
484 * Moves all items up as much as possible
486 this.floatItemsUp = function() {
487 if (this.floating === false) {
490 for (var rowIndex = 0, l = this.grid.length; rowIndex < l; ++rowIndex) {
491 var columns = this.grid[rowIndex];
495 for (var colIndex = 0, len = columns.length; colIndex < len; ++colIndex) {
496 var item = columns[colIndex];
498 this.floatItemUp(item);
505 * Float an item up to the most suitable row
507 * @param {Object} item The item to move
509 this.floatItemUp = function(item) {
510 if (this.floating === false) {
513 var colIndex = item.col,
518 rowIndex = item.row - 1;
520 while (rowIndex > -1) {
521 var items = this.getItems(rowIndex, colIndex, sizeX, sizeY, item);
522 if (items.length !== 0) {
526 bestColumn = colIndex;
529 if (bestRow !== null) {
530 this.putItem(item, bestRow, bestColumn);
535 * Update gridsters height
537 * @param {Number} plus (Optional) Additional height to add
539 this.updateHeight = function(plus) {
540 var maxHeight = this.minRows;
542 for (var rowIndex = this.grid.length; rowIndex >= 0; --rowIndex) {
543 var columns = this.grid[rowIndex];
547 for (var colIndex = 0, len = columns.length; colIndex < len; ++colIndex) {
548 if (columns[colIndex]) {
549 maxHeight = Math.max(maxHeight, rowIndex + plus + columns[colIndex].sizeY);
553 this.gridHeight = this.maxRows - maxHeight > 0 ? Math.min(this.maxRows, maxHeight) : Math.max(this.maxRows, maxHeight);
557 * Returns the number of rows that will fit in given amount of pixels
559 * @param {Number} pixels
560 * @param {Boolean} ceilOrFloor (Optional) Determines rounding method
562 this.pixelsToRows = function(pixels, ceilOrFloor) {
563 if (ceilOrFloor === true) {
564 return Math.ceil(pixels / this.curRowHeight);
565 } else if (ceilOrFloor === false) {
566 return Math.floor(pixels / this.curRowHeight);
569 return Math.round(pixels / this.curRowHeight);
573 * Returns the number of columns that will fit in a given amount of pixels
575 * @param {Number} pixels
576 * @param {Boolean} ceilOrFloor (Optional) Determines rounding method
577 * @returns {Number} The number of columns
579 this.pixelsToColumns = function(pixels, ceilOrFloor) {
580 if (ceilOrFloor === true) {
581 return Math.ceil(pixels / this.curColWidth);
582 } else if (ceilOrFloor === false) {
583 return Math.floor(pixels / this.curColWidth);
586 return Math.round(pixels / this.curColWidth);
591 .directive('gridsterPreview', function() {
595 require: '^gridster',
596 template: '<div ng-style="previewStyle()" class="gridster-item gridster-preview-holder"></div>',
597 link: function(scope, $el, attrs, gridster) {
600 * @returns {Object} style object for preview element
602 scope.previewStyle = function() {
604 if (!gridster.movingItem) {
612 height: (gridster.movingItem.sizeY * gridster.curRowHeight - gridster.margins[0]) + 'px',
613 width: (gridster.movingItem.sizeX * gridster.curColWidth - gridster.margins[1]) + 'px',
614 top: (gridster.movingItem.row * gridster.curRowHeight + (gridster.outerMargin ? gridster.margins[0] : 0)) + 'px',
615 left: (gridster.movingItem.col * gridster.curColWidth + (gridster.outerMargin ? gridster.margins[1] : 0)) + 'px'
623 * The gridster directive
625 * @param {Function} $timeout
626 * @param {Object} $window
627 * @param {Object} $rootScope
628 * @param {Function} gridsterDebounce
630 .directive('gridster', ['$timeout', '$window', '$rootScope', 'gridsterDebounce',
631 function($timeout, $window, $rootScope, gridsterDebounce) {
635 controller: 'GridsterCtrl',
636 controllerAs: 'gridster',
637 compile: function($tplElem) {
639 $tplElem.prepend('<div ng-if="gridster.movingItem" gridster-preview></div>');
641 return function(scope, $elem, attrs, gridster) {
642 gridster.loaded = false;
644 gridster.$element = $elem;
646 scope.gridster = gridster;
648 $elem.addClass('gridster');
650 var isVisible = function(ele) {
651 return ele.style.visibility !== 'hidden' && ele.style.display !== 'none';
654 function refresh(config) {
655 gridster.setOptions(config);
657 if (!isVisible($elem[0])) {
661 // resolve "auto" & "match" values
662 if (gridster.width === 'auto') {
663 gridster.curWidth = $elem[0].offsetWidth || parseInt($elem.css('width'), 10);
665 gridster.curWidth = gridster.width;
668 if (gridster.colWidth === 'auto') {
669 gridster.curColWidth = (gridster.curWidth + (gridster.outerMargin ? -gridster.margins[1] : gridster.margins[1])) / gridster.columns;
671 gridster.curColWidth = gridster.colWidth;
674 gridster.curRowHeight = gridster.rowHeight;
675 if (typeof gridster.rowHeight === 'string') {
676 if (gridster.rowHeight === 'match') {
677 gridster.curRowHeight = Math.round(gridster.curColWidth);
678 } else if (gridster.rowHeight.indexOf('*') !== -1) {
679 gridster.curRowHeight = Math.round(gridster.curColWidth * gridster.rowHeight.replace('*', '').replace(' ', ''));
680 } else if (gridster.rowHeight.indexOf('/') !== -1) {
681 gridster.curRowHeight = Math.round(gridster.curColWidth / gridster.rowHeight.replace('/', '').replace(' ', ''));
685 gridster.isMobile = gridster.mobileModeEnabled && gridster.curWidth <= gridster.mobileBreakPoint;
687 // loop through all items and reset their CSS
688 for (var rowIndex = 0, l = gridster.grid.length; rowIndex < l; ++rowIndex) {
689 var columns = gridster.grid[rowIndex];
694 for (var colIndex = 0, len = columns.length; colIndex < len; ++colIndex) {
695 if (columns[colIndex]) {
696 var item = columns[colIndex];
697 item.setElementPosition();
698 item.setElementSizeY();
699 item.setElementSizeX();
707 var optionsKey = attrs.gridster;
709 scope.$parent.$watch(optionsKey, function(newConfig) {
716 scope.$watch(function() {
717 return gridster.loaded;
719 if (gridster.loaded) {
720 $elem.addClass('gridster-loaded');
722 $elem.removeClass('gridster-loaded');
726 scope.$watch(function() {
727 return gridster.isMobile;
729 if (gridster.isMobile) {
730 $elem.addClass('gridster-mobile').removeClass('gridster-desktop');
732 $elem.removeClass('gridster-mobile').addClass('gridster-desktop');
734 $rootScope.$broadcast('gridster-mobile-changed', gridster);
737 scope.$watch(function() {
738 return gridster.draggable;
740 $rootScope.$broadcast('gridster-draggable-changed', gridster);
743 scope.$watch(function() {
744 return gridster.resizable;
746 $rootScope.$broadcast('gridster-resizable-changed', gridster);
749 function updateHeight() {
750 if(gridster.gridHeight){ //need to put this check, otherwise fail in IE8
751 $elem.css('height', (gridster.gridHeight * gridster.curRowHeight) + (gridster.outerMargin ? gridster.margins[0] : -gridster.margins[0]) + 'px');
755 scope.$watch(function() {
756 return gridster.gridHeight;
759 scope.$watch(function() {
760 return gridster.movingItem;
762 gridster.updateHeight(gridster.movingItem ? gridster.movingItem.sizeY : 0);
765 var prevWidth = $elem[0].offsetWidth || parseInt($elem.css('width'), 10);
767 var resize = function() {
768 var width = $elem[0].offsetWidth || parseInt($elem.css('width'), 10);
770 if (!width || width === prevWidth || gridster.movingItem) {
775 if (gridster.loaded) {
776 $elem.removeClass('gridster-loaded');
781 if (gridster.loaded) {
782 $elem.addClass('gridster-loaded');
785 $rootScope.$broadcast('gridster-resized', [width, $elem[0].offsetHeight], gridster);
788 // track element width changes any way we can
789 var onResize = gridsterDebounce(function onResize() {
791 $timeout(function() {
796 scope.$watch(function() {
797 return isVisible($elem[0]);
800 // see https://github.com/sdecima/javascript-detect-element-resize
801 if (typeof window.addResizeListener === 'function') {
802 window.addResizeListener($elem[0], onResize);
804 scope.$watch(function() {
805 return $elem[0].offsetWidth || parseInt($elem.css('width'), 10);
808 var $win = angular.element($window);
809 $win.on('resize', onResize);
811 // be sure to cleanup
812 scope.$on('$destroy', function() {
814 $win.off('resize', onResize);
815 if (typeof window.removeResizeListener === 'function') {
816 window.removeResizeListener($elem[0], onResize);
820 // allow a little time to place items before floating up
821 $timeout(function() {
822 scope.$watch('gridster.floating', function() {
823 gridster.floatItemsUp();
825 gridster.loaded = true;
833 .controller('GridsterItemCtrl', function() {
834 this.$element = null;
835 this.gridster = null;
842 this.maxSizeX = null;
843 this.maxSizeY = null;
845 this.init = function($element, gridster) {
846 this.$element = $element;
847 this.gridster = gridster;
848 this.sizeX = gridster.defaultSizeX;
849 this.sizeY = gridster.defaultSizeY;
852 this.destroy = function() {
853 // set these to null to avoid the possibility of circular references
854 this.gridster = null;
855 this.$element = null;
859 * Returns the items most important attributes
861 this.toJSON = function() {
870 this.isMoving = function() {
871 return this.gridster.movingItem === this;
875 * Set the items position
877 * @param {Number} row
878 * @param {Number} column
880 this.setPosition = function(row, column) {
881 this.gridster.putItem(this, row, column);
883 if (!this.isMoving()) {
884 this.setElementPosition();
889 * Sets a specified size property
891 * @param {String} key Can be either "x" or "y"
892 * @param {Number} value The size amount
893 * @param {Boolean} preventMove
895 this.setSize = function(key, value, preventMove) {
896 key = key.toUpperCase();
897 var camelCase = 'size' + key,
898 titleCase = 'Size' + key;
902 value = parseInt(value, 10);
903 if (isNaN(value) || value === 0) {
904 value = this.gridster['default' + titleCase];
906 var max = key === 'X' ? this.gridster.columns : this.gridster.maxRows;
907 if (this['max' + titleCase]) {
908 max = Math.min(this['max' + titleCase], max);
910 if (this.gridster['max' + titleCase]) {
911 max = Math.min(this.gridster['max' + titleCase], max);
913 if (key === 'X' && this.cols) {
915 } else if (key === 'Y' && this.rows) {
920 if (this['min' + titleCase]) {
921 min = Math.max(this['min' + titleCase], min);
923 if (this.gridster['min' + titleCase]) {
924 min = Math.max(this.gridster['min' + titleCase], min);
927 value = Math.max(Math.min(value, max), min);
929 var changed = (this[camelCase] !== value || (this['old' + titleCase] && this['old' + titleCase] !== value));
930 this['old' + titleCase] = this[camelCase] = value;
932 if (!this.isMoving()) {
933 this['setElement' + titleCase]();
935 if (!preventMove && changed) {
936 this.gridster.moveOverlappingItems(this);
937 this.gridster.layoutChanged();
944 * Sets the items sizeY property
946 * @param {Number} rows
947 * @param {Boolean} preventMove
949 this.setSizeY = function(rows, preventMove) {
950 return this.setSize('Y', rows, preventMove);
954 * Sets the items sizeX property
956 * @param {Number} columns
957 * @param {Boolean} preventMove
959 this.setSizeX = function(columns, preventMove) {
960 return this.setSize('X', columns, preventMove);
964 * Sets an elements position on the page
966 this.setElementPosition = function() {
967 if (this.gridster.isMobile) {
969 marginLeft: this.gridster.margins[0] + 'px',
970 marginRight: this.gridster.margins[0] + 'px',
971 marginTop: this.gridster.margins[1] + 'px',
972 marginBottom: this.gridster.margins[1] + 'px',
979 top: (this.row * this.gridster.curRowHeight + (this.gridster.outerMargin ? this.gridster.margins[0] : 0)) + 'px',
980 left: (this.col * this.gridster.curColWidth + (this.gridster.outerMargin ? this.gridster.margins[1] : 0)) + 'px'
986 * Sets an elements height
988 this.setElementSizeY = function() {
989 if (this.gridster.isMobile && !this.gridster.saveGridItemCalculatedHeightInMobile) {
990 this.$element.css('height', '');
992 this.$element.css('height', (this.sizeY * this.gridster.curRowHeight - this.gridster.margins[0]) + 'px');
997 * Sets an elements width
999 this.setElementSizeX = function() {
1000 if (this.gridster.isMobile) {
1001 this.$element.css('width', '');
1003 this.$element.css('width', (this.sizeX * this.gridster.curColWidth - this.gridster.margins[1]) + 'px');
1008 * Gets an element's width
1010 this.getElementSizeX = function() {
1011 return (this.sizeX * this.gridster.curColWidth - this.gridster.margins[1]);
1015 * Gets an element's height
1017 this.getElementSizeY = function() {
1018 return (this.sizeY * this.gridster.curRowHeight - this.gridster.margins[0]);
1023 .factory('GridsterTouch', [function() {
1024 return function GridsterTouch(target, startEvent, moveEvent, endEvent) {
1025 var lastXYById = {};
1027 // Opera doesn't have Object.keys so we use this wrapper
1028 var numberOfKeys = function(theObject) {
1030 return Object.keys(theObject).length;
1035 for (key in theObject) {
1042 // 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
1043 var computeDocumentToElementDelta = function(theElement) {
1044 var elementLeft = 0;
1046 var oldIEUserAgent = navigator.userAgent.match(/\bMSIE\b/);
1048 for (var offsetElement = theElement; offsetElement != null; offsetElement = offsetElement.offsetParent) {
1049 // 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
1050 // this may not be a general solution to IE7's problem with offsetLeft/offsetParent
1051 if (oldIEUserAgent &&
1052 (!document.documentMode || document.documentMode < 8) &&
1053 offsetElement.currentStyle.position === 'relative' && offsetElement.offsetParent && offsetElement.offsetParent.currentStyle.position === 'relative' && offsetElement.offsetLeft === offsetElement.offsetParent.offsetLeft) {
1055 elementTop += offsetElement.offsetTop;
1057 elementLeft += offsetElement.offsetLeft;
1058 elementTop += offsetElement.offsetTop;
1068 // cache the delta from the document to our event target (reinitialized each mousedown/MSPointerDown/touchstart)
1069 var documentToTargetDelta = computeDocumentToElementDelta(target);
1071 // common event handler for the mouse/pointer/touch models and their down/start, move, up/end, and cancel events
1072 var doEvent = function(theEvtObj) {
1074 if (theEvtObj.type === 'mousemove' && numberOfKeys(lastXYById) === 0) {
1080 var pointerList = theEvtObj.changedTouches ? theEvtObj.changedTouches : [theEvtObj];
1082 for (var i = 0; i < pointerList.length; ++i) {
1083 var pointerObj = pointerList[i];
1084 var pointerId = (typeof pointerObj.identifier !== 'undefined') ? pointerObj.identifier : (typeof pointerObj.pointerId !== 'undefined') ? pointerObj.pointerId : 1;
1086 // 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)
1087 if (typeof pointerObj.pageX === 'undefined') {
1089 // initialize assuming our source element is our target
1091 pointerObj.pageX = pointerObj.offsetX + documentToTargetDelta.x;
1092 pointerObj.pageY = pointerObj.offsetY + documentToTargetDelta.y;
1095 pointerObj.pageX = pointerObj.clientX;
1096 pointerObj.pageY = pointerObj.clientY;
1099 if (pointerObj.srcElement.offsetParent === target && document.documentMode && document.documentMode === 8 && pointerObj.type === 'mousedown') {
1100 // 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
1101 pointerObj.pageX += pointerObj.srcElement.offsetLeft;
1102 pointerObj.pageY += pointerObj.srcElement.offsetTop;
1103 } else if (pointerObj.srcElement !== target && !document.documentMode || document.documentMode < 8) {
1104 // 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 -
1105 // the offsetX/Y values are unpredictable so use the clientX/Y values and adjust by the scroll offsets of its parents
1106 // to get the document-relative coordinates (the same as pageX/Y)
1108 sy = -2; // adjust for old IE's 2-pixel border
1109 for (var scrollElement = pointerObj.srcElement; scrollElement !== null; scrollElement = scrollElement.parentNode) {
1110 sx += scrollElement.scrollLeft ? scrollElement.scrollLeft : 0;
1111 sy += scrollElement.scrollTop ? scrollElement.scrollTop : 0;
1114 pointerObj.pageX = pointerObj.clientX + sx;
1115 pointerObj.pageY = pointerObj.clientY + sy;
1120 var pageX = pointerObj.pageX;
1121 var pageY = pointerObj.pageY;
1123 if (theEvtObj.type.match(/(start|down)$/i)) {
1124 // clause for processing MSPointerDown, touchstart, and mousedown
1126 // refresh the document-to-target delta on start in case the target has moved relative to document
1127 documentToTargetDelta = computeDocumentToElementDelta(target);
1129 // protect against failing to get an up or end on this pointerId
1130 if (lastXYById[pointerId]) {
1133 target: theEvtObj.target,
1134 which: theEvtObj.which,
1135 pointerId: pointerId,
1141 delete lastXYById[pointerId];
1146 prevent = startEvent({
1147 target: theEvtObj.target,
1148 which: theEvtObj.which,
1149 pointerId: pointerId,
1156 // init last page positions for this pointer
1157 lastXYById[pointerId] = {
1163 if (target.msSetPointerCapture) {
1164 target.msSetPointerCapture(pointerId);
1165 } else if (theEvtObj.type === 'mousedown' && numberOfKeys(lastXYById) === 1) {
1166 if (useSetReleaseCapture) {
1167 target.setCapture(true);
1169 document.addEventListener('mousemove', doEvent, false);
1170 document.addEventListener('mouseup', doEvent, false);
1173 } else if (theEvtObj.type.match(/move$/i)) {
1174 // clause handles mousemove, MSPointerMove, and touchmove
1176 if (lastXYById[pointerId] && !(lastXYById[pointerId].x === pageX && lastXYById[pointerId].y === pageY)) {
1177 // only extend if the pointer is down and it's not the same as the last point
1179 if (moveEvent && prevent) {
1180 prevent = moveEvent({
1181 target: theEvtObj.target,
1182 which: theEvtObj.which,
1183 pointerId: pointerId,
1189 // update last page positions for this pointer
1190 lastXYById[pointerId].x = pageX;
1191 lastXYById[pointerId].y = pageY;
1193 } else if (lastXYById[pointerId] && theEvtObj.type.match(/(up|end|cancel)$/i)) {
1194 // clause handles up/end/cancel
1196 if (endEvent && prevent) {
1197 prevent = endEvent({
1198 target: theEvtObj.target,
1199 which: theEvtObj.which,
1200 pointerId: pointerId,
1206 // delete last page positions for this pointer
1207 delete lastXYById[pointerId];
1209 // in the Microsoft pointer model, release the capture for this pointer
1210 // in the mouse model, release the capture or remove document-level event handlers if there are no down points
1211 // nothing is required for the iOS touch model because capture is implied on touchstart
1212 if (target.msReleasePointerCapture) {
1213 target.msReleasePointerCapture(pointerId);
1214 } else if (theEvtObj.type === 'mouseup' && numberOfKeys(lastXYById) === 0) {
1215 if (useSetReleaseCapture) {
1216 target.releaseCapture();
1218 document.removeEventListener('mousemove', doEvent, false);
1219 document.removeEventListener('mouseup', doEvent, false);
1226 if (theEvtObj.preventDefault) {
1227 theEvtObj.preventDefault();
1230 if (theEvtObj.preventManipulation) {
1231 theEvtObj.preventManipulation();
1234 if (theEvtObj.preventMouseEvent) {
1235 theEvtObj.preventMouseEvent();
1240 var useSetReleaseCapture = false;
1241 // saving the settings for contentZooming and touchaction before activation
1242 var contentZooming, msTouchAction;
1244 this.enable = function() {
1246 if (window.navigator.msPointerEnabled) {
1247 // Microsoft pointer model
1248 target.addEventListener('MSPointerDown', doEvent, false);
1249 target.addEventListener('MSPointerMove', doEvent, false);
1250 target.addEventListener('MSPointerUp', doEvent, false);
1251 target.addEventListener('MSPointerCancel', doEvent, false);
1253 // css way to prevent panning in our target area
1254 if (typeof target.style.msContentZooming !== 'undefined') {
1255 contentZooming = target.style.msContentZooming;
1256 target.style.msContentZooming = 'none';
1259 // new in Windows Consumer Preview: css way to prevent all built-in touch actions on our target
1260 // without this, you cannot touch draw on the element because IE will intercept the touch events
1261 if (typeof target.style.msTouchAction !== 'undefined') {
1262 msTouchAction = target.style.msTouchAction;
1263 target.style.msTouchAction = 'none';
1265 } else if (target.addEventListener) {
1267 target.addEventListener('touchstart', doEvent, false);
1268 target.addEventListener('touchmove', doEvent, false);
1269 target.addEventListener('touchend', doEvent, false);
1270 target.addEventListener('touchcancel', doEvent, false);
1273 target.addEventListener('mousedown', doEvent, false);
1275 // mouse model with capture
1276 // rejecting gecko because, unlike ie, firefox does not send events to target when the mouse is outside target
1277 if (target.setCapture && !window.navigator.userAgent.match(/\bGecko\b/)) {
1278 useSetReleaseCapture = true;
1280 target.addEventListener('mousemove', doEvent, false);
1281 target.addEventListener('mouseup', doEvent, false);
1283 } else if (target.attachEvent && target.setCapture) {
1284 // legacy IE mode - mouse with capture
1285 useSetReleaseCapture = true;
1286 target.attachEvent('onmousedown', function() {
1287 doEvent(window.event);
1288 window.event.returnValue = false;
1291 target.attachEvent('onmousemove', function() {
1292 doEvent(window.event);
1293 window.event.returnValue = false;
1296 target.attachEvent('onmouseup', function() {
1297 doEvent(window.event);
1298 window.event.returnValue = false;
1304 this.disable = function() {
1305 if (window.navigator.msPointerEnabled) {
1306 // Microsoft pointer model
1307 target.removeEventListener('MSPointerDown', doEvent, false);
1308 target.removeEventListener('MSPointerMove', doEvent, false);
1309 target.removeEventListener('MSPointerUp', doEvent, false);
1310 target.removeEventListener('MSPointerCancel', doEvent, false);
1312 // reset zooming to saved value
1313 if (contentZooming) {
1314 target.style.msContentZooming = contentZooming;
1317 // reset touch action setting
1318 if (msTouchAction) {
1319 target.style.msTouchAction = msTouchAction;
1321 } else if (target.removeEventListener) {
1323 target.removeEventListener('touchstart', doEvent, false);
1324 target.removeEventListener('touchmove', doEvent, false);
1325 target.removeEventListener('touchend', doEvent, false);
1326 target.removeEventListener('touchcancel', doEvent, false);
1329 target.removeEventListener('mousedown', doEvent, false);
1331 // mouse model with capture
1332 // rejecting gecko because, unlike ie, firefox does not send events to target when the mouse is outside target
1333 if (target.setCapture && !window.navigator.userAgent.match(/\bGecko\b/)) {
1334 useSetReleaseCapture = true;
1336 target.removeEventListener('mousemove', doEvent, false);
1337 target.removeEventListener('mouseup', doEvent, false);
1339 } else if (target.detachEvent && target.setCapture) {
1340 // legacy IE mode - mouse with capture
1341 useSetReleaseCapture = true;
1342 target.detachEvent('onmousedown');
1343 target.detachEvent('onmousemove');
1344 target.detachEvent('onmouseup');
1352 .factory('GridsterDraggable', ['$document', '$timeout', '$window', 'GridsterTouch',
1353 function($document, $timeout, $window, GridsterTouch) {
1354 function GridsterDraggable($el, scope, gridster, item, itemOptions) {
1356 var elmX, elmY, elmW, elmH,
1368 realdocument = $document[0];
1370 var originalCol, originalRow;
1371 var inputTags = ['select', 'input', 'textarea', 'button'];
1373 var gridsterItemDragElement = $el[0].querySelector('[gridster-item-drag]');
1374 //console.log(gridsterItemDragElement);
1375 var isDraggableAreaDefined = gridsterItemDragElement?true:false;
1376 //console.log(isDraggableAreaDefined);
1378 function mouseDown(e) {
1381 e.target = window.event.srcElement;
1382 e.which = window.event.button;
1385 if(isDraggableAreaDefined && (!gridsterItemDragElement.contains(e.target))){
1389 if (inputTags.indexOf(e.target.nodeName.toLowerCase()) !== -1) {
1393 var $target = angular.element(e.target);
1395 // exit, if a resize handle was hit
1396 if ($target.hasClass('gridster-item-resizable-handler')) {
1400 // exit, if the target has it's own click event
1401 if ($target.attr('onclick') || $target.attr('ng-click')) {
1405 // only works if you have jQuery
1406 if ($target.closest && $target.closest('.gridster-no-drag').length) {
1412 // left mouse button
1416 // right or middle mouse button
1420 lastMouseX = e.pageX;
1421 lastMouseY = e.pageY;
1423 elmX = parseInt($el.css('left'), 10);
1424 elmY = parseInt($el.css('top'), 10);
1425 elmW = $el[0].offsetWidth;
1426 elmH = $el[0].offsetHeight;
1428 originalCol = item.col;
1429 originalRow = item.row;
1436 function mouseMove(e) {
1437 if (!$el.hasClass('gridster-item-moving') || $el.hasClass('gridster-item-resizing')) {
1441 var maxLeft = gridster.curWidth - 1;
1443 // Get the current mouse position.
1448 var diffX = mouseX - lastMouseX + mOffX;
1449 var diffY = mouseY - lastMouseY + mOffY;
1452 // Update last processed mouse positions.
1453 lastMouseX = mouseX;
1454 lastMouseY = mouseY;
1458 if (elmX + dX < minLeft) {
1459 diffX = minLeft - elmX;
1461 } else if (elmX + elmW + dX > maxLeft) {
1462 diffX = maxLeft - elmX - elmW;
1466 if (elmY + dY < minTop) {
1467 diffY = minTop - elmY;
1469 } else if (elmY + elmH + dY > maxTop) {
1470 diffY = maxTop - elmY - elmH;
1487 function mouseUp(e) {
1488 if (!$el.hasClass('gridster-item-moving') || $el.hasClass('gridster-item-resizing')) {
1499 function dragStart(event) {
1500 $el.addClass('gridster-item-moving');
1501 gridster.movingItem = item;
1503 gridster.updateHeight(item.sizeY);
1504 scope.$apply(function() {
1505 if (gridster.draggable && gridster.draggable.start) {
1506 gridster.draggable.start(event, $el, itemOptions);
1511 function drag(event) {
1512 var oldRow = item.row,
1514 hasCallback = gridster.draggable && gridster.draggable.drag,
1515 scrollSensitivity = gridster.draggable.scrollSensitivity,
1516 scrollSpeed = gridster.draggable.scrollSpeed;
1518 var row = gridster.pixelsToRows(elmY);
1519 var col = gridster.pixelsToColumns(elmX);
1521 var itemsInTheWay = gridster.getItems(row, col, item.sizeX, item.sizeY, item);
1522 var hasItemsInTheWay = itemsInTheWay.length !== 0;
1524 if (gridster.swapping === true && hasItemsInTheWay) {
1525 var boundingBoxItem = gridster.getBoundingBox(itemsInTheWay),
1526 sameSize = boundingBoxItem.sizeX === item.sizeX && boundingBoxItem.sizeY === item.sizeY,
1527 sameRow = boundingBoxItem.row === oldRow,
1528 sameCol = boundingBoxItem.col === oldCol,
1529 samePosition = boundingBoxItem.row === row && boundingBoxItem.col === col,
1530 inline = sameRow || sameCol;
1532 if (sameSize && itemsInTheWay.length === 1) {
1534 gridster.swapItems(item, itemsInTheWay[0]);
1535 } else if (inline) {
1538 } else if (boundingBoxItem.sizeX <= item.sizeX && boundingBoxItem.sizeY <= item.sizeY && inline) {
1539 var emptyRow = item.row <= row ? item.row : row + item.sizeY,
1540 emptyCol = item.col <= col ? item.col : col + item.sizeX,
1541 rowOffset = emptyRow - boundingBoxItem.row,
1542 colOffset = emptyCol - boundingBoxItem.col;
1544 for (var i = 0, l = itemsInTheWay.length; i < l; ++i) {
1545 var itemInTheWay = itemsInTheWay[i];
1547 var itemsInFreeSpace = gridster.getItems(
1548 itemInTheWay.row + rowOffset,
1549 itemInTheWay.col + colOffset,
1555 if (itemsInFreeSpace.length === 0) {
1556 gridster.putItem(itemInTheWay, itemInTheWay.row + rowOffset, itemInTheWay.col + colOffset);
1562 if (gridster.pushing !== false || !hasItemsInTheWay) {
1567 if(($window.navigator.appName === 'Microsoft Internet Explorer' && !ie8) || $window.navigator.userAgent.indexOf("Firefox")!==-1){
1568 if (event.pageY - realdocument.documentElement.scrollTop < scrollSensitivity) {
1569 realdocument.documentElement.scrollTop = realdocument.documentElement.scrollTop - scrollSpeed;
1570 } else if ($window.innerHeight - (event.pageY - realdocument.documentElement.scrollTop) < scrollSensitivity) {
1571 realdocument.documentElement.scrollTop = realdocument.documentElement.scrollTop + scrollSpeed;
1575 if (event.pageY - realdocument.body.scrollTop < scrollSensitivity) {
1576 realdocument.body.scrollTop = realdocument.body.scrollTop - scrollSpeed;
1577 } else if ($window.innerHeight - (event.pageY - realdocument.body.scrollTop) < scrollSensitivity) {
1578 realdocument.body.scrollTop = realdocument.body.scrollTop + scrollSpeed;
1584 if (event.pageX - realdocument.body.scrollLeft < scrollSensitivity) {
1585 realdocument.body.scrollLeft = realdocument.body.scrollLeft - scrollSpeed;
1586 } else if ($window.innerWidth - (event.pageX - realdocument.body.scrollLeft) < scrollSensitivity) {
1587 realdocument.body.scrollLeft = realdocument.body.scrollLeft + scrollSpeed;
1590 if (hasCallback || oldRow !== item.row || oldCol !== item.col) {
1591 scope.$apply(function() {
1593 gridster.draggable.drag(event, $el, itemOptions);
1599 function dragStop(event) {
1600 $el.removeClass('gridster-item-moving');
1601 var row = gridster.pixelsToRows(elmY);
1602 var col = gridster.pixelsToColumns(elmX);
1603 if (gridster.pushing !== false || gridster.getItems(row, col, item.sizeX, item.sizeY, item).length === 0) {
1607 gridster.movingItem = null;
1608 item.setPosition(item.row, item.col);
1610 scope.$apply(function() {
1611 if (gridster.draggable && gridster.draggable.stop) {
1612 gridster.draggable.stop(event, $el, itemOptions);
1618 var $dragHandles = null;
1619 var unifiedInputs = [];
1621 this.enable = function() {
1622 if (enabled === true) {
1626 // disable and timeout required for some template rendering
1627 $timeout(function() {
1628 // disable any existing draghandles
1629 for (var u = 0, ul = unifiedInputs.length; u < ul; ++u) {
1630 unifiedInputs[u].disable();
1634 if (gridster.draggable && gridster.draggable.handle) {
1635 $dragHandles = angular.element($el[0].querySelectorAll(gridster.draggable.handle));
1636 if ($dragHandles.length === 0) {
1637 // fall back to element if handle not found...
1644 for (var h = 0, hl = $dragHandles.length; h < hl; ++h) {
1645 unifiedInputs[h] = new GridsterTouch($dragHandles[h], mouseDown, mouseMove, mouseUp);
1646 unifiedInputs[h].enable();
1653 this.disable = function() {
1654 if (enabled === false) {
1658 // timeout to avoid race contition with the enable timeout
1659 $timeout(function() {
1661 for (var u = 0, ul = unifiedInputs.length; u < ul; ++u) {
1662 unifiedInputs[u].disable();
1670 this.toggle = function(enabled) {
1678 this.destroy = function() {
1683 return GridsterDraggable;
1687 .factory('GridsterResizable', ['GridsterTouch', function(GridsterTouch) {
1688 function GridsterResizable($el, scope, gridster, item, itemOptions) {
1690 function ResizeHandle(handleClass) {
1692 var hClass = handleClass;
1694 var elmX, elmY, elmW, elmH,
1707 var getMinHeight = function() {
1708 return (item.minSizeY ? item.minSizeY : 1) * gridster.curRowHeight - gridster.margins[0];
1710 var getMinWidth = function() {
1711 return (item.minSizeX ? item.minSizeX : 1) * gridster.curColWidth - gridster.margins[1];
1714 var originalWidth, originalHeight;
1717 function mouseDown(e) {
1720 // left mouse button
1724 // right or middle mouse button
1728 // save the draggable setting to restore after resize
1729 savedDraggable = gridster.draggable.enabled;
1730 if (savedDraggable) {
1731 gridster.draggable.enabled = false;
1732 scope.$broadcast('gridster-draggable-changed', gridster);
1735 // Get the current mouse position.
1736 lastMouseX = e.pageX;
1737 lastMouseY = e.pageY;
1739 // Record current widget dimensions
1740 elmX = parseInt($el.css('left'), 10);
1741 elmY = parseInt($el.css('top'), 10);
1742 elmW = $el[0].offsetWidth;
1743 elmH = $el[0].offsetHeight;
1745 originalWidth = item.sizeX;
1746 originalHeight = item.sizeY;
1753 function resizeStart(e) {
1754 $el.addClass('gridster-item-moving');
1755 $el.addClass('gridster-item-resizing');
1757 gridster.movingItem = item;
1759 item.setElementSizeX();
1760 item.setElementSizeY();
1761 item.setElementPosition();
1762 gridster.updateHeight(1);
1764 scope.$apply(function() {
1766 if (gridster.resizable && gridster.resizable.start) {
1767 gridster.resizable.start(e, $el, itemOptions); // options is the item model
1772 function mouseMove(e) {
1773 var maxLeft = gridster.curWidth - 1;
1775 // Get the current mouse position.
1780 var diffX = mouseX - lastMouseX + mOffX;
1781 var diffY = mouseY - lastMouseY + mOffY;
1784 // Update last processed mouse positions.
1785 lastMouseX = mouseX;
1786 lastMouseY = mouseY;
1791 if (hClass.indexOf('n') >= 0) {
1792 if (elmH - dY < getMinHeight()) {
1793 diffY = elmH - getMinHeight();
1795 } else if (elmY + dY < minTop) {
1796 diffY = minTop - elmY;
1802 if (hClass.indexOf('s') >= 0) {
1803 if (elmH + dY < getMinHeight()) {
1804 diffY = getMinHeight() - elmH;
1806 } else if (elmY + elmH + dY > maxTop) {
1807 diffY = maxTop - elmY - elmH;
1812 if (hClass.indexOf('w') >= 0) {
1813 if (elmW - dX < getMinWidth()) {
1814 diffX = elmW - getMinWidth();
1816 } else if (elmX + dX < minLeft) {
1817 diffX = minLeft - elmX;
1823 if (hClass.indexOf('e') >= 0) {
1824 if (elmW + dX < getMinWidth()) {
1825 diffX = getMinWidth() - elmW;
1827 } else if (elmX + elmW + dX > maxLeft) {
1828 diffX = maxLeft - elmX - elmW;
1837 'left': elmX + 'px',
1838 'width': elmW + 'px',
1839 'height': elmH + 'px'
1847 function mouseUp(e) {
1848 // restore draggable setting to its original state
1849 if (gridster.draggable.enabled !== savedDraggable) {
1850 gridster.draggable.enabled = savedDraggable;
1851 scope.$broadcast('gridster-draggable-changed', gridster);
1861 function resize(e) {
1862 var oldRow = item.row,
1864 oldSizeX = item.sizeX,
1865 oldSizeY = item.sizeY,
1866 hasCallback = gridster.resizable && gridster.resizable.resize;
1869 // only change column if grabbing left edge
1870 if (['w', 'nw', 'sw'].indexOf(handleClass) !== -1) {
1871 col = gridster.pixelsToColumns(elmX, false);
1875 // only change row if grabbing top edge
1876 if (['n', 'ne', 'nw'].indexOf(handleClass) !== -1) {
1877 row = gridster.pixelsToRows(elmY, false);
1880 var sizeX = item.sizeX;
1881 // only change row if grabbing left or right edge
1882 if (['n', 's'].indexOf(handleClass) === -1) {
1883 sizeX = gridster.pixelsToColumns(elmW, true);
1886 var sizeY = item.sizeY;
1887 // only change row if grabbing top or bottom edge
1888 if (['e', 'w'].indexOf(handleClass) === -1) {
1889 sizeY = gridster.pixelsToRows(elmH, true);
1892 if (gridster.pushing !== false || gridster.getItems(row, col, sizeX, sizeY, item).length === 0) {
1898 var isChanged = item.row !== oldRow || item.col !== oldCol || item.sizeX !== oldSizeX || item.sizeY !== oldSizeY;
1900 if (hasCallback || isChanged) {
1901 scope.$apply(function() {
1903 gridster.resizable.resize(e, $el, itemOptions); // options is the item model
1909 function resizeStop(e) {
1910 $el.removeClass('gridster-item-moving');
1911 $el.removeClass('gridster-item-resizing');
1913 gridster.movingItem = null;
1915 item.setPosition(item.row, item.col);
1916 item.setSizeY(item.sizeY);
1917 item.setSizeX(item.sizeX);
1919 scope.$apply(function() {
1920 if (gridster.resizable && gridster.resizable.stop) {
1921 gridster.resizable.stop(e, $el, itemOptions); // options is the item model
1926 var $dragHandle = null;
1929 this.enable = function() {
1931 $dragHandle = angular.element('<div class="gridster-item-resizable-handler handle-' + hClass + '"></div>');
1932 $el.append($dragHandle);
1935 unifiedInput = new GridsterTouch($dragHandle[0], mouseDown, mouseMove, mouseUp);
1936 unifiedInput.enable();
1939 this.disable = function() {
1941 $dragHandle.remove();
1945 unifiedInput.disable();
1946 unifiedInput = undefined;
1949 this.destroy = function() {
1955 var handlesOpts = gridster.resizable.handles;
1956 if (typeof handlesOpts === 'string') {
1957 handlesOpts = gridster.resizable.handles.split(',');
1959 var enabled = false;
1961 for (var c = 0, l = handlesOpts.length; c < l; c++) {
1962 handles.push(new ResizeHandle(handlesOpts[c]));
1965 this.enable = function() {
1969 for (var c = 0, l = handles.length; c < l; c++) {
1970 handles[c].enable();
1975 this.disable = function() {
1979 for (var c = 0, l = handles.length; c < l; c++) {
1980 handles[c].disable();
1985 this.toggle = function(enabled) {
1993 this.destroy = function() {
1994 for (var c = 0, l = handles.length; c < l; c++) {
1995 handles[c].destroy();
1999 return GridsterResizable;
2002 .factory('gridsterDebounce', function() {
2003 return function gridsterDebounce(func, wait, immediate) {
2008 var later = function() {
2011 func.apply(context, args);
2014 var callNow = immediate && !timeout;
2015 clearTimeout(timeout);
2016 timeout = setTimeout(later, wait);
2018 func.apply(context, args);
2025 * GridsterItem directive
2027 * @param GridsterDraggable
2028 * @param GridsterResizable
2029 * @param gridsterDebounce
2031 .directive('gridsterItem', ['$parse', 'GridsterDraggable', 'GridsterResizable', 'gridsterDebounce',
2032 function($parse, GridsterDraggable, GridsterResizable, gridsterDebounce) {
2036 controller: 'GridsterItemCtrl',
2037 controllerAs: 'gridsterItem',
2038 require: ['^gridster', 'gridsterItem'],
2039 link: function(scope, $el, attrs, controllers) {
2040 var optionsKey = attrs.gridsterItem,
2043 var gridster = controllers[0],
2044 item = controllers[1];
2046 scope.gridster = gridster;
2049 // bind the item's position properties
2050 // options can be an object specified by gridster-item="object"
2051 // or the options can be the element html attributes object
2053 var $optionsGetter = $parse(optionsKey);
2054 options = $optionsGetter(scope) || {};
2055 if (!options && $optionsGetter.assign) {
2066 $optionsGetter.assign(scope, options);
2072 item.init($el, gridster);
2074 $el.addClass('gridster-item');
2076 var aspects = ['minSizeX', 'maxSizeX', 'minSizeY', 'maxSizeY', 'sizeX', 'sizeY', 'row', 'col'],
2079 var expressions = [];
2080 var aspectFn = function(aspect) {
2082 if (typeof options[aspect] === 'string') {
2083 // watch the expression in the scope
2084 expression = options[aspect];
2085 } else if (typeof options[aspect.toLowerCase()] === 'string') {
2086 // watch the expression in the scope
2087 expression = options[aspect.toLowerCase()];
2088 } else if (optionsKey) {
2089 // watch the expression on the options object in the scope
2090 expression = optionsKey + '.' + aspect;
2094 expressions.push('"' + aspect + '":' + expression);
2095 $getters[aspect] = $parse(expression);
2098 var val = $getters[aspect](scope);
2099 if (typeof val === 'number') {
2104 for (var i = 0, l = aspects.length; i < l; ++i) {
2105 aspectFn(aspects[i]);
2108 var watchExpressions = '{' + expressions.join(',') + '}';
2110 // when the value changes externally, update the internal item object
2111 scope.$watchCollection(watchExpressions, function(newVals, oldVals) {
2112 for (var aspect in newVals) {
2113 var newVal = newVals[aspect];
2114 var oldVal = oldVals[aspect];
2115 if (oldVal === newVal) {
2118 newVal = parseInt(newVal, 10);
2119 if (!isNaN(newVal)) {
2120 item[aspect] = newVal;
2125 function positionChanged() {
2126 // call setPosition so the element and gridster controller are updated
2127 item.setPosition(item.row, item.col);
2129 // when internal item position changes, update externally bound values
2130 if ($getters.row && $getters.row.assign) {
2131 $getters.row.assign(scope, item.row);
2133 if ($getters.col && $getters.col.assign) {
2134 $getters.col.assign(scope, item.col);
2137 scope.$watch(function() {
2138 return item.row + ',' + item.col;
2139 }, positionChanged);
2141 function sizeChanged() {
2142 var changedX = item.setSizeX(item.sizeX, true);
2143 if (changedX && $getters.sizeX && $getters.sizeX.assign) {
2144 $getters.sizeX.assign(scope, item.sizeX);
2146 var changedY = item.setSizeY(item.sizeY, true);
2147 if (changedY && $getters.sizeY && $getters.sizeY.assign) {
2148 $getters.sizeY.assign(scope, item.sizeY);
2151 if (changedX || changedY) {
2152 item.gridster.moveOverlappingItems(item);
2153 gridster.layoutChanged();
2154 scope.$broadcast('gridster-item-resized', item);
2158 scope.$watch(function() {
2159 return item.sizeY + ',' + item.sizeX + ',' + item.minSizeX + ',' + item.maxSizeX + ',' + item.minSizeY + ',' + item.maxSizeY;
2162 var draggable = new GridsterDraggable($el, scope, gridster, item, options);
2163 var resizable = new GridsterResizable($el, scope, gridster, item, options);
2165 var updateResizable = function() {
2166 resizable.toggle(!gridster.isMobile && gridster.resizable && gridster.resizable.enabled);
2170 var updateDraggable = function() {
2171 draggable.toggle(!gridster.isMobile && gridster.draggable && gridster.draggable.enabled);
2175 scope.$on('gridster-draggable-changed', updateDraggable);
2176 scope.$on('gridster-resizable-changed', updateResizable);
2177 scope.$on('gridster-resized', updateResizable);
2178 scope.$on('gridster-mobile-changed', function() {
2183 function whichTransitionEvent() {
2184 var el = document.createElement('div');
2186 'transition': 'transitionend',
2187 'OTransition': 'oTransitionEnd',
2188 'MozTransition': 'transitionend',
2189 'WebkitTransition': 'webkitTransitionEnd'
2191 for (var t in transitions) {
2192 if (el.style[t] !== undefined) {
2193 return transitions[t];
2198 var debouncedTransitionEndPublisher = gridsterDebounce(function() {
2199 scope.$apply(function() {
2200 scope.$broadcast('gridster-item-transition-end', item);
2204 if(whichTransitionEvent()){ //check for IE8, as it evaluates to null
2205 $el.on(whichTransitionEvent(), debouncedTransitionEndPublisher);
2208 scope.$broadcast('gridster-item-initialized', item);
2210 return scope.$on('$destroy', function() {
2212 resizable.destroy();
2213 draggable.destroy();
2217 gridster.removeItem(item);
2229 .directive('gridsterNoDrag', function() {
2232 link: function(scope, $element) {
2233 $element.addClass('gridster-no-drag');