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) {
20 // This returned angular module 'gridster' is what is exported.
21 return angular.module('gridster', [])
23 .constant('gridsterConfig', {
24 columns: 6, // number of columns in the grid
25 pushing: true, // whether to push other items out of the way
26 floating: true, // whether to automatically float items up so they stack
27 swapping: false, // whether or not to have items switch places instead of push down if they are the same size
28 width: 'auto', // width of the grid. "auto" will expand the grid to its parent container
29 colWidth: 'auto', // width of grid columns. "auto" will divide the width of the grid evenly among the columns
30 rowHeight: 'match', // height of grid rows. 'match' will make it the same as the column width, a numeric value will be interpreted as pixels, '/2' is half the column width, '*5' is five times the column width, etc.
31 margins: [10, 10], // margins in between grid items
33 sparse: false, // "true" can increase performance of dragging and resizing for big grid (e.g. 20x50)
34 isMobile: false, // toggle mobile view
35 mobileBreakPoint: 600, // width threshold to toggle mobile mode
36 mobileModeEnabled: true, // whether or not to toggle mobile mode when screen width is less than mobileBreakPoint
37 minColumns: 1, // minimum amount of columns the grid can scale down to
38 minRows: 1, // minimum amount of rows to show if the grid is empty
39 maxRows: 100, // maximum amount of rows in the grid
40 defaultSizeX: 2, // default width of an item in columns
41 defaultSizeY: 1, // default height of an item in rows
42 minSizeX: 1, // minimum column width of an item
43 maxSizeX: null, // maximum column width of an item
44 minSizeY: 1, // minumum row height of an item
45 maxSizeY: null, // maximum row height of an item
46 saveGridItemCalculatedHeightInMobile: false, // grid item height in mobile display. true- to use the calculated height by sizeY given
47 resizable: { // options to pass to resizable handler
49 handles: ['s', 'e', 'n', 'w', 'se', 'ne', 'sw', 'nw']
51 draggable: { // options to pass to draggable handler
53 scrollSensitivity: 20, // Distance in pixels from the edge of the viewport after which the viewport should scroll, relative to pointer
54 scrollSpeed: 15 // Speed at which the window should scroll once the mouse pointer gets within scrollSensitivity distance
58 .controller('GridsterCtrl', ['gridsterConfig', '$timeout',
59 function(gridsterConfig, $timeout) {
64 * Create options from gridsterConfig constant
66 angular.extend(this, gridsterConfig);
68 this.resizable = angular.extend({}, gridsterConfig.resizable || {});
69 this.draggable = angular.extend({}, gridsterConfig.draggable || {});
72 this.layoutChanged = function() {
79 if (gridster.loaded) {
80 gridster.floatItemsUp();
82 gridster.updateHeight(gridster.movingItem ? gridster.movingItem.sizeY : 0);
87 * A positional array of the items in the grid
93 * Clean up after yourself
95 this.destroy = function() {
96 // empty the grid to cut back on the possibility
97 // of circular references
101 this.$element = null;
104 this.allItems.length = 0;
105 this.allItems = null;
110 * Overrides default options
112 * @param {Object} options The options to override
114 this.setOptions = function(options) {
119 options = angular.extend({}, options);
121 // all this to avoid using jQuery...
122 if (options.draggable) {
123 angular.extend(this.draggable, options.draggable);
124 delete(options.draggable);
126 if (options.resizable) {
127 angular.extend(this.resizable, options.resizable);
128 delete(options.resizable);
131 angular.extend(this, options);
133 if (!this.margins || this.margins.length !== 2) {
134 this.margins = [0, 0];
136 for (var x = 0, l = this.margins.length; x < l; ++x) {
137 this.margins[x] = parseInt(this.margins[x], 10);
138 if (isNaN(this.margins[x])) {
146 * Check if item can occupy a specified position in the grid
148 * @param {Object} item The item in question
149 * @param {Number} row The row index
150 * @param {Number} column The column index
151 * @returns {Boolean} True if if item fits
153 this.canItemOccupy = function(item, row, column) {
154 return row > -1 && column > -1 && item.sizeX + column <= this.columns && item.sizeY + row <= this.maxRows;
158 * Set the item in the first suitable position
160 * @param {Object} item The item to insert
162 this.autoSetItemPosition = function(item) {
163 // walk through each row and column looking for a place it will fit
164 for (var rowIndex = 0; rowIndex < this.maxRows; ++rowIndex) {
165 for (var colIndex = 0; colIndex < this.columns; ++colIndex) {
166 // only insert if position is not already taken and it can fit
167 var items = this.getItems(rowIndex, colIndex, item.sizeX, item.sizeY, item);
168 if (items.length === 0 && this.canItemOccupy(item, rowIndex, colIndex)) {
169 this.putItem(item, rowIndex, colIndex);
174 throw new Error('Unable to place item!');
178 * Gets items at a specific coordinate
180 * @param {Number} row
181 * @param {Number} column
182 * @param {Number} sizeX
183 * @param {Number} sizeY
184 * @param {Array} excludeItems An array of items to exclude from selection
185 * @returns {Array} Items that match the criteria
187 this.getItems = function(row, column, sizeX, sizeY, excludeItems) {
189 if (!sizeX || !sizeY) {
192 if (excludeItems && !(excludeItems instanceof Array)) {
193 excludeItems = [excludeItems];
196 if (this.sparse === false) { // check all cells
197 for (var h = 0; h < sizeY; ++h) {
198 for (var w = 0; w < sizeX; ++w) {
199 item = this.getItem(row + h, column + w, excludeItems);
200 if (item && (!excludeItems || excludeItems.indexOf(item) === -1) && items.indexOf(item) === -1) {
205 } else { // check intersection with all items
206 var bottom = row + sizeY - 1;
207 var right = column + sizeX - 1;
208 for (var i = 0; i < this.allItems.length; ++i) {
209 item = this.allItems[i];
210 if (item && (!excludeItems || excludeItems.indexOf(item) === -1) && items.indexOf(item) === -1 && this.intersect(item, column, right, row, bottom)) {
219 * @param {Array} items
220 * @returns {Object} An item that represents the bounding box of the items
222 this.getBoundingBox = function(items) {
224 if (items.length === 0) {
227 if (items.length === 1) {
231 sizeY: items[0].sizeY,
232 sizeX: items[0].sizeX
241 for (var i = 0, l = items.length; i < l; ++i) {
243 minRow = Math.min(item.row, minRow);
244 minCol = Math.min(item.col, minCol);
245 maxRow = Math.max(item.row + item.sizeY, maxRow);
246 maxCol = Math.max(item.col + item.sizeX, maxCol);
252 sizeY: maxRow - minRow,
253 sizeX: maxCol - minCol
258 * Checks if item intersects specified box
260 * @param {object} item
261 * @param {number} left
262 * @param {number} right
263 * @param {number} top
264 * @param {number} bottom
267 this.intersect = function(item, left, right, top, bottom) {
268 return (left <= item.col + item.sizeX - 1 &&
270 top <= item.row + item.sizeY - 1 &&
276 * Removes an item from the grid
278 * @param {Object} item
280 this.removeItem = function(item) {
282 for (var rowIndex = 0, l = this.grid.length; rowIndex < l; ++rowIndex) {
283 var columns = this.grid[rowIndex];
287 index = columns.indexOf(item);
289 columns[index] = null;
294 index = this.allItems.indexOf(item);
296 this.allItems.splice(index, 1);
299 this.layoutChanged();
303 * Returns the item at a specified coordinate
305 * @param {Number} row
306 * @param {Number} column
307 * @param {Array} excludeItems Items to exclude from selection
308 * @returns {Object} The matched item or null
310 this.getItem = function(row, column, excludeItems) {
311 if (excludeItems && !(excludeItems instanceof Array)) {
312 excludeItems = [excludeItems];
319 var items = this.grid[row];
321 var item = items[col];
322 if (item && (!excludeItems || excludeItems.indexOf(item) === -1) && item.sizeX >= sizeX && item.sizeY >= sizeY) {
336 * Insert an array of items into the grid
338 * @param {Array} items An array of items to insert
340 this.putItems = function(items) {
341 for (var i = 0, l = items.length; i < l; ++i) {
342 this.putItem(items[i]);
347 * Insert a single item into the grid
349 * @param {Object} item The item to insert
350 * @param {Number} row (Optional) Specifies the items row index
351 * @param {Number} column (Optional) Specifies the items column index
352 * @param {Array} ignoreItems
354 this.putItem = function(item, row, column, ignoreItems) {
355 // auto place item if no row specified
356 if (typeof row === 'undefined' || row === null) {
359 if (typeof row === 'undefined' || row === null) {
360 this.autoSetItemPosition(item);
365 // keep item within allowed bounds
366 if (!this.canItemOccupy(item, row, column)) {
367 column = Math.min(this.columns - item.sizeX, Math.max(0, column));
368 row = Math.min(this.maxRows - item.sizeY, Math.max(0, row));
371 // check if item is already in grid
372 if (item.oldRow !== null && typeof item.oldRow !== 'undefined') {
373 var samePosition = item.oldRow === row && item.oldColumn === column;
374 var inGrid = this.grid[row] && this.grid[row][column] === item;
375 if (samePosition && inGrid) {
380 // remove from old position
381 var oldRow = this.grid[item.oldRow];
382 if (oldRow && oldRow[item.oldColumn] === item) {
383 delete oldRow[item.oldColumn];
388 item.oldRow = item.row = row;
389 item.oldColumn = item.col = column;
391 this.moveOverlappingItems(item, ignoreItems);
393 if (!this.grid[row]) {
396 this.grid[row][column] = item;
398 if (this.sparse && this.allItems.indexOf(item) === -1) {
399 this.allItems.push(item);
402 if (this.movingItem === item) {
403 this.floatItemUp(item);
405 this.layoutChanged();
409 * Trade row and column if item1 with item2
411 * @param {Object} item1
412 * @param {Object} item2
414 this.swapItems = function(item1, item2) {
415 this.grid[item1.row][item1.col] = item2;
416 this.grid[item2.row][item2.col] = item1;
418 var item1Row = item1.row;
419 var item1Col = item1.col;
420 item1.row = item2.row;
421 item1.col = item2.col;
422 item2.row = item1Row;
423 item2.col = item1Col;
427 * Prevents items from being overlapped
429 * @param {Object} item The item that should remain
430 * @param {Array} ignoreItems
432 this.moveOverlappingItems = function(item, ignoreItems) {
433 // don't move item, so ignore it
435 ignoreItems = [item];
436 } else if (ignoreItems.indexOf(item) === -1) {
437 ignoreItems = ignoreItems.slice(0);
438 ignoreItems.push(item);
441 // get the items in the space occupied by the item's coordinates
442 var overlappingItems = this.getItems(
449 this.moveItemsDown(overlappingItems, item.row + item.sizeY, ignoreItems);
453 * Moves an array of items to a specified row
455 * @param {Array} items The items to move
456 * @param {Number} newRow The target row
457 * @param {Array} ignoreItems
459 this.moveItemsDown = function(items, newRow, ignoreItems) {
460 if (!items || items.length === 0) {
463 items.sort(function(a, b) {
464 return a.row - b.row;
467 ignoreItems = ignoreItems ? ignoreItems.slice(0) : [];
471 // calculate the top rows in each column
472 for (i = 0, l = items.length; i < l; ++i) {
474 var topRow = topRows[item.col];
475 if (typeof topRow === 'undefined' || item.row < topRow) {
476 topRows[item.col] = item.row;
480 // move each item down from the top row in its column to the row
481 for (i = 0, l = items.length; i < l; ++i) {
483 var rowsToMove = newRow - topRows[item.col];
484 this.moveItemDown(item, item.row + rowsToMove, ignoreItems);
485 ignoreItems.push(item);
490 * Moves an item down to a specified row
492 * @param {Object} item The item to move
493 * @param {Number} newRow The target row
494 * @param {Array} ignoreItems
496 this.moveItemDown = function(item, newRow, ignoreItems) {
497 if (item.row >= newRow) {
500 while (item.row < newRow) {
502 this.moveOverlappingItems(item, ignoreItems);
504 this.putItem(item, item.row, item.col, ignoreItems);
508 * Moves all items up as much as possible
510 this.floatItemsUp = function() {
511 if (this.floating === false) {
514 for (var rowIndex = 0, l = this.grid.length; rowIndex < l; ++rowIndex) {
515 var columns = this.grid[rowIndex];
519 for (var colIndex = 0, len = columns.length; colIndex < len; ++colIndex) {
520 var item = columns[colIndex];
522 this.floatItemUp(item);
529 * Float an item up to the most suitable row
531 * @param {Object} item The item to move
533 this.floatItemUp = function(item) {
534 if (this.floating === false) {
537 var colIndex = item.col,
542 rowIndex = item.row - 1;
544 while (rowIndex > -1) {
545 var items = this.getItems(rowIndex, colIndex, sizeX, sizeY, item);
546 if (items.length !== 0) {
550 bestColumn = colIndex;
553 if (bestRow !== null) {
554 this.putItem(item, bestRow, bestColumn);
559 * Update gridsters height
561 * @param {Number} plus (Optional) Additional height to add
563 this.updateHeight = function(plus) {
564 var maxHeight = this.minRows;
566 for (var rowIndex = this.grid.length; rowIndex >= 0; --rowIndex) {
567 var columns = this.grid[rowIndex];
571 for (var colIndex = 0, len = columns.length; colIndex < len; ++colIndex) {
572 if (columns[colIndex]) {
573 maxHeight = Math.max(maxHeight, rowIndex + plus + columns[colIndex].sizeY);
577 this.gridHeight = this.maxRows - maxHeight > 0 ? Math.min(this.maxRows, maxHeight) : Math.max(this.maxRows, maxHeight);
581 * Returns the number of rows that will fit in given amount of pixels
583 * @param {Number} pixels
584 * @param {Boolean} ceilOrFloor (Optional) Determines rounding method
586 this.pixelsToRows = function(pixels, ceilOrFloor) {
587 if (!this.outerMargin) {
588 pixels += this.margins[0] / 2;
591 if (ceilOrFloor === true) {
592 return Math.ceil(pixels / this.curRowHeight);
593 } else if (ceilOrFloor === false) {
594 return Math.floor(pixels / this.curRowHeight);
597 return Math.round(pixels / this.curRowHeight);
601 * Returns the number of columns that will fit in a given amount of pixels
603 * @param {Number} pixels
604 * @param {Boolean} ceilOrFloor (Optional) Determines rounding method
605 * @returns {Number} The number of columns
607 this.pixelsToColumns = function(pixels, ceilOrFloor) {
608 if (!this.outerMargin) {
609 pixels += this.margins[1] / 2;
612 if (ceilOrFloor === true) {
613 return Math.ceil(pixels / this.curColWidth);
614 } else if (ceilOrFloor === false) {
615 return Math.floor(pixels / this.curColWidth);
618 return Math.round(pixels / this.curColWidth);
623 .directive('gridsterPreview', function() {
627 require: '^gridster',
628 template: '<div ng-style="previewStyle()" class="gridster-item gridster-preview-holder"></div>',
629 link: function(scope, $el, attrs, gridster) {
632 * @returns {Object} style object for preview element
634 scope.previewStyle = function() {
635 if (!gridster.movingItem) {
643 height: (gridster.movingItem.sizeY * gridster.curRowHeight - gridster.margins[0]) + 'px',
644 width: (gridster.movingItem.sizeX * gridster.curColWidth - gridster.margins[1]) + 'px',
645 top: (gridster.movingItem.row * gridster.curRowHeight + (gridster.outerMargin ? gridster.margins[0] : 0)) + 'px',
646 left: (gridster.movingItem.col * gridster.curColWidth + (gridster.outerMargin ? gridster.margins[1] : 0)) + 'px'
654 * The gridster directive
656 * @param {Function} $timeout
657 * @param {Object} $window
658 * @param {Object} $rootScope
659 * @param {Function} gridsterDebounce
661 .directive('gridster', ['$timeout', '$window', '$rootScope', 'gridsterDebounce',
662 function($timeout, $window, $rootScope, gridsterDebounce) {
666 controller: 'GridsterCtrl',
667 controllerAs: 'gridster',
668 compile: function($tplElem) {
670 $tplElem.prepend('<div ng-if="gridster.movingItem" gridster-preview></div>');
672 return function(scope, $elem, attrs, gridster) {
673 gridster.loaded = false;
675 gridster.$element = $elem;
677 scope.gridster = gridster;
679 $elem.addClass('gridster');
681 var isVisible = function(ele) {
682 return ele.style.visibility !== 'hidden' && ele.style.display !== 'none';
685 function updateHeight() {
686 $elem.css('height', (gridster.gridHeight * gridster.curRowHeight) + (gridster.outerMargin ? gridster.margins[0] : -gridster.margins[0]) + 'px');
689 scope.$watch(function() {
690 return gridster.gridHeight;
693 scope.$watch(function() {
694 return gridster.movingItem;
696 gridster.updateHeight(gridster.movingItem ? gridster.movingItem.sizeY : 0);
699 function refresh(config) {
700 gridster.setOptions(config);
702 if (!isVisible($elem[0])) {
706 // resolve "auto" & "match" values
707 if (gridster.width === 'auto') {
708 gridster.curWidth = $elem[0].offsetWidth || parseInt($elem.css('width'), 10);
710 gridster.curWidth = gridster.width;
713 if (gridster.colWidth === 'auto') {
714 gridster.curColWidth = (gridster.curWidth + (gridster.outerMargin ? -gridster.margins[1] : gridster.margins[1])) / gridster.columns;
716 gridster.curColWidth = gridster.colWidth;
719 gridster.curRowHeight = gridster.rowHeight;
720 if (typeof gridster.rowHeight === 'string') {
721 if (gridster.rowHeight === 'match') {
722 gridster.curRowHeight = Math.round(gridster.curColWidth);
723 } else if (gridster.rowHeight.indexOf('*') !== -1) {
724 gridster.curRowHeight = Math.round(gridster.curColWidth * gridster.rowHeight.replace('*', '').replace(' ', ''));
725 } else if (gridster.rowHeight.indexOf('/') !== -1) {
726 gridster.curRowHeight = Math.round(gridster.curColWidth / gridster.rowHeight.replace('/', '').replace(' ', ''));
730 gridster.isMobile = gridster.mobileModeEnabled && gridster.curWidth <= gridster.mobileBreakPoint;
732 // loop through all items and reset their CSS
733 for (var rowIndex = 0, l = gridster.grid.length; rowIndex < l; ++rowIndex) {
734 var columns = gridster.grid[rowIndex];
739 for (var colIndex = 0, len = columns.length; colIndex < len; ++colIndex) {
740 if (columns[colIndex]) {
741 var item = columns[colIndex];
742 item.setElementPosition();
743 item.setElementSizeY();
744 item.setElementSizeX();
752 var optionsKey = attrs.gridster;
754 scope.$parent.$watch(optionsKey, function(newConfig) {
761 scope.$watch(function() {
762 return gridster.loaded;
764 if (gridster.loaded) {
765 $elem.addClass('gridster-loaded');
766 $rootScope.$broadcast('gridster-loaded', gridster);
768 $elem.removeClass('gridster-loaded');
772 scope.$watch(function() {
773 return gridster.isMobile;
775 if (gridster.isMobile) {
776 $elem.addClass('gridster-mobile').removeClass('gridster-desktop');
778 $elem.removeClass('gridster-mobile').addClass('gridster-desktop');
780 $rootScope.$broadcast('gridster-mobile-changed', gridster);
783 scope.$watch(function() {
784 return gridster.draggable;
786 $rootScope.$broadcast('gridster-draggable-changed', gridster);
789 scope.$watch(function() {
790 return gridster.resizable;
792 $rootScope.$broadcast('gridster-resizable-changed', gridster);
795 var prevWidth = $elem[0].offsetWidth || parseInt($elem.css('width'), 10);
797 var resize = function() {
798 var width = $elem[0].offsetWidth || parseInt($elem.css('width'), 10);
800 if (!width || width === prevWidth || gridster.movingItem) {
805 if (gridster.loaded) {
806 $elem.removeClass('gridster-loaded');
811 if (gridster.loaded) {
812 $elem.addClass('gridster-loaded');
815 $rootScope.$broadcast('gridster-resized', [width, $elem[0].offsetHeight], gridster);
818 // track element width changes any way we can
819 var onResize = gridsterDebounce(function onResize() {
821 $timeout(function() {
826 scope.$watch(function() {
827 return isVisible($elem[0]);
830 // see https://github.com/sdecima/javascript-detect-element-resize
831 if (typeof window.addResizeListener === 'function') {
832 window.addResizeListener($elem[0], onResize);
834 scope.$watch(function() {
835 return $elem[0].offsetWidth || parseInt($elem.css('width'), 10);
838 var $win = angular.element($window);
839 $win.on('resize', onResize);
841 // be sure to cleanup
842 scope.$on('$destroy', function() {
844 $win.off('resize', onResize);
845 if (typeof window.removeResizeListener === 'function') {
846 window.removeResizeListener($elem[0], onResize);
850 // allow a little time to place items before floating up
851 $timeout(function() {
852 scope.$watch('gridster.floating', function() {
853 gridster.floatItemsUp();
855 gridster.loaded = true;
863 .controller('GridsterItemCtrl', function() {
864 this.$element = null;
865 this.gridster = null;
872 this.maxSizeX = null;
873 this.maxSizeY = null;
875 this.init = function($element, gridster) {
876 this.$element = $element;
877 this.gridster = gridster;
878 this.sizeX = gridster.defaultSizeX;
879 this.sizeY = gridster.defaultSizeY;
882 this.destroy = function() {
883 // set these to null to avoid the possibility of circular references
884 this.gridster = null;
885 this.$element = null;
889 * Returns the items most important attributes
891 this.toJSON = function() {
900 this.isMoving = function() {
901 return this.gridster.movingItem === this;
905 * Set the items position
907 * @param {Number} row
908 * @param {Number} column
910 this.setPosition = function(row, column) {
911 this.gridster.putItem(this, row, column);
913 if (!this.isMoving()) {
914 this.setElementPosition();
919 * Sets a specified size property
921 * @param {String} key Can be either "x" or "y"
922 * @param {Number} value The size amount
923 * @param {Boolean} preventMove
925 this.setSize = function(key, value, preventMove) {
926 key = key.toUpperCase();
927 var camelCase = 'size' + key,
928 titleCase = 'Size' + key;
932 value = parseInt(value, 10);
933 if (isNaN(value) || value === 0) {
934 value = this.gridster['default' + titleCase];
936 var max = key === 'X' ? this.gridster.columns : this.gridster.maxRows;
937 if (this['max' + titleCase]) {
938 max = Math.min(this['max' + titleCase], max);
940 if (this.gridster['max' + titleCase]) {
941 max = Math.min(this.gridster['max' + titleCase], max);
943 if (key === 'X' && this.cols) {
945 } else if (key === 'Y' && this.rows) {
950 if (this['min' + titleCase]) {
951 min = Math.max(this['min' + titleCase], min);
953 if (this.gridster['min' + titleCase]) {
954 min = Math.max(this.gridster['min' + titleCase], min);
957 value = Math.max(Math.min(value, max), min);
959 var changed = (this[camelCase] !== value || (this['old' + titleCase] && this['old' + titleCase] !== value));
960 this['old' + titleCase] = this[camelCase] = value;
962 if (!this.isMoving()) {
963 this['setElement' + titleCase]();
965 if (!preventMove && changed) {
966 this.gridster.moveOverlappingItems(this);
967 this.gridster.layoutChanged();
974 * Sets the items sizeY property
976 * @param {Number} rows
977 * @param {Boolean} preventMove
979 this.setSizeY = function(rows, preventMove) {
980 return this.setSize('Y', rows, preventMove);
984 * Sets the items sizeX property
986 * @param {Number} columns
987 * @param {Boolean} preventMove
989 this.setSizeX = function(columns, preventMove) {
990 return this.setSize('X', columns, preventMove);
994 * Sets an elements position on the page
996 this.setElementPosition = function() {
997 if (this.gridster.isMobile) {
999 marginLeft: this.gridster.margins[0] + 'px',
1000 marginRight: this.gridster.margins[0] + 'px',
1001 marginTop: this.gridster.margins[1] + 'px',
1002 marginBottom: this.gridster.margins[1] + 'px',
1009 top: (this.row * this.gridster.curRowHeight + (this.gridster.outerMargin ? this.gridster.margins[0] : 0)) + 'px',
1010 left: (this.col * this.gridster.curColWidth + (this.gridster.outerMargin ? this.gridster.margins[1] : 0)) + 'px'
1016 * Sets an elements height
1018 this.setElementSizeY = function() {
1019 if (this.gridster.isMobile && !this.gridster.saveGridItemCalculatedHeightInMobile) {
1020 this.$element.css('height', '');
1022 this.$element.css('height', (this.sizeY * this.gridster.curRowHeight - this.gridster.margins[0]) + 'px');
1027 * Sets an elements width
1029 this.setElementSizeX = function() {
1030 if (this.gridster.isMobile) {
1031 this.$element.css('width', '');
1033 this.$element.css('width', (this.sizeX * this.gridster.curColWidth - this.gridster.margins[1]) + 'px');
1038 * Gets an element's width
1040 this.getElementSizeX = function() {
1041 return (this.sizeX * this.gridster.curColWidth - this.gridster.margins[1]);
1045 * Gets an element's height
1047 this.getElementSizeY = function() {
1048 return (this.sizeY * this.gridster.curRowHeight - this.gridster.margins[0]);
1053 .factory('GridsterTouch', [function() {
1054 return function GridsterTouch(target, startEvent, moveEvent, endEvent) {
1055 var lastXYById = {};
1057 // Opera doesn't have Object.keys so we use this wrapper
1058 var numberOfKeys = function(theObject) {
1060 return Object.keys(theObject).length;
1065 for (key in theObject) {
1072 // this calculates the delta needed to convert pageX/Y to offsetX/Y because offsetX/Y don't exist in the TouchEvent object or in Firefox's MouseEvent object
1073 var computeDocumentToElementDelta = function(theElement) {
1074 var elementLeft = 0;
1076 var oldIEUserAgent = navigator.userAgent.match(/\bMSIE\b/);
1078 for (var offsetElement = theElement; offsetElement != null; offsetElement = offsetElement.offsetParent) {
1079 // the following is a major hack for versions of IE less than 8 to avoid an apparent problem on the IEBlog with double-counting the offsets
1080 // this may not be a general solution to IE7's problem with offsetLeft/offsetParent
1081 if (oldIEUserAgent &&
1082 (!document.documentMode || document.documentMode < 8) &&
1083 offsetElement.currentStyle.position === 'relative' && offsetElement.offsetParent && offsetElement.offsetParent.currentStyle.position === 'relative' && offsetElement.offsetLeft === offsetElement.offsetParent.offsetLeft) {
1085 elementTop += offsetElement.offsetTop;
1087 elementLeft += offsetElement.offsetLeft;
1088 elementTop += offsetElement.offsetTop;
1098 // cache the delta from the document to our event target (reinitialized each mousedown/MSPointerDown/touchstart)
1099 var documentToTargetDelta = computeDocumentToElementDelta(target);
1100 var useSetReleaseCapture = false;
1102 // common event handler for the mouse/pointer/touch models and their down/start, move, up/end, and cancel events
1103 var doEvent = function(theEvtObj) {
1105 if (theEvtObj.type === 'mousemove' && numberOfKeys(lastXYById) === 0) {
1111 var pointerList = theEvtObj.changedTouches ? theEvtObj.changedTouches : [theEvtObj];
1112 for (var i = 0; i < pointerList.length; ++i) {
1113 var pointerObj = pointerList[i];
1114 var pointerId = (typeof pointerObj.identifier !== 'undefined') ? pointerObj.identifier : (typeof pointerObj.pointerId !== 'undefined') ? pointerObj.pointerId : 1;
1116 // use the pageX/Y coordinates to compute target-relative coordinates when we have them (in ie < 9, we need to do a little work to put them there)
1117 if (typeof pointerObj.pageX === 'undefined') {
1118 // initialize assuming our source element is our target
1119 pointerObj.pageX = pointerObj.offsetX + documentToTargetDelta.x;
1120 pointerObj.pageY = pointerObj.offsetY + documentToTargetDelta.y;
1122 if (pointerObj.srcElement.offsetParent === target && document.documentMode && document.documentMode === 8 && pointerObj.type === 'mousedown') {
1123 // source element is a child piece of VML, we're in IE8, and we've not called setCapture yet - add the origin of the source element
1124 pointerObj.pageX += pointerObj.srcElement.offsetLeft;
1125 pointerObj.pageY += pointerObj.srcElement.offsetTop;
1126 } else if (pointerObj.srcElement !== target && !document.documentMode || document.documentMode < 8) {
1127 // source element isn't the target (most likely it's a child piece of VML) and we're in a version of IE before IE8 -
1128 // the offsetX/Y values are unpredictable so use the clientX/Y values and adjust by the scroll offsets of its parents
1129 // to get the document-relative coordinates (the same as pageX/Y)
1131 sy = -2; // adjust for old IE's 2-pixel border
1132 for (var scrollElement = pointerObj.srcElement; scrollElement !== null; scrollElement = scrollElement.parentNode) {
1133 sx += scrollElement.scrollLeft ? scrollElement.scrollLeft : 0;
1134 sy += scrollElement.scrollTop ? scrollElement.scrollTop : 0;
1137 pointerObj.pageX = pointerObj.clientX + sx;
1138 pointerObj.pageY = pointerObj.clientY + sy;
1143 var pageX = pointerObj.pageX;
1144 var pageY = pointerObj.pageY;
1146 if (theEvtObj.type.match(/(start|down)$/i)) {
1147 // clause for processing MSPointerDown, touchstart, and mousedown
1149 // refresh the document-to-target delta on start in case the target has moved relative to document
1150 documentToTargetDelta = computeDocumentToElementDelta(target);
1152 // protect against failing to get an up or end on this pointerId
1153 if (lastXYById[pointerId]) {
1156 target: theEvtObj.target,
1157 which: theEvtObj.which,
1158 pointerId: pointerId,
1164 delete lastXYById[pointerId];
1169 prevent = startEvent({
1170 target: theEvtObj.target,
1171 which: theEvtObj.which,
1172 pointerId: pointerId,
1179 // init last page positions for this pointer
1180 lastXYById[pointerId] = {
1186 if (target.msSetPointerCapture && prevent) {
1187 target.msSetPointerCapture(pointerId);
1188 } else if (theEvtObj.type === 'mousedown' && numberOfKeys(lastXYById) === 1) {
1189 if (useSetReleaseCapture) {
1190 target.setCapture(true);
1192 document.addEventListener('mousemove', doEvent, false);
1193 document.addEventListener('mouseup', doEvent, false);
1196 } else if (theEvtObj.type.match(/move$/i)) {
1197 // clause handles mousemove, MSPointerMove, and touchmove
1199 if (lastXYById[pointerId] && !(lastXYById[pointerId].x === pageX && lastXYById[pointerId].y === pageY)) {
1200 // only extend if the pointer is down and it's not the same as the last point
1202 if (moveEvent && prevent) {
1203 prevent = moveEvent({
1204 target: theEvtObj.target,
1205 which: theEvtObj.which,
1206 pointerId: pointerId,
1212 // update last page positions for this pointer
1213 lastXYById[pointerId].x = pageX;
1214 lastXYById[pointerId].y = pageY;
1216 } else if (lastXYById[pointerId] && theEvtObj.type.match(/(up|end|cancel)$/i)) {
1217 // clause handles up/end/cancel
1219 if (endEvent && prevent) {
1220 prevent = endEvent({
1221 target: theEvtObj.target,
1222 which: theEvtObj.which,
1223 pointerId: pointerId,
1229 // delete last page positions for this pointer
1230 delete lastXYById[pointerId];
1232 // in the Microsoft pointer model, release the capture for this pointer
1233 // in the mouse model, release the capture or remove document-level event handlers if there are no down points
1234 // nothing is required for the iOS touch model because capture is implied on touchstart
1235 if (target.msReleasePointerCapture) {
1236 target.msReleasePointerCapture(pointerId);
1237 } else if (theEvtObj.type === 'mouseup' && numberOfKeys(lastXYById) === 0) {
1238 if (useSetReleaseCapture) {
1239 target.releaseCapture();
1241 document.removeEventListener('mousemove', doEvent, false);
1242 document.removeEventListener('mouseup', doEvent, false);
1249 if (theEvtObj.preventDefault) {
1250 theEvtObj.preventDefault();
1253 if (theEvtObj.preventManipulation) {
1254 theEvtObj.preventManipulation();
1257 if (theEvtObj.preventMouseEvent) {
1258 theEvtObj.preventMouseEvent();
1263 // saving the settings for contentZooming and touchaction before activation
1264 var contentZooming, msTouchAction;
1266 this.enable = function() {
1268 if (window.navigator.msPointerEnabled) {
1269 // Microsoft pointer model
1270 target.addEventListener('MSPointerDown', doEvent, false);
1271 target.addEventListener('MSPointerMove', doEvent, false);
1272 target.addEventListener('MSPointerUp', doEvent, false);
1273 target.addEventListener('MSPointerCancel', doEvent, false);
1275 // css way to prevent panning in our target area
1276 if (typeof target.style.msContentZooming !== 'undefined') {
1277 contentZooming = target.style.msContentZooming;
1278 target.style.msContentZooming = 'none';
1281 // new in Windows Consumer Preview: css way to prevent all built-in touch actions on our target
1282 // without this, you cannot touch draw on the element because IE will intercept the touch events
1283 if (typeof target.style.msTouchAction !== 'undefined') {
1284 msTouchAction = target.style.msTouchAction;
1285 target.style.msTouchAction = 'none';
1287 } else if (target.addEventListener) {
1289 target.addEventListener('touchstart', doEvent, false);
1290 target.addEventListener('touchmove', doEvent, false);
1291 target.addEventListener('touchend', doEvent, false);
1292 target.addEventListener('touchcancel', doEvent, false);
1295 target.addEventListener('mousedown', doEvent, false);
1297 // mouse model with capture
1298 // rejecting gecko because, unlike ie, firefox does not send events to target when the mouse is outside target
1299 if (target.setCapture && !window.navigator.userAgent.match(/\bGecko\b/)) {
1300 useSetReleaseCapture = true;
1302 target.addEventListener('mousemove', doEvent, false);
1303 target.addEventListener('mouseup', doEvent, false);
1305 } else if (target.attachEvent && target.setCapture) {
1306 // legacy IE mode - mouse with capture
1307 useSetReleaseCapture = true;
1308 target.attachEvent('onmousedown', function() {
1309 doEvent(window.event);
1310 window.event.returnValue = false;
1313 target.attachEvent('onmousemove', function() {
1314 doEvent(window.event);
1315 window.event.returnValue = false;
1318 target.attachEvent('onmouseup', function() {
1319 doEvent(window.event);
1320 window.event.returnValue = false;
1326 this.disable = function() {
1327 if (window.navigator.msPointerEnabled) {
1328 // Microsoft pointer model
1329 target.removeEventListener('MSPointerDown', doEvent, false);
1330 target.removeEventListener('MSPointerMove', doEvent, false);
1331 target.removeEventListener('MSPointerUp', doEvent, false);
1332 target.removeEventListener('MSPointerCancel', doEvent, false);
1334 // reset zooming to saved value
1335 if (contentZooming) {
1336 target.style.msContentZooming = contentZooming;
1339 // reset touch action setting
1340 if (msTouchAction) {
1341 target.style.msTouchAction = msTouchAction;
1343 } else if (target.removeEventListener) {
1345 target.removeEventListener('touchstart', doEvent, false);
1346 target.removeEventListener('touchmove', doEvent, false);
1347 target.removeEventListener('touchend', doEvent, false);
1348 target.removeEventListener('touchcancel', doEvent, false);
1351 target.removeEventListener('mousedown', doEvent, false);
1353 // mouse model with capture
1354 // rejecting gecko because, unlike ie, firefox does not send events to target when the mouse is outside target
1355 if (target.setCapture && !window.navigator.userAgent.match(/\bGecko\b/)) {
1356 useSetReleaseCapture = true;
1358 target.removeEventListener('mousemove', doEvent, false);
1359 target.removeEventListener('mouseup', doEvent, false);
1361 } else if (target.detachEvent && target.setCapture) {
1362 // legacy IE mode - mouse with capture
1363 useSetReleaseCapture = true;
1364 target.detachEvent('onmousedown');
1365 target.detachEvent('onmousemove');
1366 target.detachEvent('onmouseup');
1374 .factory('GridsterDraggable', ['$document', '$window', 'GridsterTouch',
1375 function($document, $window, GridsterTouch) {
1376 function GridsterDraggable($el, scope, gridster, item, itemOptions) {
1378 var elmX, elmY, elmW, elmH,
1389 realdocument = $document[0];
1391 var originalCol, originalRow;
1392 var inputTags = ['select', 'option', 'input', 'textarea', 'button'];
1394 function dragStart(event) {
1395 $el.addClass('gridster-item-moving');
1396 gridster.movingItem = item;
1398 gridster.updateHeight(item.sizeY);
1399 scope.$apply(function() {
1400 if (gridster.draggable && gridster.draggable.start) {
1401 gridster.draggable.start(event, $el, itemOptions, item);
1406 function drag(event) {
1407 var oldRow = item.row,
1409 hasCallback = gridster.draggable && gridster.draggable.drag,
1410 scrollSensitivity = gridster.draggable.scrollSensitivity,
1411 scrollSpeed = gridster.draggable.scrollSpeed;
1413 var row = Math.min(gridster.pixelsToRows(elmY), gridster.maxRows - 1);
1414 var col = Math.min(gridster.pixelsToColumns(elmX), gridster.columns - 1);
1416 var itemsInTheWay = gridster.getItems(row, col, item.sizeX, item.sizeY, item);
1417 var hasItemsInTheWay = itemsInTheWay.length !== 0;
1419 if (gridster.swapping === true && hasItemsInTheWay) {
1420 var boundingBoxItem = gridster.getBoundingBox(itemsInTheWay),
1421 sameSize = boundingBoxItem.sizeX === item.sizeX && boundingBoxItem.sizeY === item.sizeY,
1422 sameRow = boundingBoxItem.row === oldRow,
1423 sameCol = boundingBoxItem.col === oldCol,
1424 samePosition = boundingBoxItem.row === row && boundingBoxItem.col === col,
1425 inline = sameRow || sameCol;
1427 if (sameSize && itemsInTheWay.length === 1) {
1429 gridster.swapItems(item, itemsInTheWay[0]);
1430 } else if (inline) {
1433 } else if (boundingBoxItem.sizeX <= item.sizeX && boundingBoxItem.sizeY <= item.sizeY && inline) {
1434 var emptyRow = item.row <= row ? item.row : row + item.sizeY,
1435 emptyCol = item.col <= col ? item.col : col + item.sizeX,
1436 rowOffset = emptyRow - boundingBoxItem.row,
1437 colOffset = emptyCol - boundingBoxItem.col;
1439 for (var i = 0, l = itemsInTheWay.length; i < l; ++i) {
1440 var itemInTheWay = itemsInTheWay[i];
1442 var itemsInFreeSpace = gridster.getItems(
1443 itemInTheWay.row + rowOffset,
1444 itemInTheWay.col + colOffset,
1450 if (itemsInFreeSpace.length === 0) {
1451 gridster.putItem(itemInTheWay, itemInTheWay.row + rowOffset, itemInTheWay.col + colOffset);
1457 if (gridster.pushing !== false || !hasItemsInTheWay) {
1462 if (event.pageY - realdocument.body.scrollTop < scrollSensitivity) {
1463 realdocument.body.scrollTop = realdocument.body.scrollTop - scrollSpeed;
1464 } else if ($window.innerHeight - (event.pageY - realdocument.body.scrollTop) < scrollSensitivity) {
1465 realdocument.body.scrollTop = realdocument.body.scrollTop + scrollSpeed;
1468 if (event.pageX - realdocument.body.scrollLeft < scrollSensitivity) {
1469 realdocument.body.scrollLeft = realdocument.body.scrollLeft - scrollSpeed;
1470 } else if ($window.innerWidth - (event.pageX - realdocument.body.scrollLeft) < scrollSensitivity) {
1471 realdocument.body.scrollLeft = realdocument.body.scrollLeft + scrollSpeed;
1474 if (hasCallback || oldRow !== item.row || oldCol !== item.col) {
1475 scope.$apply(function() {
1477 gridster.draggable.drag(event, $el, itemOptions, item);
1483 function dragStop(event) {
1484 $el.removeClass('gridster-item-moving');
1485 var row = Math.min(gridster.pixelsToRows(elmY), gridster.maxRows - 1);
1486 var col = Math.min(gridster.pixelsToColumns(elmX), gridster.columns - 1);
1487 if (gridster.pushing !== false || gridster.getItems(row, col, item.sizeX, item.sizeY, item).length === 0) {
1491 gridster.movingItem = null;
1492 item.setPosition(item.row, item.col);
1494 scope.$apply(function() {
1495 if (gridster.draggable && gridster.draggable.stop) {
1496 gridster.draggable.stop(event, $el, itemOptions, item);
1501 function mouseDown(e) {
1502 if (inputTags.indexOf(e.target.nodeName.toLowerCase()) !== -1) {
1506 var $target = angular.element(e.target);
1508 // exit, if a resize handle was hit
1509 if ($target.hasClass('gridster-item-resizable-handler')) {
1513 // exit, if the target has it's own click event
1514 if ($target.attr('onclick') || $target.attr('ng-click')) {
1518 // only works if you have jQuery
1519 if ($target.closest && $target.closest('.gridster-no-drag').length) {
1523 // apply drag handle filter
1524 if (gridster.draggable && gridster.draggable.handle) {
1525 var $dragHandles = angular.element($el[0].querySelectorAll(gridster.draggable.handle));
1528 for (var h = 0, hl = $dragHandles.length; h < hl; ++h) {
1529 var handle = $dragHandles[h];
1530 if (handle === e.target) {
1534 var target = e.target;
1535 for (var p = 0; p < 20; ++p) {
1536 var parent = target.parentNode;
1537 if (parent === $el[0] || !parent) {
1540 if (parent === handle) {
1554 // left mouse button
1558 // right or middle mouse button
1562 lastMouseX = e.pageX;
1563 lastMouseY = e.pageY;
1565 elmX = parseInt($el.css('left'), 10);
1566 elmY = parseInt($el.css('top'), 10);
1567 elmW = $el[0].offsetWidth;
1568 elmH = $el[0].offsetHeight;
1570 originalCol = item.col;
1571 originalRow = item.row;
1578 function mouseMove(e) {
1579 if (!$el.hasClass('gridster-item-moving') || $el.hasClass('gridster-item-resizing')) {
1583 var maxLeft = gridster.curWidth - 1;
1584 var maxTop = gridster.curRowHeight * gridster.maxRows - 1;
1586 // Get the current mouse position.
1591 var diffX = mouseX - lastMouseX + mOffX;
1592 var diffY = mouseY - lastMouseY + mOffY;
1595 // Update last processed mouse positions.
1596 lastMouseX = mouseX;
1597 lastMouseY = mouseY;
1601 if (elmX + dX < minLeft) {
1602 diffX = minLeft - elmX;
1604 } else if (elmX + elmW + dX > maxLeft) {
1605 diffX = maxLeft - elmX - elmW;
1609 if (elmY + dY < minTop) {
1610 diffY = minTop - elmY;
1612 } else if (elmY + elmH + dY > maxTop) {
1613 diffY = maxTop - elmY - elmH;
1630 function mouseUp(e) {
1631 if (!$el.hasClass('gridster-item-moving') || $el.hasClass('gridster-item-resizing')) {
1643 var gridsterTouch = null;
1645 this.enable = function() {
1646 if (enabled === true) {
1651 if (gridsterTouch) {
1652 gridsterTouch.enable();
1656 gridsterTouch = new GridsterTouch($el[0], mouseDown, mouseMove, mouseUp);
1657 gridsterTouch.enable();
1660 this.disable = function() {
1661 if (enabled === false) {
1666 if (gridsterTouch) {
1667 gridsterTouch.disable();
1671 this.toggle = function(enabled) {
1679 this.destroy = function() {
1684 return GridsterDraggable;
1688 .factory('GridsterResizable', ['GridsterTouch', function(GridsterTouch) {
1689 function GridsterResizable($el, scope, gridster, item, itemOptions) {
1691 function ResizeHandle(handleClass) {
1693 var hClass = handleClass;
1695 var elmX, elmY, elmW, elmH,
1708 var getMinHeight = function() {
1709 return (item.minSizeY ? item.minSizeY : 1) * gridster.curRowHeight - gridster.margins[0];
1711 var getMinWidth = function() {
1712 return (item.minSizeX ? item.minSizeX : 1) * gridster.curColWidth - gridster.margins[1];
1715 var originalWidth, originalHeight;
1718 function resizeStart(e) {
1719 $el.addClass('gridster-item-moving');
1720 $el.addClass('gridster-item-resizing');
1722 gridster.movingItem = item;
1724 item.setElementSizeX();
1725 item.setElementSizeY();
1726 item.setElementPosition();
1727 gridster.updateHeight(1);
1729 scope.$apply(function() {
1731 if (gridster.resizable && gridster.resizable.start) {
1732 gridster.resizable.start(e, $el, itemOptions, item); // options is the item model
1737 function resize(e) {
1738 var oldRow = item.row,
1740 oldSizeX = item.sizeX,
1741 oldSizeY = item.sizeY,
1742 hasCallback = gridster.resizable && gridster.resizable.resize;
1745 // only change column if grabbing left edge
1746 if (['w', 'nw', 'sw'].indexOf(handleClass) !== -1) {
1747 col = gridster.pixelsToColumns(elmX, false);
1751 // only change row if grabbing top edge
1752 if (['n', 'ne', 'nw'].indexOf(handleClass) !== -1) {
1753 row = gridster.pixelsToRows(elmY, false);
1756 var sizeX = item.sizeX;
1757 // only change row if grabbing left or right edge
1758 if (['n', 's'].indexOf(handleClass) === -1) {
1759 sizeX = gridster.pixelsToColumns(elmW, true);
1762 var sizeY = item.sizeY;
1763 // only change row if grabbing top or bottom edge
1764 if (['e', 'w'].indexOf(handleClass) === -1) {
1765 sizeY = gridster.pixelsToRows(elmH, true);
1769 var canOccupy = row > -1 && col > -1 && sizeX + col <= gridster.columns && sizeY + row <= gridster.maxRows;
1770 if (canOccupy && (gridster.pushing !== false || gridster.getItems(row, col, sizeX, sizeY, item).length === 0)) {
1776 var isChanged = item.row !== oldRow || item.col !== oldCol || item.sizeX !== oldSizeX || item.sizeY !== oldSizeY;
1778 if (hasCallback || isChanged) {
1779 scope.$apply(function() {
1781 gridster.resizable.resize(e, $el, itemOptions, item); // options is the item model
1787 function resizeStop(e) {
1788 $el.removeClass('gridster-item-moving');
1789 $el.removeClass('gridster-item-resizing');
1791 gridster.movingItem = null;
1793 item.setPosition(item.row, item.col);
1794 item.setSizeY(item.sizeY);
1795 item.setSizeX(item.sizeX);
1797 scope.$apply(function() {
1798 if (gridster.resizable && gridster.resizable.stop) {
1799 gridster.resizable.stop(e, $el, itemOptions, item); // options is the item model
1804 function mouseDown(e) {
1807 // left mouse button
1811 // right or middle mouse button
1815 // save the draggable setting to restore after resize
1816 savedDraggable = gridster.draggable.enabled;
1817 if (savedDraggable) {
1818 gridster.draggable.enabled = false;
1819 scope.$broadcast('gridster-draggable-changed', gridster);
1822 // Get the current mouse position.
1823 lastMouseX = e.pageX;
1824 lastMouseY = e.pageY;
1826 // Record current widget dimensions
1827 elmX = parseInt($el.css('left'), 10);
1828 elmY = parseInt($el.css('top'), 10);
1829 elmW = $el[0].offsetWidth;
1830 elmH = $el[0].offsetHeight;
1832 originalWidth = item.sizeX;
1833 originalHeight = item.sizeY;
1840 function mouseMove(e) {
1841 var maxLeft = gridster.curWidth - 1;
1843 // Get the current mouse position.
1848 var diffX = mouseX - lastMouseX + mOffX;
1849 var diffY = mouseY - lastMouseY + mOffY;
1852 // Update last processed mouse positions.
1853 lastMouseX = mouseX;
1854 lastMouseY = mouseY;
1859 if (hClass.indexOf('n') >= 0) {
1860 if (elmH - dY < getMinHeight()) {
1861 diffY = elmH - getMinHeight();
1863 } else if (elmY + dY < minTop) {
1864 diffY = minTop - elmY;
1870 if (hClass.indexOf('s') >= 0) {
1871 if (elmH + dY < getMinHeight()) {
1872 diffY = getMinHeight() - elmH;
1874 } else if (elmY + elmH + dY > maxTop) {
1875 diffY = maxTop - elmY - elmH;
1880 if (hClass.indexOf('w') >= 0) {
1881 if (elmW - dX < getMinWidth()) {
1882 diffX = elmW - getMinWidth();
1884 } else if (elmX + dX < minLeft) {
1885 diffX = minLeft - elmX;
1891 if (hClass.indexOf('e') >= 0) {
1892 if (elmW + dX < getMinWidth()) {
1893 diffX = getMinWidth() - elmW;
1895 } else if (elmX + elmW + dX > maxLeft) {
1896 diffX = maxLeft - elmX - elmW;
1905 'left': elmX + 'px',
1906 'width': elmW + 'px',
1907 'height': elmH + 'px'
1915 function mouseUp(e) {
1916 // restore draggable setting to its original state
1917 if (gridster.draggable.enabled !== savedDraggable) {
1918 gridster.draggable.enabled = savedDraggable;
1919 scope.$broadcast('gridster-draggable-changed', gridster);
1929 var $dragHandle = null;
1932 this.enable = function() {
1934 $dragHandle = angular.element('<div class="gridster-item-resizable-handler handle-' + hClass + '"></div>');
1935 $el.append($dragHandle);
1938 unifiedInput = new GridsterTouch($dragHandle[0], mouseDown, mouseMove, mouseUp);
1939 unifiedInput.enable();
1942 this.disable = function() {
1944 $dragHandle.remove();
1948 unifiedInput.disable();
1949 unifiedInput = undefined;
1952 this.destroy = function() {
1958 var handlesOpts = gridster.resizable.handles;
1959 if (typeof handlesOpts === 'string') {
1960 handlesOpts = gridster.resizable.handles.split(',');
1962 var enabled = false;
1964 for (var c = 0, l = handlesOpts.length; c < l; c++) {
1965 handles.push(new ResizeHandle(handlesOpts[c]));
1968 this.enable = function() {
1972 for (var c = 0, l = handles.length; c < l; c++) {
1973 handles[c].enable();
1978 this.disable = function() {
1982 for (var c = 0, l = handles.length; c < l; c++) {
1983 handles[c].disable();
1988 this.toggle = function(enabled) {
1996 this.destroy = function() {
1997 for (var c = 0, l = handles.length; c < l; c++) {
1998 handles[c].destroy();
2002 return GridsterResizable;
2005 .factory('gridsterDebounce', function() {
2006 return function gridsterDebounce(func, wait, immediate) {
2011 var later = function() {
2014 func.apply(context, args);
2017 var callNow = immediate && !timeout;
2018 clearTimeout(timeout);
2019 timeout = setTimeout(later, wait);
2021 func.apply(context, args);
2028 * GridsterItem directive
2030 * @param GridsterDraggable
2031 * @param GridsterResizable
2032 * @param gridsterDebounce
2034 .directive('gridsterItem', ['$parse', 'GridsterDraggable', 'GridsterResizable', 'gridsterDebounce',
2035 function($parse, GridsterDraggable, GridsterResizable, gridsterDebounce) {
2039 controller: 'GridsterItemCtrl',
2040 controllerAs: 'gridsterItem',
2041 require: ['^gridster', 'gridsterItem'],
2042 link: function(scope, $el, attrs, controllers) {
2043 var optionsKey = attrs.gridsterItem,
2046 var gridster = controllers[0],
2047 item = controllers[1];
2049 scope.gridster = gridster;
2051 // bind the item's position properties
2052 // options can be an object specified by gridster-item="object"
2053 // or the options can be the element html attributes object
2055 var $optionsGetter = $parse(optionsKey);
2056 options = $optionsGetter(scope) || {};
2057 if (!options && $optionsGetter.assign) {
2068 $optionsGetter.assign(scope, options);
2074 item.init($el, gridster);
2076 $el.addClass('gridster-item');
2078 var aspects = ['minSizeX', 'maxSizeX', 'minSizeY', 'maxSizeY', 'sizeX', 'sizeY', 'row', 'col'],
2081 var expressions = [];
2082 var aspectFn = function(aspect) {
2084 if (typeof options[aspect] === 'string') {
2085 // watch the expression in the scope
2086 expression = options[aspect];
2087 } else if (typeof options[aspect.toLowerCase()] === 'string') {
2088 // watch the expression in the scope
2089 expression = options[aspect.toLowerCase()];
2090 } else if (optionsKey) {
2091 // watch the expression on the options object in the scope
2092 expression = optionsKey + '.' + aspect;
2096 expressions.push('"' + aspect + '":' + expression);
2097 $getters[aspect] = $parse(expression);
2100 var val = $getters[aspect](scope);
2101 if (typeof val === 'number') {
2106 for (var i = 0, l = aspects.length; i < l; ++i) {
2107 aspectFn(aspects[i]);
2110 var watchExpressions = '{' + expressions.join(',') + '}';
2111 // when the value changes externally, update the internal item object
2112 scope.$watchCollection(watchExpressions, function(newVals, oldVals) {
2113 for (var aspect in newVals) {
2114 var newVal = newVals[aspect];
2115 var oldVal = oldVals[aspect];
2116 if (oldVal === newVal) {
2119 newVal = parseInt(newVal, 10);
2120 if (!isNaN(newVal)) {
2121 item[aspect] = newVal;
2126 function positionChanged() {
2127 // call setPosition so the element and gridster controller are updated
2128 item.setPosition(item.row, item.col);
2130 // when internal item position changes, update externally bound values
2131 if ($getters.row && $getters.row.assign) {
2132 $getters.row.assign(scope, item.row);
2134 if ($getters.col && $getters.col.assign) {
2135 $getters.col.assign(scope, item.col);
2138 scope.$watch(function() {
2139 return item.row + ',' + item.col;
2140 }, positionChanged);
2142 function sizeChanged() {
2143 var changedX = item.setSizeX(item.sizeX, true);
2144 if (changedX && $getters.sizeX && $getters.sizeX.assign) {
2145 $getters.sizeX.assign(scope, item.sizeX);
2147 var changedY = item.setSizeY(item.sizeY, true);
2148 if (changedY && $getters.sizeY && $getters.sizeY.assign) {
2149 $getters.sizeY.assign(scope, item.sizeY);
2152 if (changedX || changedY) {
2153 item.gridster.moveOverlappingItems(item);
2154 gridster.layoutChanged();
2155 scope.$broadcast('gridster-item-resized', item);
2159 scope.$watch(function() {
2160 return item.sizeY + ',' + item.sizeX + ',' + item.minSizeX + ',' + item.maxSizeX + ',' + item.minSizeY + ',' + item.maxSizeY;
2163 var draggable = new GridsterDraggable($el, scope, gridster, item, options);
2164 var resizable = new GridsterResizable($el, scope, gridster, item, options);
2166 var updateResizable = function() {
2167 resizable.toggle(!gridster.isMobile && gridster.resizable && gridster.resizable.enabled);
2171 var updateDraggable = function() {
2172 draggable.toggle(!gridster.isMobile && gridster.draggable && gridster.draggable.enabled);
2176 scope.$on('gridster-draggable-changed', updateDraggable);
2177 scope.$on('gridster-resizable-changed', updateResizable);
2178 scope.$on('gridster-resized', updateResizable);
2179 scope.$on('gridster-mobile-changed', function() {
2184 function whichTransitionEvent() {
2185 var el = document.createElement('div');
2187 'transition': 'transitionend',
2188 'OTransition': 'oTransitionEnd',
2189 'MozTransition': 'transitionend',
2190 'WebkitTransition': 'webkitTransitionEnd'
2192 for (var t in transitions) {
2193 if (el.style[t] !== undefined) {
2194 return transitions[t];
2199 var debouncedTransitionEndPublisher = gridsterDebounce(function() {
2200 scope.$apply(function() {
2201 scope.$broadcast('gridster-item-transition-end', item);
2205 $el.on(whichTransitionEvent(), debouncedTransitionEndPublisher);
2207 scope.$broadcast('gridster-item-initialized', item);
2209 return scope.$on('$destroy', function() {
2211 resizable.destroy();
2212 draggable.destroy();
2216 gridster.removeItem(item);
2228 .directive('gridsterNoDrag', function() {
2231 link: function(scope, $element) {
2232 $element.addClass('gridster-no-drag');