diff --git a/Gruntfile.js b/Gruntfile.js index 470f156d..1487db18 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -19,7 +19,7 @@ module.exports = function(grunt) { }, connect: { options: { - port: 9000, + port: 9001, hostname: 'localhost' }, dev: { @@ -87,6 +87,24 @@ module.exports = function(grunt) { } } }, + concat: { + default: { + src: ['src/angular-gridster.module.js', 'src/**/*.js'], + dest: 'dist/angular-gridster.js' + }, + }, + umd: { + default: { + options: { + src: 'dist/angular-gridster.js', + dest: 'dist/angular-gridster.js', + objectToExport: 'angular', + deps: { + cjs: ['angular'] + } + } + } + }, uglify: { dist: { options: { @@ -100,13 +118,13 @@ module.exports = function(grunt) { ].join('\n') }, files: { - 'dist/angular-gridster.min.js': ['src/angular-gridster.js'] + 'dist/angular-gridster.min.js': ['dist/angular-gridster.js'] } } }, watch: { dev: { - files: ['Gruntfile.js', 'karma.conf.js', 'ptor.conf.js', 'src/*', 'test/**/*.js'], + files: ['Gruntfile.js', 'karma.conf.js', 'ptor.conf.js', 'src/**/*.js', 'test/**/*.js'], tasks: ['jsbeautifier', 'jshint', 'uglify', 'less', 'karma:unit:run'], options: { reload: true, @@ -121,7 +139,7 @@ module.exports = function(grunt) { } }); - grunt.registerTask('default', ['jsbeautifier', 'jshint', 'uglify', 'less']); + grunt.registerTask('default', ['jsbeautifier', 'jshint', 'concat', 'umd', 'uglify', 'less']); grunt.registerTask('dev', ['connect:dev', 'karma:unit:start', 'watch:dev']); grunt.registerTask('e2e', ['connect:cli', 'protractor', 'watch:e2e']); diff --git a/bower.json b/bower.json index 89ecc02f..4e4aac74 100644 --- a/bower.json +++ b/bower.json @@ -7,12 +7,14 @@ ], "dependencies": { "angular": ">= 1.2.0", - "javascript-detect-element-resize": "~0.5.1" + "javascript-detect-element-resize": "~0.5.1", + "lodash": "4.17.4", + "jquery": "2.1.x" }, "devDependencies": { "angular-mocks": ">= 1.2.0", "angular-animate": ">= 1.2.0", - "jquery": "*", + "jquery": "2.1.x", "jquery-simulate": "*" }, "ignore": [ diff --git a/src/angular-gridster.js b/dist/angular-gridster.js old mode 100755 new mode 100644 similarity index 56% rename from src/angular-gridster.js rename to dist/angular-gridster.js index 5fa8499f..802a874e --- a/src/angular-gridster.js +++ b/dist/angular-gridster.js @@ -1,866 +1,382 @@ -/*global define:true*/ -(function(root, factory) { - +(function (root, factory) { + if (typeof define === 'function' && define.amd) { + // AMD. Register as an anonymous module unless amdModuleId is set + define([], function () { + return (root['angular'] = factory()); + }); + } else if (typeof module === 'object' && module.exports) { + // Node. Does not work with strict CommonJS, but + // only CommonJS-like environments that support module.exports, + // like Node. + module.exports = factory(require("angular")); + } else { + root['angular'] = factory(); + } +}(this, function () { + +(function(angular) { 'use strict'; - if (typeof define === 'function' && define.amd) { - // AMD - define(['angular'], factory); - } else if (typeof exports === 'object') { - // CommonJS - module.exports = factory(require('angular')); - } else { - // Browser, nothing "exported". Only registered as a module with angular. - factory(root.angular); - } -}(this, function(angular) { + 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, + sparse: false, // "true" can increase performance of dragging and resizing for big grid (e.g. 20x50) + 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: 15 // Speed at which the window should scroll once the mouse pointer gets within scrollSensitivity distance + } + }); +})(window.angular); +(function(angular) { 'use strict'; - // This returned angular module 'gridster' is what is exported. - return 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, - sparse: false, // "true" can increase performance of dragging and resizing for big grid (e.g. 20x50) - 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: 15 // Speed at which the window should scroll once the mouse pointer gets within scrollSensitivity distance - } - }) + angular.module('gridster').factory('GridsterDraggable', ['$document', '$window', 'GridsterTouch', + function($document, $window, GridsterTouch) { + function GridsterDraggable($el, scope, gridster, item, itemOptions) { + + var elmX, elmY, elmW, elmH, - .controller('GridsterCtrl', ['gridsterConfig', '$timeout', - function(gridsterConfig, $timeout) { + mouseX = 0, + mouseY = 0, + lastMouseX = 0, + lastMouseY = 0, + mOffX = 0, + mOffY = 0, - var gridster = this; + minTop = 0, + minLeft = 0, + realdocument = $document[0]; - /** - * Create options from gridsterConfig constant - */ - angular.extend(this, gridsterConfig); + var originalCol, originalRow; + var inputTags = ['select', 'option', 'input', 'textarea', 'button']; - this.resizable = angular.extend({}, gridsterConfig.resizable || {}); - this.draggable = angular.extend({}, gridsterConfig.draggable || {}); + function dragStart(event) { + $el.addClass('gridster-item-moving'); + gridster.movingItem = item; - var flag = false; - this.layoutChanged = function() { - if (flag) { - return; + gridster.updateHeight(item.sizeY); + if (_.chain(gridster).get('draggable.start').isFunction().valueOf()) { + scope.$apply(function() { + gridster.draggable.start(event, $el, itemOptions, item); + }); + } } - flag = true; - $timeout(function() { - flag = false; - if (gridster.loaded) { - gridster.floatItemsUp(); - } - gridster.updateHeight(gridster.movingItem ? gridster.movingItem.sizeY : 0); - }, 30); - }; - /** - * A positional array of the items in the grid - */ - this.grid = []; - this.allItems = []; + function drag(event) { + var oldRow = item.row, + oldCol = item.col, + hasCallback = gridster.draggable && gridster.draggable.drag, + scrollSensitivity = gridster.draggable.scrollSensitivity, + scrollSpeed = gridster.draggable.scrollSpeed; - /** - * Clean up after yourself - */ - this.destroy = function() { - // empty the grid to cut back on the possibility - // of circular references - if (this.grid) { - this.grid = []; - } - this.$element = null; + var row = Math.min(gridster.pixelsToRows(elmY), gridster.maxRows - 1); + var col = Math.min(gridster.pixelsToColumns(elmX), gridster.columns - 1); - if (this.allItems) { - this.allItems.length = 0; - this.allItems = null; - } - }; + var itemsInTheWay = gridster.getItems(row, col, item.sizeX, item.sizeY, item); + var hasItemsInTheWay = itemsInTheWay.length !== 0; - /** - * Overrides default options - * - * @param {Object} options The options to override - */ - this.setOptions = function(options) { - if (!options) { - return; - } + if (gridster.swapping === true && hasItemsInTheWay) { + var boundingBoxItem = gridster.getBoundingBox(itemsInTheWay), + sameSize = boundingBoxItem.sizeX === item.sizeX && boundingBoxItem.sizeY === item.sizeY, + sameRow = boundingBoxItem.row === oldRow, + sameCol = boundingBoxItem.col === oldCol, + samePosition = boundingBoxItem.row === row && boundingBoxItem.col === col, + inline = sameRow || sameCol; - options = angular.extend({}, options); + if (sameSize && itemsInTheWay.length === 1) { + if (samePosition) { + gridster.swapItems(item, itemsInTheWay[0]); + } else if (inline) { + return; + } + } else if (boundingBoxItem.sizeX <= item.sizeX && boundingBoxItem.sizeY <= item.sizeY && inline) { + var emptyRow = item.row <= row ? item.row : row + item.sizeY, + emptyCol = item.col <= col ? item.col : col + item.sizeX, + rowOffset = emptyRow - boundingBoxItem.row, + colOffset = emptyCol - boundingBoxItem.col; - // 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); - } + for (var i = 0, l = itemsInTheWay.length; i < l; ++i) { + var itemInTheWay = itemsInTheWay[i]; - angular.extend(this, options); + var itemsInFreeSpace = gridster.getItems( + itemInTheWay.row + rowOffset, + itemInTheWay.col + colOffset, + itemInTheWay.sizeX, + itemInTheWay.sizeY, + item + ); - 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; + if (itemsInFreeSpace.length === 0) { + gridster.putItem(itemInTheWay, itemInTheWay.row + rowOffset, itemInTheWay.col + colOffset); + } + } } } - } - }; - /** - * 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; - }; + if (gridster.pushing !== false || !hasItemsInTheWay) { + item.row = row; + item.col = col; + } - /** - * 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; - } + 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; } - } - 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]; - } - var item; - if (this.sparse === false) { // check all cells - for (var h = 0; h < sizeY; ++h) { - for (var w = 0; w < sizeX; ++w) { - item = this.getItem(row + h, column + w, excludeItems); - if (item && (!excludeItems || excludeItems.indexOf(item) === -1) && items.indexOf(item) === -1) { - items.push(item); - } - } + 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; } - } else { // check intersection with all items - var bottom = row + sizeY - 1; - var right = column + sizeX - 1; - for (var i = 0; i < this.allItems.length; ++i) { - item = this.allItems[i]; - if (item && (!excludeItems || excludeItems.indexOf(item) === -1) && items.indexOf(item) === -1 && this.intersect(item, column, right, row, bottom)) { - items.push(item); - } + + if (hasCallback && (oldRow !== item.row || oldCol !== item.col)) { + scope.$apply(function() { + gridster.draggable.drag(event, $el, itemOptions, item); + }); } } - return items; - }; - /** - * @param {Array} items - * @returns {Object} An item that represents the bounding box of the items - */ - this.getBoundingBox = function(items) { - - if (items.length === 0) { - return null; - } - if (items.length === 1) { - return { - row: items[0].row, - col: items[0].col, - sizeY: items[0].sizeY, - sizeX: items[0].sizeX - }; - } + function dragStop(event) { + $el.removeClass('gridster-item-moving'); + var row = Math.min(gridster.pixelsToRows(elmY), gridster.maxRows - 1); + var col = Math.min(gridster.pixelsToColumns(elmX), gridster.columns - 1); + 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); - var maxRow = 0; - var maxCol = 0; - var minRow = 9999; - var minCol = 9999; - - for (var i = 0, l = items.length; i < l; ++i) { - var item = items[i]; - minRow = Math.min(item.row, minRow); - minCol = Math.min(item.col, minCol); - maxRow = Math.max(item.row + item.sizeY, maxRow); - maxCol = Math.max(item.col + item.sizeX, maxCol); + if (_.chain(gridster).get('draggable.stop').isFunction().valueOf()) { + scope.$apply(function() { + gridster.draggable.stop(event, $el, itemOptions, item); + }); + } } - return { - row: minRow, - col: minCol, - sizeY: maxRow - minRow, - sizeX: maxCol - minCol - }; - }; - - /** - * Checks if item intersects specified box - * - * @param {object} item - * @param {number} left - * @param {number} right - * @param {number} top - * @param {number} bottom - */ - - this.intersect = function(item, left, right, top, bottom) { - return (left <= item.col + item.sizeX - 1 && - right >= item.col && - top <= item.row + item.sizeY - 1 && - bottom >= item.row); - }; + function mouseDown(e) { + if (inputTags.indexOf(e.target.nodeName.toLowerCase()) !== -1) { + return false; + } + var $target = angular.element(e.target); - /** - * Removes an item from the grid - * - * @param {Object} item - */ - this.removeItem = function(item) { - var index; - for (var rowIndex = 0, l = this.grid.length; rowIndex < l; ++rowIndex) { - var columns = this.grid[rowIndex]; - if (!columns) { - continue; + // exit, if a resize handle was hit + if ($target.hasClass('gridster-item-resizable-handler')) { + return false; } - index = columns.indexOf(item); - if (index !== -1) { - columns[index] = null; - break; + + // exit, if the target has it's own click event + if ($target.attr('onclick') || $target.attr('ng-click')) { + return false; } - } - if (this.sparse) { - index = this.allItems.indexOf(item); - if (index !== -1) { - this.allItems.splice(index, 1); + + // only works if you have jQuery + if ($target.closest && $target.closest('.gridster-no-drag').length) { + return false; } - } - this.layoutChanged(); - }; - /** - * 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; + // apply drag handle filter + if (gridster.draggable && gridster.draggable.handle) { + var $dragHandles = angular.element($el[0].querySelectorAll(gridster.draggable.handle)); + var match = false; + outerloop: + for (var h = 0, hl = $dragHandles.length; h < hl; ++h) { + var handle = $dragHandles[h]; + if (handle === e.target) { + match = true; + break; + } + var target = e.target; + for (var p = 0; p < 20; ++p) { + var parent = target.parentNode; + if (parent === $el[0] || !parent) { + break; + } + if (parent === handle) { + match = true; + break outerloop; + } + target = parent; + } } + if (!match) { + return false; } - ++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) { - // auto place item if no row specified - if (typeof row === 'undefined' || row === null) { - row = item.row; - column = item.col; - if (typeof row === 'undefined' || row === null) { - this.autoSetItemPosition(item); - return; + switch (e.which) { + case 1: + // left mouse button + break; + case 2: + case 3: + // right or middle mouse button + return; } - } - - // keep item within allowed bounds - 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)); - } - // check if item is already in grid - if (item.oldRow !== null && typeof item.oldRow !== 'undefined') { - var samePosition = item.oldRow === row && item.oldColumn === column; - var inGrid = this.grid[row] && this.grid[row][column] === item; - if (samePosition && inGrid) { - 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]; - } - } - } + lastMouseX = e.pageX; + lastMouseY = e.pageY; - item.oldRow = item.row = row; - item.oldColumn = item.col = column; + elmX = parseInt($el.css('left'), 10); + elmY = parseInt($el.css('top'), 10); + elmW = $el[0].offsetWidth; + elmH = $el[0].offsetHeight; - this.moveOverlappingItems(item, ignoreItems); + originalCol = item.col; + originalRow = item.row; - if (!this.grid[row]) { - this.grid[row] = []; - } - this.grid[row][column] = item; + dragStart(e); - if (this.sparse && this.allItems.indexOf(item) === -1) { - this.allItems.push(item); + return true; } - if (this.movingItem === item) { - this.floatItemUp(item); - } - this.layoutChanged(); - }; + var mouseMove = _.throttle(function(e) { + if (!$el.hasClass('gridster-item-moving') || $el.hasClass('gridster-item-resizing')) { + return false; + } - /** - * 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; - }; + var maxLeft = gridster.curWidth - 1; + var maxTop = gridster.curRowHeight * gridster.maxRows - 1; - /** - * Prevents items from being overlapped - * - * @param {Object} item The item that should remain - * @param {Array} ignoreItems - */ - this.moveOverlappingItems = function(item, ignoreItems) { - // don't move item, so ignore it - if (!ignoreItems) { - ignoreItems = [item]; - } else if (ignoreItems.indexOf(item) === -1) { - ignoreItems = ignoreItems.slice(0); - ignoreItems.push(item); - } + // Get the current mouse position. + mouseX = e.pageX; + mouseY = e.pageY; - // get the items in the space occupied by the item's coordinates - var overlappingItems = this.getItems( - item.row, - item.col, - item.sizeX, - item.sizeY, - ignoreItems - ); - this.moveItemsDown(overlappingItems, item.row + item.sizeY, ignoreItems); - }; + // Get the deltas + var diffX = mouseX - lastMouseX + mOffX; + var diffY = mouseY - lastMouseY + mOffY; + mOffX = mOffY = 0; - /** - * 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; - }); + // Update last processed mouse positions. + lastMouseX = mouseX; + lastMouseY = mouseY; - ignoreItems = ignoreItems ? ignoreItems.slice(0) : []; - var topRows = {}, - item, i, l; + 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; + } - // 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; + 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; - // 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); - } - }; - - /** - * Moves an item down to a specified row - * - * @param {Object} item The item to move - * @param {Number} newRow The target row - * @param {Array} ignoreItems - */ - 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); - }; + // set new position + $el.css({ + 'top': elmY + 'px', + 'left': elmX + 'px' + }); - /** - * 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) { - var item = columns[colIndex]; - if (item) { - this.floatItemUp(item); - } - } - } - }; + drag(e); - /** - * 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); - } - }; + return true; + }, 100, { + leading: true, + trailing: true + }); - /** - * 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); - } + function mouseUp(e) { + if (!$el.hasClass('gridster-item-moving') || $el.hasClass('gridster-item-resizing')) { + return false; } - } - 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 (!this.outerMargin) { - pixels += this.margins[0] / 2; - } - - 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); - }; + mOffX = mOffY = 0; - /** - * 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 (!this.outerMargin) { - pixels += this.margins[1] / 2; - } + dragStop(e); - if (ceilOrFloor === true) { - return Math.ceil(pixels / this.curColWidth); - } else if (ceilOrFloor === false) { - return Math.floor(pixels / this.curColWidth); + return true; } - return Math.round(pixels / this.curColWidth); - }; - } - ]) + var enabled = null; + var gridsterTouch = null; - .directive('gridsterPreview', function() { - return { - replace: true, - scope: true, - require: '^gridster', - template: '
', - link: function(scope, $el, attrs, gridster) { + this.enable = function() { + if (enabled === true) { + return; + } + enabled = true; - /** - * @returns {Object} style object for preview element - */ - scope.previewStyle = function() { - if (!gridster.movingItem) { - return { - display: 'none' - }; + if (gridsterTouch) { + gridsterTouch.enable(); + return; } - 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' - }; + gridsterTouch = new GridsterTouch($el[0], mouseDown, mouseMove, mouseUp); + gridsterTouch.enable(); }; - } - }; - }) - - /** - * The gridster directive - * - * @param {Function} $timeout - * @param {Object} $window - * @param {Object} $rootScope - * @param {Function} gridsterDebounce - */ - .directive('gridster', ['$timeout', '$window', '$rootScope', 'gridsterDebounce', - function($timeout, $window, $rootScope, gridsterDebounce) { - return { - scope: true, - restrict: 'EAC', - controller: 'GridsterCtrl', - controllerAs: 'gridster', - compile: function($tplElem) { - - $tplElem.prepend('
'); - - return function(scope, $elem, attrs, gridster) { - gridster.loaded = false; - - gridster.$element = $elem; - - scope.gridster = gridster; - - $elem.addClass('gridster'); - - var isVisible = function(ele) { - return ele.style.visibility !== 'hidden' && ele.style.display !== 'none'; - }; - - function updateHeight() { - $elem.css('height', (gridster.gridHeight * gridster.curRowHeight) + (gridster.outerMargin ? gridster.margins[0] : -gridster.margins[0]) + 'px'); - } - - scope.$watch(function() { - return gridster.gridHeight; - }, updateHeight); - - scope.$watch(function() { - return gridster.movingItem; - }, function() { - gridster.updateHeight(gridster.movingItem ? gridster.movingItem.sizeY : 0); - }); - - function refresh(config) { - gridster.setOptions(config); - - if (!isVisible($elem[0])) { - return; - } - - // 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(); - } - - var optionsKey = attrs.gridster; - if (optionsKey) { - scope.$parent.$watch(optionsKey, function(newConfig) { - refresh(newConfig); - }, true); - } else { - refresh({}); - } - - scope.$watch(function() { - return gridster.loaded; - }, function() { - if (gridster.loaded) { - $elem.addClass('gridster-loaded'); - $rootScope.$broadcast('gridster-loaded', gridster); - } else { - $elem.removeClass('gridster-loaded'); - } - }); - - scope.$watch(function() { - return gridster.isMobile; - }, function() { - if (gridster.isMobile) { - $elem.addClass('gridster-mobile').removeClass('gridster-desktop'); - } else { - $elem.removeClass('gridster-mobile').addClass('gridster-desktop'); - } - $rootScope.$broadcast('gridster-mobile-changed', gridster); - }); - - scope.$watch(function() { - return gridster.draggable; - }, function() { - $rootScope.$broadcast('gridster-draggable-changed', gridster); - }, true); - - scope.$watch(function() { - return gridster.resizable; - }, function() { - $rootScope.$broadcast('gridster-resizable-changed', gridster); - }, true); - - var prevWidth = $elem[0].offsetWidth || parseInt($elem.css('width'), 10); - - var resize = function() { - 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'); - } - - $rootScope.$broadcast('gridster-resized', [width, $elem[0].offsetHeight], gridster); - }; + this.disable = function() { + if (enabled === false) { + return; + } - // track element width changes any way we can - var onResize = gridsterDebounce(function onResize() { - resize(); - $timeout(function() { - scope.$apply(); - }); - }, 100); + enabled = false; + if (gridsterTouch) { + gridsterTouch.disable(); + } + }; - scope.$watch(function() { - return isVisible($elem[0]); - }, onResize); + this.toggle = function(enabled) { + if (enabled) { + this.enable(); + } else { + this.disable(); + } + }; - // see https://github.com/sdecima/javascript-detect-element-resize - if (typeof window.addResizeListener === 'function') { - window.addResizeListener($elem[0], onResize); - } else { - scope.$watch(function() { - return $elem[0].offsetWidth || parseInt($elem.css('width'), 10); - }, resize); - } - var $win = angular.element($window); - $win.on('resize', onResize); - - // be sure to cleanup - scope.$on('$destroy', function() { - gridster.destroy(); - $win.off('resize', onResize); - if (typeof window.removeResizeListener === 'function') { - window.removeResizeListener($elem[0], onResize); - } - }); + this.destroy = function() { + this.disable(); + }; + } - // allow a little time to place items before floating up - $timeout(function() { - scope.$watch('gridster.floating', function() { - gridster.floatItemsUp(); - }); - gridster.loaded = true; - }, 100); - }; - } - }; + return GridsterDraggable; } - ]) + ]); +})(window.angular); + +(function(angular, _) { + 'use strict'; - .controller('GridsterItemCtrl', function() { + angular.module('gridster').controller('GridsterItemCtrl', ['GridsterViewport', function(GridsterViewport) { this.$element = null; this.gridster = null; this.row = null; @@ -877,12 +393,48 @@ this.gridster = gridster; this.sizeX = gridster.defaultSizeX; this.sizeY = gridster.defaultSizeY; + this.gridsterViewport = new GridsterViewport($element); }; this.destroy = function() { // set these to null to avoid the possibility of circular references this.gridster = null; this.$element = null; + this.gridsterViewport.destroy(); + this.gridsterViewport = null; + }; + + /** + * Will update the viewport status of the instance and notify + * the $elements' scope. + */ + this.viewportNotify = function() { + this.gridsterViewport.identify().notify(this); + }; + + /** + * Will reset the viewport status of the instance and notify + * the $elements' scope. + */ + this.viewportReset = function() { + this.gridsterViewport.reset(this); + }; + + /** + * Will determine if $element is visible in viewport + * @returns {boolean} + */ + this.isVisible = function() { + return this.gridsterViewport.isVisible(); + }; + + /** + * Will determine if $element is visible in the viewport + * for the first time. + * @returns {boolean} + */ + this.isFirstTimeVisible = function() { + return this.gridsterViewport.isFirstTimeVisible(); }; /** @@ -1048,31 +600,557 @@ return (this.sizeY * this.gridster.curRowHeight - this.gridster.margins[0]); }; - }) + }]); +})(window.angular, window._); - .factory('GridsterTouch', [function() { - return function GridsterTouch(target, startEvent, moveEvent, endEvent) { - var lastXYById = {}; +(function(angular) { + 'use strict'; + /** + * GridsterItem directive + * @param $parse + * @param GridsterDraggable + * @param GridsterResizable + */ + angular.module('gridster').directive('gridsterItem', ['$parse', 'GridsterDraggable', 'GridsterResizable', + function($parse, GridsterDraggable, GridsterResizable) { + return { + scope: true, + restrict: 'EA', + controller: 'GridsterItemCtrl', + controllerAs: 'gridsterItem', + require: ['^gridster', 'gridsterItem'], + link: function(scope, $el, attrs, controllers) { + var optionsKey = attrs.gridsterItem, + options; - // Opera doesn't have Object.keys so we use this wrapper - var numberOfKeys = function(theObject) { - if (Object.keys) { - return Object.keys(theObject).length; - } + var gridster = controllers[0], + item = controllers[1]; - var n = 0, - key; - for (key in theObject) { - ++n; - } + scope.gridster = gridster; - return n; - }; + // bind the item's position properties + // options can be an object specified by gridster-item="object" + // or the options can be the element html attributes object + 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; + } - // 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; + item.init($el, gridster); + + $el.addClass('gridster-item'); + + var aspects = ['minSizeX', 'maxSizeX', 'minSizeY', 'maxSizeY', 'sizeX', 'sizeY', 'row', 'col'], + $getters = {}; + + var expressions = []; + var aspectFn = function(aspect) { + var expression; + if (typeof options[aspect] === 'string') { + // watch the expression in the scope + expression = options[aspect]; + } else if (typeof options[aspect.toLowerCase()] === 'string') { + // watch the expression in the scope + expression = options[aspect.toLowerCase()]; + } else if (optionsKey) { + // watch the expression on the options object in the scope + expression = optionsKey + '.' + aspect; + } else { + return; + } + expressions.push('"' + aspect + '":' + expression); + $getters[aspect] = $parse(expression); + + // 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]); + } + + var watchExpressions = '{' + expressions.join(',') + '}'; + // when the value changes externally, update the internal item object + scope.$watchCollection(watchExpressions, function(newVals, oldVals) { + for (var aspect in newVals) { + var newVal = newVals[aspect]; + var oldVal = oldVals[aspect]; + if (oldVal === newVal) { + continue; + } + newVal = parseInt(newVal, 10); + if (!isNaN(newVal)) { + item[aspect] = newVal; + } + } + }); + + 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() { + var changedX = item.setSizeX(item.sizeX, true); + if (changedX && $getters.sizeX && $getters.sizeX.assign) { + $getters.sizeX.assign(scope, item.sizeX); + } + var changedY = item.setSizeY(item.sizeY, true); + if (changedY && $getters.sizeY && $getters.sizeY.assign) { + $getters.sizeY.assign(scope, item.sizeY); + } + + if (changedX || changedY) { + item.gridster.moveOverlappingItems(item); + gridster.layoutChanged(); + scope.$broadcast('gridster-item-resized', item); + } + } + + 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); + + var updateResizable = function() { + resizable.toggle(!gridster.isMobile && gridster.resizable && gridster.resizable.enabled); + }; + updateResizable(); + + var updateDraggable = function() { + draggable.toggle(!gridster.isMobile && gridster.draggable && gridster.draggable.enabled); + }; + updateDraggable(); + + scope.$on('gridster-draggable-changed', updateDraggable); + scope.$on('gridster-resizable-changed', updateResizable); + scope.$on('gridster-resized', updateResizable); + scope.$on('gridster-mobile-changed', function() { + updateResizable(); + updateDraggable(); + }); + + 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]; + } + } + } + + var debouncedTransitionEndPublisher = _.debounce(function() { + scope.$broadcast('gridster-item-transition-end', item); + }, 50, { + leading: true, + trailing: true + }); + + $el.on(whichTransitionEvent(), debouncedTransitionEndPublisher); + + scope.$broadcast('gridster-item-initialized', item); + + return scope.$on('$destroy', function() { + try { + resizable.destroy(); + draggable.destroy(); + } catch (e) {} + + try { + gridster.removeItem(item); + } catch (e) {} + + try { + item.destroy(); + } catch (e) {} + }); + } + }; + } + ]); +})(window.angular); + +(function(angular) { + 'use strict'; + + angular.module('gridster').factory('GridsterResizable', ['GridsterTouch', function(GridsterTouch) { + 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 = 999999, + minLeft = 0; + + var getMinHeight = function() { + return (item.minSizeY ? item.minSizeY : 1) * gridster.curRowHeight - gridster.margins[0]; + }; + var getMinWidth = function() { + return (item.minSizeX ? item.minSizeX : 1) * gridster.curColWidth - gridster.margins[1]; + }; + + var originalWidth, originalHeight; + var savedDraggable; + + 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); + + if (_.chain(gridster).get('resizable.start').isFunction().valueOf()) { + scope.$apply(function() { + gridster.resizable.start(e, $el, itemOptions, item); // options is the item model + }); + } + } + + 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); + } + + + var canOccupy = row > -1 && col > -1 && sizeX + col <= gridster.columns && sizeY + row <= gridster.maxRows; + if (canOccupy && (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() { + gridster.resizable.resize(e, $el, itemOptions, item); // 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); + + if (_.chain(gridster).get('resizable.stop').isFunction().valueOf()) { + scope.$apply(function() { + gridster.resizable.stop(e, $el, itemOptions, item); // options is the item model + }); + } + } + + 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', gridster); + } + + // 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 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', gridster); + } + + mOffX = mOffY = 0; + + resizeStop(e); + + return true; + } + + var $dragHandle = null; + var unifiedInput; + + this.enable = function() { + if (!$dragHandle) { + $dragHandle = angular.element('
'); + $el.append($dragHandle); + } + + unifiedInput = new GridsterTouch($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; + }]); +})(window.angular); + +(function(angular) { + 'use strict'; + + angular.module('gridster').factory('GridsterTouch', [function() { + return function GridsterTouch(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; var oldIEUserAgent = navigator.userAgent.match(/\bMSIE\b/); for (var offsetElement = theElement; offsetElement != null; offsetElement = offsetElement.offsetParent) { @@ -1369,871 +1447,1036 @@ return this; }; - }]) - - .factory('GridsterDraggable', ['$document', '$window', 'GridsterTouch', - function($document, $window, GridsterTouch) { - 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, - minLeft = 0, - realdocument = $document[0]; - - var originalCol, originalRow; - var inputTags = ['select', 'option', 'input', 'textarea', 'button']; - - 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, item); - } - }); - } - - 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 = Math.min(gridster.pixelsToRows(elmY), gridster.maxRows - 1); - var col = Math.min(gridster.pixelsToColumns(elmX), gridster.columns - 1); - - var itemsInTheWay = gridster.getItems(row, col, item.sizeX, item.sizeY, item); - var hasItemsInTheWay = itemsInTheWay.length !== 0; - - if (gridster.swapping === true && hasItemsInTheWay) { - var boundingBoxItem = gridster.getBoundingBox(itemsInTheWay), - sameSize = boundingBoxItem.sizeX === item.sizeX && boundingBoxItem.sizeY === item.sizeY, - sameRow = boundingBoxItem.row === oldRow, - sameCol = boundingBoxItem.col === oldCol, - samePosition = boundingBoxItem.row === row && boundingBoxItem.col === col, - inline = sameRow || sameCol; - - if (sameSize && itemsInTheWay.length === 1) { - if (samePosition) { - gridster.swapItems(item, itemsInTheWay[0]); - } else if (inline) { - return; - } - } else if (boundingBoxItem.sizeX <= item.sizeX && boundingBoxItem.sizeY <= item.sizeY && inline) { - var emptyRow = item.row <= row ? item.row : row + item.sizeY, - emptyCol = item.col <= col ? item.col : col + item.sizeX, - rowOffset = emptyRow - boundingBoxItem.row, - colOffset = emptyCol - boundingBoxItem.col; - - for (var i = 0, l = itemsInTheWay.length; i < l; ++i) { - var itemInTheWay = itemsInTheWay[i]; - - var itemsInFreeSpace = gridster.getItems( - itemInTheWay.row + rowOffset, - itemInTheWay.col + colOffset, - itemInTheWay.sizeX, - itemInTheWay.sizeY, - item - ); - - if (itemsInFreeSpace.length === 0) { - gridster.putItem(itemInTheWay, itemInTheWay.row + rowOffset, itemInTheWay.col + colOffset); - } - } - } - } - - 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, item); - } - }); - } - } - - function dragStop(event) { - $el.removeClass('gridster-item-moving'); - var row = Math.min(gridster.pixelsToRows(elmY), gridster.maxRows - 1); - var col = Math.min(gridster.pixelsToColumns(elmX), gridster.columns - 1); - 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); - - scope.$apply(function() { - if (gridster.draggable && gridster.draggable.stop) { - gridster.draggable.stop(event, $el, itemOptions, item); - } - }); - } - - function mouseDown(e) { - if (inputTags.indexOf(e.target.nodeName.toLowerCase()) !== -1) { - return false; - } - - var $target = angular.element(e.target); - - // exit, if a resize handle was hit - if ($target.hasClass('gridster-item-resizable-handler')) { - return false; - } - - // exit, if the target has it's own click event - if ($target.attr('onclick') || $target.attr('ng-click')) { - return false; - } - - // only works if you have jQuery - if ($target.closest && $target.closest('.gridster-no-drag').length) { - return false; - } - - // apply drag handle filter - if (gridster.draggable && gridster.draggable.handle) { - var $dragHandles = angular.element($el[0].querySelectorAll(gridster.draggable.handle)); - var match = false; - outerloop: - for (var h = 0, hl = $dragHandles.length; h < hl; ++h) { - var handle = $dragHandles[h]; - if (handle === e.target) { - match = true; - break; - } - var target = e.target; - for (var p = 0; p < 20; ++p) { - var parent = target.parentNode; - if (parent === $el[0] || !parent) { - break; - } - if (parent === handle) { - match = true; - break outerloop; - } - target = parent; - } - } - if (!match) { - 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; - var maxTop = gridster.curRowHeight * gridster.maxRows - 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; + }]); +})(window.angular); - // Update last processed mouse positions. - lastMouseX = mouseX; - lastMouseY = mouseY; +(function(angular, _) { + 'use strict'; - 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; - } + angular.module('gridster').factory('GridsterViewport', GridsterViewportFactory); - 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; + GridsterViewportFactory.$inject = ['$injector']; - // set new position - $el.css({ - 'top': elmY + 'px', - 'left': elmX + 'px' - }); + function GridsterViewportFactory($injector) { + /** + * Constructor + */ + function GridsterViewport($element) { + this.$element = $element; + this.viewport = { + count: 0, + isIn: false + }; + } - drag(e); + /** + * Will determine if the $element of the instance is in the view port or not. + * @returns {boolean} + * @private + */ + GridsterViewport.prototype.isInViewPort_ = function isInViewPort_() { + if (!_.chain(this).get('$element.0.getBoundingClientRect').isFunction().valueOf()) { + return false; + } - return true; - } + var isVisible = Boolean(this.$element[0].offsetWidth * this.$element[0].offsetHeight); - function mouseUp(e) { - if (!$el.hasClass('gridster-item-moving') || $el.hasClass('gridster-item-resizing')) { - return false; - } + if (!isVisible) { + return isVisible; + } - mOffX = mOffY = 0; + var winHeight = $injector.get('$window').innerHeight; + var rect_ = this.$element[0].getBoundingClientRect(); + // Is top part visible + var topEdgeVisible = rect_.top >= 0 && rect_.top < winHeight; + // Is bottom part visible + var bottomEdgeVisible = rect_.bottom > 0 && rect_.bottom <= winHeight; - dragStop(e); + return topEdgeVisible || bottomEdgeVisible; + }; - return true; - } + /** + * Will return the scope of the instances' $element. + * @returns {Object} + * @private + */ + GridsterViewport.prototype.getScope_ = function getScope_() { + if (!_.chain(this).get('$element.scope').isFunction().valueOf()) { + return null; + } - var enabled = null; - var gridsterTouch = null; + return this.$element.scope(); + }; - this.enable = function() { - if (enabled === true) { - return; - } - enabled = true; + /** + * Will check if the $element is in the viewport and update viewport properties + * accordingly. + * @returns {Object} + */ + GridsterViewport.prototype.identify = function identify() { + if (this.isInViewPort_()) { + this.viewport.count = this.viewport.count + 1; + this.viewport.isIn = true; + } else { + this.viewport.isIn = false; + } - if (gridsterTouch) { - gridsterTouch.enable(); - return; - } + return this; + }; - gridsterTouch = new GridsterTouch($el[0], mouseDown, mouseMove, mouseUp); - gridsterTouch.enable(); - }; + /** + * Will fire a broadcast on the scope of $element passing item + * @param {GridsterItemCtrl} item + * @returns {Object} + */ + GridsterViewport.prototype.notify = function notify(item) { + if (!this.getScope_()) { + return this; + } - this.disable = function() { - if (enabled === false) { - return; - } + this.getScope_().$broadcast('gridster-item-viewport-status', item); - enabled = false; - if (gridsterTouch) { - gridsterTouch.disable(); - } - }; + return this; + }; - this.toggle = function(enabled) { - if (enabled) { - this.enable(); - } else { - this.disable(); - } - }; + /** + * Will reset the viewport hash and fire a broadcast on the scope of the $element passing the + * grid item + * @param {GridsterItemCtrl} item + * @returns {Object} + */ + GridsterViewport.prototype.reset = function reset(item) { + this.viewport = { + count: 0, + isIn: false + }; - this.destroy = function() { - this.disable(); - }; + if (!this.getScope_()) { + return this; } - return GridsterDraggable; - } - ]) + this.getScope_().$broadcast('gridster-item-viewport-reset', item); - .factory('GridsterResizable', ['GridsterTouch', function(GridsterTouch) { - function GridsterResizable($el, scope, gridster, item, itemOptions) { + return this; + }; - function ResizeHandle(handleClass) { + /** + * Plain getter + * @returns {boolean} + */ + GridsterViewport.prototype.isVisible = function isVisible() { + return _.get(this, 'viewport.isIn', false); + }; - var hClass = handleClass; + /** + * Will determine if $element is currently visible and if it has been in the viewport only once + * @returns {boolean} + */ + GridsterViewport.prototype.isFirstTimeVisible = function isFirstTimeVisible() { + return this.isVisible() && _.get(this, 'viewport.count') === 1; + }; - var elmX, elmY, elmW, elmH, + /** + * Kills references inside this instance + */ + GridsterViewport.prototype.destroy = function destroy() { + this.$element = null; + this.viewport = null; + }; - mouseX = 0, - mouseY = 0, - lastMouseX = 0, - lastMouseY = 0, - mOffX = 0, - mOffY = 0, + return GridsterViewport; + } +})(window.angular, window._); - minTop = 0, - maxTop = 9999, - minLeft = 0; +(function(angular) { + 'use strict'; - var getMinHeight = function() { - return (item.minSizeY ? item.minSizeY : 1) * gridster.curRowHeight - gridster.margins[0]; - }; - var getMinWidth = function() { - return (item.minSizeX ? item.minSizeX : 1) * gridster.curColWidth - gridster.margins[1]; - }; + angular.module('gridster').directive('gridsterNoDrag', function() { + return { + restrict: 'A', + link: function(scope, $element) { + $element.addClass('gridster-no-drag'); + } + }; + }); +})(window.angular); - var originalWidth, originalHeight; - var savedDraggable; +(function(angular) { + 'use strict'; - function resizeStart(e) { - $el.addClass('gridster-item-moving'); - $el.addClass('gridster-item-resizing'); + angular.module('gridster').directive('gridsterPreview', function() { + return { + replace: true, + scope: true, + require: '^gridster', + template: '
', + link: function(scope, $el, attrs, gridster) { - gridster.movingItem = item; + /** + * @returns {Object} style object for preview element + */ + scope.previewStyle = function() { + if (!gridster.movingItem) { + return { + display: 'none' + }; + } - item.setElementSizeX(); - item.setElementSizeY(); - item.setElementPosition(); - gridster.updateHeight(1); + 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' + }; + }; + } + }; + }); +})(window.angular); - scope.$apply(function() { - // callback - if (gridster.resizable && gridster.resizable.start) { - gridster.resizable.start(e, $el, itemOptions, item); // options is the item model - } - }); - } +(function(angular) { + 'use strict'; - function resize(e) { - var oldRow = item.row, - oldCol = item.col, - oldSizeX = item.sizeX, - oldSizeY = item.sizeY, - hasCallback = gridster.resizable && gridster.resizable.resize; + angular.module('gridster') + .controller('GridsterCtrl', ['gridsterConfig', '$injector', + function(gridsterConfig, $injector) { - var col = item.col; - // only change column if grabbing left edge - if (['w', 'nw', 'sw'].indexOf(handleClass) !== -1) { - col = gridster.pixelsToColumns(elmX, false); - } + var gridster = this; - var row = item.row; - // only change row if grabbing top edge - if (['n', 'ne', 'nw'].indexOf(handleClass) !== -1) { - row = gridster.pixelsToRows(elmY, false); - } + /** + * Create options from gridsterConfig constant + */ + angular.extend(this, gridsterConfig); - var sizeX = item.sizeX; - // only change row if grabbing left or right edge - if (['n', 's'].indexOf(handleClass) === -1) { - sizeX = gridster.pixelsToColumns(elmW, true); - } + this.resizable = angular.extend({}, gridsterConfig.resizable || {}); + this.draggable = angular.extend({}, gridsterConfig.draggable || {}); - var sizeY = item.sizeY; - // only change row if grabbing top or bottom edge - if (['e', 'w'].indexOf(handleClass) === -1) { - sizeY = gridster.pixelsToRows(elmH, true); + var flag = false; + this.layoutChanged = function() { + if (flag) { + return; } + flag = true; + $injector.get('$timeout')(function() { + flag = false; + if (gridster.loaded) { + gridster.floatItemsUp(); + } + gridster.updateHeight(gridster.movingItem ? gridster.movingItem.sizeY : 0); + }, 30); + }; + /** + * A positional array of the items in the grid + */ + this.grid = []; + this.allItems = []; - var canOccupy = row > -1 && col > -1 && sizeX + col <= gridster.columns && sizeY + row <= gridster.maxRows; - if (canOccupy && (gridster.pushing !== false || gridster.getItems(row, col, sizeX, sizeY, item).length === 0)) { - item.row = row; - item.col = col; - item.sizeX = sizeX; - item.sizeY = sizeY; + /** + * Clean up after yourself + */ + this.destroy = function() { + // empty the grid to cut back on the possibility + // of circular references + if (this.grid) { + this.grid = []; } - var isChanged = item.row !== oldRow || item.col !== oldCol || item.sizeX !== oldSizeX || item.sizeY !== oldSizeY; + this.$element = null; - if (hasCallback || isChanged) { - scope.$apply(function() { - if (hasCallback) { - gridster.resizable.resize(e, $el, itemOptions, item); // options is the item model - } - }); + if (this.allItems) { + this.allItems.length = 0; + this.allItems = null; } - } - - 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); + /** + * Overrides default options + * + * @param {Object} options The options to override + */ + this.setOptions = function(options) { + if (!options) { + return; + } - scope.$apply(function() { - if (gridster.resizable && gridster.resizable.stop) { - gridster.resizable.stop(e, $el, itemOptions, item); // options is the item model - } - }); - } + options = angular.extend({}, options); - function mouseDown(e) { - switch (e.which) { - case 1: - // left mouse button - break; - case 2: - case 3: - // right or middle mouse button - return; + // 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); - // save the draggable setting to restore after resize - savedDraggable = gridster.draggable.enabled; - if (savedDraggable) { - gridster.draggable.enabled = false; - scope.$broadcast('gridster-draggable-changed', gridster); + 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; + } + } } + }; - // Get the current mouse position. - lastMouseX = e.pageX; - lastMouseY = e.pageY; + /** + * 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; + }; - // Record current widget dimensions - elmX = parseInt($el.css('left'), 10); - elmY = parseInt($el.css('top'), 10); - elmW = $el[0].offsetWidth; - elmH = $el[0].offsetHeight; + /** + * 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!'); + }; - originalWidth = item.sizeX; - originalHeight = item.sizeY; + /** + * 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]; + } + var item; + if (this.sparse === false) { // check all cells + for (var h = 0; h < sizeY; ++h) { + for (var w = 0; w < sizeX; ++w) { + item = this.getItem(row + h, column + w, excludeItems); + if (item && (!excludeItems || excludeItems.indexOf(item) === -1) && items.indexOf(item) === -1) { + items.push(item); + } + } + } + } else { // check intersection with all items + var bottom = row + sizeY - 1; + var right = column + sizeX - 1; + for (var i = 0; i < this.allItems.length; ++i) { + item = this.allItems[i]; + if (item && (!excludeItems || excludeItems.indexOf(item) === -1) && items.indexOf(item) === -1 && this.intersect(item, column, right, row, bottom)) { + items.push(item); + } + } + } + return items; + }; - resizeStart(e); + /** + * @param {Array} items + * @returns {Object} An item that represents the bounding box of the items + */ + this.getBoundingBox = function(items) { - return true; - } + if (items.length === 0) { + return null; + } + if (items.length === 1) { + return { + row: items[0].row, + col: items[0].col, + sizeY: items[0].sizeY, + sizeX: items[0].sizeX + }; + } - function mouseMove(e) { - var maxLeft = gridster.curWidth - 1; + var maxRow = 0; + var maxCol = 0; + var minRow = 9999; + var minCol = 9999; - // Get the current mouse position. - mouseX = e.pageX; - mouseY = e.pageY; + for (var i = 0, l = items.length; i < l; ++i) { + var item = items[i]; + minRow = Math.min(item.row, minRow); + minCol = Math.min(item.col, minCol); + maxRow = Math.max(item.row + item.sizeY, maxRow); + maxCol = Math.max(item.col + item.sizeX, maxCol); + } - // Get the deltas - var diffX = mouseX - lastMouseX + mOffX; - var diffY = mouseY - lastMouseY + mOffY; - mOffX = mOffY = 0; + return { + row: minRow, + col: minCol, + sizeY: maxRow - minRow, + sizeX: maxCol - minCol + }; + }; - // Update last processed mouse positions. - lastMouseX = mouseX; - lastMouseY = mouseY; + /** + * Checks if item intersects specified box + * + * @param {object} item + * @param {number} left + * @param {number} right + * @param {number} top + * @param {number} bottom + */ - var dY = diffY, - dX = diffX; + this.intersect = function(item, left, right, top, bottom) { + return (left <= item.col + item.sizeX - 1 && + right >= item.col && + top <= item.row + item.sizeY - 1 && + bottom >= item.row); + }; - 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; + + /** + * Removes an item from the grid + * + * @param {Object} item + */ + this.removeItem = function(item) { + var index; + for (var rowIndex = 0, l = this.grid.length; rowIndex < l; ++rowIndex) { + var columns = this.grid[rowIndex]; + if (!columns) { + continue; + } + index = columns.indexOf(item); + if (index !== -1) { + columns[index] = null; + break; } - 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; + if (this.sparse) { + index = this.allItems.indexOf(item); + if (index !== -1) { + this.allItems.splice(index, 1); } - 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; + this.layoutChanged(); + }; + + /** + * 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; } - elmX += diffX; - elmW -= diffX; + --row; + ++sizeY; } - 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; + 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) { + // auto place item if no row specified + if (typeof row === 'undefined' || row === null) { + row = item.row; + column = item.col; + if (typeof row === 'undefined' || row === null) { + this.autoSetItemPosition(item); + return; } - elmW += diffX; } - // set new position - $el.css({ - 'top': elmY + 'px', - 'left': elmX + 'px', - 'width': elmW + 'px', - 'height': elmH + 'px' - }); + // keep item within allowed bounds + 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)); + } - resize(e); + // check if item is already in grid + if (item.oldRow !== null && typeof item.oldRow !== 'undefined') { + var samePosition = item.oldRow === row && item.oldColumn === column; + var inGrid = this.grid[row] && this.grid[row][column] === item; + if (samePosition && inGrid) { + 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]; + } + } + } - return true; - } + item.oldRow = item.row = row; + item.oldColumn = item.col = column; - function mouseUp(e) { - // restore draggable setting to its original state - if (gridster.draggable.enabled !== savedDraggable) { - gridster.draggable.enabled = savedDraggable; - scope.$broadcast('gridster-draggable-changed', gridster); + this.moveOverlappingItems(item, ignoreItems); + + if (!this.grid[row]) { + this.grid[row] = []; } + this.grid[row][column] = item; - mOffX = mOffY = 0; + if (this.sparse && this.allItems.indexOf(item) === -1) { + this.allItems.push(item); + } - resizeStop(e); + if (this.movingItem === item) { + this.floatItemUp(item); + } + this.layoutChanged(); + }; - return true; - } + /** + * 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; + }; - var $dragHandle = null; - var unifiedInput; + /** + * Prevents items from being overlapped + * + * @param {Object} item The item that should remain + * @param {Array} ignoreItems + */ + this.moveOverlappingItems = function(item, ignoreItems) { + // don't move item, so ignore it + if (!ignoreItems) { + ignoreItems = [item]; + } else if (ignoreItems.indexOf(item) === -1) { + ignoreItems = ignoreItems.slice(0); + ignoreItems.push(item); + } + + // get the items in the space occupied by the item's coordinates + var overlappingItems = this.getItems( + item.row, + item.col, + item.sizeX, + item.sizeY, + ignoreItems + ); + this.moveItemsDown(overlappingItems, item.row + item.sizeY, ignoreItems); + }; - this.enable = function() { - if (!$dragHandle) { - $dragHandle = angular.element('
'); - $el.append($dragHandle); + /** + * 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; + }); - unifiedInput = new GridsterTouch($dragHandle[0], mouseDown, mouseMove, mouseUp); - unifiedInput.enable(); + 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.disable = function() { - if ($dragHandle) { - $dragHandle.remove(); - $dragHandle = null; + /** + * Moves an item down to a specified row + * + * @param {Object} item The item to move + * @param {Number} newRow The target row + * @param {Array} ignoreItems + */ + 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); + }; - unifiedInput.disable(); - unifiedInput = undefined; + /** + * 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) { + var item = columns[colIndex]; + if (item) { + this.floatItemUp(item); + } + } + } + this.onScroll(); }; - this.destroy = function() { - this.disable(); + /** + * 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); + } }; - } - var handles = []; - var handlesOpts = gridster.resizable.handles; - if (typeof handlesOpts === 'string') { - handlesOpts = gridster.resizable.handles.split(','); - } - var enabled = false; + /** + * 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); + }; - for (var c = 0, l = handlesOpts.length; c < l; c++) { - handles.push(new ResizeHandle(handlesOpts[c])); - } + /** + * 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 (!this.outerMargin) { + pixels += this.margins[0] / 2; + } - this.enable = function() { - if (enabled) { - return; - } - for (var c = 0, l = handles.length; c < l; c++) { - handles[c].enable(); - } - enabled = true; - }; + if (ceilOrFloor === true) { + return Math.ceil(pixels / this.curRowHeight); + } else if (ceilOrFloor === false) { + return Math.floor(pixels / this.curRowHeight); + } - this.disable = function() { - if (!enabled) { - return; - } - for (var c = 0, l = handles.length; c < l; c++) { - handles[c].disable(); - } - enabled = false; - }; + return Math.round(pixels / this.curRowHeight); + }; - this.toggle = function(enabled) { - if (enabled) { - this.enable(); - } else { - this.disable(); - } - }; + /** + * 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 (!this.outerMargin) { + pixels += this.margins[1] / 2; + } - this.destroy = function() { - for (var c = 0, l = handles.length; c < l; c++) { - handles[c].destroy(); - } - }; - } - return GridsterResizable; - }]) - - .factory('gridsterDebounce', function() { - return function gridsterDebounce(func, wait, immediate) { - var timeout; - return function() { - var context = this, - args = arguments; - var later = function() { - timeout = null; - if (!immediate) { - func.apply(context, args); + 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); }; - var callNow = immediate && !timeout; - clearTimeout(timeout); - timeout = setTimeout(later, wait); - if (callNow) { - func.apply(context, args); - } - }; - }; - }) + /** + * Returns all current grid items + * @returns {Array} + */ + this.getItemList = function() { + return _.chain(gridster) + .get('grid') + .flattenDeep() + .compact() + .valueOf(); + }; + + /** + * Callback for scroll event. Will call viewportNotify on all elements + * placed inside the grid + * @private + */ + this.onScroll_ = function() { + gridster.notifyWidgets(); + }; + + this.onScroll = _.throttle(gridster.onScroll_, 1000, { + leading: true, + trailing: true + }); + + /** + * Will call viewportNotify on all gridster widgets broadcasting + * 'gridster-item-viewport-status' events on each and every one of them + * @returns {GridsterCtrl} + * @chainable + */ + this.notifyWidgets = function() { + _.chain(gridster.getItemList()) + .forEach(function(item) { + item.viewportNotify(); + }) + .valueOf(); + return gridster; + }; + + /** + * Will call viewportReset on all gridster widgets reseting their viewport 'status' hash + * @returns {GridsterCtrl} + * @chainable + */ + this.resetWidgets = function() { + _.chain(gridster.getItemList()) + .forEach(function(item) { + item.viewportReset(); + }) + .valueOf(); + return gridster; + }; + } + ]); +})(window.angular); + +(function(angular, _) { + 'use strict'; /** - * GridsterItem directive - * @param $parse - * @param GridsterDraggable - * @param GridsterResizable - * @param gridsterDebounce + * The gridster directive + * + * @param {Function} $timeout + * @param {Object} $window + * @param {Object} $rootScope */ - .directive('gridsterItem', ['$parse', 'GridsterDraggable', 'GridsterResizable', 'gridsterDebounce', - function($parse, GridsterDraggable, GridsterResizable, gridsterDebounce) { - return { - scope: true, - restrict: 'EA', - controller: 'GridsterItemCtrl', - controllerAs: 'gridsterItem', - require: ['^gridster', 'gridsterItem'], - link: function(scope, $el, attrs, controllers) { - var optionsKey = attrs.gridsterItem, - options; + angular.module('gridster') + .directive('gridster', ['$timeout', '$window', '$rootScope', + function($timeout, $window, $rootScope) { + return { + scope: true, + restrict: 'EAC', + controller: 'GridsterCtrl', + controllerAs: 'gridster', + compile: function($tplElem) { - var gridster = controllers[0], - item = controllers[1]; + $tplElem.prepend('
'); - scope.gridster = gridster; + return function(scope, $elem, attrs, gridster) { + gridster.loaded = false; - // bind the item's position properties - // options can be an object specified by gridster-item="object" - // or the options can be the element html attributes object - 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 + gridster.$element = $elem; + + scope.gridster = gridster; + + $elem.addClass('gridster'); + + var isVisible = function(ele) { + return ele.style.visibility !== 'hidden' && ele.style.display !== 'none'; }; - $optionsGetter.assign(scope, options); - } - } else { - options = attrs; - } - item.init($el, gridster); + function updateHeight() { + $elem.css('height', (gridster.gridHeight * gridster.curRowHeight) + (gridster.outerMargin ? gridster.margins[0] : -gridster.margins[0]) + 'px'); + } - $el.addClass('gridster-item'); + scope.$watch(function() { + return gridster.gridHeight; + }, updateHeight); - var aspects = ['minSizeX', 'maxSizeX', 'minSizeY', 'maxSizeY', 'sizeX', 'sizeY', 'row', 'col'], - $getters = {}; + scope.$watch(function() { + return gridster.movingItem; + }, function() { + gridster.updateHeight(gridster.movingItem ? gridster.movingItem.sizeY : 0); + }); - var expressions = []; - var aspectFn = function(aspect) { - var expression; - if (typeof options[aspect] === 'string') { - // watch the expression in the scope - expression = options[aspect]; - } else if (typeof options[aspect.toLowerCase()] === 'string') { - // watch the expression in the scope - expression = options[aspect.toLowerCase()]; - } else if (optionsKey) { - // watch the expression on the options object in the scope - expression = optionsKey + '.' + aspect; - } else { - return; - } - expressions.push('"' + aspect + '":' + expression); - $getters[aspect] = $parse(expression); + function refresh(config) { + gridster.setOptions(config); - // initial set - var val = $getters[aspect](scope); - if (typeof val === 'number') { - item[aspect] = val; - } - }; + if (!isVisible($elem[0])) { + return; + } - for (var i = 0, l = aspects.length; i < l; ++i) { - aspectFn(aspects[i]); - } + // resolve "auto" & "match" values + if (gridster.width === 'auto') { + gridster.curWidth = $elem[0].offsetWidth || parseInt($elem.css('width'), 10); + } else { + gridster.curWidth = gridster.width; + } - var watchExpressions = '{' + expressions.join(',') + '}'; - // when the value changes externally, update the internal item object - scope.$watchCollection(watchExpressions, function(newVals, oldVals) { - for (var aspect in newVals) { - var newVal = newVals[aspect]; - var oldVal = oldVals[aspect]; - if (oldVal === newVal) { - continue; - } - newVal = parseInt(newVal, 10); - if (!isNaN(newVal)) { - item[aspect] = newVal; - } - } - }); + if (gridster.colWidth === 'auto') { + gridster.curColWidth = (gridster.curWidth + (gridster.outerMargin ? -gridster.margins[1] : gridster.margins[1])) / gridster.columns; + } else { + gridster.curColWidth = gridster.colWidth; + } - function positionChanged() { - // call setPosition so the element and gridster controller are updated - item.setPosition(item.row, item.col); + 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(' ', '')); + } + } - // 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); + gridster.isMobile = gridster.mobileModeEnabled && gridster.curWidth <= gridster.mobileBreakPoint; - function sizeChanged() { - var changedX = item.setSizeX(item.sizeX, true); - if (changedX && $getters.sizeX && $getters.sizeX.assign) { - $getters.sizeX.assign(scope, item.sizeX); - } - var changedY = item.setSizeY(item.sizeY, true); - if (changedY && $getters.sizeY && $getters.sizeY.assign) { - $getters.sizeY.assign(scope, item.sizeY); - } + // 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; + } - if (changedX || changedY) { - item.gridster.moveOverlappingItems(item); - gridster.layoutChanged(); - scope.$broadcast('gridster-item-resized', item); - } - } + for (var colIndex = 0, len = columns.length; colIndex < len; ++colIndex) { + if (columns[colIndex]) { + var item = columns[colIndex]; + item.setElementPosition(); + item.setElementSizeY(); + item.setElementSizeX(); + } + } + } - scope.$watch(function() { - return item.sizeY + ',' + item.sizeX + ',' + item.minSizeX + ',' + item.maxSizeX + ',' + item.minSizeY + ',' + item.maxSizeY; - }, sizeChanged); + updateHeight(); + } - var draggable = new GridsterDraggable($el, scope, gridster, item, options); - var resizable = new GridsterResizable($el, scope, gridster, item, options); + var optionsKey = attrs.gridster; + if (optionsKey) { + scope.$parent.$watch(optionsKey, function(newConfig) { + refresh(newConfig); + }, true); + } else { + refresh({}); + } - var updateResizable = function() { - resizable.toggle(!gridster.isMobile && gridster.resizable && gridster.resizable.enabled); - }; - updateResizable(); + scope.$watch(function() { + return gridster.loaded; + }, function() { + if (gridster.loaded) { + $elem.addClass('gridster-loaded'); + $rootScope.$broadcast('gridster-loaded', gridster); + } else { + $elem.removeClass('gridster-loaded'); + } + }); - var updateDraggable = function() { - draggable.toggle(!gridster.isMobile && gridster.draggable && gridster.draggable.enabled); - }; - updateDraggable(); + scope.$watch(function() { + return gridster.isMobile; + }, function() { + if (gridster.isMobile) { + $elem.addClass('gridster-mobile').removeClass('gridster-desktop'); + } else { + $elem.removeClass('gridster-mobile').addClass('gridster-desktop'); + } + $rootScope.$broadcast('gridster-mobile-changed', gridster); + }); - scope.$on('gridster-draggable-changed', updateDraggable); - scope.$on('gridster-resizable-changed', updateResizable); - scope.$on('gridster-resized', updateResizable); - scope.$on('gridster-mobile-changed', function() { - updateResizable(); - updateDraggable(); - }); + scope.$watch(function() { + return gridster.draggable; + }, function() { + $rootScope.$broadcast('gridster-draggable-changed', gridster); + }, true); - 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]; - } - } - } + scope.$watch(function() { + return gridster.resizable; + }, function() { + $rootScope.$broadcast('gridster-resizable-changed', gridster); + }, true); - var debouncedTransitionEndPublisher = gridsterDebounce(function() { - scope.$apply(function() { - scope.$broadcast('gridster-item-transition-end', item); - }); - }, 50); + var prevWidth = $elem[0].offsetWidth || parseInt($elem.css('width'), 10); - $el.on(whichTransitionEvent(), debouncedTransitionEndPublisher); + var resize = function() { + var width = $elem[0].offsetWidth || parseInt($elem.css('width'), 10); - scope.$broadcast('gridster-item-initialized', item); + if (!width || width === prevWidth || gridster.movingItem) { + return; + } + prevWidth = width; - return scope.$on('$destroy', function() { - try { - resizable.destroy(); - draggable.destroy(); - } catch (e) {} + if (gridster.loaded) { + $elem.removeClass('gridster-loaded'); + } - try { - gridster.removeItem(item); - } catch (e) {} + refresh(); - try { - item.destroy(); - } catch (e) {} - }); - } - }; - } - ]) + if (gridster.loaded) { + $elem.addClass('gridster-loaded'); + } - .directive('gridsterNoDrag', function() { - return { - restrict: 'A', - link: function(scope, $element) { - $element.addClass('gridster-no-drag'); + $rootScope.$broadcast('gridster-resized', [width, $elem[0].offsetHeight], gridster); + }; + + // track element width changes any way we can + var onResize = _.debounce(function onResize() { + resize(); + }, 100, { + leading: false, + trailing: true + }); + + scope.$watch(function() { + return isVisible($elem[0]); + }, onResize); + + // see https://github.com/sdecima/javascript-detect-element-resize + if (typeof window.addResizeListener === 'function') { + window.addResizeListener($elem[0], onResize); + } else { + scope.$watch(function() { + return $elem[0].offsetWidth || parseInt($elem.css('width'), 10); + }, resize); + } + var $win = angular.element($window); + $win.on('resize', onResize); + + $win.on('scroll', gridster.onScroll); + + // be sure to cleanup + scope.$on('$destroy', function() { + gridster.destroy(); + $win.off('resize', onResize); + $win.off('scroll', gridster.onScroll); + if (typeof window.removeResizeListener === 'function') { + window.removeResizeListener($elem[0], onResize); + } + }); + + // allow a little time to place items before floating up + $timeout(function() { + scope.$watch('gridster.floating', function() { + gridster.floatItemsUp(); + }); + gridster.loaded = true; + }, 100); + }; + } + }; } - }; - }) + ]); +})(window.angular, window._); - ; +return angular; })); diff --git a/dist/angular-gridster.min.js b/dist/angular-gridster.min.js index 07d7c878..d45fa02a 100644 --- a/dist/angular-gridster.min.js +++ b/dist/angular-gridster.min.js @@ -5,4 +5,4 @@ * @version: 0.13.14 * @license: MIT */ -!function(a,b){"use strict";"function"==typeof define&&define.amd?define(["angular"],b):"object"==typeof exports?module.exports=b(require("angular")):b(a.angular)}(this,function(a){"use strict";return a.module("gridster",[]).constant("gridsterConfig",{columns:6,pushing:!0,floating:!0,swapping:!1,width:"auto",colWidth:"auto",rowHeight:"match",margins:[10,10],outerMargin:!0,sparse:!1,isMobile:!1,mobileBreakPoint:600,mobileModeEnabled:!0,minColumns:1,minRows:1,maxRows:100,defaultSizeX:2,defaultSizeY:1,minSizeX:1,maxSizeX:null,minSizeY:1,maxSizeY:null,saveGridItemCalculatedHeightInMobile:!1,resizable:{enabled:!0,handles:["s","e","n","w","se","ne","sw","nw"]},draggable:{enabled:!0,scrollSensitivity:20,scrollSpeed:15}}).controller("GridsterCtrl",["gridsterConfig","$timeout",function(b,c){var d=this;a.extend(this,b),this.resizable=a.extend({},b.resizable||{}),this.draggable=a.extend({},b.draggable||{});var e=!1;this.layoutChanged=function(){e||(e=!0,c(function(){e=!1,d.loaded&&d.floatItemsUp(),d.updateHeight(d.movingItem?d.movingItem.sizeY:0)},30))},this.grid=[],this.allItems=[],this.destroy=function(){this.grid&&(this.grid=[]),this.$element=null,this.allItems&&(this.allItems.length=0,this.allItems=null)},this.setOptions=function(b){if(b)if(b=a.extend({},b),b.draggable&&(a.extend(this.draggable,b.draggable),delete b.draggable),b.resizable&&(a.extend(this.resizable,b.resizable),delete b.resizable),a.extend(this,b),this.margins&&2===this.margins.length)for(var c=0,d=this.margins.length;c-1&&c>-1&&a.sizeX+c<=this.columns&&a.sizeY+b<=this.maxRows},this.autoSetItemPosition=function(a){for(var b=0;b=a.col&&d<=a.row+a.sizeY-1&&e>=a.row},this.removeItem=function(a){for(var b,c=0,d=this.grid.length;c-1;){for(var e=1,f=b;f>-1;){var g=this.grid[a];if(g){var h=g[f];if(h&&(!c||c.indexOf(h)===-1)&&h.sizeX>=e&&h.sizeY>=d)return h}++e,--f}--a,++d}return null},this.putItems=function(a){for(var b=0,c=a.length;b=b)){for(;a.row-1;){var h=this.getItems(g,b,d,c,a);if(0!==h.length)break;e=g,f=b,--g}null!==e&&this.putItem(a,e,f)}},this.updateHeight=function(a){var b=this.minRows;a=a||0;for(var c=this.grid.length;c>=0;--c){var d=this.grid[c];if(d)for(var e=0,f=d.length;e0?Math.min(this.maxRows,b):Math.max(this.maxRows,b)},this.pixelsToRows=function(a,b){return this.outerMargin||(a+=this.margins[0]/2),b===!0?Math.ceil(a/this.curRowHeight):b===!1?Math.floor(a/this.curRowHeight):Math.round(a/this.curRowHeight)},this.pixelsToColumns=function(a,b){return this.outerMargin||(a+=this.margins[1]/2),b===!0?Math.ceil(a/this.curColWidth):b===!1?Math.floor(a/this.curColWidth):Math.round(a/this.curColWidth)}}]).directive("gridsterPreview",function(){return{replace:!0,scope:!0,require:"^gridster",template:'
',link:function(a,b,c,d){a.previewStyle=function(){return d.movingItem?{display:"block",height:d.movingItem.sizeY*d.curRowHeight-d.margins[0]+"px",width:d.movingItem.sizeX*d.curColWidth-d.margins[1]+"px",top:d.movingItem.row*d.curRowHeight+(d.outerMargin?d.margins[0]:0)+"px",left:d.movingItem.col*d.curColWidth+(d.outerMargin?d.margins[1]:0)+"px"}:{display:"none"}}}}}).directive("gridster",["$timeout","$window","$rootScope","gridsterDebounce",function(b,c,d,e){return{scope:!0,restrict:"EAC",controller:"GridsterCtrl",controllerAs:"gridster",compile:function(f){return f.prepend('
'),function(f,g,h,i){function j(){g.css("height",i.gridHeight*i.curRowHeight+(i.outerMargin?i.margins[0]:-i.margins[0])+"px")}function k(a){if(i.setOptions(a),l(g[0])){"auto"===i.width?i.curWidth=g[0].offsetWidth||parseInt(g.css("width"),10):i.curWidth=i.width,"auto"===i.colWidth?i.curColWidth=(i.curWidth+(i.outerMargin?-i.margins[1]:i.margins[1]))/i.columns:i.curColWidth=i.colWidth,i.curRowHeight=i.rowHeight,"string"==typeof i.rowHeight&&("match"===i.rowHeight?i.curRowHeight=Math.round(i.curColWidth):i.rowHeight.indexOf("*")!==-1?i.curRowHeight=Math.round(i.curColWidth*i.rowHeight.replace("*","").replace(" ","")):i.rowHeight.indexOf("/")!==-1&&(i.curRowHeight=Math.round(i.curColWidth/i.rowHeight.replace("/","").replace(" ","")))),i.isMobile=i.mobileModeEnabled&&i.curWidth<=i.mobileBreakPoint;for(var b=0,c=i.grid.length;bb&&(d=b-p-r,z=h-d),q+ic&&(f=c-q-s,A=i-f),p+=d,q+=f,e.css({top:q+"px",left:p+"px"}),k(a),!0}function o(a){return!(!e.hasClass("gridster-item-moving")||e.hasClass("gridster-item-resizing"))&&(z=A=0,l(a),!0)}var p,q,r,s,t,u,v=0,w=0,x=0,y=0,z=0,A=0,B=0,C=0,D=b[0],E=["select","option","input","textarea","button"],F=null,G=null;this.enable=function(){if(F!==!0){if(F=!0,G)return void G.enable();G=new d(e[0],m,n,o),G.enable()}},this.disable=function(){F!==!1&&(F=!1,G&&G.disable())},this.toggle=function(a){a?this.enable():this.disable()},this.destroy=function(){this.disable()}}return e}]).factory("GridsterResizable",["GridsterTouch",function(b){function c(c,d,e,f,g){function h(h){function i(a){c.addClass("gridster-item-moving"),c.addClass("gridster-item-resizing"),e.movingItem=f,f.setElementSizeX(),f.setElementSizeY(),f.setElementPosition(),e.updateHeight(1),d.$apply(function(){e.resizable&&e.resizable.start&&e.resizable.start(a,c,g,f)})}function j(a){var b=f.row,i=f.col,j=f.sizeX,k=f.sizeY,l=e.resizable&&e.resizable.resize,m=f.col;["w","nw","sw"].indexOf(h)!==-1&&(m=e.pixelsToColumns(o,!1));var n=f.row;["n","ne","nw"].indexOf(h)!==-1&&(n=e.pixelsToRows(p,!1));var s=f.sizeX;["n","s"].indexOf(h)===-1&&(s=e.pixelsToColumns(q,!0));var t=f.sizeY;["e","w"].indexOf(h)===-1&&(t=e.pixelsToRows(r,!0));var u=n>-1&&m>-1&&s+m<=e.columns&&t+n<=e.maxRows;!u||e.pushing===!1&&0!==e.getItems(n,m,s,t,f).length||(f.row=n,f.col=m,f.sizeX=s,f.sizeY=t);var v=f.row!==b||f.col!==i||f.sizeX!==j||f.sizeY!==k;(l||v)&&d.$apply(function(){l&&e.resizable.resize(a,c,g,f)})}function k(a){c.removeClass("gridster-item-moving"),c.removeClass("gridster-item-resizing"),e.movingItem=null,f.setPosition(f.row,f.col),f.setSizeY(f.sizeY),f.setSizeX(f.sizeX),d.$apply(function(){e.resizable&&e.resizable.stop&&e.resizable.stop(a,c,g,f)})}function l(a){switch(a.which){case 1:break;case 2:case 3:return}return u=e.draggable.enabled,u&&(e.draggable.enabled=!1,d.$broadcast("gridster-draggable-changed",e)),z=a.pageX,A=a.pageY,o=parseInt(c.css("left"),10),p=parseInt(c.css("top"),10),q=c[0].offsetWidth,r=c[0].offsetHeight,s=f.sizeX,t=f.sizeY,i(a),!0}function m(a){var b=e.curWidth-1;x=a.pageX,y=a.pageY;var d=x-z+B,f=y-A+C;B=C=0,z=x,A=y;var g=f,h=d;return w.indexOf("n")>=0&&(r-g=0&&(r+gE&&(f=E-p-r,C=g-f),r+=f),w.indexOf("w")>=0&&(q-h=0&&(q+hb&&(d=b-o-q,B=h-d),q+=d),c.css({top:p+"px",left:o+"px",width:q+"px",height:r+"px"}),j(a),!0}function n(a){return e.draggable.enabled!==u&&(e.draggable.enabled=u,d.$broadcast("gridster-draggable-changed",e)),B=C=0,k(a),!0}var o,p,q,r,s,t,u,v,w=h,x=0,y=0,z=0,A=0,B=0,C=0,D=0,E=9999,F=0,G=function(){return(f.minSizeY?f.minSizeY:1)*e.curRowHeight-e.margins[0]},H=function(){return(f.minSizeX?f.minSizeX:1)*e.curColWidth-e.margins[1]},I=null;this.enable=function(){I||(I=a.element('
'),c.append(I)),v=new b(I[0],l,m,n),v.enable()},this.disable=function(){I&&(I.remove(),I=null),v.disable(),v=void 0},this.destroy=function(){this.disable()}}var i=[],j=e.resizable.handles;"string"==typeof j&&(j=e.resizable.handles.split(","));for(var k=!1,l=0,m=j.length;lb&&(d=b-o-q,y=h-d),p+ic&&(f=c-p-r,z=i-f),o+=d,p+=f,e.css({top:p+"px",left:o+"px"}),k(a),!0},100,{leading:!0,trailing:!0}),F=null,G=null;this.enable=function(){if(F!==!0){if(F=!0,G)return void G.enable();G=new d(e[0],m,E,n),G.enable()}},this.disable=function(){F!==!1&&(F=!1,G&&G.disable())},this.toggle=function(a){a?this.enable():this.disable()},this.destroy=function(){this.disable()}}return e}])}(window.angular),function(a,b){"use strict";a.module("gridster").controller("GridsterItemCtrl",["GridsterViewport",function(a){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(b,c){this.$element=b,this.gridster=c,this.sizeX=c.defaultSizeX,this.sizeY=c.defaultSizeY,this.gridsterViewport=new a(b)},this.destroy=function(){this.gridster=null,this.$element=null,this.gridsterViewport.destroy(),this.gridsterViewport=null},this.viewportNotify=function(){this.gridsterViewport.identify().notify(this)},this.viewportReset=function(){this.gridsterViewport.reset(this)},this.isVisible=function(){return this.gridsterViewport.isVisible()},this.isFirstTimeVisible=function(){return this.gridsterViewport.isFirstTimeVisible()},this.toJSON=function(){return{row:this.row,col:this.col,sizeY:this.sizeY,sizeX:this.sizeX}},this.isMoving=function(){return this.gridster.movingItem===this},this.setPosition=function(a,b){this.gridster.putItem(this,a,b),this.isMoving()||this.setElementPosition()},this.setSize=function(a,b,c){a=a.toUpperCase();var d="size"+a,e="Size"+a;if(""!==b){b=parseInt(b,10),(isNaN(b)||0===b)&&(b=this.gridster["default"+e]);var f="X"===a?this.gridster.columns:this.gridster.maxRows;this["max"+e]&&(f=Math.min(this["max"+e],f)),this.gridster["max"+e]&&(f=Math.min(this.gridster["max"+e],f)),"X"===a&&this.cols?f-=this.cols:"Y"===a&&this.rows&&(f-=this.rows);var g=0;this["min"+e]&&(g=Math.max(this["min"+e],g)),this.gridster["min"+e]&&(g=Math.max(this.gridster["min"+e],g)),b=Math.max(Math.min(b,f),g);var h=this[d]!==b||this["old"+e]&&this["old"+e]!==b;return this["old"+e]=this[d]=b,this.isMoving()||this["setElement"+e](),!c&&h&&(this.gridster.moveOverlappingItems(this),this.gridster.layoutChanged()),h}},this.setSizeY=function(a,b){return this.setSize("Y",a,b)},this.setSizeX=function(a,b){return this.setSize("X",a,b)},this.setElementPosition=function(){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:""}):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"})},this.setElementSizeY=function(){this.gridster.isMobile&&!this.gridster.saveGridItemCalculatedHeightInMobile?this.$element.css("height",""):this.$element.css("height",this.sizeY*this.gridster.curRowHeight-this.gridster.margins[0]+"px")},this.setElementSizeX=function(){this.gridster.isMobile?this.$element.css("width",""):this.$element.css("width",this.sizeX*this.gridster.curColWidth-this.gridster.margins[1]+"px")},this.getElementSizeX=function(){return this.sizeX*this.gridster.curColWidth-this.gridster.margins[1]},this.getElementSizeY=function(){return this.sizeY*this.gridster.curRowHeight-this.gridster.margins[0]}}])}(window.angular,window._),function(a){"use strict";a.module("gridster").directive("gridsterItem",["$parse","GridsterDraggable","GridsterResizable",function(a,b,c){return{scope:!0,restrict:"EA",controller:"GridsterItemCtrl",controllerAs:"gridsterItem",require:["^gridster","gridsterItem"],link:function(d,e,f,g){function h(){n.setPosition(n.row,n.col),q.row&&q.row.assign&&q.row.assign(d,n.row),q.col&&q.col.assign&&q.col.assign(d,n.col)}function i(){var a=n.setSizeX(n.sizeX,!0);a&&q.sizeX&&q.sizeX.assign&&q.sizeX.assign(d,n.sizeX);var b=n.setSizeY(n.sizeY,!0);b&&q.sizeY&&q.sizeY.assign&&q.sizeY.assign(d,n.sizeY),(a||b)&&(n.gridster.moveOverlappingItems(n),m.layoutChanged(),d.$broadcast("gridster-item-resized",n))}function j(){var a=document.createElement("div"),b={transition:"transitionend",OTransition:"oTransitionEnd",MozTransition:"transitionend",WebkitTransition:"webkitTransitionEnd"};for(var c in b)if(void 0!==a.style[c])return b[c]}var k,l=f.gridsterItem,m=g[0],n=g[1];if(d.gridster=m,l){var o=a(l);k=o(d)||{},!k&&o.assign&&(k={row:n.row,col:n.col,sizeX:n.sizeX,sizeY:n.sizeY,minSizeX:0,minSizeY:0,maxSizeX:null,maxSizeY:null},o.assign(d,k))}else k=f;n.init(e,m),e.addClass("gridster-item");for(var p=["minSizeX","maxSizeX","minSizeY","maxSizeY","sizeX","sizeY","row","col"],q={},r=[],s=function(b){var c;if("string"==typeof k[b])c=k[b];else if("string"==typeof k[b.toLowerCase()])c=k[b.toLowerCase()];else{if(!l)return;c=l+"."+b}r.push('"'+b+'":'+c),q[b]=a(c);var e=q[b](d);"number"==typeof e&&(n[b]=e)},t=0,u=p.length;t-1&&m>-1&&s+m<=e.columns&&t+n<=e.maxRows;!u||e.pushing===!1&&0!==e.getItems(n,m,s,t,f).length||(f.row=n,f.col=m,f.sizeX=s,f.sizeY=t);var v=f.row!==b||f.col!==i||f.sizeX!==j||f.sizeY!==k;l&&v&&d.$apply(function(){e.resizable.resize(a,c,g,f)})}function k(a){c.removeClass("gridster-item-moving"),c.removeClass("gridster-item-resizing"),e.movingItem=null,f.setPosition(f.row,f.col),f.setSizeY(f.sizeY),f.setSizeX(f.sizeX),_.chain(e).get("resizable.stop").isFunction().valueOf()&&d.$apply(function(){e.resizable.stop(a,c,g,f)})}function l(a){switch(a.which){case 1:break;case 2:case 3:return}return u=e.draggable.enabled,u&&(e.draggable.enabled=!1,d.$broadcast("gridster-draggable-changed",e)),z=a.pageX,A=a.pageY,o=parseInt(c.css("left"),10),p=parseInt(c.css("top"),10),q=c[0].offsetWidth,r=c[0].offsetHeight,s=f.sizeX,t=f.sizeY,i(a),!0}function m(a){var b=e.curWidth-1;x=a.pageX,y=a.pageY;var d=x-z+B,f=y-A+C;B=C=0,z=x,A=y;var g=f,h=d;return w.indexOf("n")>=0&&(r-g=0&&(r+gE&&(f=E-p-r,C=g-f),r+=f),w.indexOf("w")>=0&&(q-h=0&&(q+hb&&(d=b-o-q,B=h-d),q+=d),c.css({top:p+"px",left:o+"px",width:q+"px",height:r+"px"}),j(a),!0}function n(a){return e.draggable.enabled!==u&&(e.draggable.enabled=u,d.$broadcast("gridster-draggable-changed",e)),B=C=0,k(a),!0}var o,p,q,r,s,t,u,v,w=h,x=0,y=0,z=0,A=0,B=0,C=0,D=0,E=999999,F=0,G=function(){return(f.minSizeY?f.minSizeY:1)*e.curRowHeight-e.margins[0]},H=function(){return(f.minSizeX?f.minSizeX:1)*e.curColWidth-e.margins[1]},I=null;this.enable=function(){I||(I=a.element('
'),c.append(I)),v=new b(I[0],l,m,n),v.enable()},this.disable=function(){I&&(I.remove(),I=null),v.disable(),v=void 0},this.destroy=function(){this.disable()}}var i=[],j=e.resizable.handles;"string"==typeof j&&(j=e.resizable.handles.split(","));for(var k=!1,l=0,m=j.length;l=0&&e.top0&&e.bottom<=d;return f||g},c.prototype.getScope_=function(){return b.chain(this).get("$element.scope").isFunction().valueOf()?this.$element.scope():null},c.prototype.identify=function(){return this.isInViewPort_()?(this.viewport.count=this.viewport.count+1,this.viewport.isIn=!0):this.viewport.isIn=!1,this},c.prototype.notify=function(a){return this.getScope_()?(this.getScope_().$broadcast("gridster-item-viewport-status",a),this):this},c.prototype.reset=function(a){return this.viewport={count:0,isIn:!1},this.getScope_()?(this.getScope_().$broadcast("gridster-item-viewport-reset",a),this):this},c.prototype.isVisible=function(){return b.get(this,"viewport.isIn",!1)},c.prototype.isFirstTimeVisible=function(){return this.isVisible()&&1===b.get(this,"viewport.count")},c.prototype.destroy=function(){this.$element=null,this.viewport=null},c}a.module("gridster").factory("GridsterViewport",c),c.$inject=["$injector"]}(window.angular,window._),function(a){"use strict";a.module("gridster").directive("gridsterNoDrag",function(){return{restrict:"A",link:function(a,b){b.addClass("gridster-no-drag")}}})}(window.angular),function(a){"use strict";a.module("gridster").directive("gridsterPreview",function(){return{replace:!0,scope:!0,require:"^gridster",template:'
',link:function(a,b,c,d){a.previewStyle=function(){return d.movingItem?{display:"block",height:d.movingItem.sizeY*d.curRowHeight-d.margins[0]+"px",width:d.movingItem.sizeX*d.curColWidth-d.margins[1]+"px",top:d.movingItem.row*d.curRowHeight+(d.outerMargin?d.margins[0]:0)+"px",left:d.movingItem.col*d.curColWidth+(d.outerMargin?d.margins[1]:0)+"px"}:{display:"none"}}}}})}(window.angular),function(a){"use strict";a.module("gridster").controller("GridsterCtrl",["gridsterConfig","$injector",function(b,c){var d=this;a.extend(this,b),this.resizable=a.extend({},b.resizable||{}),this.draggable=a.extend({},b.draggable||{});var e=!1;this.layoutChanged=function(){e||(e=!0,c.get("$timeout")(function(){e=!1,d.loaded&&d.floatItemsUp(),d.updateHeight(d.movingItem?d.movingItem.sizeY:0)},30))},this.grid=[],this.allItems=[],this.destroy=function(){this.grid&&(this.grid=[]),this.$element=null,this.allItems&&(this.allItems.length=0,this.allItems=null)},this.setOptions=function(b){if(b)if(b=a.extend({},b),b.draggable&&(a.extend(this.draggable,b.draggable),delete b.draggable),b.resizable&&(a.extend(this.resizable,b.resizable),delete b.resizable),a.extend(this,b),this.margins&&2===this.margins.length)for(var c=0,d=this.margins.length;c-1&&c>-1&&a.sizeX+c<=this.columns&&a.sizeY+b<=this.maxRows},this.autoSetItemPosition=function(a){for(var b=0;b=a.col&&d<=a.row+a.sizeY-1&&e>=a.row},this.removeItem=function(a){for(var b,c=0,d=this.grid.length;c-1;){for(var e=1,f=b;f>-1;){var g=this.grid[a];if(g){var h=g[f];if(h&&(!c||c.indexOf(h)===-1)&&h.sizeX>=e&&h.sizeY>=d)return h}++e,--f}--a,++d}return null},this.putItems=function(a){for(var b=0,c=a.length;b=b)){for(;a.row-1;){var h=this.getItems(g,b,d,c,a);if(0!==h.length)break;e=g,f=b,--g}null!==e&&this.putItem(a,e,f)}},this.updateHeight=function(a){var b=this.minRows;a=a||0;for(var c=this.grid.length;c>=0;--c){var d=this.grid[c];if(d)for(var e=0,f=d.length;e0?Math.min(this.maxRows,b):Math.max(this.maxRows,b)},this.pixelsToRows=function(a,b){return this.outerMargin||(a+=this.margins[0]/2),b===!0?Math.ceil(a/this.curRowHeight):b===!1?Math.floor(a/this.curRowHeight):Math.round(a/this.curRowHeight)},this.pixelsToColumns=function(a,b){return this.outerMargin||(a+=this.margins[1]/2),b===!0?Math.ceil(a/this.curColWidth):b===!1?Math.floor(a/this.curColWidth):Math.round(a/this.curColWidth)},this.getItemList=function(){return _.chain(d).get("grid").flattenDeep().compact().valueOf()},this.onScroll_=function(){d.notifyWidgets()},this.onScroll=_.throttle(d.onScroll_,1e3,{leading:!0,trailing:!0}),this.notifyWidgets=function(){return _.chain(d.getItemList()).forEach(function(a){a.viewportNotify()}).valueOf(),d},this.resetWidgets=function(){return _.chain(d.getItemList()).forEach(function(a){a.viewportReset()}).valueOf(),d}}])}(window.angular),function(a,b){"use strict";a.module("gridster").directive("gridster",["$timeout","$window","$rootScope",function(c,d,e){return{scope:!0,restrict:"EAC",controller:"GridsterCtrl",controllerAs:"gridster",compile:function(f){return f.prepend('
'),function(f,g,h,i){function j(){g.css("height",i.gridHeight*i.curRowHeight+(i.outerMargin?i.margins[0]:-i.margins[0])+"px")}function k(a){if(i.setOptions(a),l(g[0])){"auto"===i.width?i.curWidth=g[0].offsetWidth||parseInt(g.css("width"),10):i.curWidth=i.width,"auto"===i.colWidth?i.curColWidth=(i.curWidth+(i.outerMargin?-i.margins[1]:i.margins[1]))/i.columns:i.curColWidth=i.colWidth,i.curRowHeight=i.rowHeight,"string"==typeof i.rowHeight&&("match"===i.rowHeight?i.curRowHeight=Math.round(i.curColWidth):i.rowHeight.indexOf("*")!==-1?i.curRowHeight=Math.round(i.curColWidth*i.rowHeight.replace("*","").replace(" ","")):i.rowHeight.indexOf("/")!==-1&&(i.curRowHeight=Math.round(i.curColWidth/i.rowHeight.replace("/","").replace(" ","")))),i.isMobile=i.mobileModeEnabled&&i.curWidth<=i.mobileBreakPoint;for(var b=0,c=i.grid.length;b + + - + --> - + + + - +