1 /*global define:true*/
\r
2 (function(root, factory) {
\r
6 if (typeof define === 'function' && define.amd) {
\r
8 define(['angular'], factory);
\r
9 } else if (typeof exports === 'object') {
\r
11 module.exports = factory(require('angular'));
\r
13 // Browser, nothing "exported". Only registered as a module with angular.
\r
14 factory(root.angular);
\r
16 }(this, function(angular) {
\r
22 var getInternetExplorerVersion = function ()
\r
23 // Returns the version of Internet Explorer >4 or
\r
24 // undefined(indicating the use of another browser).
\r
26 var isIE10 = (eval("/*@cc_on!@*/false") && document.documentMode === 10);
\r
31 div = document.createElement('div'),
\r
32 all = div.getElementsByTagName('i');
\r
34 div.innerHTML = '<!--[if gt IE ' + (++v) + ']><i></i><![endif]-->';
\r
36 return v > 4 ? v : undefined;
\r
39 var browserVersion = getInternetExplorerVersion();
\r
41 if (browserVersion && browserVersion < 9) {
\r
45 // This returned angular module 'gridster' is what is exported.
\r
46 return angular.module('attGridsterLib', [])
\r
48 .constant('gridsterConfig', {
\r
49 columns: 6, // number of columns in the grid
\r
50 pushing: true, // whether to push other items out of the way
\r
51 floating: true, // whether to automatically float items up so they stack
\r
52 swapping: true, // whether or not to have items switch places instead of push down if they are the same size
\r
53 width: 'auto', // width of the grid. "auto" will expand the grid to its parent container
\r
54 colWidth: 'auto', // width of grid columns. "auto" will divide the width of the grid evenly among the columns
\r
55 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.
\r
56 margins: [10, 10], // margins in between grid items
\r
58 isMobile: false, // toggle mobile view
\r
59 mobileBreakPoint: 100, // width threshold to toggle mobile mode
\r
60 mobileModeEnabled: true, // whether or not to toggle mobile mode when screen width is less than mobileBreakPoint
\r
61 minColumns: 1, // minimum amount of columns the grid can scale down to
\r
62 minRows: 1, // minimum amount of rows to show if the grid is empty
\r
63 maxRows: 100, // maximum amount of rows in the grid
\r
64 defaultSizeX: 1, // default width of an item in columns
\r
65 defaultSizeY: 1, // default height of an item in rows
\r
66 minSizeX: 1, // minimum column width of an item
\r
67 maxSizeX: null, // maximum column width of an item
\r
68 minSizeY: 1, // minumum row height of an item
\r
69 maxSizeY: null, // maximum row height of an item
\r
70 saveGridItemCalculatedHeightInMobile: false, // grid item height in mobile display. true- to use the calculated height by sizeY given
\r
71 resizable: { // options to pass to resizable handler
\r
73 handles: ['s', 'e', 'n', 'w', 'se', 'ne', 'sw', 'nw']
\r
75 draggable: { // options to pass to draggable handler
\r
77 scrollSensitivity: 20, // Distance in pixels from the edge of the viewport after which the viewport should scroll, relative to pointer
\r
78 scrollSpeed: 15 // Speed at which the window should scroll once the mouse pointer gets within scrollSensitivity distance
\r
82 .controller('GridsterCtrl', ['gridsterConfig', '$timeout',
\r
83 function(gridsterConfig, $timeout) {
\r
85 var gridster = this;
\r
88 * Create options from gridsterConfig constant
\r
90 angular.extend(this, gridsterConfig);
\r
92 this.resizable = angular.extend({}, gridsterConfig.resizable || {});
\r
93 this.draggable = angular.extend({}, gridsterConfig.draggable || {});
\r
96 this.layoutChanged = function() {
\r
101 $timeout(function() {
\r
103 if (gridster.loaded) {
\r
104 gridster.floatItemsUp();
\r
106 gridster.updateHeight(gridster.movingItem ? gridster.movingItem.sizeY : 0);
\r
111 * A positional array of the items in the grid
\r
116 * Clean up after yourself
\r
118 this.destroy = function() {
\r
119 // empty the grid to cut back on the possibility
\r
120 // of circular references
\r
124 this.$element = null;
\r
128 * Overrides default options
\r
130 * @param {Object} options The options to override
\r
132 this.setOptions = function(options) {
\r
137 options = angular.extend({}, options);
\r
139 // all this to avoid using jQuery...
\r
140 if (options.draggable) {
\r
141 angular.extend(this.draggable, options.draggable);
\r
142 delete(options.draggable);
\r
144 if (options.resizable) {
\r
145 angular.extend(this.resizable, options.resizable);
\r
146 delete(options.resizable);
\r
149 angular.extend(this, options);
\r
151 if (!this.margins || this.margins.length !== 2) {
\r
152 this.margins = [0, 0];
\r
154 for (var x = 0, l = this.margins.length; x < l; ++x) {
\r
155 this.margins[x] = parseInt(this.margins[x], 10);
\r
156 if (isNaN(this.margins[x])) {
\r
157 this.margins[x] = 0;
\r
164 * Check if item can occupy a specified position in the grid
\r
166 * @param {Object} item The item in question
\r
167 * @param {Number} row The row index
\r
168 * @param {Number} column The column index
\r
169 * @returns {Boolean} True if if item fits
\r
171 this.canItemOccupy = function(item, row, column) {
\r
172 return row > -1 && column > -1 && item.sizeX + column <= this.columns && item.sizeY + row <= this.maxRows;
\r
176 * Set the item in the first suitable position
\r
178 * @param {Object} item The item to insert
\r
180 this.autoSetItemPosition = function(item) {
\r
181 // walk through each row and column looking for a place it will fit
\r
182 for (var rowIndex = 0; rowIndex < this.maxRows; ++rowIndex) {
\r
183 for (var colIndex = 0; colIndex < this.columns; ++colIndex) {
\r
184 // only insert if position is not already taken and it can fit
\r
185 var items = this.getItems(rowIndex, colIndex, item.sizeX, item.sizeY, item);
\r
186 if (items.length === 0 && this.canItemOccupy(item, rowIndex, colIndex)) {
\r
187 this.putItem(item, rowIndex, colIndex);
\r
192 throw new Error('Unable to place item!');
\r
196 * Gets items at a specific coordinate
\r
198 * @param {Number} row
\r
199 * @param {Number} column
\r
200 * @param {Number} sizeX
\r
201 * @param {Number} sizeY
\r
202 * @param {Array} excludeItems An array of items to exclude from selection
\r
203 * @returns {Array} Items that match the criteria
\r
205 this.getItems = function(row, column, sizeX, sizeY, excludeItems) {
\r
207 if (!sizeX || !sizeY) {
\r
210 if (excludeItems && !(excludeItems instanceof Array)) {
\r
211 excludeItems = [excludeItems];
\r
213 for (var h = 0; h < sizeY; ++h) {
\r
214 for (var w = 0; w < sizeX; ++w) {
\r
215 var item = this.getItem(row + h, column + w, excludeItems);
\r
216 if (item && (!excludeItems || excludeItems.indexOf(item) === -1) && items.indexOf(item) === -1) {
\r
225 * @param {Array} items
\r
226 * @returns {Object} An item that represents the bounding box of the items
\r
228 this.getBoundingBox = function(items) {
\r
230 if (items.length === 0) {
\r
233 if (items.length === 1) {
\r
237 sizeY: items[0].sizeY,
\r
238 sizeX: items[0].sizeX
\r
247 for (var i = 0, l = items.length; i < l; ++i) {
\r
248 var item = items[i];
\r
249 minRow = Math.min(item.row, minRow);
\r
250 minCol = Math.min(item.col, minCol);
\r
251 maxRow = Math.max(item.row + item.sizeY, maxRow);
\r
252 maxCol = Math.max(item.col + item.sizeX, maxCol);
\r
258 sizeY: maxRow - minRow,
\r
259 sizeX: maxCol - minCol
\r
265 * Removes an item from the grid
\r
267 * @param {Object} item
\r
269 this.removeItem = function(item) {
\r
270 for (var rowIndex = 0, l = this.grid.length; rowIndex < l; ++rowIndex) {
\r
271 var columns = this.grid[rowIndex];
\r
275 var index = columns.indexOf(item);
\r
276 if (index !== -1) {
\r
277 columns[index] = null;
\r
281 this.layoutChanged();
\r
285 * Returns the item at a specified coordinate
\r
287 * @param {Number} row
\r
288 * @param {Number} column
\r
289 * @param {Array} excludeItems Items to exclude from selection
\r
290 * @returns {Object} The matched item or null
\r
292 this.getItem = function(row, column, excludeItems) {
\r
293 if (excludeItems && !(excludeItems instanceof Array)) {
\r
294 excludeItems = [excludeItems];
\r
301 var items = this.grid[row];
\r
303 var item = items[col];
\r
304 if (item && (!excludeItems || excludeItems.indexOf(item) === -1) && item.sizeX >= sizeX && item.sizeY >= sizeY) {
\r
318 * Insert an array of items into the grid
\r
320 * @param {Array} items An array of items to insert
\r
322 this.putItems = function(items) {
\r
323 for (var i = 0, l = items.length; i < l; ++i) {
\r
324 this.putItem(items[i]);
\r
329 * Insert a single item into the grid
\r
331 * @param {Object} item The item to insert
\r
332 * @param {Number} row (Optional) Specifies the items row index
\r
333 * @param {Number} column (Optional) Specifies the items column index
\r
334 * @param {Array} ignoreItems
\r
336 this.putItem = function(item, row, column, ignoreItems) {
\r
337 // auto place item if no row specified
\r
338 if (typeof row === 'undefined' || row === null) {
\r
341 if (typeof row === 'undefined' || row === null) {
\r
342 this.autoSetItemPosition(item);
\r
347 // keep item within allowed bounds
\r
348 if (!this.canItemOccupy(item, row, column)) {
\r
349 column = Math.min(this.columns - item.sizeX, Math.max(0, column));
\r
350 row = Math.min(this.maxRows - item.sizeY, Math.max(0, row));
\r
353 // check if item is already in grid
\r
354 if (item.oldRow !== null && typeof item.oldRow !== 'undefined') {
\r
355 var samePosition = item.oldRow === row && item.oldColumn === column;
\r
356 var inGrid = this.grid[row] && this.grid[row][column] === item;
\r
357 if (samePosition && inGrid) {
\r
362 // remove from old position
\r
363 var oldRow = this.grid[item.oldRow];
\r
364 if (oldRow && oldRow[item.oldColumn] === item) {
\r
365 delete oldRow[item.oldColumn];
\r
370 item.oldRow = item.row = row;
\r
371 item.oldColumn = item.col = column;
\r
373 this.moveOverlappingItems(item, ignoreItems);
\r
375 if (!this.grid[row]) {
\r
376 this.grid[row] = [];
\r
378 this.grid[row][column] = item;
\r
380 if (this.movingItem === item) {
\r
381 this.floatItemUp(item);
\r
383 this.layoutChanged();
\r
387 * Trade row and column if item1 with item2
\r
389 * @param {Object} item1
\r
390 * @param {Object} item2
\r
392 this.swapItems = function(item1, item2) {
\r
393 this.grid[item1.row][item1.col] = item2;
\r
394 this.grid[item2.row][item2.col] = item1;
\r
396 var item1Row = item1.row;
\r
397 var item1Col = item1.col;
\r
398 item1.row = item2.row;
\r
399 item1.col = item2.col;
\r
400 item2.row = item1Row;
\r
401 item2.col = item1Col;
\r
405 * Prevents items from being overlapped
\r
407 * @param {Object} item The item that should remain
\r
408 * @param {Array} ignoreItems
\r
410 this.moveOverlappingItems = function(item, ignoreItems) {
\r
411 // don't move item, so ignore it
\r
412 if (!ignoreItems) {
\r
413 ignoreItems = [item];
\r
414 } else if (ignoreItems.indexOf(item) === -1) {
\r
415 ignoreItems = ignoreItems.slice(0);
\r
416 ignoreItems.push(item);
\r
419 // get the items in the space occupied by the item's coordinates
\r
420 var overlappingItems = this.getItems(
\r
427 this.moveItemsDown(overlappingItems, item.row + item.sizeY, ignoreItems);
\r
431 * Moves an array of items to a specified row
\r
433 * @param {Array} items The items to move
\r
434 * @param {Number} newRow The target row
\r
435 * @param {Array} ignoreItems
\r
437 this.moveItemsDown = function(items, newRow, ignoreItems) {
\r
438 if (!items || items.length === 0) {
\r
441 items.sort(function(a, b) {
\r
442 return a.row - b.row;
\r
445 ignoreItems = ignoreItems ? ignoreItems.slice(0) : [];
\r
449 // calculate the top rows in each column
\r
450 for (i = 0, l = items.length; i < l; ++i) {
\r
452 var topRow = topRows[item.col];
\r
453 if (typeof topRow === 'undefined' || item.row < topRow) {
\r
454 topRows[item.col] = item.row;
\r
458 // move each item down from the top row in its column to the row
\r
459 for (i = 0, l = items.length; i < l; ++i) {
\r
461 var rowsToMove = newRow - topRows[item.col];
\r
462 this.moveItemDown(item, item.row + rowsToMove, ignoreItems);
\r
463 ignoreItems.push(item);
\r
468 * Moves an item down to a specified row
\r
470 * @param {Object} item The item to move
\r
471 * @param {Number} newRow The target row
\r
472 * @param {Array} ignoreItems
\r
474 this.moveItemDown = function(item, newRow, ignoreItems) {
\r
475 if (item.row >= newRow) {
\r
478 while (item.row < newRow) {
\r
480 this.moveOverlappingItems(item, ignoreItems);
\r
482 this.putItem(item, item.row, item.col, ignoreItems);
\r
486 * Moves all items up as much as possible
\r
488 this.floatItemsUp = function() {
\r
489 if (this.floating === false) {
\r
492 for (var rowIndex = 0, l = this.grid.length; rowIndex < l; ++rowIndex) {
\r
493 var columns = this.grid[rowIndex];
\r
497 for (var colIndex = 0, len = columns.length; colIndex < len; ++colIndex) {
\r
498 var item = columns[colIndex];
\r
500 this.floatItemUp(item);
\r
507 * Float an item up to the most suitable row
\r
509 * @param {Object} item The item to move
\r
511 this.floatItemUp = function(item) {
\r
512 if (this.floating === false) {
\r
515 var colIndex = item.col,
\r
516 sizeY = item.sizeY,
\r
517 sizeX = item.sizeX,
\r
520 rowIndex = item.row - 1;
\r
522 while (rowIndex > -1) {
\r
523 var items = this.getItems(rowIndex, colIndex, sizeX, sizeY, item);
\r
524 if (items.length !== 0) {
\r
527 bestRow = rowIndex;
\r
528 bestColumn = colIndex;
\r
531 if (bestRow !== null) {
\r
532 this.putItem(item, bestRow, bestColumn);
\r
537 * Update gridsters height
\r
539 * @param {Number} plus (Optional) Additional height to add
\r
541 this.updateHeight = function(plus) {
\r
542 var maxHeight = this.minRows;
\r
544 for (var rowIndex = this.grid.length; rowIndex >= 0; --rowIndex) {
\r
545 var columns = this.grid[rowIndex];
\r
549 for (var colIndex = 0, len = columns.length; colIndex < len; ++colIndex) {
\r
550 if (columns[colIndex]) {
\r
551 maxHeight = Math.max(maxHeight, rowIndex + plus + columns[colIndex].sizeY);
\r
555 this.gridHeight = this.maxRows - maxHeight > 0 ? Math.min(this.maxRows, maxHeight) : Math.max(this.maxRows, maxHeight);
\r
559 * Returns the number of rows that will fit in given amount of pixels
\r
561 * @param {Number} pixels
\r
562 * @param {Boolean} ceilOrFloor (Optional) Determines rounding method
\r
564 this.pixelsToRows = function(pixels, ceilOrFloor) {
\r
565 if (ceilOrFloor === true) {
\r
566 return Math.ceil(pixels / this.curRowHeight);
\r
567 } else if (ceilOrFloor === false) {
\r
568 return Math.floor(pixels / this.curRowHeight);
\r
571 return Math.round(pixels / this.curRowHeight);
\r
575 * Returns the number of columns that will fit in a given amount of pixels
\r
577 * @param {Number} pixels
\r
578 * @param {Boolean} ceilOrFloor (Optional) Determines rounding method
\r
579 * @returns {Number} The number of columns
\r
581 this.pixelsToColumns = function(pixels, ceilOrFloor) {
\r
582 if (ceilOrFloor === true) {
\r
583 return Math.ceil(pixels / this.curColWidth);
\r
584 } else if (ceilOrFloor === false) {
\r
585 return Math.floor(pixels / this.curColWidth);
\r
588 return Math.round(pixels / this.curColWidth);
\r
593 .directive('gridsterPreview', function() {
\r
597 require: '^gridster',
\r
598 template: '<div ng-style="previewStyle()" class="gridster-item gridster-preview-holder"></div>',
\r
599 link: function(scope, $el, attrs, gridster) {
\r
602 * @returns {Object} style object for preview element
\r
604 scope.previewStyle = function() {
\r
606 if (!gridster.movingItem) {
\r
614 height: (gridster.movingItem.sizeY * gridster.curRowHeight - gridster.margins[0]) + 'px',
\r
615 width: (gridster.movingItem.sizeX * gridster.curColWidth - gridster.margins[1]) + 'px',
\r
616 top: (gridster.movingItem.row * gridster.curRowHeight + (gridster.outerMargin ? gridster.margins[0] : 0)) + 'px',
\r
617 left: (gridster.movingItem.col * gridster.curColWidth + (gridster.outerMargin ? gridster.margins[1] : 0)) + 'px'
\r
625 * The gridster directive
\r
627 * @param {Function} $timeout
\r
628 * @param {Object} $window
\r
629 * @param {Object} $rootScope
\r
630 * @param {Function} gridsterDebounce
\r
632 .directive('gridster', ['$timeout', '$window', '$rootScope', 'gridsterDebounce',
\r
633 function($timeout, $window, $rootScope, gridsterDebounce) {
\r
637 controller: 'GridsterCtrl',
\r
638 controllerAs: 'gridster',
\r
639 compile: function($tplElem) {
\r
641 $tplElem.prepend('<div ng-if="gridster.movingItem" gridster-preview></div>');
\r
643 return function(scope, $elem, attrs, gridster) {
\r
644 gridster.loaded = false;
\r
646 gridster.$element = $elem;
\r
648 scope.gridster = gridster;
\r
650 $elem.addClass('gridster');
\r
652 var isVisible = function(ele) {
\r
653 return ele.style.visibility !== 'hidden' && ele.style.display !== 'none';
\r
656 function refresh(config) {
\r
657 gridster.setOptions(config);
\r
659 if (!isVisible($elem[0])) {
\r
663 // resolve "auto" & "match" values
\r
664 if (gridster.width === 'auto') {
\r
665 gridster.curWidth = $elem[0].offsetWidth || parseInt($elem.css('width'), 10);
\r
667 gridster.curWidth = gridster.width;
\r
670 if (gridster.colWidth === 'auto') {
\r
671 gridster.curColWidth = (gridster.curWidth + (gridster.outerMargin ? -gridster.margins[1] : gridster.margins[1])) / gridster.columns;
\r
673 gridster.curColWidth = gridster.colWidth;
\r
676 gridster.curRowHeight = gridster.rowHeight;
\r
677 if (typeof gridster.rowHeight === 'string') {
\r
678 if (gridster.rowHeight === 'match') {
\r
679 gridster.curRowHeight = Math.round(gridster.curColWidth);
\r
680 } else if (gridster.rowHeight.indexOf('*') !== -1) {
\r
681 gridster.curRowHeight = Math.round(gridster.curColWidth * gridster.rowHeight.replace('*', '').replace(' ', ''));
\r
682 } else if (gridster.rowHeight.indexOf('/') !== -1) {
\r
683 gridster.curRowHeight = Math.round(gridster.curColWidth / gridster.rowHeight.replace('/', '').replace(' ', ''));
\r
687 gridster.isMobile = gridster.mobileModeEnabled && gridster.curWidth <= gridster.mobileBreakPoint;
\r
689 // loop through all items and reset their CSS
\r
690 for (var rowIndex = 0, l = gridster.grid.length; rowIndex < l; ++rowIndex) {
\r
691 var columns = gridster.grid[rowIndex];
\r
696 for (var colIndex = 0, len = columns.length; colIndex < len; ++colIndex) {
\r
697 if (columns[colIndex]) {
\r
698 var item = columns[colIndex];
\r
699 item.setElementPosition();
\r
700 item.setElementSizeY();
\r
701 item.setElementSizeX();
\r
709 var optionsKey = attrs.gridster;
\r
711 scope.$parent.$watch(optionsKey, function(newConfig) {
\r
712 refresh(newConfig);
\r
718 scope.$watch(function() {
\r
719 return gridster.loaded;
\r
721 if (gridster.loaded) {
\r
722 $elem.addClass('gridster-loaded');
\r
724 $elem.removeClass('gridster-loaded');
\r
728 scope.$watch(function() {
\r
729 return gridster.isMobile;
\r
731 if (gridster.isMobile) {
\r
732 $elem.addClass('gridster-mobile').removeClass('gridster-desktop');
\r
734 $elem.removeClass('gridster-mobile').addClass('gridster-desktop');
\r
736 $rootScope.$broadcast('gridster-mobile-changed', gridster);
\r
739 scope.$watch(function() {
\r
740 return gridster.draggable;
\r
742 $rootScope.$broadcast('gridster-draggable-changed', gridster);
\r
745 scope.$watch(function() {
\r
746 return gridster.resizable;
\r
748 $rootScope.$broadcast('gridster-resizable-changed', gridster);
\r
751 function updateHeight() {
\r
752 if(gridster.gridHeight){ //need to put this check, otherwise fail in IE8
\r
753 $elem.css('height', (gridster.gridHeight * gridster.curRowHeight) + (gridster.outerMargin ? gridster.margins[0] : -gridster.margins[0]) + 'px');
\r
757 scope.$watch(function() {
\r
758 return gridster.gridHeight;
\r
761 scope.$watch(function() {
\r
762 return gridster.movingItem;
\r
764 gridster.updateHeight(gridster.movingItem ? gridster.movingItem.sizeY : 0);
\r
767 var prevWidth = $elem[0].offsetWidth || parseInt($elem.css('width'), 10);
\r
769 var resize = function() {
\r
770 var width = $elem[0].offsetWidth || parseInt($elem.css('width'), 10);
\r
772 if (!width || width === prevWidth || gridster.movingItem) {
\r
777 if (gridster.loaded) {
\r
778 $elem.removeClass('gridster-loaded');
\r
783 if (gridster.loaded) {
\r
784 $elem.addClass('gridster-loaded');
\r
787 $rootScope.$broadcast('gridster-resized', [width, $elem[0].offsetHeight], gridster);
\r
790 // track element width changes any way we can
\r
791 var onResize = gridsterDebounce(function onResize() {
\r
793 $timeout(function() {
\r
798 scope.$watch(function() {
\r
799 return isVisible($elem[0]);
\r
802 // see https://github.com/sdecima/javascript-detect-element-resize
\r
803 if (typeof window.addResizeListener === 'function') {
\r
804 window.addResizeListener($elem[0], onResize);
\r
806 scope.$watch(function() {
\r
807 return $elem[0].offsetWidth || parseInt($elem.css('width'), 10);
\r
810 var $win = angular.element($window);
\r
811 $win.on('resize', onResize);
\r
813 // be sure to cleanup
\r
814 scope.$on('$destroy', function() {
\r
815 gridster.destroy();
\r
816 $win.off('resize', onResize);
\r
817 if (typeof window.removeResizeListener === 'function') {
\r
818 window.removeResizeListener($elem[0], onResize);
\r
822 // allow a little time to place items before floating up
\r
823 $timeout(function() {
\r
824 scope.$watch('gridster.floating', function() {
\r
825 gridster.floatItemsUp();
\r
827 gridster.loaded = true;
\r
835 .controller('GridsterItemCtrl', function() {
\r
836 this.$element = null;
\r
837 this.gridster = null;
\r
844 this.maxSizeX = null;
\r
845 this.maxSizeY = null;
\r
847 this.init = function($element, gridster) {
\r
848 this.$element = $element;
\r
849 this.gridster = gridster;
\r
850 this.sizeX = gridster.defaultSizeX;
\r
851 this.sizeY = gridster.defaultSizeY;
\r
854 this.destroy = function() {
\r
855 // set these to null to avoid the possibility of circular references
\r
856 this.gridster = null;
\r
857 this.$element = null;
\r
861 * Returns the items most important attributes
\r
863 this.toJSON = function() {
\r
872 this.isMoving = function() {
\r
873 return this.gridster.movingItem === this;
\r
877 * Set the items position
\r
879 * @param {Number} row
\r
880 * @param {Number} column
\r
882 this.setPosition = function(row, column) {
\r
883 this.gridster.putItem(this, row, column);
\r
885 if (!this.isMoving()) {
\r
886 this.setElementPosition();
\r
891 * Sets a specified size property
\r
893 * @param {String} key Can be either "x" or "y"
\r
894 * @param {Number} value The size amount
\r
895 * @param {Boolean} preventMove
\r
897 this.setSize = function(key, value, preventMove) {
\r
898 key = key.toUpperCase();
\r
899 var camelCase = 'size' + key,
\r
900 titleCase = 'Size' + key;
\r
901 if (value === '') {
\r
904 value = parseInt(value, 10);
\r
905 if (isNaN(value) || value === 0) {
\r
906 value = this.gridster['default' + titleCase];
\r
908 var max = key === 'X' ? this.gridster.columns : this.gridster.maxRows;
\r
909 if (this['max' + titleCase]) {
\r
910 max = Math.min(this['max' + titleCase], max);
\r
912 if (this.gridster['max' + titleCase]) {
\r
913 max = Math.min(this.gridster['max' + titleCase], max);
\r
915 if (key === 'X' && this.cols) {
\r
917 } else if (key === 'Y' && this.rows) {
\r
922 if (this['min' + titleCase]) {
\r
923 min = Math.max(this['min' + titleCase], min);
\r
925 if (this.gridster['min' + titleCase]) {
\r
926 min = Math.max(this.gridster['min' + titleCase], min);
\r
929 value = Math.max(Math.min(value, max), min);
\r
931 var changed = (this[camelCase] !== value || (this['old' + titleCase] && this['old' + titleCase] !== value));
\r
932 this['old' + titleCase] = this[camelCase] = value;
\r
934 if (!this.isMoving()) {
\r
935 this['setElement' + titleCase]();
\r
937 if (!preventMove && changed) {
\r
938 this.gridster.moveOverlappingItems(this);
\r
939 this.gridster.layoutChanged();
\r
946 * Sets the items sizeY property
\r
948 * @param {Number} rows
\r
949 * @param {Boolean} preventMove
\r
951 this.setSizeY = function(rows, preventMove) {
\r
952 return this.setSize('Y', rows, preventMove);
\r
956 * Sets the items sizeX property
\r
958 * @param {Number} columns
\r
959 * @param {Boolean} preventMove
\r
961 this.setSizeX = function(columns, preventMove) {
\r
962 return this.setSize('X', columns, preventMove);
\r
966 * Sets an elements position on the page
\r
968 this.setElementPosition = function() {
\r
969 if (this.gridster.isMobile) {
\r
970 this.$element.css({
\r
971 marginLeft: this.gridster.margins[0] + 'px',
\r
972 marginRight: this.gridster.margins[0] + 'px',
\r
973 marginTop: this.gridster.margins[1] + 'px',
\r
974 marginBottom: this.gridster.margins[1] + 'px',
\r
979 this.$element.css({
\r
981 top: (this.row * this.gridster.curRowHeight + (this.gridster.outerMargin ? this.gridster.margins[0] : 0)) + 'px',
\r
982 left: (this.col * this.gridster.curColWidth + (this.gridster.outerMargin ? this.gridster.margins[1] : 0)) + 'px'
\r
988 * Sets an elements height
\r
990 this.setElementSizeY = function() {
\r
991 if (this.gridster.isMobile && !this.gridster.saveGridItemCalculatedHeightInMobile) {
\r
992 this.$element.css('height', '');
\r
994 var computedHeight = (this.sizeY * this.gridster.curRowHeight - this.gridster.margins[0]) + 'px';
\r
995 //this.$element.css('height', computedHeight);
\r
996 this.$element.attr('style', this.$element.attr('style') + '; ' + 'height: '+computedHeight+' !important;');
\r
1001 * Sets an elements width
\r
1003 this.setElementSizeX = function() {
\r
1004 if (this.gridster.isMobile) {
\r
1005 this.$element.css('width', '');
\r
1007 this.$element.css('width', (this.sizeX * this.gridster.curColWidth - this.gridster.margins[1]) + 'px');
\r
1012 * Gets an element's width
\r
1014 this.getElementSizeX = function() {
\r
1015 return (this.sizeX * this.gridster.curColWidth - this.gridster.margins[1]);
\r
1019 * Gets an element's height
\r
1021 this.getElementSizeY = function() {
\r
1022 return (this.sizeY * this.gridster.curRowHeight - this.gridster.margins[0]);
\r
1027 .factory('GridsterTouch', [function() {
\r
1028 return function GridsterTouch(target, startEvent, moveEvent, endEvent) {
\r
1029 var lastXYById = {};
\r
1031 // Opera doesn't have Object.keys so we use this wrapper
\r
1032 var numberOfKeys = function(theObject) {
\r
1033 if (Object.keys) {
\r
1034 return Object.keys(theObject).length;
\r
1039 for (key in theObject) {
\r
1046 // 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
\r
1047 var computeDocumentToElementDelta = function(theElement) {
\r
1048 var elementLeft = 0;
\r
1049 var elementTop = 0;
\r
1050 var oldIEUserAgent = navigator.userAgent.match(/\bMSIE\b/);
\r
1052 for (var offsetElement = theElement; offsetElement != null; offsetElement = offsetElement.offsetParent) {
\r
1053 // the following is a major hack for versions of IE less than 8 to avoid an apparent problem on the IEBlog with double-counting the offsets
\r
1054 // this may not be a general solution to IE7's problem with offsetLeft/offsetParent
\r
1055 if (oldIEUserAgent &&
\r
1056 (!document.documentMode || document.documentMode < 8) &&
\r
1057 offsetElement.currentStyle.position === 'relative' && offsetElement.offsetParent && offsetElement.offsetParent.currentStyle.position === 'relative' && offsetElement.offsetLeft === offsetElement.offsetParent.offsetLeft) {
\r
1058 // add only the top
\r
1059 elementTop += offsetElement.offsetTop;
\r
1061 elementLeft += offsetElement.offsetLeft;
\r
1062 elementTop += offsetElement.offsetTop;
\r
1072 // cache the delta from the document to our event target (reinitialized each mousedown/MSPointerDown/touchstart)
\r
1073 var documentToTargetDelta = computeDocumentToElementDelta(target);
\r
1075 // common event handler for the mouse/pointer/touch models and their down/start, move, up/end, and cancel events
\r
1076 var doEvent = function(theEvtObj) {
\r
1078 if (theEvtObj.type === 'mousemove' && numberOfKeys(lastXYById) === 0) {
\r
1082 var prevent = true;
\r
1084 var pointerList = theEvtObj.changedTouches ? theEvtObj.changedTouches : [theEvtObj];
\r
1086 for (var i = 0; i < pointerList.length; ++i) {
\r
1087 var pointerObj = pointerList[i];
\r
1088 var pointerId = (typeof pointerObj.identifier !== 'undefined') ? pointerObj.identifier : (typeof pointerObj.pointerId !== 'undefined') ? pointerObj.pointerId : 1;
\r
1090 // use the pageX/Y coordinates to compute target-relative coordinates when we have them (in ie < 9, we need to do a little work to put them there)
\r
1091 if (typeof pointerObj.pageX === 'undefined') {
\r
1093 // initialize assuming our source element is our target
\r
1095 pointerObj.pageX = pointerObj.offsetX + documentToTargetDelta.x;
\r
1096 pointerObj.pageY = pointerObj.offsetY + documentToTargetDelta.y;
\r
1099 pointerObj.pageX = pointerObj.clientX;
\r
1100 pointerObj.pageY = pointerObj.clientY;
\r
1103 if (pointerObj.srcElement.offsetParent === target && document.documentMode && document.documentMode === 8 && pointerObj.type === 'mousedown') {
\r
1104 // source element is a child piece of VML, we're in IE8, and we've not called setCapture yet - add the origin of the source element
\r
1105 pointerObj.pageX += pointerObj.srcElement.offsetLeft;
\r
1106 pointerObj.pageY += pointerObj.srcElement.offsetTop;
\r
1107 } else if (pointerObj.srcElement !== target && !document.documentMode || document.documentMode < 8) {
\r
1108 // source element isn't the target (most likely it's a child piece of VML) and we're in a version of IE before IE8 -
\r
1109 // the offsetX/Y values are unpredictable so use the clientX/Y values and adjust by the scroll offsets of its parents
\r
1110 // to get the document-relative coordinates (the same as pageX/Y)
\r
1112 sy = -2; // adjust for old IE's 2-pixel border
\r
1113 for (var scrollElement = pointerObj.srcElement; scrollElement !== null; scrollElement = scrollElement.parentNode) {
\r
1114 sx += scrollElement.scrollLeft ? scrollElement.scrollLeft : 0;
\r
1115 sy += scrollElement.scrollTop ? scrollElement.scrollTop : 0;
\r
1118 pointerObj.pageX = pointerObj.clientX + sx;
\r
1119 pointerObj.pageY = pointerObj.clientY + sy;
\r
1124 var pageX = pointerObj.pageX;
\r
1125 var pageY = pointerObj.pageY;
\r
1127 if (theEvtObj.type.match(/(start|down)$/i)) {
\r
1128 // clause for processing MSPointerDown, touchstart, and mousedown
\r
1130 // refresh the document-to-target delta on start in case the target has moved relative to document
\r
1131 documentToTargetDelta = computeDocumentToElementDelta(target);
\r
1133 // protect against failing to get an up or end on this pointerId
\r
1134 if (lastXYById[pointerId]) {
\r
1137 target: theEvtObj.target,
\r
1138 which: theEvtObj.which,
\r
1139 pointerId: pointerId,
\r
1145 delete lastXYById[pointerId];
\r
1150 prevent = startEvent({
\r
1151 target: theEvtObj.target,
\r
1152 which: theEvtObj.which,
\r
1153 pointerId: pointerId,
\r
1160 // init last page positions for this pointer
\r
1161 lastXYById[pointerId] = {
\r
1166 // IE pointer model
\r
1167 if (target.msSetPointerCapture) {
\r
1168 target.msSetPointerCapture(pointerId);
\r
1169 } else if (theEvtObj.type === 'mousedown' && numberOfKeys(lastXYById) === 1) {
\r
1170 if (useSetReleaseCapture) {
\r
1171 target.setCapture(true);
\r
1173 document.addEventListener('mousemove', doEvent, false);
\r
1174 document.addEventListener('mouseup', doEvent, false);
\r
1177 } else if (theEvtObj.type.match(/move$/i)) {
\r
1178 // clause handles mousemove, MSPointerMove, and touchmove
\r
1180 if (lastXYById[pointerId] && !(lastXYById[pointerId].x === pageX && lastXYById[pointerId].y === pageY)) {
\r
1181 // only extend if the pointer is down and it's not the same as the last point
\r
1183 if (moveEvent && prevent) {
\r
1184 prevent = moveEvent({
\r
1185 target: theEvtObj.target,
\r
1186 which: theEvtObj.which,
\r
1187 pointerId: pointerId,
\r
1193 // update last page positions for this pointer
\r
1194 lastXYById[pointerId].x = pageX;
\r
1195 lastXYById[pointerId].y = pageY;
\r
1197 } else if (lastXYById[pointerId] && theEvtObj.type.match(/(up|end|cancel)$/i)) {
\r
1198 // clause handles up/end/cancel
\r
1200 if (endEvent && prevent) {
\r
1201 prevent = endEvent({
\r
1202 target: theEvtObj.target,
\r
1203 which: theEvtObj.which,
\r
1204 pointerId: pointerId,
\r
1210 // delete last page positions for this pointer
\r
1211 delete lastXYById[pointerId];
\r
1213 // in the Microsoft pointer model, release the capture for this pointer
\r
1214 // in the mouse model, release the capture or remove document-level event handlers if there are no down points
\r
1215 // nothing is required for the iOS touch model because capture is implied on touchstart
\r
1216 if (target.msReleasePointerCapture) {
\r
1217 target.msReleasePointerCapture(pointerId);
\r
1218 } else if (theEvtObj.type === 'mouseup' && numberOfKeys(lastXYById) === 0) {
\r
1219 if (useSetReleaseCapture) {
\r
1220 target.releaseCapture();
\r
1222 document.removeEventListener('mousemove', doEvent, false);
\r
1223 document.removeEventListener('mouseup', doEvent, false);
\r
1230 if (theEvtObj.preventDefault) {
\r
1231 theEvtObj.preventDefault();
\r
1234 if (theEvtObj.preventManipulation) {
\r
1235 theEvtObj.preventManipulation();
\r
1238 if (theEvtObj.preventMouseEvent) {
\r
1239 theEvtObj.preventMouseEvent();
\r
1244 var useSetReleaseCapture = false;
\r
1245 // saving the settings for contentZooming and touchaction before activation
\r
1246 var contentZooming, msTouchAction;
\r
1248 this.enable = function() {
\r
1250 if (window.navigator.msPointerEnabled) {
\r
1251 // Microsoft pointer model
\r
1252 target.addEventListener('MSPointerDown', doEvent, false);
\r
1253 target.addEventListener('MSPointerMove', doEvent, false);
\r
1254 target.addEventListener('MSPointerUp', doEvent, false);
\r
1255 target.addEventListener('MSPointerCancel', doEvent, false);
\r
1257 // css way to prevent panning in our target area
\r
1258 if (typeof target.style.msContentZooming !== 'undefined') {
\r
1259 contentZooming = target.style.msContentZooming;
\r
1260 target.style.msContentZooming = 'none';
\r
1263 // new in Windows Consumer Preview: css way to prevent all built-in touch actions on our target
\r
1264 // without this, you cannot touch draw on the element because IE will intercept the touch events
\r
1265 if (typeof target.style.msTouchAction !== 'undefined') {
\r
1266 msTouchAction = target.style.msTouchAction;
\r
1267 target.style.msTouchAction = 'none';
\r
1269 } else if (target.addEventListener) {
\r
1270 // iOS touch model
\r
1271 target.addEventListener('touchstart', doEvent, false);
\r
1272 target.addEventListener('touchmove', doEvent, false);
\r
1273 target.addEventListener('touchend', doEvent, false);
\r
1274 target.addEventListener('touchcancel', doEvent, false);
\r
1277 target.addEventListener('mousedown', doEvent, false);
\r
1279 // mouse model with capture
\r
1280 // rejecting gecko because, unlike ie, firefox does not send events to target when the mouse is outside target
\r
1281 if (target.setCapture && !window.navigator.userAgent.match(/\bGecko\b/)) {
\r
1282 useSetReleaseCapture = true;
\r
1284 target.addEventListener('mousemove', doEvent, false);
\r
1285 target.addEventListener('mouseup', doEvent, false);
\r
1287 } else if (target.attachEvent && target.setCapture) {
\r
1288 // legacy IE mode - mouse with capture
\r
1289 useSetReleaseCapture = true;
\r
1290 target.attachEvent('onmousedown', function() {
\r
1291 doEvent(window.event);
\r
1292 window.event.returnValue = false;
\r
1295 target.attachEvent('onmousemove', function() {
\r
1296 doEvent(window.event);
\r
1297 window.event.returnValue = false;
\r
1300 target.attachEvent('onmouseup', function() {
\r
1301 doEvent(window.event);
\r
1302 window.event.returnValue = false;
\r
1308 this.disable = function() {
\r
1309 if (window.navigator.msPointerEnabled) {
\r
1310 // Microsoft pointer model
\r
1311 target.removeEventListener('MSPointerDown', doEvent, false);
\r
1312 target.removeEventListener('MSPointerMove', doEvent, false);
\r
1313 target.removeEventListener('MSPointerUp', doEvent, false);
\r
1314 target.removeEventListener('MSPointerCancel', doEvent, false);
\r
1316 // reset zooming to saved value
\r
1317 if (contentZooming) {
\r
1318 target.style.msContentZooming = contentZooming;
\r
1321 // reset touch action setting
\r
1322 if (msTouchAction) {
\r
1323 target.style.msTouchAction = msTouchAction;
\r
1325 } else if (target.removeEventListener) {
\r
1326 // iOS touch model
\r
1327 target.removeEventListener('touchstart', doEvent, false);
\r
1328 target.removeEventListener('touchmove', doEvent, false);
\r
1329 target.removeEventListener('touchend', doEvent, false);
\r
1330 target.removeEventListener('touchcancel', doEvent, false);
\r
1333 target.removeEventListener('mousedown', doEvent, false);
\r
1335 // mouse model with capture
\r
1336 // rejecting gecko because, unlike ie, firefox does not send events to target when the mouse is outside target
\r
1337 if (target.setCapture && !window.navigator.userAgent.match(/\bGecko\b/)) {
\r
1338 useSetReleaseCapture = true;
\r
1340 target.removeEventListener('mousemove', doEvent, false);
\r
1341 target.removeEventListener('mouseup', doEvent, false);
\r
1343 } else if (target.detachEvent && target.setCapture) {
\r
1344 // legacy IE mode - mouse with capture
\r
1345 useSetReleaseCapture = true;
\r
1346 target.detachEvent('onmousedown');
\r
1347 target.detachEvent('onmousemove');
\r
1348 target.detachEvent('onmouseup');
\r
1356 .factory('GridsterDraggable', ['$document', '$timeout', '$window', 'GridsterTouch',
\r
1357 function($document, $timeout, $window, GridsterTouch) {
\r
1358 function GridsterDraggable($el, scope, gridster, item, itemOptions) {
\r
1360 var elmX, elmY, elmW, elmH,
\r
1372 realdocument = $document[0];
\r
1374 var originalCol, originalRow;
\r
1375 var inputTags = ['select', 'input', 'textarea', 'button'];
\r
1377 var gridsterItemDragElement = $el[0].querySelector('[gridster-item-drag]');
\r
1378 //console.log(gridsterItemDragElement);
\r
1379 var isDraggableAreaDefined = gridsterItemDragElement?true:false;
\r
1380 //console.log(isDraggableAreaDefined);
\r
1382 function mouseDown(e) {
\r
1385 e.target = window.event.srcElement;
\r
1386 e.which = window.event.button;
\r
1389 if(isDraggableAreaDefined && (!gridsterItemDragElement.contains(e.target))){
\r
1393 if (inputTags.indexOf(e.target.nodeName.toLowerCase()) !== -1) {
\r
1397 var $target = angular.element(e.target);
\r
1399 // exit, if a resize handle was hit
\r
1400 if ($target.hasClass('gridster-item-resizable-handler')) {
\r
1404 // exit, if the target has it's own click event
\r
1405 if ($target.attr('onclick') || $target.attr('ng-click')) {
\r
1409 // only works if you have jQuery
\r
1410 if ($target.closest && $target.closest('.gridster-no-drag').length) {
\r
1414 switch (e.which) {
\r
1416 // left mouse button
\r
1420 // right or middle mouse button
\r
1424 lastMouseX = e.pageX;
\r
1425 lastMouseY = e.pageY;
\r
1427 elmX = parseInt($el.css('left'), 10);
\r
1428 elmY = parseInt($el.css('top'), 10);
\r
1429 elmW = $el[0].offsetWidth;
\r
1430 elmH = $el[0].offsetHeight;
\r
1432 originalCol = item.col;
\r
1433 originalRow = item.row;
\r
1440 function mouseMove(e) {
\r
1441 if (!$el.hasClass('gridster-item-moving') || $el.hasClass('gridster-item-resizing')) {
\r
1445 var maxLeft = gridster.curWidth - 1;
\r
1447 // Get the current mouse position.
\r
1452 var diffX = mouseX - lastMouseX + mOffX;
\r
1453 var diffY = mouseY - lastMouseY + mOffY;
\r
1454 mOffX = mOffY = 0;
\r
1456 // Update last processed mouse positions.
\r
1457 lastMouseX = mouseX;
\r
1458 lastMouseY = mouseY;
\r
1462 if (elmX + dX < minLeft) {
\r
1463 diffX = minLeft - elmX;
\r
1464 mOffX = dX - diffX;
\r
1465 } else if (elmX + elmW + dX > maxLeft) {
\r
1466 diffX = maxLeft - elmX - elmW;
\r
1467 mOffX = dX - diffX;
\r
1470 if (elmY + dY < minTop) {
\r
1471 diffY = minTop - elmY;
\r
1472 mOffY = dY - diffY;
\r
1473 } else if (elmY + elmH + dY > maxTop) {
\r
1474 diffY = maxTop - elmY - elmH;
\r
1475 mOffY = dY - diffY;
\r
1480 // set new position
\r
1482 'top': elmY + 'px',
\r
1483 'left': elmX + 'px'
\r
1491 function mouseUp(e) {
\r
1492 if (!$el.hasClass('gridster-item-moving') || $el.hasClass('gridster-item-resizing')) {
\r
1496 mOffX = mOffY = 0;
\r
1503 function dragStart(event) {
\r
1504 $el.addClass('gridster-item-moving');
\r
1505 gridster.movingItem = item;
\r
1507 gridster.updateHeight(item.sizeY);
\r
1508 scope.$apply(function() {
\r
1509 if (gridster.draggable && gridster.draggable.start) {
\r
1510 gridster.draggable.start(event, $el, itemOptions);
\r
1515 function drag(event) {
\r
1516 var oldRow = item.row,
\r
1517 oldCol = item.col,
\r
1518 hasCallback = gridster.draggable && gridster.draggable.drag,
\r
1519 scrollSensitivity = gridster.draggable.scrollSensitivity,
\r
1520 scrollSpeed = gridster.draggable.scrollSpeed;
\r
1522 var row = gridster.pixelsToRows(elmY);
\r
1523 var col = gridster.pixelsToColumns(elmX);
\r
1525 var itemsInTheWay = gridster.getItems(row, col, item.sizeX, item.sizeY, item);
\r
1526 var hasItemsInTheWay = itemsInTheWay.length !== 0;
\r
1528 if (gridster.swapping === true && hasItemsInTheWay) {
\r
1529 var boundingBoxItem = gridster.getBoundingBox(itemsInTheWay),
\r
1530 sameSize = boundingBoxItem.sizeX === item.sizeX && boundingBoxItem.sizeY === item.sizeY,
\r
1531 sameRow = boundingBoxItem.row === oldRow,
\r
1532 sameCol = boundingBoxItem.col === oldCol,
\r
1533 samePosition = boundingBoxItem.row === row && boundingBoxItem.col === col,
\r
1534 inline = sameRow || sameCol;
\r
1536 if (sameSize && itemsInTheWay.length === 1) {
\r
1537 if (samePosition) {
\r
1538 gridster.swapItems(item, itemsInTheWay[0]);
\r
1539 } else if (inline) {
\r
1542 } else if (boundingBoxItem.sizeX <= item.sizeX && boundingBoxItem.sizeY <= item.sizeY && inline) {
\r
1543 var emptyRow = item.row <= row ? item.row : row + item.sizeY,
\r
1544 emptyCol = item.col <= col ? item.col : col + item.sizeX,
\r
1545 rowOffset = emptyRow - boundingBoxItem.row,
\r
1546 colOffset = emptyCol - boundingBoxItem.col;
\r
1548 for (var i = 0, l = itemsInTheWay.length; i < l; ++i) {
\r
1549 var itemInTheWay = itemsInTheWay[i];
\r
1551 var itemsInFreeSpace = gridster.getItems(
\r
1552 itemInTheWay.row + rowOffset,
\r
1553 itemInTheWay.col + colOffset,
\r
1554 itemInTheWay.sizeX,
\r
1555 itemInTheWay.sizeY,
\r
1559 if (itemsInFreeSpace.length === 0) {
\r
1560 gridster.putItem(itemInTheWay, itemInTheWay.row + rowOffset, itemInTheWay.col + colOffset);
\r
1566 if (gridster.pushing !== false || !hasItemsInTheWay) {
\r
1571 if(($window.navigator.appName === 'Microsoft Internet Explorer' && !ie8) || $window.navigator.userAgent.indexOf("Firefox")!==-1){
\r
1572 if (event.pageY - realdocument.documentElement.scrollTop < scrollSensitivity) {
\r
1573 realdocument.documentElement.scrollTop = realdocument.documentElement.scrollTop - scrollSpeed;
\r
1574 } else if ($window.innerHeight - (event.pageY - realdocument.documentElement.scrollTop) < scrollSensitivity) {
\r
1575 realdocument.documentElement.scrollTop = realdocument.documentElement.scrollTop + scrollSpeed;
\r
1579 if (event.pageY - realdocument.body.scrollTop < scrollSensitivity) {
\r
1580 realdocument.body.scrollTop = realdocument.body.scrollTop - scrollSpeed;
\r
1581 } else if ($window.innerHeight - (event.pageY - realdocument.body.scrollTop) < scrollSensitivity) {
\r
1582 realdocument.body.scrollTop = realdocument.body.scrollTop + scrollSpeed;
\r
1588 if (event.pageX - realdocument.body.scrollLeft < scrollSensitivity) {
\r
1589 realdocument.body.scrollLeft = realdocument.body.scrollLeft - scrollSpeed;
\r
1590 } else if ($window.innerWidth - (event.pageX - realdocument.body.scrollLeft) < scrollSensitivity) {
\r
1591 realdocument.body.scrollLeft = realdocument.body.scrollLeft + scrollSpeed;
\r
1594 if (hasCallback || oldRow !== item.row || oldCol !== item.col) {
\r
1595 scope.$apply(function() {
\r
1596 if (hasCallback) {
\r
1597 gridster.draggable.drag(event, $el, itemOptions);
\r
1603 function dragStop(event) {
\r
1604 $el.removeClass('gridster-item-moving');
\r
1605 var row = gridster.pixelsToRows(elmY);
\r
1606 var col = gridster.pixelsToColumns(elmX);
\r
1607 if (gridster.pushing !== false || gridster.getItems(row, col, item.sizeX, item.sizeY, item).length === 0) {
\r
1611 gridster.movingItem = null;
\r
1612 item.setPosition(item.row, item.col);
\r
1614 scope.$apply(function() {
\r
1615 if (gridster.draggable && gridster.draggable.stop) {
\r
1616 gridster.draggable.stop(event, $el, itemOptions);
\r
1621 var enabled = null;
\r
1622 var $dragHandles = null;
\r
1623 var unifiedInputs = [];
\r
1625 this.enable = function() {
\r
1626 if (enabled === true) {
\r
1630 // disable and timeout required for some template rendering
\r
1631 $timeout(function() {
\r
1632 // disable any existing draghandles
\r
1633 for (var u = 0, ul = unifiedInputs.length; u < ul; ++u) {
\r
1634 unifiedInputs[u].disable();
\r
1636 unifiedInputs = [];
\r
1638 if (gridster.draggable && gridster.draggable.handle) {
\r
1639 $dragHandles = angular.element($el[0].querySelectorAll(gridster.draggable.handle));
\r
1640 if ($dragHandles.length === 0) {
\r
1641 // fall back to element if handle not found...
\r
1642 $dragHandles = $el;
\r
1645 $dragHandles = $el;
\r
1648 for (var h = 0, hl = $dragHandles.length; h < hl; ++h) {
\r
1649 unifiedInputs[h] = new GridsterTouch($dragHandles[h], mouseDown, mouseMove, mouseUp);
\r
1650 unifiedInputs[h].enable();
\r
1657 this.disable = function() {
\r
1658 if (enabled === false) {
\r
1662 // timeout to avoid race contition with the enable timeout
\r
1663 $timeout(function() {
\r
1665 for (var u = 0, ul = unifiedInputs.length; u < ul; ++u) {
\r
1666 unifiedInputs[u].disable();
\r
1669 unifiedInputs = [];
\r
1674 this.toggle = function(enabled) {
\r
1682 this.destroy = function() {
\r
1687 return GridsterDraggable;
\r
1691 .factory('GridsterResizable', ['GridsterTouch', function(GridsterTouch) {
\r
1692 function GridsterResizable($el, scope, gridster, item, itemOptions) {
\r
1694 function ResizeHandle(handleClass) {
\r
1696 var hClass = handleClass;
\r
1698 var elmX, elmY, elmW, elmH,
\r
1711 var getMinHeight = function() {
\r
1712 return (item.minSizeY ? item.minSizeY : 1) * gridster.curRowHeight - gridster.margins[0];
\r
1714 var getMinWidth = function() {
\r
1715 return (item.minSizeX ? item.minSizeX : 1) * gridster.curColWidth - gridster.margins[1];
\r
1718 var originalWidth, originalHeight;
\r
1719 var savedDraggable;
\r
1721 function mouseDown(e) {
\r
1722 switch (e.which) {
\r
1724 // left mouse button
\r
1728 // right or middle mouse button
\r
1732 // save the draggable setting to restore after resize
\r
1733 savedDraggable = gridster.draggable.enabled;
\r
1734 if (savedDraggable) {
\r
1735 gridster.draggable.enabled = false;
\r
1736 scope.$broadcast('gridster-draggable-changed', gridster);
\r
1739 // Get the current mouse position.
\r
1740 lastMouseX = e.pageX;
\r
1741 lastMouseY = e.pageY;
\r
1743 // Record current widget dimensions
\r
1744 elmX = parseInt($el.css('left'), 10);
\r
1745 elmY = parseInt($el.css('top'), 10);
\r
1746 elmW = $el[0].offsetWidth;
\r
1747 elmH = $el[0].offsetHeight;
\r
1749 originalWidth = item.sizeX;
\r
1750 originalHeight = item.sizeY;
\r
1757 function resizeStart(e) {
\r
1758 $el.addClass('gridster-item-moving');
\r
1759 $el.addClass('gridster-item-resizing');
\r
1761 gridster.movingItem = item;
\r
1763 item.setElementSizeX();
\r
1764 item.setElementSizeY();
\r
1765 item.setElementPosition();
\r
1766 gridster.updateHeight(1);
\r
1768 scope.$apply(function() {
\r
1770 if (gridster.resizable && gridster.resizable.start) {
\r
1771 gridster.resizable.start(e, $el, itemOptions); // options is the item model
\r
1776 function mouseMove(e) {
\r
1777 var maxLeft = gridster.curWidth - 1;
\r
1779 // Get the current mouse position.
\r
1784 var diffX = mouseX - lastMouseX + mOffX;
\r
1785 var diffY = mouseY - lastMouseY + mOffY;
\r
1786 mOffX = mOffY = 0;
\r
1788 // Update last processed mouse positions.
\r
1789 lastMouseX = mouseX;
\r
1790 lastMouseY = mouseY;
\r
1795 if (hClass.indexOf('n') >= 0) {
\r
1796 if (elmH - dY < getMinHeight()) {
\r
1797 diffY = elmH - getMinHeight();
\r
1798 mOffY = dY - diffY;
\r
1799 } else if (elmY + dY < minTop) {
\r
1800 diffY = minTop - elmY;
\r
1801 mOffY = dY - diffY;
\r
1806 if (hClass.indexOf('s') >= 0) {
\r
1807 if (elmH + dY < getMinHeight()) {
\r
1808 diffY = getMinHeight() - elmH;
\r
1809 mOffY = dY - diffY;
\r
1810 } else if (elmY + elmH + dY > maxTop) {
\r
1811 diffY = maxTop - elmY - elmH;
\r
1812 mOffY = dY - diffY;
\r
1816 if (hClass.indexOf('w') >= 0) {
\r
1817 if (elmW - dX < getMinWidth()) {
\r
1818 diffX = elmW - getMinWidth();
\r
1819 mOffX = dX - diffX;
\r
1820 } else if (elmX + dX < minLeft) {
\r
1821 diffX = minLeft - elmX;
\r
1822 mOffX = dX - diffX;
\r
1827 if (hClass.indexOf('e') >= 0) {
\r
1828 if (elmW + dX < getMinWidth()) {
\r
1829 diffX = getMinWidth() - elmW;
\r
1830 mOffX = dX - diffX;
\r
1831 } else if (elmX + elmW + dX > maxLeft) {
\r
1832 diffX = maxLeft - elmX - elmW;
\r
1833 mOffX = dX - diffX;
\r
1838 // set new position
\r
1840 'top': elmY + 'px',
\r
1841 'left': elmX + 'px',
\r
1842 'width': elmW + 'px',
\r
1843 'height': elmH + 'px'
\r
1851 function mouseUp(e) {
\r
1852 // restore draggable setting to its original state
\r
1853 if (gridster.draggable.enabled !== savedDraggable) {
\r
1854 gridster.draggable.enabled = savedDraggable;
\r
1855 scope.$broadcast('gridster-draggable-changed', gridster);
\r
1858 mOffX = mOffY = 0;
\r
1865 function resize(e) {
\r
1866 var oldRow = item.row,
\r
1867 oldCol = item.col,
\r
1868 oldSizeX = item.sizeX,
\r
1869 oldSizeY = item.sizeY,
\r
1870 hasCallback = gridster.resizable && gridster.resizable.resize;
\r
1872 var col = item.col;
\r
1873 // only change column if grabbing left edge
\r
1874 if (['w', 'nw', 'sw'].indexOf(handleClass) !== -1) {
\r
1875 col = gridster.pixelsToColumns(elmX, false);
\r
1878 var row = item.row;
\r
1879 // only change row if grabbing top edge
\r
1880 if (['n', 'ne', 'nw'].indexOf(handleClass) !== -1) {
\r
1881 row = gridster.pixelsToRows(elmY, false);
\r
1884 var sizeX = item.sizeX;
\r
1885 // only change row if grabbing left or right edge
\r
1886 if (['n', 's'].indexOf(handleClass) === -1) {
\r
1887 sizeX = gridster.pixelsToColumns(elmW, true);
\r
1890 var sizeY = item.sizeY;
\r
1891 // only change row if grabbing top or bottom edge
\r
1892 if (['e', 'w'].indexOf(handleClass) === -1) {
\r
1893 sizeY = gridster.pixelsToRows(elmH, true);
\r
1896 if (gridster.pushing !== false || gridster.getItems(row, col, sizeX, sizeY, item).length === 0) {
\r
1899 item.sizeX = sizeX;
\r
1900 item.sizeY = sizeY;
\r
1902 var isChanged = item.row !== oldRow || item.col !== oldCol || item.sizeX !== oldSizeX || item.sizeY !== oldSizeY;
\r
1904 if (hasCallback || isChanged) {
\r
1905 scope.$apply(function() {
\r
1906 if (hasCallback) {
\r
1907 gridster.resizable.resize(e, $el, itemOptions); // options is the item model
\r
1913 function resizeStop(e) {
\r
1914 $el.removeClass('gridster-item-moving');
\r
1915 $el.removeClass('gridster-item-resizing');
\r
1917 gridster.movingItem = null;
\r
1919 item.setPosition(item.row, item.col);
\r
1920 item.setSizeY(item.sizeY);
\r
1921 item.setSizeX(item.sizeX);
\r
1923 scope.$apply(function() {
\r
1924 if (gridster.resizable && gridster.resizable.stop) {
\r
1925 gridster.resizable.stop(e, $el, itemOptions); // options is the item model
\r
1930 var $dragHandle = null;
\r
1933 this.enable = function() {
\r
1934 if (!$dragHandle) {
\r
1935 $dragHandle = angular.element('<div class="gridster-item-resizable-handler handle-' + hClass + '"></div>');
\r
1936 $el.append($dragHandle);
\r
1939 unifiedInput = new GridsterTouch($dragHandle[0], mouseDown, mouseMove, mouseUp);
\r
1940 unifiedInput.enable();
\r
1943 this.disable = function() {
\r
1944 if ($dragHandle) {
\r
1945 $dragHandle.remove();
\r
1946 $dragHandle = null;
\r
1949 unifiedInput.disable();
\r
1950 unifiedInput = undefined;
\r
1953 this.destroy = function() {
\r
1959 var handlesOpts = gridster.resizable.handles;
\r
1960 if (typeof handlesOpts === 'string') {
\r
1961 handlesOpts = gridster.resizable.handles.split(',');
\r
1963 var enabled = false;
\r
1965 for (var c = 0, l = handlesOpts.length; c < l; c++) {
\r
1966 handles.push(new ResizeHandle(handlesOpts[c]));
\r
1969 this.enable = function() {
\r
1973 for (var c = 0, l = handles.length; c < l; c++) {
\r
1974 handles[c].enable();
\r
1979 this.disable = function() {
\r
1983 for (var c = 0, l = handles.length; c < l; c++) {
\r
1984 handles[c].disable();
\r
1989 this.toggle = function(enabled) {
\r
1997 this.destroy = function() {
\r
1998 for (var c = 0, l = handles.length; c < l; c++) {
\r
1999 handles[c].destroy();
\r
2003 return GridsterResizable;
\r
2006 .factory('gridsterDebounce', function() {
\r
2007 return function gridsterDebounce(func, wait, immediate) {
\r
2009 return function() {
\r
2010 var context = this,
\r
2012 var later = function() {
\r
2015 func.apply(context, args);
\r
2018 var callNow = immediate && !timeout;
\r
2019 clearTimeout(timeout);
\r
2020 timeout = setTimeout(later, wait);
\r
2022 func.apply(context, args);
\r
2029 * GridsterItem directive
\r
2031 * @param GridsterDraggable
\r
2032 * @param GridsterResizable
\r
2033 * @param gridsterDebounce
\r
2035 .directive('gridsterItem', ['$parse', 'GridsterDraggable', 'GridsterResizable', 'gridsterDebounce',
\r
2036 function($parse, GridsterDraggable, GridsterResizable, gridsterDebounce) {
\r
2040 controller: 'GridsterItemCtrl',
\r
2041 controllerAs: 'gridsterItem',
\r
2042 require: ['^gridster', 'gridsterItem'],
\r
2043 link: function(scope, $el, attrs, controllers) {
\r
2044 var optionsKey = attrs.gridsterItem,
\r
2047 var gridster = controllers[0],
\r
2048 item = controllers[1];
\r
2050 scope.gridster = gridster;
\r
2053 // bind the item's position properties
\r
2054 // options can be an object specified by gridster-item="object"
\r
2055 // or the options can be the element html attributes object
\r
2057 var $optionsGetter = $parse(optionsKey);
\r
2058 options = $optionsGetter(scope) || {};
\r
2059 if (!options && $optionsGetter.assign) {
\r
2063 sizeX: item.sizeX,
\r
2064 sizeY: item.sizeY,
\r
2070 $optionsGetter.assign(scope, options);
\r
2076 item.init($el, gridster);
\r
2078 $el.addClass('gridster-item');
\r
2080 var aspects = ['minSizeX', 'maxSizeX', 'minSizeY', 'maxSizeY', 'sizeX', 'sizeY', 'row', 'col'],
\r
2083 var expressions = [];
\r
2084 var aspectFn = function(aspect) {
\r
2086 if (typeof options[aspect] === 'string') {
\r
2087 // watch the expression in the scope
\r
2088 expression = options[aspect];
\r
2089 } else if (typeof options[aspect.toLowerCase()] === 'string') {
\r
2090 // watch the expression in the scope
\r
2091 expression = options[aspect.toLowerCase()];
\r
2092 } else if (optionsKey) {
\r
2093 // watch the expression on the options object in the scope
\r
2094 expression = optionsKey + '.' + aspect;
\r
2098 expressions.push('"' + aspect + '":' + expression);
\r
2099 $getters[aspect] = $parse(expression);
\r
2102 var val = $getters[aspect](scope);
\r
2103 if (typeof val === 'number') {
\r
2104 item[aspect] = val;
\r
2108 for (var i = 0, l = aspects.length; i < l; ++i) {
\r
2109 aspectFn(aspects[i]);
\r
2112 var watchExpressions = '{' + expressions.join(',') + '}';
\r
2114 // when the value changes externally, update the internal item object
\r
2115 scope.$watchCollection(watchExpressions, function(newVals, oldVals) {
\r
2116 for (var aspect in newVals) {
\r
2117 var newVal = newVals[aspect];
\r
2118 var oldVal = oldVals[aspect];
\r
2119 if (oldVal === newVal) {
\r
2122 newVal = parseInt(newVal, 10);
\r
2123 if (!isNaN(newVal)) {
\r
2124 item[aspect] = newVal;
\r
2129 function positionChanged() {
\r
2130 // call setPosition so the element and gridster controller are updated
\r
2131 item.setPosition(item.row, item.col);
\r
2133 // when internal item position changes, update externally bound values
\r
2134 if ($getters.row && $getters.row.assign) {
\r
2135 $getters.row.assign(scope, item.row);
\r
2137 if ($getters.col && $getters.col.assign) {
\r
2138 $getters.col.assign(scope, item.col);
\r
2141 scope.$watch(function() {
\r
2142 return item.row + ',' + item.col;
\r
2143 }, positionChanged);
\r
2145 function sizeChanged() {
\r
2146 var changedX = item.setSizeX(item.sizeX, true);
\r
2147 if (changedX && $getters.sizeX && $getters.sizeX.assign) {
\r
2148 $getters.sizeX.assign(scope, item.sizeX);
\r
2150 var changedY = item.setSizeY(item.sizeY, true);
\r
2151 if (changedY && $getters.sizeY && $getters.sizeY.assign) {
\r
2152 $getters.sizeY.assign(scope, item.sizeY);
\r
2155 if (changedX || changedY) {
\r
2156 item.gridster.moveOverlappingItems(item);
\r
2157 gridster.layoutChanged();
\r
2158 scope.$broadcast('gridster-item-resized', item);
\r
2162 scope.$watch(function() {
\r
2163 return item.sizeY + ',' + item.sizeX + ',' + item.minSizeX + ',' + item.maxSizeX + ',' + item.minSizeY + ',' + item.maxSizeY;
\r
2166 var draggable = new GridsterDraggable($el, scope, gridster, item, options);
\r
2167 var resizable = new GridsterResizable($el, scope, gridster, item, options);
\r
2169 var updateResizable = function() {
\r
2170 resizable.toggle(!gridster.isMobile && gridster.resizable && gridster.resizable.enabled);
\r
2172 updateResizable();
\r
2174 var updateDraggable = function() {
\r
2175 draggable.toggle(!gridster.isMobile && gridster.draggable && gridster.draggable.enabled);
\r
2177 updateDraggable();
\r
2179 scope.$on('gridster-draggable-changed', updateDraggable);
\r
2180 scope.$on('gridster-resizable-changed', updateResizable);
\r
2181 scope.$on('gridster-resized', updateResizable);
\r
2182 scope.$on('gridster-mobile-changed', function() {
\r
2183 updateResizable();
\r
2184 updateDraggable();
\r
2187 function whichTransitionEvent() {
\r
2188 var el = document.createElement('div');
\r
2189 var transitions = {
\r
2190 'transition': 'transitionend',
\r
2191 'OTransition': 'oTransitionEnd',
\r
2192 'MozTransition': 'transitionend',
\r
2193 'WebkitTransition': 'webkitTransitionEnd'
\r
2195 for (var t in transitions) {
\r
2196 if (el.style[t] !== undefined) {
\r
2197 return transitions[t];
\r
2202 var debouncedTransitionEndPublisher = gridsterDebounce(function() {
\r
2203 scope.$apply(function() {
\r
2204 scope.$broadcast('gridster-item-transition-end', item);
\r
2208 if(whichTransitionEvent()){ //check for IE8, as it evaluates to null
\r
2209 $el.on(whichTransitionEvent(), debouncedTransitionEndPublisher);
\r
2212 scope.$broadcast('gridster-item-initialized', item);
\r
2214 return scope.$on('$destroy', function() {
\r
2216 resizable.destroy();
\r
2217 draggable.destroy();
\r
2221 gridster.removeItem(item);
\r
2233 .directive('gridsterNoDrag', function() {
\r
2236 link: function(scope, $element) {
\r
2237 $element.addClass('gridster-no-drag');
\r