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
15 factory(root.angular);
17 }(this, function(angular) {
23 var getInternetExplorerVersion = function ()
24 // Returns the version of Internet Explorer >4 or
25 // undefined(indicating the use of another browser).
27 var isIE10 = (eval("/*@cc_on!@*/false") && document.documentMode === 10);
32 div = document.createElement('div'),
33 all = div.getElementsByTagName('i');
35 div.innerHTML = '<!--[if gt IE ' + (++v) + ']><i></i><![endif]-->';
37 return v > 4 ? v : undefined;
40 var browserVersion = getInternetExplorerVersion();
42 if (browserVersion && browserVersion < 9) {
46 // This returned angular module 'gridster' is what is exported.
47 return angular.module('attGridsterLib', [])
49 .constant('gridsterConfig', {
50 columns: 6, // number of columns in the grid
51 pushing: true, // whether to push other items out of the way
52 floating: true, // whether to automatically float items up so they stack
53 swapping: true, // whether or not to have items switch places instead of
54 // push down if they are the same size
55 width: 'auto', // width of the grid. "auto" will expand the grid to its
57 colWidth: 'auto', // width of grid columns. "auto" will divide the
58 // width of the grid evenly among the columns
59 rowHeight: 'match', // height of grid rows. 'match' will make it the
60 // same as the column width, a numeric value will be
61 // interpreted as pixels, '/2' is half the column
62 // width, '*5' is five times the column width, etc.
63 margins: [10, 10], // margins in between grid items
65 isMobile: false, // toggle mobile view
66 mobileBreakPoint: 100, // width threshold to toggle mobile mode
67 mobileModeEnabled: true, // whether or not to toggle mobile mode when
68 // screen width is less than
70 minColumns: 1, // minimum amount of columns the grid can scale down to
71 minRows: 1, // minimum amount of rows to show if the grid is empty
72 maxRows: 100, // maximum amount of rows in the grid
73 defaultSizeX: 1, // default width of an item in columns
74 defaultSizeY: 1, // default height of an item in rows
75 minSizeX: 1, // minimum column width of an item
76 maxSizeX: null, // maximum column width of an item
77 minSizeY: 1, // minumum row height of an item
78 maxSizeY: null, // maximum row height of an item
79 saveGridItemCalculatedHeightInMobile: false, // grid item height in
80 // mobile display. true-
81 // to use the calculated
82 // height by sizeY given
83 resizable: { // options to pass to resizable handler
85 handles: ['s', 'e', 'n', 'w', 'se', 'ne', 'sw', 'nw']
87 draggable: { // options to pass to draggable handler
89 scrollSensitivity: 20, // Distance in pixels from the edge of the
90 // viewport after which the viewport should
91 // scroll, relative to pointer
92 scrollSpeed: 15 // Speed at which the window should scroll once the
93 // mouse pointer gets within scrollSensitivity
98 .controller('GridsterCtrl', ['gridsterConfig', '$timeout',
99 function(gridsterConfig, $timeout) {
104 * Create options from gridsterConfig constant
106 angular.extend(this, gridsterConfig);
108 this.resizable = angular.extend({}, gridsterConfig.resizable || {});
109 this.draggable = angular.extend({}, gridsterConfig.draggable || {});
112 this.layoutChanged = function() {
117 $timeout(function() {
119 if (gridster.loaded) {
120 gridster.floatItemsUp();
122 gridster.updateHeight(gridster.movingItem ? gridster.movingItem.sizeY : 0);
127 * A positional array of the items in the grid
132 * Clean up after yourself
134 this.destroy = function() {
135 // empty the grid to cut back on the possibility
136 // of circular references
140 this.$element = null;
144 * Overrides default options
147 * options The options to override
149 this.setOptions = function(options) {
154 options = angular.extend({}, options);
156 // all this to avoid using jQuery...
157 if (options.draggable) {
158 angular.extend(this.draggable, options.draggable);
159 delete(options.draggable);
161 if (options.resizable) {
162 angular.extend(this.resizable, options.resizable);
163 delete(options.resizable);
166 angular.extend(this, options);
168 if (!this.margins || this.margins.length !== 2) {
169 this.margins = [0, 0];
171 for (var x = 0, l = this.margins.length; x < l; ++x) {
172 this.margins[x] = parseInt(this.margins[x], 10);
173 if (isNaN(this.margins[x])) {
181 * Check if item can occupy a specified position in the grid
184 * item The item in question
188 * column The column index
189 * @returns {Boolean} True if if item fits
191 this.canItemOccupy = function(item, row, column) {
192 return row > -1 && column > -1 && item.sizeX + column <= this.columns && item.sizeY + row <= this.maxRows;
196 * Set the item in the first suitable position
199 * item The item to insert
201 this.autoSetItemPosition = function(item) {
202 // walk through each row and column looking for a place it will
204 for (var rowIndex = 0; rowIndex < this.maxRows; ++rowIndex) {
205 for (var colIndex = 0; colIndex < this.columns; ++colIndex) {
206 // only insert if position is not already taken and it
208 var items = this.getItems(rowIndex, colIndex, item.sizeX, item.sizeY, item);
209 if (items.length === 0 && this.canItemOccupy(item, rowIndex, colIndex)) {
210 this.putItem(item, rowIndex, colIndex);
215 throw new Error('Unable to place item!');
219 * Gets items at a specific coordinate
230 * excludeItems An array of items to exclude from
232 * @returns {Array} Items that match the criteria
234 this.getItems = function(row, column, sizeX, sizeY, excludeItems) {
236 if (!sizeX || !sizeY) {
239 if (excludeItems && !(excludeItems instanceof Array)) {
240 excludeItems = [excludeItems];
242 for (var h = 0; h < sizeY; ++h) {
243 for (var w = 0; w < sizeX; ++w) {
244 var item = this.getItem(row + h, column + w, excludeItems);
245 if (item && (!excludeItems || excludeItems.indexOf(item) === -1) && items.indexOf(item) === -1) {
256 * @returns {Object} An item that represents the bounding box of the
259 this.getBoundingBox = function(items) {
261 if (items.length === 0) {
264 if (items.length === 1) {
268 sizeY: items[0].sizeY,
269 sizeX: items[0].sizeX
278 for (var i = 0, l = items.length; i < l; ++i) {
280 minRow = Math.min(item.row, minRow);
281 minCol = Math.min(item.col, minCol);
282 maxRow = Math.max(item.row + item.sizeY, maxRow);
283 maxCol = Math.max(item.col + item.sizeX, maxCol);
289 sizeY: maxRow - minRow,
290 sizeX: maxCol - minCol
296 * Removes an item from the grid
301 this.removeItem = function(item) {
302 for (var rowIndex = 0, l = this.grid.length; rowIndex < l; ++rowIndex) {
303 var columns = this.grid[rowIndex];
307 var index = columns.indexOf(item);
309 columns[index] = null;
313 this.layoutChanged();
317 * Returns the item at a specified coordinate
324 * excludeItems Items to exclude from selection
325 * @returns {Object} The matched item or null
327 this.getItem = function(row, column, excludeItems) {
328 if (excludeItems && !(excludeItems instanceof Array)) {
329 excludeItems = [excludeItems];
336 var items = this.grid[row];
338 var item = items[col];
339 if (item && (!excludeItems || excludeItems.indexOf(item) === -1) && item.sizeX >= sizeX && item.sizeY >= sizeY) {
353 * Insert an array of items into the grid
356 * items An array of items to insert
358 this.putItems = function(items) {
359 for (var i = 0, l = items.length; i < l; ++i) {
360 this.putItem(items[i]);
365 * Insert a single item into the grid
368 * item The item to insert
370 * row (Optional) Specifies the items row index
372 * column (Optional) Specifies the items column index
376 this.putItem = function(item, row, column, ignoreItems) {
377 // auto place item if no row specified
378 if (typeof row === 'undefined' || row === null) {
381 if (typeof row === 'undefined' || row === null) {
382 this.autoSetItemPosition(item);
387 // keep item within allowed bounds
388 if (!this.canItemOccupy(item, row, column)) {
389 column = Math.min(this.columns - item.sizeX, Math.max(0, column));
390 row = Math.min(this.maxRows - item.sizeY, Math.max(0, row));
393 // check if item is already in grid
394 if (item.oldRow !== null && typeof item.oldRow !== 'undefined') {
395 var samePosition = item.oldRow === row && item.oldColumn === column;
396 var inGrid = this.grid[row] && this.grid[row][column] === item;
397 if (samePosition && inGrid) {
402 // remove from old position
403 var oldRow = this.grid[item.oldRow];
404 if (oldRow && oldRow[item.oldColumn] === item) {
405 delete oldRow[item.oldColumn];
410 item.oldRow = item.row = row;
411 item.oldColumn = item.col = column;
413 this.moveOverlappingItems(item, ignoreItems);
415 if (!this.grid[row]) {
418 this.grid[row][column] = item;
420 if (this.movingItem === item) {
421 this.floatItemUp(item);
423 this.layoutChanged();
427 * Trade row and column if item1 with item2
434 this.swapItems = function(item1, item2) {
435 this.grid[item1.row][item1.col] = item2;
436 this.grid[item2.row][item2.col] = item1;
438 var item1Row = item1.row;
439 var item1Col = item1.col;
440 item1.row = item2.row;
441 item1.col = item2.col;
442 item2.row = item1Row;
443 item2.col = item1Col;
447 * Prevents items from being overlapped
450 * item The item that should remain
454 this.moveOverlappingItems = function(item, ignoreItems) {
455 // don't move item, so ignore it
457 ignoreItems = [item];
458 } else if (ignoreItems.indexOf(item) === -1) {
459 ignoreItems = ignoreItems.slice(0);
460 ignoreItems.push(item);
463 // get the items in the space occupied by the item's coordinates
464 var overlappingItems = this.getItems(
471 this.moveItemsDown(overlappingItems, item.row + item.sizeY, ignoreItems);
475 * Moves an array of items to a specified row
478 * items The items to move
480 * newRow The target row
484 this.moveItemsDown = function(items, newRow, ignoreItems) {
485 if (!items || items.length === 0) {
488 items.sort(function(a, b) {
489 return a.row - b.row;
492 ignoreItems = ignoreItems ? ignoreItems.slice(0) : [];
496 // calculate the top rows in each column
497 for (i = 0, l = items.length; i < l; ++i) {
499 var topRow = topRows[item.col];
500 if (typeof topRow === 'undefined' || item.row < topRow) {
501 topRows[item.col] = item.row;
505 // move each item down from the top row in its column to the row
506 for (i = 0, l = items.length; i < l; ++i) {
508 var rowsToMove = newRow - topRows[item.col];
509 this.moveItemDown(item, item.row + rowsToMove, ignoreItems);
510 ignoreItems.push(item);
515 * Moves an item down to a specified row
518 * item The item to move
520 * newRow The target row
524 this.moveItemDown = function(item, newRow, ignoreItems) {
525 if (item.row >= newRow) {
528 while (item.row < newRow) {
530 this.moveOverlappingItems(item, ignoreItems);
532 this.putItem(item, item.row, item.col, ignoreItems);
536 * Moves all items up as much as possible
538 this.floatItemsUp = function() {
539 if (this.floating === false) {
542 for (var rowIndex = 0, l = this.grid.length; rowIndex < l; ++rowIndex) {
543 var columns = this.grid[rowIndex];
547 for (var colIndex = 0, len = columns.length; colIndex < len; ++colIndex) {
548 var item = columns[colIndex];
550 this.floatItemUp(item);
557 * Float an item up to the most suitable row
560 * item The item to move
562 this.floatItemUp = function(item) {
563 if (this.floating === false) {
566 var colIndex = item.col,
571 rowIndex = item.row - 1;
573 while (rowIndex > -1) {
574 var items = this.getItems(rowIndex, colIndex, sizeX, sizeY, item);
575 if (items.length !== 0) {
579 bestColumn = colIndex;
582 if (bestRow !== null) {
583 this.putItem(item, bestRow, bestColumn);
588 * Update gridsters height
591 * plus (Optional) Additional height to add
593 this.updateHeight = function(plus) {
594 var maxHeight = this.minRows;
596 for (var rowIndex = this.grid.length; rowIndex >= 0; --rowIndex) {
597 var columns = this.grid[rowIndex];
601 for (var colIndex = 0, len = columns.length; colIndex < len; ++colIndex) {
602 if (columns[colIndex]) {
603 maxHeight = Math.max(maxHeight, rowIndex + plus + columns[colIndex].sizeY);
607 this.gridHeight = this.maxRows - maxHeight > 0 ? Math.min(this.maxRows, maxHeight) : Math.max(this.maxRows, maxHeight);
611 * Returns the number of rows that will fit in given amount of
617 * ceilOrFloor (Optional) Determines rounding method
619 this.pixelsToRows = function(pixels, ceilOrFloor) {
620 if (ceilOrFloor === true) {
621 return Math.ceil(pixels / this.curRowHeight);
622 } else if (ceilOrFloor === false) {
623 return Math.floor(pixels / this.curRowHeight);
626 return Math.round(pixels / this.curRowHeight);
630 * Returns the number of columns that will fit in a given amount of
636 * ceilOrFloor (Optional) Determines rounding method
637 * @returns {Number} The number of columns
639 this.pixelsToColumns = function(pixels, ceilOrFloor) {
640 if (ceilOrFloor === true) {
641 return Math.ceil(pixels / this.curColWidth);
642 } else if (ceilOrFloor === false) {
643 return Math.floor(pixels / this.curColWidth);
646 return Math.round(pixels / this.curColWidth);
651 .directive('gridsterPreview', function() {
655 require: '^gridster',
656 template: '<div ng-style="previewStyle()" class="gridster-item gridster-preview-holder"></div>',
657 link: function(scope, $el, attrs, gridster) {
660 * @returns {Object} style object for preview element
662 scope.previewStyle = function() {
664 if (!gridster.movingItem) {
672 height: (gridster.movingItem.sizeY * gridster.curRowHeight - gridster.margins[0]) + 'px',
673 width: (gridster.movingItem.sizeX * gridster.curColWidth - gridster.margins[1]) + 'px',
674 top: (gridster.movingItem.row * gridster.curRowHeight + (gridster.outerMargin ? gridster.margins[0] : 0)) + 'px',
675 left: (gridster.movingItem.col * gridster.curColWidth + (gridster.outerMargin ? gridster.margins[1] : 0)) + 'px'
683 * The gridster directive
694 .directive('gridster', ['$timeout', '$window', '$rootScope', 'gridsterDebounce',
695 function($timeout, $window, $rootScope, gridsterDebounce) {
699 controller: 'GridsterCtrl',
700 controllerAs: 'gridster',
701 compile: function($tplElem) {
703 $tplElem.prepend('<div ng-if="gridster.movingItem" gridster-preview></div>');
705 return function(scope, $elem, attrs, gridster) {
706 gridster.loaded = false;
708 gridster.$element = $elem;
710 scope.gridster = gridster;
712 $elem.addClass('gridster');
714 var isVisible = function(ele) {
715 return ele.style.visibility !== 'hidden' && ele.style.display !== 'none';
718 function refresh(config) {
719 gridster.setOptions(config);
721 if (!isVisible($elem[0])) {
725 // resolve "auto" & "match" values
726 if (gridster.width === 'auto') {
727 gridster.curWidth = $elem[0].offsetWidth || parseInt($elem.css('width'), 10);
729 gridster.curWidth = gridster.width;
732 if (gridster.colWidth === 'auto') {
733 gridster.curColWidth = (gridster.curWidth + (gridster.outerMargin ? -gridster.margins[1] : gridster.margins[1])) / gridster.columns;
735 gridster.curColWidth = gridster.colWidth;
738 gridster.curRowHeight = gridster.rowHeight;
739 if (typeof gridster.rowHeight === 'string') {
740 if (gridster.rowHeight === 'match') {
741 gridster.curRowHeight = Math.round(gridster.curColWidth);
742 } else if (gridster.rowHeight.indexOf('*') !== -1) {
743 gridster.curRowHeight = Math.round(gridster.curColWidth * gridster.rowHeight.replace('*', '').replace(' ', ''));
744 } else if (gridster.rowHeight.indexOf('/') !== -1) {
745 gridster.curRowHeight = Math.round(gridster.curColWidth / gridster.rowHeight.replace('/', '').replace(' ', ''));
749 gridster.isMobile = gridster.mobileModeEnabled && gridster.curWidth <= gridster.mobileBreakPoint;
751 // loop through all items and reset their CSS
752 for (var rowIndex = 0, l = gridster.grid.length; rowIndex < l; ++rowIndex) {
753 var columns = gridster.grid[rowIndex];
758 for (var colIndex = 0, len = columns.length; colIndex < len; ++colIndex) {
759 if (columns[colIndex]) {
760 var item = columns[colIndex];
761 item.setElementPosition();
762 item.setElementSizeY();
763 item.setElementSizeX();
771 var optionsKey = attrs.gridster;
773 scope.$parent.$watch(optionsKey, function(newConfig) {
780 scope.$watch(function() {
781 return gridster.loaded;
783 if (gridster.loaded) {
784 $elem.addClass('gridster-loaded');
786 $elem.removeClass('gridster-loaded');
790 scope.$watch(function() {
791 return gridster.isMobile;
793 if (gridster.isMobile) {
794 $elem.addClass('gridster-mobile').removeClass('gridster-desktop');
796 $elem.removeClass('gridster-mobile').addClass('gridster-desktop');
798 $rootScope.$broadcast('gridster-mobile-changed', gridster);
801 scope.$watch(function() {
802 return gridster.draggable;
804 $rootScope.$broadcast('gridster-draggable-changed', gridster);
807 scope.$watch(function() {
808 return gridster.resizable;
810 $rootScope.$broadcast('gridster-resizable-changed', gridster);
813 function updateHeight() {
814 if(gridster.gridHeight){ // need
823 $elem.css('height', (gridster.gridHeight * gridster.curRowHeight) + (gridster.outerMargin ? gridster.margins[0] : -gridster.margins[0]) + 'px');
827 scope.$watch(function() {
828 return gridster.gridHeight;
831 scope.$watch(function() {
832 return gridster.movingItem;
834 gridster.updateHeight(gridster.movingItem ? gridster.movingItem.sizeY : 0);
837 var prevWidth = $elem[0].offsetWidth || parseInt($elem.css('width'), 10);
839 var resize = function() {
840 var width = $elem[0].offsetWidth || parseInt($elem.css('width'), 10);
842 if (!width || width === prevWidth || gridster.movingItem) {
847 if (gridster.loaded) {
848 $elem.removeClass('gridster-loaded');
853 if (gridster.loaded) {
854 $elem.addClass('gridster-loaded');
857 $rootScope.$broadcast('gridster-resized', [width, $elem[0].offsetHeight], gridster);
860 // track element width changes any way we can
861 var onResize = gridsterDebounce(function onResize() {
863 $timeout(function() {
868 scope.$watch(function() {
869 return isVisible($elem[0]);
873 // https://github.com/sdecima/javascript-detect-element-resize
874 if (typeof window.addResizeListener === 'function') {
875 window.addResizeListener($elem[0], onResize);
877 scope.$watch(function() {
878 return $elem[0].offsetWidth || parseInt($elem.css('width'), 10);
881 var $win = angular.element($window);
882 $win.on('resize', onResize);
884 // be sure to cleanup
885 scope.$on('$destroy', function() {
887 $win.off('resize', onResize);
888 if (typeof window.removeResizeListener === 'function') {
889 window.removeResizeListener($elem[0], onResize);
893 // allow a little time to place items before floating up
894 $timeout(function() {
895 scope.$watch('gridster.floating', function() {
896 gridster.floatItemsUp();
898 gridster.loaded = true;
906 .controller('GridsterItemCtrl', function() {
907 this.$element = null;
908 this.gridster = null;
915 this.maxSizeX = null;
916 this.maxSizeY = null;
918 this.init = function($element, gridster) {
919 this.$element = $element;
920 this.gridster = gridster;
921 this.sizeX = gridster.defaultSizeX;
922 this.sizeY = gridster.defaultSizeY;
925 this.destroy = function() {
926 // set these to null to avoid the possibility of circular references
927 this.gridster = null;
928 this.$element = null;
932 * Returns the items most important attributes
934 this.toJSON = function() {
943 this.isMoving = function() {
944 return this.gridster.movingItem === this;
948 * Set the items position
955 this.setPosition = function(row, column) {
956 this.gridster.putItem(this, row, column);
958 if (!this.isMoving()) {
959 this.setElementPosition();
964 * Sets a specified size property
967 * key Can be either "x" or "y"
969 * value The size amount
973 this.setSize = function(key, value, preventMove) {
974 key = key.toUpperCase();
975 var camelCase = 'size' + key,
976 titleCase = 'Size' + key;
980 value = parseInt(value, 10);
981 if (isNaN(value) || value === 0) {
982 value = this.gridster['default' + titleCase];
984 var max = key === 'X' ? this.gridster.columns : this.gridster.maxRows;
985 if (this['max' + titleCase]) {
986 max = Math.min(this['max' + titleCase], max);
988 if (this.gridster['max' + titleCase]) {
989 max = Math.min(this.gridster['max' + titleCase], max);
991 if (key === 'X' && this.cols) {
993 } else if (key === 'Y' && this.rows) {
998 if (this['min' + titleCase]) {
999 min = Math.max(this['min' + titleCase], min);
1001 if (this.gridster['min' + titleCase]) {
1002 min = Math.max(this.gridster['min' + titleCase], min);
1005 value = Math.max(Math.min(value, max), min);
1007 var changed = (this[camelCase] !== value || (this['old' + titleCase] && this['old' + titleCase] !== value));
1008 this['old' + titleCase] = this[camelCase] = value;
1010 if (!this.isMoving()) {
1011 this['setElement' + titleCase]();
1013 if (!preventMove && changed) {
1014 this.gridster.moveOverlappingItems(this);
1015 this.gridster.layoutChanged();
1022 * Sets the items sizeY property
1029 this.setSizeY = function(rows, preventMove) {
1030 return this.setSize('Y', rows, preventMove);
1034 * Sets the items sizeX property
1041 this.setSizeX = function(columns, preventMove) {
1042 return this.setSize('X', columns, preventMove);
1046 * Sets an elements position on the page
1048 this.setElementPosition = function() {
1049 if (this.gridster.isMobile) {
1051 marginLeft: this.gridster.margins[0] + 'px',
1052 marginRight: this.gridster.margins[0] + 'px',
1053 marginTop: this.gridster.margins[1] + 'px',
1054 marginBottom: this.gridster.margins[1] + 'px',
1061 top: (this.row * this.gridster.curRowHeight + (this.gridster.outerMargin ? this.gridster.margins[0] : 0)) + 'px',
1062 left: (this.col * this.gridster.curColWidth + (this.gridster.outerMargin ? this.gridster.margins[1] : 0)) + 'px'
1068 * Sets an elements height
1070 this.setElementSizeY = function() {
1071 if (this.gridster.isMobile && !this.gridster.saveGridItemCalculatedHeightInMobile) {
1072 this.$element.css('height', '');
1074 var computedHeight = (this.sizeY * this.gridster.curRowHeight - this.gridster.margins[0]) + 'px';
1075 // this.$element.css('height', computedHeight);
1076 this.$element.attr('style', this.$element.attr('style') + '; ' + 'height: '+computedHeight+' !important;');
1081 * Sets an elements width
1083 this.setElementSizeX = function() {
1084 if (this.gridster.isMobile) {
1085 this.$element.css('width', '');
1087 this.$element.css('width', (this.sizeX * this.gridster.curColWidth - this.gridster.margins[1]) + 'px');
1092 * Gets an element's width
1094 this.getElementSizeX = function() {
1095 return (this.sizeX * this.gridster.curColWidth - this.gridster.margins[1]);
1099 * Gets an element's height
1101 this.getElementSizeY = function() {
1102 return (this.sizeY * this.gridster.curRowHeight - this.gridster.margins[0]);
1107 .factory('GridsterTouch', [function() {
1108 return function GridsterTouch(target, startEvent, moveEvent, endEvent) {
1109 var lastXYById = {};
1111 // Opera doesn't have Object.keys so we use this wrapper
1112 var numberOfKeys = function(theObject) {
1114 return Object.keys(theObject).length;
1119 for (key in theObject) {
1126 // this calculates the delta needed to convert pageX/Y to offsetX/Y
1127 // because offsetX/Y don't exist in the TouchEvent object or in
1128 // Firefox's MouseEvent object
1129 var computeDocumentToElementDelta = function(theElement) {
1130 var elementLeft = 0;
1132 var oldIEUserAgent = navigator.userAgent.match(/\bMSIE\b/);
1134 for (var offsetElement = theElement; offsetElement != null; offsetElement = offsetElement.offsetParent) {
1135 // the following is a major hack for versions of IE less
1136 // than 8 to avoid an apparent problem on the IEBlog with
1137 // double-counting the offsets
1138 // this may not be a general solution to IE7's problem with
1139 // offsetLeft/offsetParent
1140 if (oldIEUserAgent &&
1141 (!document.documentMode || document.documentMode < 8) &&
1142 offsetElement.currentStyle.position === 'relative' && offsetElement.offsetParent && offsetElement.offsetParent.currentStyle.position === 'relative' && offsetElement.offsetLeft === offsetElement.offsetParent.offsetLeft) {
1144 elementTop += offsetElement.offsetTop;
1146 elementLeft += offsetElement.offsetLeft;
1147 elementTop += offsetElement.offsetTop;
1157 // cache the delta from the document to our event target
1158 // (reinitialized each mousedown/MSPointerDown/touchstart)
1159 var documentToTargetDelta = computeDocumentToElementDelta(target);
1161 // common event handler for the mouse/pointer/touch models and their
1162 // down/start, move, up/end, and cancel events
1163 var doEvent = function(theEvtObj) {
1165 if (theEvtObj.type === 'mousemove' && numberOfKeys(lastXYById) === 0) {
1171 var pointerList = theEvtObj.changedTouches ? theEvtObj.changedTouches : [theEvtObj];
1173 for (var i = 0; i < pointerList.length; ++i) {
1174 var pointerObj = pointerList[i];
1175 var pointerId = (typeof pointerObj.identifier !== 'undefined') ? pointerObj.identifier : (typeof pointerObj.pointerId !== 'undefined') ? pointerObj.pointerId : 1;
1177 // use the pageX/Y coordinates to
1178 // compute target-relative coordinates
1179 // when we have them (in ie < 9, we need
1180 // to do a little work to put them
1182 if (typeof pointerObj.pageX === 'undefined') {
1184 // initialize assuming our
1185 // source element is our target
1187 pointerObj.pageX = pointerObj.offsetX + documentToTargetDelta.x;
1188 pointerObj.pageY = pointerObj.offsetY + documentToTargetDelta.y;
1191 pointerObj.pageX = pointerObj.clientX;
1192 pointerObj.pageY = pointerObj.clientY;
1195 if (pointerObj.srcElement.offsetParent === target && document.documentMode && document.documentMode === 8 && pointerObj.type === 'mousedown') {
1196 // source element is a child piece of VML, we're in
1197 // IE8, and we've not called setCapture yet - add
1198 // the origin of the source element
1199 pointerObj.pageX += pointerObj.srcElement.offsetLeft;
1200 pointerObj.pageY += pointerObj.srcElement.offsetTop;
1201 } else if (pointerObj.srcElement !== target && !document.documentMode || document.documentMode < 8) {
1202 // source element isn't the target (most likely it's
1203 // a child piece of VML) and we're in a version of
1205 // the offsetX/Y values are unpredictable so use the
1206 // clientX/Y values and adjust by the scroll offsets
1208 // to get the document-relative coordinates (the
1211 sy = -2; // adjust for old IE's 2-pixel
1213 for (var scrollElement = pointerObj.srcElement; scrollElement !== null; scrollElement = scrollElement.parentNode) {
1214 sx += scrollElement.scrollLeft ? scrollElement.scrollLeft : 0;
1215 sy += scrollElement.scrollTop ? scrollElement.scrollTop : 0;
1218 pointerObj.pageX = pointerObj.clientX + sx;
1219 pointerObj.pageY = pointerObj.clientY + sy;
1224 var pageX = pointerObj.pageX;
1225 var pageY = pointerObj.pageY;
1227 if (theEvtObj.type.match(/(start|down)$/i)) {
1228 // clause for processing MSPointerDown, touchstart, and
1231 // refresh the document-to-target delta on start in case
1232 // the target has moved relative to document
1233 documentToTargetDelta = computeDocumentToElementDelta(target);
1235 // protect against failing to get an up or end on this
1237 if (lastXYById[pointerId]) {
1240 target: theEvtObj.target,
1241 which: theEvtObj.which,
1242 pointerId: pointerId,
1248 delete lastXYById[pointerId];
1253 prevent = startEvent({
1254 target: theEvtObj.target,
1255 which: theEvtObj.which,
1256 pointerId: pointerId,
1263 // init last page positions for this pointer
1264 lastXYById[pointerId] = {
1270 if (target.msSetPointerCapture) {
1271 target.msSetPointerCapture(pointerId);
1272 } else if (theEvtObj.type === 'mousedown' && numberOfKeys(lastXYById) === 1) {
1273 if (useSetReleaseCapture) {
1274 target.setCapture(true);
1276 document.addEventListener('mousemove', doEvent, false);
1277 document.addEventListener('mouseup', doEvent, false);
1280 } else if (theEvtObj.type.match(/move$/i)) {
1281 // clause handles mousemove, MSPointerMove, and
1284 if (lastXYById[pointerId] && !(lastXYById[pointerId].x === pageX && lastXYById[pointerId].y === pageY)) {
1285 // only extend if the pointer is down and it's not
1286 // the same as the last point
1288 if (moveEvent && prevent) {
1289 prevent = moveEvent({
1290 target: theEvtObj.target,
1291 which: theEvtObj.which,
1292 pointerId: pointerId,
1298 // update last page positions for this pointer
1299 lastXYById[pointerId].x = pageX;
1300 lastXYById[pointerId].y = pageY;
1302 } else if (lastXYById[pointerId] && theEvtObj.type.match(/(up|end|cancel)$/i)) {
1303 // clause handles up/end/cancel
1305 if (endEvent && prevent) {
1306 prevent = endEvent({
1307 target: theEvtObj.target,
1308 which: theEvtObj.which,
1309 pointerId: pointerId,
1315 // delete last page positions for this pointer
1316 delete lastXYById[pointerId];
1318 // in the Microsoft pointer model, release the capture
1320 // in the mouse model, release the capture or remove
1321 // document-level event handlers if there are no down
1323 // nothing is required for the iOS touch model because
1324 // capture is implied on touchstart
1325 if (target.msReleasePointerCapture) {
1326 target.msReleasePointerCapture(pointerId);
1327 } else if (theEvtObj.type === 'mouseup' && numberOfKeys(lastXYById) === 0) {
1328 if (useSetReleaseCapture) {
1329 target.releaseCapture();
1331 document.removeEventListener('mousemove', doEvent, false);
1332 document.removeEventListener('mouseup', doEvent, false);
1339 if (theEvtObj.preventDefault) {
1340 theEvtObj.preventDefault();
1343 if (theEvtObj.preventManipulation) {
1344 theEvtObj.preventManipulation();
1347 if (theEvtObj.preventMouseEvent) {
1348 theEvtObj.preventMouseEvent();
1353 var useSetReleaseCapture = false;
1354 // saving the settings for contentZooming and touchaction before
1356 var contentZooming, msTouchAction;
1358 this.enable = function() {
1360 if (window.navigator.msPointerEnabled) {
1361 // Microsoft pointer model
1362 target.addEventListener('MSPointerDown', doEvent, false);
1363 target.addEventListener('MSPointerMove', doEvent, false);
1364 target.addEventListener('MSPointerUp', doEvent, false);
1365 target.addEventListener('MSPointerCancel', doEvent, false);
1367 // css way to prevent panning in our target area
1368 if (typeof target.style.msContentZooming !== 'undefined') {
1369 contentZooming = target.style.msContentZooming;
1370 target.style.msContentZooming = 'none';
1373 // new in Windows Consumer Preview: css way to prevent all
1374 // built-in touch actions on our target
1375 // without this, you cannot touch draw on the element
1376 // because IE will intercept the touch events
1377 if (typeof target.style.msTouchAction !== 'undefined') {
1378 msTouchAction = target.style.msTouchAction;
1379 target.style.msTouchAction = 'none';
1381 } else if (target.addEventListener) {
1383 target.addEventListener('touchstart', doEvent, false);
1384 target.addEventListener('touchmove', doEvent, false);
1385 target.addEventListener('touchend', doEvent, false);
1386 target.addEventListener('touchcancel', doEvent, false);
1389 target.addEventListener('mousedown', doEvent, false);
1391 // mouse model with capture
1392 // rejecting gecko because, unlike ie, firefox does not send
1393 // events to target when the mouse is outside target
1394 if (target.setCapture && !window.navigator.userAgent.match(/\bGecko\b/)) {
1395 useSetReleaseCapture = true;
1397 target.addEventListener('mousemove', doEvent, false);
1398 target.addEventListener('mouseup', doEvent, false);
1400 } else if (target.attachEvent && target.setCapture) {
1401 // legacy IE mode - mouse with capture
1402 useSetReleaseCapture = true;
1403 target.attachEvent('onmousedown', function() {
1404 doEvent(window.event);
1405 window.event.returnValue = false;
1408 target.attachEvent('onmousemove', function() {
1409 doEvent(window.event);
1410 window.event.returnValue = false;
1413 target.attachEvent('onmouseup', function() {
1414 doEvent(window.event);
1415 window.event.returnValue = false;
1421 this.disable = function() {
1422 if (window.navigator.msPointerEnabled) {
1423 // Microsoft pointer model
1424 target.removeEventListener('MSPointerDown', doEvent, false);
1425 target.removeEventListener('MSPointerMove', doEvent, false);
1426 target.removeEventListener('MSPointerUp', doEvent, false);
1427 target.removeEventListener('MSPointerCancel', doEvent, false);
1429 // reset zooming to saved value
1430 if (contentZooming) {
1431 target.style.msContentZooming = contentZooming;
1434 // reset touch action setting
1435 if (msTouchAction) {
1436 target.style.msTouchAction = msTouchAction;
1438 } else if (target.removeEventListener) {
1440 target.removeEventListener('touchstart', doEvent, false);
1441 target.removeEventListener('touchmove', doEvent, false);
1442 target.removeEventListener('touchend', doEvent, false);
1443 target.removeEventListener('touchcancel', doEvent, false);
1446 target.removeEventListener('mousedown', doEvent, false);
1448 // mouse model with capture
1449 // rejecting gecko because, unlike ie, firefox does not send
1450 // events to target when the mouse is outside target
1451 if (target.setCapture && !window.navigator.userAgent.match(/\bGecko\b/)) {
1452 useSetReleaseCapture = true;
1454 target.removeEventListener('mousemove', doEvent, false);
1455 target.removeEventListener('mouseup', doEvent, false);
1457 } else if (target.detachEvent && target.setCapture) {
1458 // legacy IE mode - mouse with capture
1459 useSetReleaseCapture = true;
1460 target.detachEvent('onmousedown');
1461 target.detachEvent('onmousemove');
1462 target.detachEvent('onmouseup');
1470 .factory('GridsterDraggable', ['$document', '$timeout', '$window', 'GridsterTouch',
1471 function($document, $timeout, $window, GridsterTouch) {
1472 function GridsterDraggable($el, scope, gridster, item, itemOptions) {
1474 var elmX, elmY, elmW, elmH,
1486 realdocument = $document[0];
1488 var originalCol, originalRow;
1489 var inputTags = ['select', 'input', 'textarea', 'button'];
1491 var gridsterItemDragElement = $el[0].querySelector('[gridster-item-drag]');
1492 // console.log(gridsterItemDragElement);
1493 var isDraggableAreaDefined = gridsterItemDragElement?true:false;
1494 // console.log(isDraggableAreaDefined);
1496 function mouseDown(e) {
1499 e.target = window.event.srcElement;
1500 e.which = window.event.button;
1503 if(isDraggableAreaDefined && (!gridsterItemDragElement.contains(e.target))){
1507 if (inputTags.indexOf(e.target.nodeName.toLowerCase()) !== -1) {
1511 var $target = angular.element(e.target);
1513 // exit, if a resize handle was hit
1514 if ($target.hasClass('gridster-item-resizable-handler')) {
1518 // exit, if the target has it's own click event
1519 if ($target.attr('onclick') || $target.attr('ng-click')) {
1523 // only works if you have jQuery
1524 if ($target.closest && $target.closest('.gridster-no-drag').length) {
1530 // left mouse button
1534 // right or middle mouse button
1538 lastMouseX = e.pageX;
1539 lastMouseY = e.pageY;
1541 elmX = parseInt($el.css('left'), 10);
1542 elmY = parseInt($el.css('top'), 10);
1543 elmW = $el[0].offsetWidth;
1544 elmH = $el[0].offsetHeight;
1546 originalCol = item.col;
1547 originalRow = item.row;
1554 function mouseMove(e) {
1555 if (!$el.hasClass('gridster-item-moving') || $el.hasClass('gridster-item-resizing')) {
1559 var maxLeft = gridster.curWidth - 1;
1561 // Get the current mouse position.
1566 var diffX = mouseX - lastMouseX + mOffX;
1567 var diffY = mouseY - lastMouseY + mOffY;
1570 // Update last processed mouse positions.
1571 lastMouseX = mouseX;
1572 lastMouseY = mouseY;
1576 if (elmX + dX < minLeft) {
1577 diffX = minLeft - elmX;
1579 } else if (elmX + elmW + dX > maxLeft) {
1580 diffX = maxLeft - elmX - elmW;
1584 if (elmY + dY < minTop) {
1585 diffY = minTop - elmY;
1587 } else if (elmY + elmH + dY > maxTop) {
1588 diffY = maxTop - elmY - elmH;
1605 function mouseUp(e) {
1606 if (!$el.hasClass('gridster-item-moving') || $el.hasClass('gridster-item-resizing')) {
1617 function dragStart(event) {
1618 $el.addClass('gridster-item-moving');
1619 gridster.movingItem = item;
1621 gridster.updateHeight(item.sizeY);
1622 scope.$apply(function() {
1623 if (gridster.draggable && gridster.draggable.start) {
1624 gridster.draggable.start(event, $el, itemOptions);
1629 function drag(event) {
1630 var oldRow = item.row,
1632 hasCallback = gridster.draggable && gridster.draggable.drag,
1633 scrollSensitivity = gridster.draggable.scrollSensitivity,
1634 scrollSpeed = gridster.draggable.scrollSpeed;
1636 var row = gridster.pixelsToRows(elmY);
1637 var col = gridster.pixelsToColumns(elmX);
1639 var itemsInTheWay = gridster.getItems(row, col, item.sizeX, item.sizeY, item);
1640 var hasItemsInTheWay = itemsInTheWay.length !== 0;
1642 if (gridster.swapping === true && hasItemsInTheWay) {
1643 var boundingBoxItem = gridster.getBoundingBox(itemsInTheWay),
1644 sameSize = boundingBoxItem.sizeX === item.sizeX && boundingBoxItem.sizeY === item.sizeY,
1645 sameRow = boundingBoxItem.row === oldRow,
1646 sameCol = boundingBoxItem.col === oldCol,
1647 samePosition = boundingBoxItem.row === row && boundingBoxItem.col === col,
1648 inline = sameRow || sameCol;
1650 if (sameSize && itemsInTheWay.length === 1) {
1652 gridster.swapItems(item, itemsInTheWay[0]);
1653 } else if (inline) {
1656 } else if (boundingBoxItem.sizeX <= item.sizeX && boundingBoxItem.sizeY <= item.sizeY && inline) {
1657 var emptyRow = item.row <= row ? item.row : row + item.sizeY,
1658 emptyCol = item.col <= col ? item.col : col + item.sizeX,
1659 rowOffset = emptyRow - boundingBoxItem.row,
1660 colOffset = emptyCol - boundingBoxItem.col;
1662 for (var i = 0, l = itemsInTheWay.length; i < l; ++i) {
1663 var itemInTheWay = itemsInTheWay[i];
1665 var itemsInFreeSpace = gridster.getItems(
1666 itemInTheWay.row + rowOffset,
1667 itemInTheWay.col + colOffset,
1673 if (itemsInFreeSpace.length === 0) {
1674 gridster.putItem(itemInTheWay, itemInTheWay.row + rowOffset, itemInTheWay.col + colOffset);
1680 if (gridster.pushing !== false || !hasItemsInTheWay) {
1685 if(($window.navigator.appName === 'Microsoft Internet Explorer' && !ie8) || $window.navigator.userAgent.indexOf("Firefox")!==-1){
1686 if (event.pageY - realdocument.documentElement.scrollTop < scrollSensitivity) {
1687 realdocument.documentElement.scrollTop = realdocument.documentElement.scrollTop - scrollSpeed;
1688 } else if ($window.innerHeight - (event.pageY - realdocument.documentElement.scrollTop) < scrollSensitivity) {
1689 realdocument.documentElement.scrollTop = realdocument.documentElement.scrollTop + scrollSpeed;
1693 if (event.pageY - realdocument.body.scrollTop < scrollSensitivity) {
1694 realdocument.body.scrollTop = realdocument.body.scrollTop - scrollSpeed;
1695 } else if ($window.innerHeight - (event.pageY - realdocument.body.scrollTop) < scrollSensitivity) {
1696 realdocument.body.scrollTop = realdocument.body.scrollTop + scrollSpeed;
1702 if (event.pageX - realdocument.body.scrollLeft < scrollSensitivity) {
1703 realdocument.body.scrollLeft = realdocument.body.scrollLeft - scrollSpeed;
1704 } else if ($window.innerWidth - (event.pageX - realdocument.body.scrollLeft) < scrollSensitivity) {
1705 realdocument.body.scrollLeft = realdocument.body.scrollLeft + scrollSpeed;
1708 if (hasCallback || oldRow !== item.row || oldCol !== item.col) {
1709 scope.$apply(function() {
1711 gridster.draggable.drag(event, $el, itemOptions);
1717 function dragStop(event) {
1718 $el.removeClass('gridster-item-moving');
1719 var row = gridster.pixelsToRows(elmY);
1720 var col = gridster.pixelsToColumns(elmX);
1721 if (gridster.pushing !== false || gridster.getItems(row, col, item.sizeX, item.sizeY, item).length === 0) {
1725 gridster.movingItem = null;
1726 item.setPosition(item.row, item.col);
1728 scope.$apply(function() {
1729 if (gridster.draggable && gridster.draggable.stop) {
1730 gridster.draggable.stop(event, $el, itemOptions);
1736 var $dragHandles = null;
1737 var unifiedInputs = [];
1739 this.enable = function() {
1740 if (enabled === true) {
1744 // disable and timeout required for some template rendering
1745 $timeout(function() {
1746 // disable any existing draghandles
1747 for (var u = 0, ul = unifiedInputs.length; u < ul; ++u) {
1748 unifiedInputs[u].disable();
1752 if (gridster.draggable && gridster.draggable.handle) {
1753 $dragHandles = angular.element($el[0].querySelectorAll(gridster.draggable.handle));
1754 if ($dragHandles.length === 0) {
1755 // fall back to element if handle not found...
1762 for (var h = 0, hl = $dragHandles.length; h < hl; ++h) {
1763 unifiedInputs[h] = new GridsterTouch($dragHandles[h], mouseDown, mouseMove, mouseUp);
1764 unifiedInputs[h].enable();
1771 this.disable = function() {
1772 if (enabled === false) {
1776 // timeout to avoid race contition with the enable timeout
1777 $timeout(function() {
1779 for (var u = 0, ul = unifiedInputs.length; u < ul; ++u) {
1780 unifiedInputs[u].disable();
1788 this.toggle = function(enabled) {
1796 this.destroy = function() {
1801 return GridsterDraggable;
1805 .factory('GridsterResizable', ['GridsterTouch', function(GridsterTouch) {
1806 function GridsterResizable($el, scope, gridster, item, itemOptions) {
1808 function ResizeHandle(handleClass) {
1810 var hClass = handleClass;
1812 var elmX, elmY, elmW, elmH,
1825 var getMinHeight = function() {
1826 return (item.minSizeY ? item.minSizeY : 1) * gridster.curRowHeight - gridster.margins[0];
1828 var getMinWidth = function() {
1829 return (item.minSizeX ? item.minSizeX : 1) * gridster.curColWidth - gridster.margins[1];
1832 var originalWidth, originalHeight;
1835 function mouseDown(e) {
1838 // left mouse button
1842 // right or middle mouse button
1846 // save the draggable setting to restore after resize
1847 savedDraggable = gridster.draggable.enabled;
1848 if (savedDraggable) {
1849 gridster.draggable.enabled = false;
1850 scope.$broadcast('gridster-draggable-changed', gridster);
1853 // Get the current mouse position.
1854 lastMouseX = e.pageX;
1855 lastMouseY = e.pageY;
1857 // Record current widget dimensions
1858 elmX = parseInt($el.css('left'), 10);
1859 elmY = parseInt($el.css('top'), 10);
1860 elmW = $el[0].offsetWidth;
1861 elmH = $el[0].offsetHeight;
1863 originalWidth = item.sizeX;
1864 originalHeight = item.sizeY;
1871 function resizeStart(e) {
1872 $el.addClass('gridster-item-moving');
1873 $el.addClass('gridster-item-resizing');
1875 gridster.movingItem = item;
1877 item.setElementSizeX();
1878 item.setElementSizeY();
1879 item.setElementPosition();
1880 gridster.updateHeight(1);
1882 scope.$apply(function() {
1884 if (gridster.resizable && gridster.resizable.start) {
1885 gridster.resizable.start(e, $el, itemOptions); // options
1894 function mouseMove(e) {
1895 var maxLeft = gridster.curWidth - 1;
1897 // Get the current mouse position.
1902 var diffX = mouseX - lastMouseX + mOffX;
1903 var diffY = mouseY - lastMouseY + mOffY;
1906 // Update last processed mouse positions.
1907 lastMouseX = mouseX;
1908 lastMouseY = mouseY;
1913 if (hClass.indexOf('n') >= 0) {
1914 if (elmH - dY < getMinHeight()) {
1915 diffY = elmH - getMinHeight();
1917 } else if (elmY + dY < minTop) {
1918 diffY = minTop - elmY;
1924 if (hClass.indexOf('s') >= 0) {
1925 if (elmH + dY < getMinHeight()) {
1926 diffY = getMinHeight() - elmH;
1928 } else if (elmY + elmH + dY > maxTop) {
1929 diffY = maxTop - elmY - elmH;
1934 if (hClass.indexOf('w') >= 0) {
1935 if (elmW - dX < getMinWidth()) {
1936 diffX = elmW - getMinWidth();
1938 } else if (elmX + dX < minLeft) {
1939 diffX = minLeft - elmX;
1945 if (hClass.indexOf('e') >= 0) {
1946 if (elmW + dX < getMinWidth()) {
1947 diffX = getMinWidth() - elmW;
1949 } else if (elmX + elmW + dX > maxLeft) {
1950 diffX = maxLeft - elmX - elmW;
1959 'left': elmX + 'px',
1960 'width': elmW + 'px',
1961 'height': elmH + 'px'
1969 function mouseUp(e) {
1970 // restore draggable setting to its original state
1971 if (gridster.draggable.enabled !== savedDraggable) {
1972 gridster.draggable.enabled = savedDraggable;
1973 scope.$broadcast('gridster-draggable-changed', gridster);
1983 function resize(e) {
1984 var oldRow = item.row,
1986 oldSizeX = item.sizeX,
1987 oldSizeY = item.sizeY,
1988 hasCallback = gridster.resizable && gridster.resizable.resize;
1991 // only change column if grabbing left edge
1992 if (['w', 'nw', 'sw'].indexOf(handleClass) !== -1) {
1993 col = gridster.pixelsToColumns(elmX, false);
1997 // only change row if grabbing top edge
1998 if (['n', 'ne', 'nw'].indexOf(handleClass) !== -1) {
1999 row = gridster.pixelsToRows(elmY, false);
2002 var sizeX = item.sizeX;
2003 // only change row if grabbing left or right edge
2004 if (['n', 's'].indexOf(handleClass) === -1) {
2005 sizeX = gridster.pixelsToColumns(elmW, true);
2008 var sizeY = item.sizeY;
2009 // only change row if grabbing top or bottom edge
2010 if (['e', 'w'].indexOf(handleClass) === -1) {
2011 sizeY = gridster.pixelsToRows(elmH, true);
2014 if (gridster.pushing !== false || gridster.getItems(row, col, sizeX, sizeY, item).length === 0) {
2020 var isChanged = item.row !== oldRow || item.col !== oldCol || item.sizeX !== oldSizeX || item.sizeY !== oldSizeY;
2022 if (hasCallback || isChanged) {
2023 scope.$apply(function() {
2025 gridster.resizable.resize(e, $el, itemOptions); // options
2035 function resizeStop(e) {
2036 $el.removeClass('gridster-item-moving');
2037 $el.removeClass('gridster-item-resizing');
2039 gridster.movingItem = null;
2041 item.setPosition(item.row, item.col);
2042 item.setSizeY(item.sizeY);
2043 item.setSizeX(item.sizeX);
2045 scope.$apply(function() {
2046 if (gridster.resizable && gridster.resizable.stop) {
2047 gridster.resizable.stop(e, $el, itemOptions); // options
2056 var $dragHandle = null;
2059 this.enable = function() {
2061 $dragHandle = angular.element('<div class="gridster-item-resizable-handler handle-' + hClass + '"></div>');
2062 $el.append($dragHandle);
2065 unifiedInput = new GridsterTouch($dragHandle[0], mouseDown, mouseMove, mouseUp);
2066 unifiedInput.enable();
2069 this.disable = function() {
2071 $dragHandle.remove();
2075 unifiedInput.disable();
2076 unifiedInput = undefined;
2079 this.destroy = function() {
2085 var handlesOpts = gridster.resizable.handles;
2086 if (typeof handlesOpts === 'string') {
2087 handlesOpts = gridster.resizable.handles.split(',');
2089 var enabled = false;
2091 for (var c = 0, l = handlesOpts.length; c < l; c++) {
2092 handles.push(new ResizeHandle(handlesOpts[c]));
2095 this.enable = function() {
2099 for (var c = 0, l = handles.length; c < l; c++) {
2100 handles[c].enable();
2105 this.disable = function() {
2109 for (var c = 0, l = handles.length; c < l; c++) {
2110 handles[c].disable();
2115 this.toggle = function(enabled) {
2123 this.destroy = function() {
2124 for (var c = 0, l = handles.length; c < l; c++) {
2125 handles[c].destroy();
2129 return GridsterResizable;
2132 .factory('gridsterDebounce', function() {
2133 return function gridsterDebounce(func, wait, immediate) {
2138 var later = function() {
2141 func.apply(context, args);
2144 var callNow = immediate && !timeout;
2145 clearTimeout(timeout);
2146 timeout = setTimeout(later, wait);
2148 func.apply(context, args);
2155 * GridsterItem directive
2158 * @param GridsterDraggable
2159 * @param GridsterResizable
2160 * @param gridsterDebounce
2162 .directive('gridsterItem', ['$parse', 'GridsterDraggable', 'GridsterResizable', 'gridsterDebounce',
2163 function($parse, GridsterDraggable, GridsterResizable, gridsterDebounce) {
2167 controller: 'GridsterItemCtrl',
2168 controllerAs: 'gridsterItem',
2169 require: ['^gridster', 'gridsterItem'],
2170 link: function(scope, $el, attrs, controllers) {
2171 var optionsKey = attrs.gridsterItem,
2174 var gridster = controllers[0],
2175 item = controllers[1];
2177 scope.gridster = gridster;
2180 // bind the item's position properties
2181 // options can be an object specified by
2182 // gridster-item="object"
2183 // or the options can be the element html attributes object
2185 var $optionsGetter = $parse(optionsKey);
2186 options = $optionsGetter(scope) || {};
2187 if (!options && $optionsGetter.assign) {
2198 $optionsGetter.assign(scope, options);
2204 item.init($el, gridster);
2206 $el.addClass('gridster-item');
2208 var aspects = ['minSizeX', 'maxSizeX', 'minSizeY', 'maxSizeY', 'sizeX', 'sizeY', 'row', 'col'],
2211 var expressions = [];
2212 var aspectFn = function(aspect) {
2214 if (typeof options[aspect] === 'string') {
2215 // watch the expression in the scope
2216 expression = options[aspect];
2217 } else if (typeof options[aspect.toLowerCase()] === 'string') {
2218 // watch the expression in the scope
2219 expression = options[aspect.toLowerCase()];
2220 } else if (optionsKey) {
2221 // watch the expression on the options object in the
2223 expression = optionsKey + '.' + aspect;
2227 expressions.push('"' + aspect + '":' + expression);
2228 $getters[aspect] = $parse(expression);
2231 var val = $getters[aspect](scope);
2232 if (typeof val === 'number') {
2237 for (var i = 0, l = aspects.length; i < l; ++i) {
2238 aspectFn(aspects[i]);
2241 var watchExpressions = '{' + expressions.join(',') + '}';
2243 // when the value changes externally, update the internal
2245 scope.$watchCollection(watchExpressions, function(newVals, oldVals) {
2246 for (var aspect in newVals) {
2247 var newVal = newVals[aspect];
2248 var oldVal = oldVals[aspect];
2249 if (oldVal === newVal) {
2252 newVal = parseInt(newVal, 10);
2253 if (!isNaN(newVal)) {
2254 item[aspect] = newVal;
2259 function positionChanged() {
2260 // call setPosition so the element and gridster
2261 // controller are updated
2262 item.setPosition(item.row, item.col);
2264 // when internal item position changes, update
2265 // externally bound values
2266 if ($getters.row && $getters.row.assign) {
2267 $getters.row.assign(scope, item.row);
2269 if ($getters.col && $getters.col.assign) {
2270 $getters.col.assign(scope, item.col);
2273 scope.$watch(function() {
2274 return item.row + ',' + item.col;
2275 }, positionChanged);
2277 function sizeChanged() {
2278 var changedX = item.setSizeX(item.sizeX, true);
2279 if (changedX && $getters.sizeX && $getters.sizeX.assign) {
2280 $getters.sizeX.assign(scope, item.sizeX);
2282 var changedY = item.setSizeY(item.sizeY, true);
2283 if (changedY && $getters.sizeY && $getters.sizeY.assign) {
2284 $getters.sizeY.assign(scope, item.sizeY);
2287 if (changedX || changedY) {
2288 item.gridster.moveOverlappingItems(item);
2289 gridster.layoutChanged();
2290 scope.$broadcast('gridster-item-resized', item);
2294 scope.$watch(function() {
2295 return item.sizeY + ',' + item.sizeX + ',' + item.minSizeX + ',' + item.maxSizeX + ',' + item.minSizeY + ',' + item.maxSizeY;
2298 var draggable = new GridsterDraggable($el, scope, gridster, item, options);
2299 var resizable = new GridsterResizable($el, scope, gridster, item, options);
2301 var updateResizable = function() {
2302 resizable.toggle(!gridster.isMobile && gridster.resizable && gridster.resizable.enabled);
2306 var updateDraggable = function() {
2307 draggable.toggle(!gridster.isMobile && gridster.draggable && gridster.draggable.enabled);
2311 scope.$on('gridster-draggable-changed', updateDraggable);
2312 scope.$on('gridster-resizable-changed', updateResizable);
2313 scope.$on('gridster-resized', updateResizable);
2314 scope.$on('gridster-mobile-changed', function() {
2319 function whichTransitionEvent() {
2320 var el = document.createElement('div');
2322 'transition': 'transitionend',
2323 'OTransition': 'oTransitionEnd',
2324 'MozTransition': 'transitionend',
2325 'WebkitTransition': 'webkitTransitionEnd'
2327 for (var t in transitions) {
2328 if (el.style[t] !== undefined) {
2329 return transitions[t];
2334 var debouncedTransitionEndPublisher = gridsterDebounce(function() {
2335 scope.$apply(function() {
2336 scope.$broadcast('gridster-item-transition-end', item);
2340 if(whichTransitionEvent()){ // check for IE8, as it
2341 // evaluates to null
2342 $el.on(whichTransitionEvent(), debouncedTransitionEndPublisher);
2345 scope.$broadcast('gridster-item-initialized', item);
2347 return scope.$on('$destroy', function() {
2349 resizable.destroy();
2350 draggable.destroy();
2354 gridster.removeItem(item);
2366 .directive('gridsterNoDrag', function() {
2369 link: function(scope, $element) {
2370 $element.addClass('gridster-no-drag');