ManifestWebDesign / angular-gridster

An implementation of gridster-like widgets for Angular JS
http://manifestwebdesign.github.io/angular-gridster/
MIT License
964 stars 394 forks source link

When I resize an item, items next to it (right or left) should push them horizontally. #212

Open nehalahmad opened 9 years ago

nehalahmad commented 9 years ago

Hi,

First of all, thanks for this awesome angular directive. I was using it in one of my dashboard type of application and here I am struggling with one odd behaviour of gridster item. When I try to resize the gridster item (its width) right or left direction, an item next to it (right or left) is pushed down (vertically) at position just below to it, and when I revert back the width of previous item, the shifted item come back to its previous position. and here, what I want, that it should shift horizontally to right or left direction respectively.

For example, initial state of item like, Then, I increase the width of first item, the second item will be shifted down and the picture will be like, _ But I want that, it should be _ .

And further, gridster behaviour, like when I increase/decrease the item's height, the items below to it are pushing them down/up, its fine.

Please help me out.

Thanks in advance.

andrey-skl commented 9 years ago

@nehalahmad until gridster have fixed number of columns, how should widgets behave when reaching the border?

nehalahmad commented 9 years ago

When widgets reach the border, this will be moved down at position first column and next row to its current row.

And when this will be moved down, its adjacent widgets will be shifted to right and further the last widgets in that row will be moved down like wise.

nehalahmad commented 9 years ago

Hi @huston007 ,

After spending some time, I came to know, that, when you are resizing or dragging an item, you are shifting overlapping items to down in all cases.

What I am trying to do here, when I am increasing width of item to right side (or left), adjacent items should be shifted to right (or left) and when last item in that row, reach at the border of the grid, it should be dropped in next row and 1 column.

I am still trying over there, please let me know, if you can help me in any manner to adjust the logic.

andrey-skl commented 9 years ago

Hello @nehalahmad So I got that what do you want and agree it may be useful with wide grids.

nehalahmad commented 9 years ago

Hi @huston007,

I have adjusted the logic according my needs, but may be it looks to you, not a standard approach. But it seems working up to an extent.

If you want I can share it you, so that you can take it as a enhancement.

Thanks a lot buddy.

andrey-skl commented 9 years ago

@nehalahmad so you can share your solution here and if somebody other wants this feature they will try that. Thank you.

prochafilho commented 9 years ago

I would love to see this solution too, if that is ok.

nehalahmad commented 9 years ago

Hi @prochafilho,

Please find the code given below -

(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']   // Code Changes for making the chart 2d resizable
    },
    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;
                    }
                }
            }
            this.isWidthIncreasing          = null;
            this.widthIncreasingDirection   = null;
            this.isHeightIncreasing         = null;
            this.isDragging                 = false;
        };

        /**
         * 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
            );

            if (overlappingItems.length > 0) {
                if (this.isWidthIncreasing) {
                    var overLappingItemNewCol = item.col;
                    if (this.widthIncreasingDirection == "east") {
                        overLappingItemNewCol+= item.sizeX;
                    } else if (this.widthIncreasingDirection == "west") {
                        overLappingItemNewCol-= item.sizeX;
                    }
                    this.moveItemsHorizontally(overlappingItems, overLappingItemNewCol, ignoreItems);
                }
                if (this.isHeightIncreasing || this.isDragging) 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);
        };

        this.moveItemsHorizontally = function(items, newCol, 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 colsToMove = newCol - topRows[item.col];
                this.moveItemRightLeft(item, item.row + colsToMove, ignoreItems);
                ignoreItems.push(item);
            }
        };

        this.moveItemRightLeft = function(item, newCol, ignoreItems) {
            if (this.widthIncreasingDirection == "east" || item.col < newCol) {
                while (item.col < newCol) {
                    if (this.isBorderReached(newCol, item)) {
                        var nextCol = 0, nextRow = item.row+item.sizeY;
                        var nextRowOverlappingItems = this.getItems(nextRow, nextCol, item.sizeX, item.sizeY, item);

                        if (nextRowOverlappingItems.length > 0) this.moveItemsHorizontally(nextRowOverlappingItems, nextCol + item.sizeX, ignoreItems);
                        item.col = nextCol;
                        item.row = nextRow;
                        break;
                    }
                    ++item.col;
                    this.moveOverlappingItems(item, ignoreItems);
                }
            } else if (this.widthIncreasingDirection == "west") {
                while (item.col > newCol) {
                    if (this.isBorderReached(newCol, item)) {
                        var nextCol = 0, nextRow = item.row+item.sizeY;
                        var nextRowOverlappingItems = this.getItems(nextRow, nextCol, item.sizeX, item.sizeY, item);
                        if (nextRowOverlappingItems.length > 0) this.moveItemsHorizontally(nextRowOverlappingItems, nextCol + item.sizeX, ignoreItems);
                        item.col = nextCol;
                        item.row = nextRow;
                        break;
                    }
                    --item.col;
                    this.moveOverlappingItems(item, ignoreItems);
                }
            }
            this.putItem(item, item.row, item.col, ignoreItems);
        };

        this.isBorderReached = function(newCol, item) {
            var wNewCol = newCol+item.sizeX;
            if (wNewCol > this.columns || newCol < 0) return true;
        };

        /**
         * 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);
            }
        };

        this.getItemsHorizontally = 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;
        };

        /**
         * 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') || angular.element(e.target).attr('data-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);
                gridster.isDragging = true;

                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;
                        gridster.isWidthIncreasing = false;
                        gridster.isHeightIncreasing = true;
                    }
                    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;
                        gridster.isWidthIncreasing  = false;
                        gridster.isHeightIncreasing = true;
                    }
                    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;
                        gridster.isWidthIncreasing = true;
                        gridster.isHeightIncreasing = false;
                        gridster.widthIncreasingDirection = 'west';
                    }
                    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;
                        gridster.isWidthIncreasing = true;
                        gridster.isHeightIncreasing = false;
                        gridster.widthIncreasingDirection = 'east';
                    }
                    gridster.isDragging = false;

                    // 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 col = item.col;
                    // only change column if grabbing left edge
                    if (['w', 'nw', 'sw'].indexOf(handleClass) !== -1) {
                        col = gridster.pixelsToColumns(elmX, false);
                    }

                    var row = item.row;
                    // only change row if grabbing top edge
                    if (['n', 'ne', 'nw'].indexOf(handleClass) !== -1) {
                        row = gridster.pixelsToRows(elmY, false);
                    }

                    var sizeX = item.sizeX;
                    // only change row if grabbing left or right edge
                    if (['n', 's'].indexOf(handleClass) === -1) {
                        sizeX = gridster.pixelsToColumns(elmW, true);
                    }

                    var sizeY = item.sizeY;
                    // only change row if grabbing top or bottom edge
                    if (['e', 'w'].indexOf(handleClass) === -1) {
                        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);

                    // TODO: try to set settimeout through config
                    setTimeout(function() {
                        scope.$apply(function() {
                            if (gridster.resizable && gridster.resizable.stop) {
                                gridster.resizable.stop(e, $el, itemOptions); // options is the item model
                            }
                        });
                    }, 500);
                }

                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);

prochafilho commented 9 years ago

Thank you!

danomatic commented 9 years ago

Can you branch from master and submit a pull request?

tflanagan commented 8 years ago

@nehalahmad, does this patch still work? have you submitted a PR?

mobijay commented 7 years ago

This is a great patch. Is this available in master?