<!DOCTYPE html>
<html class="no-js">
  <head>
    <meta charset="utf-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <title></title>
    <meta name="description" content="" />
    <meta name="viewport" content="width=device-width" />
    <script data-require="angular.js@*" data-semver="1.2.15" src="//code.angularjs.org/1.2.15/angular.min.js"></script>
    <script type="text/javascript" src="angular-gridster.js"></script>
    <link rel="stylesheet" href="gridster.css" />
    <link rel="stylesheet" href="main.css" />
  </head>

  <body ng-app="app" id="ng-app">
    <div class="container" ng-controller="MainCtrl" ng-include=" 'main.html' "></div>
    <script src="app.js"></script>
    <script src="main.js"></script>
  </body>

</html>
/* Styles go here */

'use strict';

angular.module('app')

.controller('MainCtrl', function($scope) {

	$scope.gridsterOpts = {
		margins: [20, 20]
	};

	// these map directly to gridsterItem options
	$scope.standardItems = [
		{ sizeX: 2, sizeY: 1 },
		{ sizeX: 2, sizeY: 2 },
		{ sizeX: 1, sizeY: 1 },
		{ sizeX: 1, sizeY: 1 },
		{ sizeX: 2, sizeY: 1 },
		{ sizeX: 1, sizeY: 1 },
		{ sizeX: 1, sizeY: 2 },
		{ sizeX: 1, sizeY: 1 },
		{ sizeX: 2, sizeY: 1 },
		{ sizeX: 1, sizeY: 1 },
		{ sizeX: 1, sizeY: 1 },
		{ sizeX: 2, sizeY: 1 },
		{ sizeX: 2, sizeY: 2 },
		{ sizeX: 1, sizeY: 1 },
		{ sizeX: 1, sizeY: 1 },
		{ sizeX: 2, sizeY: 1 },
		{ sizeX: 1, sizeY: 1 },
		{ sizeX: 1, sizeY: 2 },
		{ sizeX: 1, sizeY: 1 },
		{ sizeX: 2, sizeY: 1 },
		{ sizeX: 1, sizeY: 1 },
		{ sizeX: 1, sizeY: 1 }
	];

  $scope.map = {
    sizeX: 'item.sizeX',
    sizeY: 'item.sizeY'
  }

});
'use strict';

angular.module('app', [
	'gridster'
])

.directive('integer', function(){
    return {
        require: 'ngModel',
        link: function(scope, ele, attr, ctrl){
            ctrl.$parsers.unshift(function(viewValue){
				if (viewValue === '' || viewValue === null || typeof viewValue === 'undefined') {
					return null;
				}
                return parseInt(viewValue, 10);
            });
        }
    };
})

;

<h2>Standard Items</h2>
<p>
	Each item provides its own dimensions and position using the standard fields: { row: row, col: col, sizeX: sizeX, sizeY: sizeY }.
</p>
<div gridster="gridsterOpts">
	<ul>
		<li gridster-item="map" ng-repeat="item in standardItems">
			<input type="text" integer ng-model="item.row" size="1" />
			,
			<input type="text" integer ng-model="item.col" size="1" />
			<br />
			<input type="text" integer ng-model="item.sizeX" size="1" />
			x
			<input type="text" integer ng-model="item.sizeY" size="1" />
		</li>
	</ul>
</div>
/*! gridster.js - v0.2.1 - 2013-10-28
* http://gridster.net/
* Copyright (c) 2013 ducksboard; Licensed MIT */

.gridster {
	position: relative;
	margin: auto;
	background: rgba(0, 0, 0, .1);
	height: 0;
}

.gridster-loaded {
	-webkit-transition: height .3s;
}

.gridster-loaded {
    -moz-transition: height .3s;
    -o-transition: height .3s;
    transition: height .3s;
}

.gridster ul {
	margin: 0;
	list-style: none;
	padding: 0;
}

.gridster .gridster-item {
	-webkit-box-sizing: border-box;
}

.gridster .gridster-item {
	-moz-box-sizing: border-box;
	box-sizing: border-box;
	list-style: none;
	text-align: center;
	list-style: none;
	z-index: 2;
	position: absolute;
	display: none;
}

.gridster-loaded .gridster-item  {
	display: block;
}

.gridster-loaded .gridster-item {
	-webkit-transition: opacity .3s, left .3s, top .3s, width .3s, height .3s;
}

.gridster-loaded .gridster-item {
    -moz-transition: opacity .3s, left .3s, top .3s, width .3s, height .3s;
    -o-transition: opacity .3s, left .3s, top .3s, width .3s, height .3s;
    transition: opacity .3s, left .3s, top .3s, width .3s, height .3s;
}

.gridster-mobile .gridster-item {
	position: static;
	float: none;
}

.gridster .gridster-preview-holder {
	display: none;
    z-index: 1;
    position: absolute;
    background-color: #fff;
    border-color: #fff;
    opacity: 0.2;
}

.gridster .gridster-item-moving,
.gridster .gridster-preview-holder {
	-webkit-transition: opacity 0s, left 0s, top 0s, width 0s, height 0s;
}

.gridster .gridster-item-moving,
.gridster .gridster-preview-holder {
    -moz-transition: opacity 0s, left 0s, top 0s, width 0s, height 0s;
    -o-transition: opacity 0s, left 0s, top 0s, width 0s, height 0s;
    transition: opacity 0s, left 0s, top 0s, width 0s, height 0s;
}

.gridster .gridster-item-moving {
	z-index: 3;
}
body {
	margin: 0;
	padding: 0;
	background: #004756;
	font-family: 'Helvetica Neue', Arial, sans-serif;
	color: #fff;
}

input {
	font-family: 'Helvetica Neue', Arial, sans-serif;
	font-size: 14px;
	padding: 4px;
}

.container {
	margin: auto;
	max-width: 1000px;
}

.gridster .gridster-item {
	-webkit-box-shadow: 0 0 5px rgba(0, 0, 0, 0.3);
}

.gridster .gridster-item {
	-moz-box-shadow: 0 0 5px rgba(0, 0, 0, 0.3);
	box-shadow: 0 0 5px rgba(0, 0, 0, 0.3);
	color: #004756;
}

.gridster .gridster-item {
	background: #ffffff;
	padding: 10px;
}
(function(angular) {

	'use strict';

	angular.module('gridster', [])

	.constant('gridsterConfig', {
		columns: 6, // number of columns in the grid
		pushing: true, // whether to push other items out of the way
		floating: true, // whether to automatically float items up so they stack
		swapping: false, // whether or not to have items switch places instead of push down if they are the same size
		width: 'auto', // width of the grid. "auto" will expand the grid to its parent container
		colWidth: 'auto', // width of grid columns. "auto" will divide the width of the grid evenly among the columns
		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.
		margins: [10, 10], // margins in between grid items
		outerMargin: true,
		isMobile: false, // toggle mobile view
		mobileBreakPoint: 600, // width threshold to toggle mobile mode
		mobileModeEnabled: true, // whether or not to toggle mobile mode when screen width is less than mobileBreakPoint
		minColumns: 1, // minimum amount of columns the grid can scale down to
		minRows: 1, // minimum amount of rows to show if the grid is empty
		maxRows: 100, // maximum amount of rows in the grid
		defaultSizeX: 2, // default width of an item in columns
		defaultSizeY: 1, // default height of an item in rows
		minSizeX: 1, // minimum column width of an item
		maxSizeX: null, // maximum column width of an item
		minSizeY: 1, // minumum row height of an item
		maxSizeY: null, // maximum row height of an item
		saveGridItemCalculatedHeightInMobile: false, // grid item height in mobile display. true- to use the calculated height by sizeY given
		resizable: { // options to pass to resizable handler
			enabled: true,
			handles: ['s', 'e', 'n', 'w', 'se', 'ne', 'sw', 'nw']
		},
		draggable: { // options to pass to draggable handler
			enabled: true,
			scrollSensitivity: 20, //Distance in pixels from the edge of the viewport after which the viewport should scroll, relative to pointer
			scrollSpeed: 20 //Speed at which the window should scroll once the mouse pointer gets within scrollSensitivity distance
		}
	})

	.controller('GridsterCtrl', ['gridsterConfig',
		function(gridsterConfig) {

			/**
			 * Create options from gridsterConfig constant
			 */
			angular.extend(this, gridsterConfig);

			this.resizable = angular.extend({}, gridsterConfig.resizable || {});
			this.draggable = angular.extend({}, gridsterConfig.draggable || {});

			/**
			 * A positional array of the items in the grid
			 */
			this.grid = [];

			/**
			 * Clean up after yourself
			 */
			this.destroy = function() {
				if (this.grid) {
					this.grid.length = 0;
					this.grid = null;
				}
			};

			/**
			 * Overrides default options
			 *
			 * @param {object} options The options to override
			 */
			this.setOptions = function(options) {
				if (!options) {
					return;
				}

				options = angular.extend({}, options);

				// all this to avoid using jQuery...
				if (options.draggable) {
					angular.extend(this.draggable, options.draggable);
					delete(options.draggable);
				}
				if (options.resizable) {
					angular.extend(this.resizable, options.resizable);
					delete(options.resizable);
				}

				angular.extend(this, options);

				if (!this.margins || this.margins.length !== 2) {
					this.margins = [0, 0];
				} else {
					for (var x = 0, l = this.margins.length; x < l; ++x) {
						this.margins[x] = parseInt(this.margins[x], 10);
						if (isNaN(this.margins[x])) {
							this.margins[x] = 0;
						}
					}
				}
			};

			/**
			 * Check if item can occupy a specified position in the grid
			 *
			 * @param {object} item The item in question
			 * @param {number} row The row index
			 * @param {number} column The column index
			 * @returns {boolean} True if if item fits
			 */
			this.canItemOccupy = function(item, row, column) {
				return row > -1 && column > -1 && item.sizeX + column <= this.columns && item.sizeY + row <= this.maxRows;
			};

			/**
			 * Set the item in the first suitable position
			 *
			 * @param {object} item The item to insert
			 */
			this.autoSetItemPosition = function(item) {
				// walk through each row and column looking for a place it will fit
				for (var rowIndex = 0; rowIndex < this.maxRows; ++rowIndex) {
					for (var colIndex = 0; colIndex < this.columns; ++colIndex) {
						// only insert if position is not already taken and it can fit
						var items = this.getItems(rowIndex, colIndex, item.sizeX, item.sizeY, item);
						if (items.length === 0 && this.canItemOccupy(item, rowIndex, colIndex)) {
							this.putItem(item, rowIndex, colIndex);
							return;
						}
					}
				}
				throw new Error('Unable to place item!');
			};

			/**
			 * Gets items at a specific coordinate
			 *
			 * @param {number} row
			 * @param {number} column
			 * @param {number} sizeX
			 * @param {number} sizeY
			 * @param {array} excludeItems An array of items to exclude from selection
			 * @returns {array} Items that match the criteria
			 */
			this.getItems = function(row, column, sizeX, sizeY, excludeItems) {
				var items = [];
				if (!sizeX || !sizeY) {
					sizeX = sizeY = 1;
				}
				if (excludeItems && !(excludeItems instanceof Array)) {
					excludeItems = [excludeItems];
				}
				for (var h = 0; h < sizeY; ++h) {
					for (var w = 0; w < sizeX; ++w) {
						var item = this.getItem(row + h, column + w, excludeItems);
						if (item && (!excludeItems || excludeItems.indexOf(item) === -1) && items.indexOf(item) === -1) {
							items.push(item);
						}
					}
				}
				return items;
			};

			/**
			 * Removes an item from the grid
			 *
			 * @param {object} item
			 */
			this.removeItem = function(item) {
				for (var rowIndex = 0, l = this.grid.length; rowIndex < l; ++rowIndex) {
					var columns = this.grid[rowIndex];
					if (!columns) {
						continue;
					}
					var index = columns.indexOf(item);
					if (index !== -1) {
						columns[index] = null;
						break;
					}
				}
				this.floatItemsUp();
				this.updateHeight();
			};

			/**
			 * Returns the item at a specified coordinate
			 *
			 * @param {number} row
			 * @param {number} column
			 * @param {array} excludeitems Items to exclude from selection
			 * @returns {object} The matched item or null
			 */
			this.getItem = function(row, column, excludeItems) {
				if (excludeItems && !(excludeItems instanceof Array)) {
					excludeItems = [excludeItems];
				}
				var sizeY = 1;
				while (row > -1) {
					var sizeX = 1,
						col = column;
					while (col > -1) {
						var items = this.grid[row];
						if (items) {
							var item = items[col];
							if (item && (!excludeItems || excludeItems.indexOf(item) === -1) && item.sizeX >= sizeX && item.sizeY >= sizeY) {
								return item;
							}
						}
						++sizeX;
						--col;
					}
					--row;
					++sizeY;
				}
				return null;
			};

			/**
			 * Insert an array of items into the grid
			 *
			 * @param {array} items An array of items to insert
			 */
			this.putItems = function(items) {
				for (var i = 0, l = items.length; i < l; ++i) {
					this.putItem(items[i]);
				}
			};

			/**
			 * Insert a single item into the grid
			 *
			 * @param {object} item The item to insert
			 * @param {number} row (Optional) Specifies the items row index
			 * @param {number} column (Optional) Specifies the items column index
			 * @param {array} ignoreItems
			 */
			this.putItem = function(item, row, column, ignoreItems) {
				if (typeof row === 'undefined' || row === null) {
					row = item.row;
					column = item.col;
					if (typeof row === 'undefined' || row === null) {
						this.autoSetItemPosition(item);
						return;
					}
				}
				if (!this.canItemOccupy(item, row, column)) {
					column = Math.min(this.columns - item.sizeX, Math.max(0, column));
					row = Math.min(this.maxRows - item.sizeY, Math.max(0, row));
				}

				if (item && item.oldRow !== null && typeof item.oldRow !== 'undefined') {
					if (item.oldRow === row && item.oldColumn === column) {
						item.row = row;
						item.col = column;
						return;
					} else {
						// remove from old position
						var oldRow = this.grid[item.oldRow];
						if (oldRow && oldRow[item.oldColumn] === item) {
							delete oldRow[item.oldColumn];
						}
					}
				}

				item.oldRow = item.row = row;
				item.oldColumn = item.col = column;

				this.moveOverlappingItems(item, ignoreItems);

				if (!this.grid[row]) {
					this.grid[row] = [];
				}
				this.grid[row][column] = item;
			};

			/**
			 * Trade row and column if item1 with item2
			 *
			 * @param {object} item1
			 * @param {object} item2
			 */
			this.swapItems = function(item1, item2) {
				this.grid[item1.row][item1.col] = item2;
				this.grid[item2.row][item2.col] = item1;

				var item1Row = item1.row;
				var item1Col = item1.col;
				item1.row = item2.row;
				item1.col = item2.col;
				item2.row = item1Row;
				item2.col = item1Col;
			};

			/**
			 * Prevents items from being overlapped
			 *
			 * @param {object} item The item that should remain
			 * @param {array} ignoreItems
			 */
			this.moveOverlappingItems = function(item, ignoreItems) {
				if (ignoreItems) {
					if (ignoreItems.indexOf(item) === -1) {
						ignoreItems = ignoreItems.slice(0);
						ignoreItems.push(item);
					}
				} else {
					ignoreItems = [item];
				}
				var overlappingItems = this.getItems(
					item.row,
					item.col,
					item.sizeX,
					item.sizeY,
					ignoreItems
				);
				this.moveItemsDown(overlappingItems, item.row + item.sizeY, ignoreItems);
			};

			/**
			 * Moves an array of items to a specified row
			 *
			 * @param {array} items The items to move
			 * @param {number} newRow The target row
			 * @param {array} ignoreItems
			 */
			this.moveItemsDown = function(items, newRow, ignoreItems) {
				if (!items || items.length === 0) {
					return;
				}
				items.sort(function(a, b) {
					return a.row - b.row;
				});
				ignoreItems = ignoreItems ? ignoreItems.slice(0) : [];
				var topRows = {},
					item, i, l;
				// calculate the top rows in each column
				for (i = 0, l = items.length; i < l; ++i) {
					item = items[i];
					var topRow = topRows[item.col];
					if (typeof topRow === 'undefined' || item.row < topRow) {
						topRows[item.col] = item.row;
					}
				}
				// move each item down from the top row in its column to the row
				for (i = 0, l = items.length; i < l; ++i) {
					item = items[i];
					var rowsToMove = newRow - topRows[item.col];
					this.moveItemDown(item, item.row + rowsToMove, ignoreItems);
					ignoreItems.push(item);
				}
			};

			this.moveItemDown = function(item, newRow, ignoreItems) {
				if (item.row >= newRow) {
					return;
				}
				while (item.row < newRow) {
					++item.row;
					this.moveOverlappingItems(item, ignoreItems);
				}
				this.putItem(item, item.row, item.col, ignoreItems);
			};

			/**
			 * Moves all items up as much as possible
			 */
			this.floatItemsUp = function() {
				if (this.floating === false) {
					return;
				}
				for (var rowIndex = 0, l = this.grid.length; rowIndex < l; ++rowIndex) {
					var columns = this.grid[rowIndex];
					if (!columns) {
						continue;
					}
					for (var colIndex = 0, len = columns.length; colIndex < len; ++colIndex) {
						if (columns[colIndex]) {
							this.floatItemUp(columns[colIndex]);
						}
					}
				}
			};

			/**
			 * Float an item up to the most suitable row
			 *
			 * @param {object} item The item to move
			 */
			this.floatItemUp = function(item) {
				if (this.floating === false) {
					return;
				}
				var colIndex = item.col,
					sizeY = item.sizeY,
					sizeX = item.sizeX,
					bestRow = null,
					bestColumn = null,
					rowIndex = item.row - 1;

				while (rowIndex > -1) {
					var items = this.getItems(rowIndex, colIndex, sizeX, sizeY, item);
					if (items.length !== 0) {
						break;
					}
					bestRow = rowIndex;
					bestColumn = colIndex;
					--rowIndex;
				}
				if (bestRow !== null) {
					this.putItem(item, bestRow, bestColumn);
				}
			};

			/**
			 * Update gridsters height
			 *
			 * @param {number} plus (Optional) Additional height to add
			 */
			this.updateHeight = function(plus) {
				var maxHeight = this.minRows;
				plus = plus || 0;
				for (var rowIndex = this.grid.length; rowIndex >= 0; --rowIndex) {
					var columns = this.grid[rowIndex];
					if (!columns) {
						continue;
					}
					for (var colIndex = 0, len = columns.length; colIndex < len; ++colIndex) {
						if (columns[colIndex]) {
							maxHeight = Math.max(maxHeight, rowIndex + plus + columns[colIndex].sizeY);
						}
					}
				}
				this.gridHeight = this.maxRows - maxHeight > 0 ? Math.min(this.maxRows, maxHeight) : Math.max(this.maxRows, maxHeight);
			};

			/**
			 * Returns the number of rows that will fit in given amount of pixels
			 *
			 * @param {number} pixels
			 * @param {boolean} ceilOrFloor (Optional) Determines rounding method
			 */
			this.pixelsToRows = function(pixels, ceilOrFloor) {
				if (ceilOrFloor === true) {
					return Math.ceil(pixels / this.curRowHeight);
				} else if (ceilOrFloor === false) {
					return Math.floor(pixels / this.curRowHeight);
				}

				return Math.round(pixels / this.curRowHeight);
			};

			/**
			 * Returns the number of columns that will fit in a given amount of pixels
			 *
			 * @param {number} pixels
			 * @param {boolean} ceilOrFloor (Optional) Determines rounding method
			 * @returns {number} The number of columns
			 */
			this.pixelsToColumns = function(pixels, ceilOrFloor) {
				if (ceilOrFloor === true) {
					return Math.ceil(pixels / this.curColWidth);
				} else if (ceilOrFloor === false) {
					return Math.floor(pixels / this.curColWidth);
				}

				return Math.round(pixels / this.curColWidth);
			};

			// unified input handling
			// adopted from a msdn blogs sample
			this.unifiedInput = function(target, startEvent, moveEvent, endEvent) {
				var lastXYById = {};

				//  Opera doesn't have Object.keys so we use this wrapper
				var numberOfKeys = function(theObject) {
					if (Object.keys) {
						return Object.keys(theObject).length;
					}

					var n = 0,
						key;
					for (key in theObject) {
						++n;
					}

					return n;
				};

				//  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
				var computeDocumentToElementDelta = function(theElement) {
					var elementLeft = 0;
					var elementTop = 0;

					for (var offsetElement = theElement; offsetElement != null; offsetElement = offsetElement.offsetParent) {
						//  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
						//  this may not be a general solution to IE7's problem with offsetLeft/offsetParent
						if (navigator.userAgent.match(/\bMSIE\b/) &&
							(!document.documentMode || document.documentMode < 8) &&
							offsetElement.currentStyle.position === 'relative' && offsetElement.offsetParent && offsetElement.offsetParent.currentStyle.position === 'relative' && offsetElement.offsetLeft === offsetElement.offsetParent.offsetLeft) {
							// add only the top
							elementTop += offsetElement.offsetTop;
						} else {
							elementLeft += offsetElement.offsetLeft;
							elementTop += offsetElement.offsetTop;
						}
					}

					return {
						x: elementLeft,
						y: elementTop
					};
				};

				//  cache the delta from the document to our event target (reinitialized each mousedown/MSPointerDown/touchstart)
				var documentToTargetDelta = computeDocumentToElementDelta(target);

				//  common event handler for the mouse/pointer/touch models and their down/start, move, up/end, and cancel events
				var doEvent = function(theEvtObj) {

					if (theEvtObj.type === 'mousemove' && numberOfKeys(lastXYById) === 0) {
						return;
					}

					var prevent = true;

					var pointerList = theEvtObj.changedTouches ? theEvtObj.changedTouches : [theEvtObj];
					for (var i = 0; i < pointerList.length; ++i) {
						var pointerObj = pointerList[i];
						var pointerId = (typeof pointerObj.identifier !== 'undefined') ? pointerObj.identifier : (typeof pointerObj.pointerId !== 'undefined') ? pointerObj.pointerId : 1;

						//  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)
						if (typeof pointerObj.pageX === 'undefined') {
							//  initialize assuming our source element is our target
							pointerObj.pageX = pointerObj.offsetX + documentToTargetDelta.x;
							pointerObj.pageY = pointerObj.offsetY + documentToTargetDelta.y;

							if (pointerObj.srcElement.offsetParent === target && document.documentMode && document.documentMode === 8 && pointerObj.type === 'mousedown') {
								//  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
								pointerObj.pageX += pointerObj.srcElement.offsetLeft;
								pointerObj.pageY += pointerObj.srcElement.offsetTop;
							} else if (pointerObj.srcElement !== target && !document.documentMode || document.documentMode < 8) {
								//  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 -
								//  the offsetX/Y values are unpredictable so use the clientX/Y values and adjust by the scroll offsets of its parents
								//  to get the document-relative coordinates (the same as pageX/Y)
								var sx = -2,
									sy = -2; // adjust for old IE's 2-pixel border
								for (var scrollElement = pointerObj.srcElement; scrollElement !== null; scrollElement = scrollElement.parentNode) {
									sx += scrollElement.scrollLeft ? scrollElement.scrollLeft : 0;
									sy += scrollElement.scrollTop ? scrollElement.scrollTop : 0;
								}

								pointerObj.pageX = pointerObj.clientX + sx;
								pointerObj.pageY = pointerObj.clientY + sy;
							}
						}


						var pageX = pointerObj.pageX;
						var pageY = pointerObj.pageY;

						if (theEvtObj.type.match(/(start|down)$/i)) {
							//  clause for processing MSPointerDown, touchstart, and mousedown

							//  refresh the document-to-target delta on start in case the target has moved relative to document
							documentToTargetDelta = computeDocumentToElementDelta(target);

							//  protect against failing to get an up or end on this pointerId
							if (lastXYById[pointerId]) {
								if (endEvent) {
									endEvent({
										target: theEvtObj.target,
										which: theEvtObj.which,
										pointerId: pointerId,
										pageX: pageX,
										pageY: pageY,
									});
								}

								delete lastXYById[pointerId];
							}

							if (startEvent) {
								if (prevent) {
									prevent = startEvent({
										target: theEvtObj.target,
										which: theEvtObj.which,
										pointerId: pointerId,
										pageX: pageX,
										pageY: pageY
									});
								}
							}

							//  init last page positions for this pointer
							lastXYById[pointerId] = {
								x: pageX,
								y: pageY
							};

							// IE pointer model
							if (target.msSetPointerCapture) {
								target.msSetPointerCapture(pointerId);
							} else if (theEvtObj.type === 'mousedown' && numberOfKeys(lastXYById) === 1) {
								if (useSetReleaseCapture) {
									target.setCapture(true);
								} else {
									document.addEventListener('mousemove', doEvent, false);
									document.addEventListener('mouseup', doEvent, false);
								}
							}
						} else if (theEvtObj.type.match(/move$/i)) {
							//  clause handles mousemove, MSPointerMove, and touchmove

							if (lastXYById[pointerId] && !(lastXYById[pointerId].x === pageX && lastXYById[pointerId].y === pageY)) {
								//  only extend if the pointer is down and it's not the same as the last point

								if (moveEvent && prevent) {
									prevent = moveEvent({
										target: theEvtObj.target,
										which: theEvtObj.which,
										pointerId: pointerId,
										pageX: pageX,
										pageY: pageY
									});
								}

								//  update last page positions for this pointer
								lastXYById[pointerId].x = pageX;
								lastXYById[pointerId].y = pageY;
							}
						} else if (lastXYById[pointerId] && theEvtObj.type.match(/(up|end|cancel)$/i)) {
							//  clause handles up/end/cancel

							if (endEvent && prevent) {
								prevent = endEvent({
									target: theEvtObj.target,
									which: theEvtObj.which,
									pointerId: pointerId,
									pageX: pageX,
									pageY: pageY
								});
							}

							//  delete last page positions for this pointer
							delete lastXYById[pointerId];

							//  in the Microsoft pointer model, release the capture for this pointer
							//  in the mouse model, release the capture or remove document-level event handlers if there are no down points
							//  nothing is required for the iOS touch model because capture is implied on touchstart
							if (target.msReleasePointerCapture) {
								target.msReleasePointerCapture(pointerId);
							} else if (theEvtObj.type === 'mouseup' && numberOfKeys(lastXYById) === 0) {
								if (useSetReleaseCapture) {
									target.releaseCapture();
								} else {
									document.removeEventListener('mousemove', doEvent, false);
									document.removeEventListener('mouseup', doEvent, false);
								}
							}
						}
					}

					if (prevent) {
						if (theEvtObj.preventDefault) {
							theEvtObj.preventDefault();
						}

						if (theEvtObj.preventManipulation) {
							theEvtObj.preventManipulation();
						}

						if (theEvtObj.preventMouseEvent) {
							theEvtObj.preventMouseEvent();
						}
					}
				};

				var useSetReleaseCapture = false;
				// saving the settings for contentZooming and touchaction before activation
				var contentZooming, msTouchAction;

				this.enable = function() {

					if (window.navigator.msPointerEnabled) {
						//  Microsoft pointer model
						target.addEventListener('MSPointerDown', doEvent, false);
						target.addEventListener('MSPointerMove', doEvent, false);
						target.addEventListener('MSPointerUp', doEvent, false);
						target.addEventListener('MSPointerCancel', doEvent, false);

						//  css way to prevent panning in our target area
						if (typeof target.style.msContentZooming !== 'undefined') {
							contentZooming = target.style.msContentZooming;
							target.style.msContentZooming = 'none';
						}

						//  new in Windows Consumer Preview: css way to prevent all built-in touch actions on our target
						//  without this, you cannot touch draw on the element because IE will intercept the touch events
						if (typeof target.style.msTouchAction !== 'undefined') {
							msTouchAction = target.style.msTouchAction;
							target.style.msTouchAction = 'none';
						}
					} else if (target.addEventListener) {
						//  iOS touch model
						target.addEventListener('touchstart', doEvent, false);
						target.addEventListener('touchmove', doEvent, false);
						target.addEventListener('touchend', doEvent, false);
						target.addEventListener('touchcancel', doEvent, false);

						//  mouse model
						target.addEventListener('mousedown', doEvent, false);

						//  mouse model with capture
						//  rejecting gecko because, unlike ie, firefox does not send events to target when the mouse is outside target
						if (target.setCapture && !window.navigator.userAgent.match(/\bGecko\b/)) {
							useSetReleaseCapture = true;

							target.addEventListener('mousemove', doEvent, false);
							target.addEventListener('mouseup', doEvent, false);
						}
					} else if (target.attachEvent && target.setCapture) {
						//  legacy IE mode - mouse with capture
						useSetReleaseCapture = true;
						target.attachEvent('onmousedown', function() {
							doEvent(window.event);
							window.event.returnValue = false;
							return false;
						});
						target.attachEvent('onmousemove', function() {
							doEvent(window.event);
							window.event.returnValue = false;
							return false;
						});
						target.attachEvent('onmouseup', function() {
							doEvent(window.event);
							window.event.returnValue = false;
							return false;
						});
					}
				};

				this.disable = function() {
					if (window.navigator.msPointerEnabled) {
						//  Microsoft pointer model
						target.removeEventListener('MSPointerDown', doEvent, false);
						target.removeEventListener('MSPointerMove', doEvent, false);
						target.removeEventListener('MSPointerUp', doEvent, false);
						target.removeEventListener('MSPointerCancel', doEvent, false);

						//  reset zooming to saved value
						if (contentZooming) {
							target.style.msContentZooming = contentZooming;
						}

						// reset touch action setting
						if (msTouchAction) {
							target.style.msTouchAction = msTouchAction;
						}
					} else if (target.removeEventListener) {
						//  iOS touch model
						target.removeEventListener('touchstart', doEvent, false);
						target.removeEventListener('touchmove', doEvent, false);
						target.removeEventListener('touchend', doEvent, false);
						target.removeEventListener('touchcancel', doEvent, false);

						//  mouse model
						target.removeEventListener('mousedown', doEvent, false);

						//  mouse model with capture
						//  rejecting gecko because, unlike ie, firefox does not send events to target when the mouse is outside target
						if (target.setCapture && !window.navigator.userAgent.match(/\bGecko\b/)) {
							useSetReleaseCapture = true;

							target.removeEventListener('mousemove', doEvent, false);
							target.removeEventListener('mouseup', doEvent, false);
						}
					} else if (target.detachEvent && target.setCapture) {
						//  legacy IE mode - mouse with capture
						useSetReleaseCapture = true;
						target.detachEvent('onmousedown');
						target.detachEvent('onmousemove');
						target.detachEvent('onmouseup');
					}
				};

				return this;
			};

		}
	])

	/**
	 * The gridster directive
	 *
	 * @param {object} $parse
	 * @param {object} $timeout
	 */
	.directive('gridster', ['$timeout', '$rootScope', '$window',
		function($timeout, $rootScope, $window) {
			return {
				restrict: 'EAC',
				// without transclude, some child items may lose their parent scope
				transclude: true,
				replace: true,
				template: '<div ng-class="gridsterClass()"><div ng-style="previewStyle()" class="gridster-item gridster-preview-holder"></div><div class="gridster-content" ng-transclude></div></div>',
				controller: 'GridsterCtrl',
				controllerAs: 'gridster',
				scope: {
					config: '=?gridster'
				},
				compile: function() {

					return function(scope, $elem, attrs, gridster) {
						gridster.loaded = false;

						scope.gridsterClass = function() {
							return {
								gridster: true,
								'gridster-desktop': !gridster.isMobile,
								'gridster-mobile': gridster.isMobile,
								'gridster-loaded': gridster.loaded
							};
						};

						/**
						 * @returns {Object} style object for preview element
						 */
						scope.previewStyle = function() {
							if (!gridster.movingItem) {
								return {
									display: 'none'
								};
							}

							return {
								display: 'block',
								height: (gridster.movingItem.sizeY * gridster.curRowHeight - gridster.margins[0]) + 'px',
								width: (gridster.movingItem.sizeX * gridster.curColWidth - gridster.margins[1]) + 'px',
								top: (gridster.movingItem.row * gridster.curRowHeight + (gridster.outerMargin ? gridster.margins[0] : 0)) + 'px',
								left: (gridster.movingItem.col * gridster.curColWidth + (gridster.outerMargin ? gridster.margins[1] : 0)) + 'px'
							};
						};

						var refresh = function() {
							gridster.setOptions(scope.config);

							// resolve "auto" & "match" values
							if (gridster.width === 'auto') {
								gridster.curWidth = $elem[0].offsetWidth || parseInt($elem.css('width'), 10);
							} else {
								gridster.curWidth = gridster.width;
							}

							if (gridster.colWidth === 'auto') {
								gridster.curColWidth = (gridster.curWidth + (gridster.outerMargin ? -gridster.margins[1] : gridster.margins[1])) / gridster.columns;
							} else {
								gridster.curColWidth = gridster.colWidth;
							}

							gridster.curRowHeight = gridster.rowHeight;
							if (typeof gridster.rowHeight === 'string') {
								if (gridster.rowHeight === 'match') {
									gridster.curRowHeight = Math.round(gridster.curColWidth);
								} else if (gridster.rowHeight.indexOf('*') !== -1) {
									gridster.curRowHeight = Math.round(gridster.curColWidth * gridster.rowHeight.replace('*', '').replace(' ', ''));
								} else if (gridster.rowHeight.indexOf('/') !== -1) {
									gridster.curRowHeight = Math.round(gridster.curColWidth / gridster.rowHeight.replace('/', '').replace(' ', ''));
								}
							}

							gridster.isMobile = gridster.mobileModeEnabled && gridster.curWidth <= gridster.mobileBreakPoint;

							// loop through all items and reset their CSS
							for (var rowIndex = 0, l = gridster.grid.length; rowIndex < l; ++rowIndex) {
								var columns = gridster.grid[rowIndex];
								if (!columns) {
									continue;
								}

								for (var colIndex = 0, len = columns.length; colIndex < len; ++colIndex) {
									if (columns[colIndex]) {
										var item = columns[colIndex];
										item.setElementPosition();
										item.setElementSizeY();
										item.setElementSizeX();
									}
								}
							}

							updateHeight();
						};

						// update grid items on config changes
						scope.$watch('config', refresh, true);

						scope.$watch('config.draggable', function() {
							$rootScope.$broadcast('gridster-draggable-changed');
						}, true);

						scope.$watch('config.resizable', function() {
							$rootScope.$broadcast('gridster-resizable-changed');
						}, true);

						var updateHeight = function() {
							$elem.css('height', (gridster.gridHeight * gridster.curRowHeight) + (gridster.outerMargin ? gridster.margins[0] : -gridster.margins[0]) + 'px');
						};

						scope.$watch('gridster.gridHeight', updateHeight);

						var prevWidth = $elem[0].offsetWidth || parseInt($elem.css('width'), 10);

						function resize() {
							var width = $elem[0].offsetWidth || parseInt($elem.css('width'), 10);

							if (!width || width === prevWidth || gridster.movingItem) {
								return;
							}
							prevWidth = width;

							if (gridster.loaded) {
								$elem.removeClass('gridster-loaded');
							}

							refresh();

							if (gridster.loaded) {
								$elem.addClass('gridster-loaded');
							}

							scope.$parent.$broadcast('gridster-resized', [width, $elem.offsetHeight]);
						}

						// track element width changes any way we can
						function onResize() {
							resize();
							$timeout(function() {
								scope.$apply();
							});
						}
						if (typeof $elem.resize === 'function') {
							$elem.resize(onResize);
						}
						var $win = angular.element($window);
						$win.on('resize', onResize);

						scope.$watch(function() {
							return $elem[0].offsetWidth || parseInt($elem.css('width'), 10);
						}, resize);

						// be sure to cleanup
						scope.$on('$destroy', function() {
							gridster.destroy();
							$win.off('resize', onResize);
						});

						// allow a little time to place items before floating up
						$timeout(function() {
							scope.$watch('gridster.floating', function() {
								gridster.floatItemsUp();
							});
							gridster.loaded = true;
						}, 100);
					};
				}
			};
		}
	])

	.controller('GridsterItemCtrl', function() {
		this.$element = null;
		this.gridster = null;
		this.row = null;
		this.col = null;
		this.sizeX = null;
		this.sizeY = null;
		this.minSizeX = 0;
		this.minSizeY = 0;
		this.maxSizeX = null;
		this.maxSizeY = null;

		this.init = function($element, gridster) {
			this.$element = $element;
			this.gridster = gridster;
			this.sizeX = gridster.defaultSizeX;
			this.sizeY = gridster.defaultSizeY;
		};

		this.destroy = function() {
			this.gridster = null;
			this.$element = null;
		};

		/**
		 * Returns the items most important attributes
		 */
		this.toJSON = function() {
			return {
				row: this.row,
				col: this.col,
				sizeY: this.sizeY,
				sizeX: this.sizeX
			};
		};

		this.isMoving = function() {
			return this.gridster.movingItem === this;
		};

		/**
		 * Set the items position
		 *
		 * @param {number} row
		 * @param {number} column
		 */
		this.setPosition = function(row, column) {
			this.gridster.putItem(this, row, column);
			if (this.gridster.loaded) {
				this.gridster.floatItemsUp();
			}

			this.gridster.updateHeight(this.isMoving() ? this.sizeY : 0);

			if (!this.isMoving()) {
				this.setElementPosition();
			}
		};

		/**
		 * Sets a specified size property
		 *
		 * @param {string} key Can be either "x" or "y"
		 * @param {number} value The size amount
		 */
		this.setSize = function(key, value) {
			key = key.toUpperCase();
			var camelCase = 'size' + key,
				titleCase = 'Size' + key;
			if (value === '') {
				return;
			}
			value = parseInt(value, 10);
			if (isNaN(value) || value === 0) {
				value = this.gridster['default' + titleCase];
			}
			var max = key === 'X' ? this.gridster.columns : this.gridster.maxRows;
			if (this['max' + titleCase]) {
				max = Math.min(this['max' + titleCase], max);
			}
			if (this.gridster['max' + titleCase]) {
				max = Math.min(this.gridster['max' + titleCase], max);
			}
			if (key === 'X' && this.cols) {
				max -= this.cols;
			} else if (key === 'Y' && this.rows) {
				max -= this.rows;
			}

			var min = 0;
			if (this['min' + titleCase]) {
				min = Math.max(this['min' + titleCase], min);
			}
			if (this.gridster['min' + titleCase]) {
				min = Math.max(this.gridster['min' + titleCase], min);
			}

			value = Math.max(Math.min(value, max), min);

			var changed = !(this[camelCase] === value && this['old' + titleCase] && this['old' + titleCase] === value);
			this['old' + titleCase] = this[camelCase] = value;

			if (!this.isMoving()) {
				this['setElement' + titleCase]();
			}
			if (changed) {
				this.gridster.moveOverlappingItems(this);

				if (this.gridster.loaded) {
					this.gridster.floatItemsUp();
				}

				this.gridster.updateHeight(this.isMoving() ? this.sizeY : 0);
			}
		};

		/**
		 * Sets the items sizeY property
		 *
		 * @param {number} rows
		 */
		this.setSizeY = function(rows) {
			this.setSize('Y', rows);
		};

		/**
		 * Sets the items sizeX property
		 *
		 * @param {number} rows
		 */
		this.setSizeX = function(columns) {
			this.setSize('X', columns);
		};

		/**
		 * Sets an elements position on the page
		 *
		 * @param {number} row
		 * @param {number} column
		 */
		this.setElementPosition = function() {
			if (this.gridster.isMobile) {
				this.$element.css({
					marginLeft: this.gridster.margins[0] + 'px',
					marginRight: this.gridster.margins[0] + 'px',
					marginTop: this.gridster.margins[1] + 'px',
					marginBottom: this.gridster.margins[1] + 'px',
					top: '',
					left: ''
				});
			} else {
				this.$element.css({
					margin: 0,
					top: (this.row * this.gridster.curRowHeight + (this.gridster.outerMargin ? this.gridster.margins[0] : 0)) + 'px',
					left: (this.col * this.gridster.curColWidth + (this.gridster.outerMargin ? this.gridster.margins[1] : 0)) + 'px'
				});
			}
		};

		/**
		 * Sets an elements height
		 */
		this.setElementSizeY = function() {
			if (this.gridster.isMobile && !this.gridster.saveGridItemCalculatedHeightInMobile) {
				this.$element.css('height', '');
			} else {
				this.$element.css('height', (this.sizeY * this.gridster.curRowHeight - this.gridster.margins[0]) + 'px');
			}
		};

		/**
		 * Sets an elements width
		 */
		this.setElementSizeX = function() {
			if (this.gridster.isMobile) {
				this.$element.css('width', '');
			} else {
				this.$element.css('width', (this.sizeX * this.gridster.curColWidth - this.gridster.margins[1]) + 'px');
			}
		};
	})

	.factory('GridsterDraggable', ['$document', '$timeout', '$window',
		function($document, $timeout, $window) {
			function GridsterDraggable($el, scope, gridster, item, itemOptions) {

				var elmX, elmY, elmW, elmH,

					mouseX = 0,
					mouseY = 0,
					lastMouseX = 0,
					lastMouseY = 0,
					mOffX = 0,
					mOffY = 0,

					minTop = 0,
					maxTop = 9999,
					minLeft = 0,
					realdocument = $document[0];

				var originalCol, originalRow;
				var inputTags = ['select', 'input', 'textarea', 'button'];

				function mouseDown(e) {
					if (inputTags.indexOf(e.target.nodeName.toLowerCase()) !== -1) {
						return false;
					}

					// exit, if a resize handle was hit
					if (angular.element(e.target).hasClass('gridster-item-resizable-handler')) {
						return false;
					}

					// exit, if the target has it's own click event
					if (angular.element(e.target).attr('onclick') || angular.element(e.target).attr('ng-click')) {
						return false;
					}

					switch (e.which) {
						case 1:
							// left mouse button
							break;
						case 2:
						case 3:
							// right or middle mouse button
							return;
					}

					lastMouseX = e.pageX;
					lastMouseY = e.pageY;

					elmX = parseInt($el.css('left'), 10);
					elmY = parseInt($el.css('top'), 10);
					elmW = $el[0].offsetWidth;
					elmH = $el[0].offsetHeight;

					originalCol = item.col;
					originalRow = item.row;

					dragStart(e);

					return true;
				}

				function mouseMove(e) {
					if (!$el.hasClass('gridster-item-moving') || $el.hasClass('gridster-item-resizing')) {
						return false;
					}

					var maxLeft = gridster.curWidth - 1;

					// Get the current mouse position.
					mouseX = e.pageX;
					mouseY = e.pageY;

					// Get the deltas
					var diffX = mouseX - lastMouseX + mOffX;
					var diffY = mouseY - lastMouseY + mOffY;
					mOffX = mOffY = 0;

					// Update last processed mouse positions.
					lastMouseX = mouseX;
					lastMouseY = mouseY;

					var dX = diffX,
						dY = diffY;
					if (elmX + dX < minLeft) {
						diffX = minLeft - elmX;
						mOffX = dX - diffX;
					} else if (elmX + elmW + dX > maxLeft) {
						diffX = maxLeft - elmX - elmW;
						mOffX = dX - diffX;
					}

					if (elmY + dY < minTop) {
						diffY = minTop - elmY;
						mOffY = dY - diffY;
					} else if (elmY + elmH + dY > maxTop) {
						diffY = maxTop - elmY - elmH;
						mOffY = dY - diffY;
					}
					elmX += diffX;
					elmY += diffY;

					// set new position
					$el.css({
						'top': elmY + 'px',
						'left': elmX + 'px'
					});

					drag(e);

					return true;
				}

				function mouseUp(e) {
					if (!$el.hasClass('gridster-item-moving') || $el.hasClass('gridster-item-resizing')) {
						return false;
					}

					mOffX = mOffY = 0;

					dragStop(e);

					return true;
				}

				function dragStart(event) {
					$el.addClass('gridster-item-moving');
					gridster.movingItem = item;

					gridster.updateHeight(item.sizeY);
					scope.$apply(function() {
						if (gridster.draggable && gridster.draggable.start) {
							gridster.draggable.start(event, $el, itemOptions);
						}
					});
				}

				function drag(event) {
					var oldRow = item.row,
						oldCol = item.col,
						hasCallback = gridster.draggable && gridster.draggable.drag,
						scrollSensitivity = gridster.draggable.scrollSensitivity,
						scrollSpeed = gridster.draggable.scrollSpeed;

					var row = gridster.pixelsToRows(elmY);
					var col = gridster.pixelsToColumns(elmX);

					var itemsInTheWay = gridster.getItems(row, col, item.sizeX, item.sizeY, item);
					var hasItemsInTheWay = itemsInTheWay.length !== 0;

					if (gridster.swapping === true && hasItemsInTheWay) {
						var itemInTheWay = itemsInTheWay[0];
						var sameSize = itemInTheWay.sizeX === item.sizeX && itemInTheWay.sizeY === item.sizeY;
						var sameRow = itemInTheWay.row === row;
						var sameCol = itemInTheWay.col === col;
						var samePosition = sameRow && sameCol;
						var inline = sameRow || sameCol;

						if (samePosition && sameSize) {
							gridster.swapItems(item, itemInTheWay);
						} else if (sameSize && inline) {
							return;
						}
					}
					if (gridster.pushing !== false || !hasItemsInTheWay) {
						item.row = row;
						item.col = col;
					}

					if (event.pageY - realdocument.body.scrollTop < scrollSensitivity) {
						realdocument.body.scrollTop = realdocument.body.scrollTop - scrollSpeed;
					} else if ($window.innerHeight - (event.pageY - realdocument.body.scrollTop) < scrollSensitivity) {
						realdocument.body.scrollTop = realdocument.body.scrollTop + scrollSpeed;
					}

					if (event.pageX - realdocument.body.scrollLeft < scrollSensitivity) {
						realdocument.body.scrollLeft = realdocument.body.scrollLeft - scrollSpeed;
					} else if ($window.innerWidth - (event.pageX - realdocument.body.scrollLeft) < scrollSensitivity) {
						realdocument.body.scrollLeft = realdocument.body.scrollLeft + scrollSpeed;
					}

					if (hasCallback || oldRow !== item.row || oldCol !== item.col) {
						scope.$apply(function() {
							if (hasCallback) {
								gridster.draggable.drag(event, $el, itemOptions);
							}
						});
					}
				}

				function dragStop(event) {
					$el.removeClass('gridster-item-moving');
					var row = gridster.pixelsToRows(elmY);
					var col = gridster.pixelsToColumns(elmX);
					if (gridster.pushing !== false || gridster.getItems(row, col, item.sizeX, item.sizeY, item).length === 0) {
						item.row = row;
						item.col = col;
					}
					gridster.movingItem = null;
					item.setPosition(item.row, item.col);
					item.setSizeY(item.sizeY);
					item.setSizeX(item.sizeX);

					scope.$apply(function() {
						if (gridster.draggable && gridster.draggable.stop) {
							gridster.draggable.stop(event, $el, itemOptions);
						}
					});
				}

				var enabled = false;
				var $dragHandle = null;
				var unifiedInput;

				this.enable = function() {
					var self = this;
					// disable and timeout required for some template rendering
					$timeout(function() {
						self.disable();

						if (gridster.draggable && gridster.draggable.handle) {
							$dragHandle = angular.element($el[0].querySelector(gridster.draggable.handle));
							if ($dragHandle.length === 0) {
								// fall back to element if handle not found...
								$dragHandle = $el;
							}
						} else {
							$dragHandle = $el;
						}

						unifiedInput = new gridster.unifiedInput($dragHandle[0], mouseDown, mouseMove, mouseUp);
						unifiedInput.enable();

						enabled = true;
					});
				};

				this.disable = function() {
					if (!enabled) {
						return;
					}

					unifiedInput.disable();
					unifiedInput = undefined;
					enabled = false;
				};

				this.toggle = function(enabled) {
					if (enabled) {
						this.enable();
					} else {
						this.disable();
					}
				};

				this.destroy = function() {
					this.disable();
				};
			}

			return GridsterDraggable;
		}
	])

	.factory('GridsterResizable', [
		function() {
			function GridsterResizable($el, scope, gridster, item, itemOptions) {

				function ResizeHandle(handleClass) {

					var hClass = handleClass;

					var elmX, elmY, elmW, elmH,

						mouseX = 0,
						mouseY = 0,
						lastMouseX = 0,
						lastMouseY = 0,
						mOffX = 0,
						mOffY = 0,

						minTop = 0,
						maxTop = 9999,
						minLeft = 0;

					var getMinHeight = function() {
						return gridster.curRowHeight - gridster.margins[0];
					};
					var getMinWidth = function(){
						return gridster.curColWidth - gridster.margins[1];
					};

					var originalWidth, originalHeight;
					var savedDraggable;

					function mouseDown(e) {
						switch (e.which) {
							case 1:
								// left mouse button
								break;
							case 2:
							case 3:
								// right or middle mouse button
								return;
						}

						// save the draggable setting to restore after resize
						savedDraggable = gridster.draggable.enabled;
						if (savedDraggable) {
							gridster.draggable.enabled = false;
							scope.$broadcast('gridster-draggable-changed');
						}

						// Get the current mouse position.
						lastMouseX = e.pageX;
						lastMouseY = e.pageY;

						// Record current widget dimensions
						elmX = parseInt($el.css('left'), 10);
						elmY = parseInt($el.css('top'), 10);
						elmW = $el[0].offsetWidth;
						elmH = $el[0].offsetHeight;

						originalWidth = item.sizeX;
						originalHeight = item.sizeY;

						resizeStart(e);

						return true;
					}

					function resizeStart(e) {
						$el.addClass('gridster-item-moving');
						$el.addClass('gridster-item-resizing');

						gridster.movingItem = item;

						item.setElementSizeX();
						item.setElementSizeY();
						item.setElementPosition();
						gridster.updateHeight(1);

						scope.$apply(function() {
							// callback
							if (gridster.resizable && gridster.resizable.start) {
								gridster.resizable.start(e, $el, itemOptions); // options is the item model
							}
						});
					}

					function mouseMove(e) {
						var maxLeft = gridster.curWidth - 1;

						// Get the current mouse position.
						mouseX = e.pageX;
						mouseY = e.pageY;

						// Get the deltas
						var diffX = mouseX - lastMouseX + mOffX;
						var diffY = mouseY - lastMouseY + mOffY;
						mOffX = mOffY = 0;

						// Update last processed mouse positions.
						lastMouseX = mouseX;
						lastMouseY = mouseY;

						var dY = diffY,
							dX = diffX;

						if (hClass.indexOf('n') >= 0) {
							if (elmH - dY < getMinHeight()) {
								diffY = elmH - getMinHeight();
								mOffY = dY - diffY;
							} else if (elmY + dY < minTop) {
								diffY = minTop - elmY;
								mOffY = dY - diffY;
							}
							elmY += diffY;
							elmH -= diffY;
						}
						if (hClass.indexOf('s') >= 0) {
							if (elmH + dY < getMinHeight()) {
								diffY = getMinHeight() - elmH;
								mOffY = dY - diffY;
							} else if (elmY + elmH + dY > maxTop) {
								diffY = maxTop - elmY - elmH;
								mOffY = dY - diffY;
							}
							elmH += diffY;
						}
						if (hClass.indexOf('w') >= 0) {
							if (elmW - dX < getMinWidth()) {
								diffX = elmW - getMinWidth();
								mOffX = dX - diffX;
							} else if (elmX + dX < minLeft) {
								diffX = minLeft - elmX;
								mOffX = dX - diffX;
							}
							elmX += diffX;
							elmW -= diffX;
						}
						if (hClass.indexOf('e') >= 0) {
							if (elmW + dX < getMinWidth()) {
								diffX = getMinWidth() - elmW;
								mOffX = dX - diffX;
							} else if (elmX + elmW + dX > maxLeft) {
								diffX = maxLeft - elmX - elmW;
								mOffX = dX - diffX;
							}
							elmW += diffX;
						}

						// set new position
						$el.css({
							'top': elmY + 'px',
							'left': elmX + 'px',
							'width': elmW + 'px',
							'height': elmH + 'px'
						});

						resize(e);

						return true;
					}

					function mouseUp(e) {
						// restore draggable setting to its original state
						if (gridster.draggable.enabled !== savedDraggable) {
							gridster.draggable.enabled = savedDraggable;
							scope.$broadcast('gridster-draggable-changed');
						}

						mOffX = mOffY = 0;

						resizeStop(e);

						return true;
					}

					function resize(e) {
						var oldRow = item.row,
							oldCol = item.col,
							oldSizeX = item.sizeX,
							oldSizeY = item.sizeY,
							hasCallback = gridster.resizable && gridster.resizable.resize;

						var row = gridster.pixelsToRows(elmY, false);
						var col = gridster.pixelsToColumns(elmX, false);
						var sizeX = gridster.pixelsToColumns(elmW, true);
						var sizeY = gridster.pixelsToRows(elmH, true);
						if (gridster.pushing !== false || gridster.getItems(row, col, sizeX, sizeY, item).length === 0) {
							item.row = row;
							item.col = col;
							item.sizeX = sizeX;
							item.sizeY = sizeY;
						}
						var isChanged = item.row !== oldRow || item.col !== oldCol || item.sizeX !== oldSizeX || item.sizeY !== oldSizeY;

						if (hasCallback || isChanged) {
							scope.$apply(function() {
								if (hasCallback) {
									gridster.resizable.resize(e, $el, itemOptions); // options is the item model
								}
							});
						}
					}

					function resizeStop(e) {
						$el.removeClass('gridster-item-moving');
						$el.removeClass('gridster-item-resizing');

						gridster.movingItem = null;

						item.setPosition(item.row, item.col);
						item.setSizeY(item.sizeY);
						item.setSizeX(item.sizeX);

						scope.$apply(function() {
							if (gridster.resizable && gridster.resizable.stop) {
								gridster.resizable.stop(e, $el, itemOptions); // options is the item model
							}
						});
					}

					var $dragHandle = null;
					var unifiedInput;

					this.enable = function() {
						if (!$dragHandle) {
							$dragHandle = angular.element('<div class="gridster-item-resizable-handler handle-' + hClass + '"></div>');
							$el.append($dragHandle);
						}

						unifiedInput = new gridster.unifiedInput($dragHandle[0], mouseDown, mouseMove, mouseUp);
						unifiedInput.enable();
					};

					this.disable = function() {
						if ($dragHandle) {
							$dragHandle.remove();
							$dragHandle = null;
						}

						unifiedInput.disable();
						unifiedInput = undefined;
					};

					this.destroy = function() {
						this.disable();
					};
				}

				var handles = [];
				var handlesOpts = gridster.resizable.handles;
				if (typeof handlesOpts === 'string') {
					handlesOpts = gridster.resizable.handles.split(',');
				}
				var enabled = false;

				for (var c = 0, l = handlesOpts.length; c < l; c++) {
					handles.push(new ResizeHandle(handlesOpts[c]));
				}

				this.enable = function() {
					if (enabled) {
						return;
					}
					for (var c = 0, l = handles.length; c < l; c++) {
						handles[c].enable();
					}
					enabled = true;
				};

				this.disable = function() {
					if (!enabled) {
						return;
					}
					for (var c = 0, l = handles.length; c < l; c++) {
						handles[c].disable();
					}
					enabled = false;
				};

				this.toggle = function(enabled) {
					if (enabled) {
						this.enable();
					} else {
						this.disable();
					}
				};

				this.destroy = function() {
					for (var c = 0, l = handles.length; c < l; c++) {
						handles[c].destroy();
					}
				};
			}
			return GridsterResizable;
		}
	])

	/**
	 * GridsterItem directive
	 */
	.directive('gridsterItem', ['$parse', 'GridsterDraggable', 'GridsterResizable',
		function($parse, GridsterDraggable, GridsterResizable) {
			return {
				restrict: 'EA',
				controller: 'GridsterItemCtrl',
				require: ['^gridster', 'gridsterItem'],
				link: function(scope, $el, attrs, controllers) {
					var optionsKey = attrs.gridsterItem,
						options;

					var gridster = controllers[0],
						item = controllers[1];

					// bind the item's position properties
					if (optionsKey) {
						var $optionsGetter = $parse(optionsKey);
						options = $optionsGetter(scope) || {};
						if (!options && $optionsGetter.assign) {
							options = {
								row: item.row,
								col: item.col,
								sizeX: item.sizeX,
								sizeY: item.sizeY,
								minSizeX: 0,
								minSizeY: 0,
								maxSizeX: null,
								maxSizeY: null
							};
							$optionsGetter.assign(scope, options);
						}
					} else {
						options = attrs;
					}

					item.init($el, gridster);

					$el.addClass('gridster-item');

					var aspects = ['minSizeX', 'maxSizeX', 'minSizeY', 'maxSizeY', 'sizeX', 'sizeY', 'row', 'col'],
						$getters = {};

					var aspectFn = function(aspect) {
						var key;
						if (typeof options[aspect] === 'string') {
							key = options[aspect];
						} else if (typeof options[aspect.toLowerCase()] === 'string') {
							key = options[aspect.toLowerCase()];
						} else if (optionsKey) {
							key = $parse(optionsKey + '.' + aspect);
						} else {
							return;
						}
						$getters[aspect] = $parse(key);

						// when the value changes externally, update the internal item object
						scope.$watch(key, function(newVal) {
							newVal = parseInt(newVal, 10);
							if (!isNaN(newVal)) {
								item[aspect] = newVal;
							}
						});

						// initial set
						var val = $getters[aspect](scope);
						if (typeof val === 'number') {
							item[aspect] = val;
						}
					};

					for (var i = 0, l = aspects.length; i < l; ++i) {
						aspectFn(aspects[i]);
					}

					function positionChanged() {
						// call setPosition so the element and gridster controller are updated
						item.setPosition(item.row, item.col);

						// when internal item position changes, update externally bound values
						if ($getters.row && $getters.row.assign) {
							$getters.row.assign(scope, item.row);
						}
						if ($getters.col && $getters.col.assign) {
							$getters.col.assign(scope, item.col);
						}
					}
					scope.$watch(function() {
						return item.row + ',' + item.col;
					}, positionChanged);

					function sizeChanged() {
						item.setSizeX(item.sizeX);
						if ($getters.sizeX && $getters.sizeX.assign) {
							$getters.sizeX.assign(scope, item.sizeX);
						}
						item.setSizeY(item.sizeY);
						if ($getters.sizeY && $getters.sizeY.assign) {
							$getters.sizeY.assign(scope, item.sizeY);
						}
					}
					scope.$watch(function() {
						return item.sizeY + ',' + item.sizeX + '|' + item.minSizeX + ',' + item.maxSizeX + ',' + item.minSizeY + ',' + item.maxSizeY;
					}, sizeChanged);

					var draggable = new GridsterDraggable($el, scope, gridster, item, options);
					var resizable = new GridsterResizable($el, scope, gridster, item, options);

					scope.$on('gridster-draggable-changed', function() {
						draggable.toggle(!gridster.isMobile && gridster.draggable && gridster.draggable.enabled);
					});
					scope.$on('gridster-resizable-changed', function() {
						resizable.toggle(!gridster.isMobile && gridster.resizable && gridster.resizable.enabled);
					});
					scope.$on('gridster-resized', function() {
						resizable.toggle(!gridster.isMobile && gridster.resizable && gridster.resizable.enabled);
					});
					scope.$watch(function() {
						return gridster.isMobile;
					}, function() {
						resizable.toggle(!gridster.isMobile && gridster.resizable && gridster.resizable.enabled);
						draggable.toggle(!gridster.isMobile && gridster.draggable && gridster.draggable.enabled);
					});

					function whichTransitionEvent() {
						var el = document.createElement('div');
						var transitions = {
							'transition': 'transitionend',
							'OTransition': 'oTransitionEnd',
							'MozTransition': 'transitionend',
							'WebkitTransition': 'webkitTransitionEnd'
						};
						for (var t in transitions) {
							if (el.style[t] !== undefined) {
								return transitions[t];
							}
						}
					}

					$el.on(whichTransitionEvent(), function() {
						scope.$apply(function() {
							scope.$broadcast('gridster-item-transition-end');
						});
					});

					return scope.$on('$destroy', function() {
						try {
							resizable.destroy();
							draggable.destroy();
						} catch (e) {}

						try {
							gridster.removeItem(item);
						} catch (e) {}

						try {
							item.destroy();
						} catch (e) {}
					});
				}
			};
		}
	])

	;

})(angular);