From 3672beff0af1683c92cf2599734cdc957c6741d3 Mon Sep 17 00:00:00 2001 From: Paul Lambert Date: Wed, 11 Mar 2015 15:35:57 +1300 Subject: [PATCH 001/173] Doco(FAQ): updates to FAQ --- misc/tutorial/499_FAQ.ngdoc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/misc/tutorial/499_FAQ.ngdoc b/misc/tutorial/499_FAQ.ngdoc index 14ef1a6db5..fdc3690dfa 100644 --- a/misc/tutorial/499_FAQ.ngdoc +++ b/misc/tutorial/499_FAQ.ngdoc @@ -41,5 +41,5 @@ There are a number of common gotchas in using the grid, this FAQ aims to cover m which would be something like: ``` - cellTemplate: '
{{row.grid.renderContainers.body.visibleRowsCache.indexOf(row)}}
+ cellTemplate: '
{{grid.renderContainers.body.visibleRowCache.indexOf(row)}}
' ``` \ No newline at end of file From b8202b40a259535948b157592537846992e2b4f0 Mon Sep 17 00:00:00 2001 From: Paul Lambert Date: Wed, 11 Mar 2015 16:26:58 +1300 Subject: [PATCH 002/173] Enh(grouping): fix #2961 group row header only shown if something actually grouped --- misc/tutorial/209_grouping.ngdoc | 4 +++ src/features/grouping/js/grouping.js | 44 +++++++++++++++++++++++++++- src/js/core/factories/Grid.js | 2 +- 3 files changed, 48 insertions(+), 2 deletions(-) diff --git a/misc/tutorial/209_grouping.ngdoc b/misc/tutorial/209_grouping.ngdoc index 90461c9400..b6868bf60d 100644 --- a/misc/tutorial/209_grouping.ngdoc +++ b/misc/tutorial/209_grouping.ngdoc @@ -25,6 +25,10 @@ filtered rows. Group header rows cannot be edited, and if using the selection feature, cannot be selected. They can, however, be exported. +The group rowHeader by default is only visible when one or more columns are grouped - the aim being +to have no visual impact when grouping is turned on but unused. If you'd like the groupRowHeader permanently +present then set the `groupingRowHeaderAlwaysVisible: true` gridOption. + Grouping is still alpha, and under development, however it is included in the distribution files to allow people to start using it. Notable outstandings are: diff --git a/src/features/grouping/js/grouping.js b/src/features/grouping/js/grouping.js index e8526b74ed..071307600e 100644 --- a/src/features/grouping/js/grouping.js +++ b/src/features/grouping/js/grouping.js @@ -174,6 +174,8 @@ grid.registerRowsProcessor(service.groupRows); + grid.registerColumnsProcessor(service.groupingColumnProcessor); + /** * @ngdoc object * @name ui.grid.grouping.api:PublicApi @@ -301,6 +303,16 @@ *
Defaults to 10 */ gridOptions.groupingIndent = gridOptions.groupingIndent || 10; + + /** + * @ngdoc object + * @name groupingRowHeaderAlwaysVisible + * @propertyOf ui.grid.grouping.api:GridOptions + * @description forces the groupRowHeader to always be present, even if nothing is grouped. In some situations this + * may be preferable to having the groupHeader come and go + *
Defaults to false + */ + gridOptions.groupingRowHeaderAlwaysVisible = gridOptions.groupingRowHeaderAlwaysVisible !== false; }, @@ -334,7 +346,7 @@ if (colDef.enableGrouping === false){ return; } - + /** * @ngdoc object * @name grouping @@ -459,6 +471,36 @@ }, + + /** + * @ngdoc function + * @name groupingColumnProcessor + * @methodOf ui.grid.grouping.service:uiGridGroupingService + * @description Updates the visibility of the groupingRowHeader based on whether or not + * there are any grouped columns + * + * @param {object} colDef columnDef we're basing on + * @param {GridCol} col the column we're to update + * @param {object} gridOptions the options we should use + * @returns {promise} promise for the builder - actually we do it all inline so it's immediately resolved + */ + groupingColumnProcessor: function( columns ) { + angular.forEach(columns, function(column, index){ + if (column.name === uiGridGroupingConstants.groupingRowHeaderColName) { + if (typeof(column.grid.options.groupingRowHeaderAlwaysVisible) === 'undefined' || column.grid.options.groupingRowHeaderAlwaysVisible === true) { + var groupingConfig = service.getGrouping(column.grid); + if (groupingConfig.grouping.length > 0){ + column.visible = true; + } else { + column.visible = false; + } + } + } + }); + return columns; + }, + + /** * @ngdoc function * @name groupColumn diff --git a/src/js/core/factories/Grid.js b/src/js/core/factories/Grid.js index 1865369a66..29da52d0aa 100644 --- a/src/js/core/factories/Grid.js +++ b/src/js/core/factories/Grid.js @@ -364,7 +364,7 @@ angular.module('ui.grid') * @methodOf ui.grid.class:Grid * @description When the build creates columns from column definitions, the columnbuilders will be called to add * additional properties to the column. - * @param {function(colDef, col, gridOptions)} columnsProcessor function to be called + * @param {function(colDef, col, gridOptions)} columnBuilder function to be called */ Grid.prototype.registerColumnBuilder = function registerColumnBuilder(columnBuilder) { this.columnBuilders.push(columnBuilder); From c843a04fe20876722bc1a083868845840b02a0a6 Mon Sep 17 00:00:00 2001 From: Paul Date: Wed, 11 Mar 2015 20:00:26 +1300 Subject: [PATCH 003/173] Revert "Bring back the 2.x menu behavior of custom menu templates." --- src/js/core/directives/ui-grid-menu-button.js | 65 ++--- src/js/core/directives/ui-grid-menu.js | 237 ++++++++---------- src/js/core/factories/GridOptions.js | 33 --- .../ui-grid/ui-grid-menu-button.html | 2 +- test/unit/core/factories/GridOptions.spec.js | 15 -- 5 files changed, 138 insertions(+), 214 deletions(-) diff --git a/src/js/core/directives/ui-grid-menu-button.js b/src/js/core/directives/ui-grid-menu-button.js index 09e9741d94..efba5a502f 100644 --- a/src/js/core/directives/ui-grid-menu-button.js +++ b/src/js/core/directives/ui-grid-menu-button.js @@ -195,6 +195,7 @@ angular.module('ui.grid') if ( $scope.grid.options.gridMenuShowHideColumns !== false ){ menuItems = menuItems.concat( service.showHideColumns( $scope ) ); } + return menuItems; }, @@ -273,9 +274,6 @@ angular.module('ui.grid') showHideColumns.push( menuItem ); } }); - showHideColumns.forEach( function (menuItem) { - menuItem.templateUrl = $scope.grid.options.menuItemTemplate; - }); return showHideColumns; }, @@ -335,52 +333,41 @@ angular.module('ui.grid') -.directive('uiGridMenuButton', ['$compile', 'gridUtil', 'uiGridConstants', 'uiGridGridMenuService', -function ($compile, gridUtil, uiGridConstants, uiGridGridMenuService) { - var defaultTemplate = 'ui-grid/ui-grid-menu-button'; +.directive('uiGridMenuButton', ['gridUtil', 'uiGridConstants', 'uiGridGridMenuService', +function (gridUtil, uiGridConstants, uiGridGridMenuService) { + return { priority: 0, scope: true, require: ['?^uiGrid'], + templateUrl: 'ui-grid/ui-grid-menu-button', replace: true, - compile: function ($elm, $attrs) { - return { - pre: function($scope, $elm, $attrs, controllers) { - var uiGridCtrl = controllers[0]; - var menuButtonTemplate = (uiGridCtrl.grid && uiGridCtrl.grid.options.menuButtonTemplate) ? uiGridCtrl.grid.options.menuButtonTemplate : defaultTemplate; - $scope.menuTemplate = uiGridCtrl.grid.options.menuTemplate; - gridUtil.getTemplate(menuButtonTemplate) - .then(function (contents) { - var template = angular.element(contents); - $elm.replaceWith(template); - $compile(template)($scope); - }); - }, - post: function($scope, $elm, $attrs, controllers) { - var uiGridCtrl = controllers[0]; - uiGridGridMenuService.initialize($scope, uiGridCtrl.grid); - $scope.shown = false; - $scope.toggleMenu = function () { - if ( $scope.shown ){ - $scope.$broadcast('hide-menu'); - $scope.shown = false; - } else { - $scope.menuItems = uiGridGridMenuService.getMenuItems( $scope ); - $scope.$broadcast('show-menu'); - $scope.shown = true; - } - }; - $scope.$on('menu-hidden', function() { - $scope.shown = false; - }); - } + link: function ($scope, $elm, $attrs, controllers) { + var uiGridCtrl = controllers[0]; - }; + uiGridGridMenuService.initialize($scope, uiGridCtrl.grid); + + $scope.shown = false; + + $scope.toggleMenu = function () { + if ( $scope.shown ){ + $scope.$broadcast('hide-menu'); + $scope.shown = false; + } else { + $scope.menuItems = uiGridGridMenuService.getMenuItems( $scope ); + $scope.$broadcast('show-menu'); + $scope.shown = true; + } + }; + + $scope.$on('menu-hidden', function() { + $scope.shown = false; + }); } }; }]); -})(); +})(); \ No newline at end of file diff --git a/src/js/core/directives/ui-grid-menu.js b/src/js/core/directives/ui-grid-menu.js index 9938da2bb1..1dcf86dc75 100644 --- a/src/js/core/directives/ui-grid-menu.js +++ b/src/js/core/directives/ui-grid-menu.js @@ -32,147 +32,129 @@ angular.module('ui.grid') .directive('uiGridMenu', ['$compile', '$timeout', '$window', '$document', 'gridUtil', 'uiGridConstants', function ($compile, $timeout, $window, $document, gridUtil, uiGridConstants) { - var defaultTemplate = 'ui-grid/uiGridMenu'; var uiGridMenu = { priority: 0, scope: { // shown: '&', menuItems: '=', - autoHide: '=?', - templateUrl: '=' + autoHide: '=?' }, require: '?^uiGrid', + templateUrl: 'ui-grid/uiGridMenu', replace: false, - compile: function ($elm, $attrs) { - return { - pre: function ($scope, $elm, $attrs, uiGridCtrl) { - if ( uiGridCtrl ) { - $scope.grid = uiGridCtrl.grid; - } - - var menuTemplate = $scope.templateUrl || defaultTemplate; - gridUtil.getTemplate(menuTemplate) - .then(function (contents) { - var template = angular.element(contents); - $elm.append(template); - $compile(template)($scope); - }); - }, - - post: function ($scope, $elm, $attrs, uiGridCtrl) { - var self = this; - var menuMid; - var $animate; - - // *** Show/Hide functions ****** - self.showMenu = $scope.showMenu = function(event, args) { - if ( !$scope.shown ){ - - /* - * In order to animate cleanly we remove the ng-if, wait a digest cycle, then - * animate the removal of the ng-hide. We can't successfully (so far as I can tell) - * animate removal of the ng-if, as the menu items aren't there yet. And we don't want - * to rely on ng-show only, as that leaves elements in the DOM that are needlessly evaluated - * on scroll events. - * - * Note when testing animation that animations don't run on the tutorials. When debugging it looks - * like they do, but angular has a default $animate provider that is just a stub, and that's what's - * being called. ALso don't be fooled by the fact that your browser has actually loaded the - * angular-translate.js, it's not using it. You need to test animations in an external application. - */ - $scope.shown = true; - - $timeout( function() { - $scope.shownMid = true; - $scope.$emit('menu-shown'); - }); - } else if ( !$scope.shownMid ) { - // we're probably doing a hide then show, so we don't need to wait for ng-if - $scope.shownMid = true; - $scope.$emit('menu-shown'); - } + link: function ($scope, $elm, $attrs, uiGridCtrl) { + var self = this; + var menuMid; + var $animate; + + // *** Show/Hide functions ****** + self.showMenu = $scope.showMenu = function(event, args) { + if ( !$scope.shown ){ + + /* + * In order to animate cleanly we remove the ng-if, wait a digest cycle, then + * animate the removal of the ng-hide. We can't successfully (so far as I can tell) + * animate removal of the ng-if, as the menu items aren't there yet. And we don't want + * to rely on ng-show only, as that leaves elements in the DOM that are needlessly evaluated + * on scroll events. + * + * Note when testing animation that animations don't run on the tutorials. When debugging it looks + * like they do, but angular has a default $animate provider that is just a stub, and that's what's + * being called. ALso don't be fooled by the fact that your browser has actually loaded the + * angular-translate.js, it's not using it. You need to test animations in an external application. + */ + $scope.shown = true; + + $timeout( function() { + $scope.shownMid = true; + $scope.$emit('menu-shown'); + }); + } else if ( !$scope.shownMid ) { + // we're probably doing a hide then show, so we don't need to wait for ng-if + $scope.shownMid = true; + $scope.$emit('menu-shown'); + } - var docEventType = 'click'; - if (args && args.originalEvent && args.originalEvent.type && args.originalEvent.type === 'touchstart') { - docEventType = args.originalEvent.type; - } + var docEventType = 'click'; + if (args && args.originalEvent && args.originalEvent.type && args.originalEvent.type === 'touchstart') { + docEventType = args.originalEvent.type; + } - // Turn off an existing document click handler - angular.element(document).off('click touchstart', applyHideMenu); + // Turn off an existing document click handler + angular.element(document).off('click touchstart', applyHideMenu); - // Turn on the document click handler, but in a timeout so it doesn't apply to THIS click if there is one - $timeout(function() { - angular.element(document).on(docEventType, applyHideMenu); - }); - }; + // Turn on the document click handler, but in a timeout so it doesn't apply to THIS click if there is one + $timeout(function() { + angular.element(document).on(docEventType, applyHideMenu); + }); + }; - self.hideMenu = $scope.hideMenu = function(event, args) { - if ( $scope.shown ){ - /* - * In order to animate cleanly we animate the addition of ng-hide, then use a $timeout to - * set the ng-if (shown = false) after the animation runs. In theory we can cascade off the - * callback on the addClass method, but it is very unreliable with unit tests for no discernable reason. - * - * The user may have clicked on the menu again whilst - * we're waiting, so we check that the mid isn't shown before applying the ng-if. - */ - $scope.shownMid = false; - $timeout( function() { - if ( !$scope.shownMid ){ - $scope.shown = false; - $scope.$emit('menu-hidden'); - } - }, 200); + self.hideMenu = $scope.hideMenu = function(event, args) { + if ( $scope.shown ){ + /* + * In order to animate cleanly we animate the addition of ng-hide, then use a $timeout to + * set the ng-if (shown = false) after the animation runs. In theory we can cascade off the + * callback on the addClass method, but it is very unreliable with unit tests for no discernable reason. + * + * The user may have clicked on the menu again whilst + * we're waiting, so we check that the mid isn't shown before applying the ng-if. + */ + $scope.shownMid = false; + $timeout( function() { + if ( !$scope.shownMid ){ + $scope.shown = false; + $scope.$emit('menu-hidden'); } + }, 200); + } - angular.element(document).off('click touchstart', applyHideMenu); - }; - - $scope.$on('hide-menu', function (event, args) { - $scope.hideMenu(event, args); - }); - - $scope.$on('show-menu', function (event, args) { - $scope.showMenu(event, args); - }); + angular.element(document).off('click touchstart', applyHideMenu); + }; - - // *** Auto hide when click elsewhere ****** - var applyHideMenu = function(){ - if ($scope.shown) { - $scope.$apply(function () { - $scope.hideMenu(); - }); - } - }; - - if (typeof($scope.autoHide) === 'undefined' || $scope.autoHide === undefined) { - $scope.autoHide = true; - } + $scope.$on('hide-menu', function (event, args) { + $scope.hideMenu(event, args); + }); - if ($scope.autoHide) { - angular.element($window).on('resize', applyHideMenu); - } + $scope.$on('show-menu', function (event, args) { + $scope.showMenu(event, args); + }); - $scope.$on('$destroy', function () { - angular.element(document).off('click touchstart', applyHideMenu); + + // *** Auto hide when click elsewhere ****** + var applyHideMenu = function(){ + if ($scope.shown) { + $scope.$apply(function () { + $scope.hideMenu(); }); + } + }; + + if (typeof($scope.autoHide) === 'undefined' || $scope.autoHide === undefined) { + $scope.autoHide = true; + } + if ($scope.autoHide) { + angular.element($window).on('resize', applyHideMenu); + } - $scope.$on('$destroy', function() { - angular.element($window).off('resize', applyHideMenu); - }); + $scope.$on('$destroy', function () { + angular.element(document).off('click touchstart', applyHideMenu); + }); + - if (uiGridCtrl) { - $scope.$on('$destroy', uiGridCtrl.grid.api.core.on.scrollEvent($scope, applyHideMenu )); - } + $scope.$on('$destroy', function() { + angular.element($window).off('resize', applyHideMenu); + }); - $scope.$on('$destroy', $scope.$on(uiGridConstants.events.ITEM_DRAGGING, applyHideMenu )); - } - }; - }, + if (uiGridCtrl) { + $scope.$on('$destroy', uiGridCtrl.grid.api.core.on.scrollEvent($scope, applyHideMenu )); + } + $scope.$on('$destroy', $scope.$on(uiGridConstants.events.ITEM_DRAGGING, applyHideMenu )); + }, + + controller: ['$scope', '$element', '$attrs', function ($scope, $element, $attrs) { var self = this; }] @@ -182,7 +164,6 @@ function ($compile, $timeout, $window, $document, gridUtil, uiGridConstants) { }]) .directive('uiGridMenuItem', ['gridUtil', '$compile', 'i18nService', function (gridUtil, $compile, i18nService) { - var defaultTemplate = 'ui-grid/uiGridMenuItem'; var uiGridMenuItem = { priority: 0, scope: { @@ -195,19 +176,23 @@ function ($compile, $timeout, $window, $document, gridUtil, uiGridConstants) { templateUrl: '=' }, require: ['?^uiGrid', '^uiGridMenu'], + templateUrl: 'ui-grid/uiGridMenuItem', replace: true, compile: function($elm, $attrs) { return { pre: function ($scope, $elm, $attrs, controllers) { var uiGridCtrl = controllers[0], uiGridMenuCtrl = controllers[1]; - var menuItemTemplate = $scope.templateUrl || defaultTemplate; - gridUtil.getTemplate(menuItemTemplate) - .then(function (contents) { - var template = angular.element(contents); - $compile(template)($scope); - $elm.replaceWith(template); - }); + + if ($scope.templateUrl) { + gridUtil.getTemplate($scope.templateUrl) + .then(function (contents) { + var template = angular.element(contents); + + var newElm = $compile(template)($scope); + $elm.replaceWith(newElm); + }); + } }, post: function ($scope, $elm, $attrs, controllers) { var uiGridCtrl = controllers[0], @@ -265,4 +250,4 @@ function ($compile, $timeout, $window, $document, gridUtil, uiGridConstants) { return uiGridMenuItem; }]); -})(); +})(); \ No newline at end of file diff --git a/src/js/core/factories/GridOptions.js b/src/js/core/factories/GridOptions.js index 883cf81d7e..4a983166f7 100644 --- a/src/js/core/factories/GridOptions.js +++ b/src/js/core/factories/GridOptions.js @@ -391,40 +391,7 @@ angular.module('ui.grid') * of a precompiled template (TBD how to use this). Refer to the custom footer tutorial for more information. */ baseOptions.footerTemplate = baseOptions.footerTemplate || null; - - /** - * @ngdoc string - * @name menuButtonTemplate - * @propertyOf ui.grid.class:GridOptions - * @description (optional) Null by default. When provided, this setting uses a custom grid menu - * template. Can be set to either the name of a template file 'menuButton_template.html', inline html - *
'
 
'
, or the id - * of a precompiled template (TBD how to use this). Refer to the custom footer tutorial for more information. - */ - baseOptions.menuButtonTemplate = baseOptions.menuButtonTemplate || null; - /** - * @ngdoc string - * @name menuTemplate - * @propertyOf ui.grid.class:GridOptions - * @description (optional) Null by default. When provided, this setting uses a custom grid menu - * template. Can be set to either the name of a template file 'menu_template.html', inline html - *
'
Custom Menu Header
'
, or the id - * of a precompiled template (TBD how to use this). Refer to the custom footer tutorial for more information. - */ - baseOptions.menuTemplate = baseOptions.menuTemplate || null; - - /** - * @ngdoc string - * @name menuItemTemplate - * @propertyOf ui.grid.class:GridOptions - * @description (optional) Null by default. When provided, this setting uses a custom grid menu item - * template. Can be set to either the name of a template file 'menuItem_template.html', inline html - *
'
  • '
    , or the id - * of a precompiled template (TBD how to use this). Refer to the custom footer tutorial for more information. - */ - baseOptions.menuItemTemplate = baseOptions.menuItemTemplate || null; - /** * @ngdoc string * @name rowTemplate diff --git a/src/templates/ui-grid/ui-grid-menu-button.html b/src/templates/ui-grid/ui-grid-menu-button.html index deb7401081..69f1029e98 100644 --- a/src/templates/ui-grid/ui-grid-menu-button.html +++ b/src/templates/ui-grid/ui-grid-menu-button.html @@ -2,5 +2,5 @@
     
    -
    +
    diff --git a/test/unit/core/factories/GridOptions.spec.js b/test/unit/core/factories/GridOptions.spec.js index 18e95ca8cd..2fb711b562 100644 --- a/test/unit/core/factories/GridOptions.spec.js +++ b/test/unit/core/factories/GridOptions.spec.js @@ -45,9 +45,6 @@ describe('GridOptions factory', function () { rowEquality: jasmine.any(Function), headerTemplate: null, footerTemplate: null, - menuButtonTemplate: null, - menuTemplate: null, - menuItemTemplate: null, rowTemplate: 'ui-grid/ui-grid-row', appScopeProvider: null }); @@ -89,9 +86,6 @@ describe('GridOptions factory', function () { rowEquality: testFunction, headerTemplate: 'testHeader', footerTemplate: 'testFooter', - menuButtonTemplate: 'testMenuButton', - menuTemplate: 'testMenu', - menuItemTemplate: 'testMenuItem', rowTemplate: 'testRow', extraOption: 'testExtraOption', appScopeProvider : 'anotherRef' @@ -130,9 +124,6 @@ describe('GridOptions factory', function () { rowEquality: testFunction, headerTemplate: 'testHeader', footerTemplate: 'testFooter', - menuButtonTemplate: 'testMenuButton', - menuTemplate: 'testMenu', - menuItemTemplate: 'testMenuItem', rowTemplate: 'testRow', extraOption: 'testExtraOption', appScopeProvider : 'anotherRef' @@ -175,9 +166,6 @@ describe('GridOptions factory', function () { rowEquality: testFunction, headerTemplate: 'testHeader', footerTemplate: 'testFooter', - menuButtonTemplate: 'testMenuButton', - menuTemplate: 'testMenu', - menuItemTemplate: 'testMenuItem', rowTemplate: 'testRow', extraOption: 'testExtraOption' }; @@ -215,9 +203,6 @@ describe('GridOptions factory', function () { rowEquality: testFunction, headerTemplate: 'testHeader', footerTemplate: 'testFooter', - menuButtonTemplate: 'testMenuButton', - menuTemplate: 'testMenu', - menuItemTemplate: 'testMenuItem', rowTemplate: 'testRow', extraOption: 'testExtraOption', appScopeProvider : null From fbb363197ab3975411589dfa0904495f861795c0 Mon Sep 17 00:00:00 2001 From: Brian Hann Date: Wed, 11 Mar 2015 12:23:31 -0500 Subject: [PATCH 004/173] fix(RTL): Use feature detection to determine RTL Rather than relying on browser detection to determine how scrollLeft will be defined when an element is RTL, use the method from this library: https://github.com/othree/jquery.rtl-scroll-type which creates a temporary element to see how scrollLeft turns out. Fixes #1689 --- .../directives/ui-grid-render-container.js | 4 +- src/js/core/services/ui-grid-util.js | 111 +++++++++--------- 2 files changed, 56 insertions(+), 59 deletions(-) diff --git a/src/js/core/directives/ui-grid-render-container.js b/src/js/core/directives/ui-grid-render-container.js index 02044d89b2..4b10cb4f2e 100644 --- a/src/js/core/directives/ui-grid-render-container.js +++ b/src/js/core/directives/ui-grid-render-container.js @@ -97,7 +97,7 @@ // Horizontal scroll if (args.x && $scope.bindScrollHorizontal) { containerCtrl.prevScrollArgs = args; - var newScrollLeft = args.getNewScrollLeft(colContainer,containerCtrl.viewport); + var newScrollLeft = args.getNewScrollLeft(colContainer, containerCtrl.viewport); // Make the current horizontal scroll position available in the $scope $scope.newScrollLeft = newScrollLeft; @@ -112,7 +112,7 @@ // Scroll came from somewhere else, so the viewport must be positioned if (args.source !== ScrollEvent.Sources.ViewPortScroll) { - containerCtrl.viewport[0].scrollLeft = newScrollLeft; + containerCtrl.viewport[0].scrollLeft = gridUtil.denormalizeScrollLeft(containerCtrl.viewport, newScrollLeft); } containerCtrl.prevScrollLeft = newScrollLeft; diff --git a/src/js/core/services/ui-grid-util.js b/src/js/core/services/ui-grid-util.js index da230e577e..04106be9c3 100644 --- a/src/js/core/services/ui-grid-util.js +++ b/src/js/core/services/ui-grid-util.js @@ -879,6 +879,34 @@ module.service('gridUtil', ['$log', '$window', '$document', '$http', '$templateC return 'unknown'; }; + // Borrowed from https://github.com/othree/jquery.rtl-scroll-type + // Determine the scroll "type" this browser is using for RTL + s.rtlScrollType = function rtlScrollType() { + if (rtlScrollType.type) { + return rtlScrollType.type; + } + + var definer = angular.element('
    A
    ')[0], + type = 'reverse'; + + document.body.appendChild(definer); + + if (definer.scrollLeft > 0) { + type = 'default'; + } + else { + definer.scrollLeft = 1; + if (definer.scrollLeft === 0) { + type = 'negative'; + } + } + + definer.remove(); + rtlScrollType.type = type; + + return type; + }; + /** * @ngdoc method * @name normalizeScrollLeft @@ -896,37 +924,22 @@ module.service('gridUtil', ['$log', '$window', '$document', '$http', '$templateC element = element[0]; } - var browser = s.detectBrowser(); - var scrollLeft = element.scrollLeft; - var dir = s.getStyles(element)['direction']; - - // IE stays normal in RTL - if (browser === 'ie') { - return scrollLeft; - } - // Chrome doesn't alter the scrollLeft value. So with RTL on a 400px-wide grid, the right-most position will still be 400 and the left-most will still be 0; - else if (browser === 'chrome') { - if (dir === 'rtl') { - // Get the max scroll for the element - var maxScrollLeft = element.scrollWidth - element.clientWidth; - - // Subtract the current scroll amount from the max scroll - return maxScrollLeft - scrollLeft; + var dir = s.getStyles(element).direction; + + if (dir === 'rtl') { + switch (s.rtlScrollType()) { + case 'default': + return element.scrollWidth - scrollLeft - element.clientWidth; + case 'negative': + return scrollLeft + element.scrollWidth - element.clientWidth; + case 'reverse': + return scrollLeft; } - else { - return scrollLeft; - } - } - // Firefox goes negative! - else if (browser === 'firefox') { - return Math.abs(scrollLeft); - } - else { - // TODO(c0bra): Handle other browsers? Android? iOS? Opera? - return scrollLeft; } + + return scrollLeft; }; /** @@ -947,40 +960,24 @@ module.service('gridUtil', ['$log', '$window', '$document', '$http', '$templateC element = element[0]; } - var browser = s.detectBrowser(); + var dir = s.getStyles(element).direction; - var dir = s.getStyles(element)['direction']; + if (dir === 'rtl') { + switch (s.rtlScrollType()) { + case 'default': + // Get the max scroll for the element + var maxScrollLeft = element.scrollWidth - element.clientWidth; - // IE stays normal in RTL - if (browser === 'ie') { - return scrollLeft; - } - // Chrome doesn't alter the scrollLeft value. So with RTL on a 400px-wide grid, the right-most position will still be 400 and the left-most will still be 0; - else if (browser === 'chrome') { - if (dir === 'rtl') { - // Get the max scroll for the element - var maxScrollLeft = element.scrollWidth - element.clientWidth; - - // Subtract the current scroll amount from the max scroll - return maxScrollLeft - scrollLeft; - } - else { - return scrollLeft; + // Subtract the current scroll amount from the max scroll + return maxScrollLeft - scrollLeft; + case 'negative': + return scrollLeft * -1; + case 'reverse': + return scrollLeft; } } - // Firefox goes negative! - else if (browser === 'firefox') { - if (dir === 'rtl') { - return scrollLeft * -1; - } - else { - return scrollLeft; - } - } - else { - // TODO(c0bra): Handle other browsers? Android? iOS? Opera? - return scrollLeft; - } + + return scrollLeft; }; /** From 9e6242199703a43c695c860412d1e2ecda5780d3 Mon Sep 17 00:00:00 2001 From: Paul Lambert Date: Wed, 11 Mar 2015 21:27:13 +1300 Subject: [PATCH 005/173] Enh(grouping): fix #2972 show number of grouped items --- misc/tutorial/209_grouping.ngdoc | 7 +- src/features/grouping/js/grouping.js | 104 ++++++++++++-------- src/features/grouping/test/grouping.spec.js | 43 ++++++-- 3 files changed, 101 insertions(+), 53 deletions(-) diff --git a/misc/tutorial/209_grouping.ngdoc b/misc/tutorial/209_grouping.ngdoc index b6868bf60d..f9434bbc2f 100644 --- a/misc/tutorial/209_grouping.ngdoc +++ b/misc/tutorial/209_grouping.ngdoc @@ -51,7 +51,8 @@ Options to watch out for include: - `groupingSuppressAggregationText`: if your column has a cellFilter, the insertion of text (e.g. 'min: xxxx') usually breaks the cellFilter. So you can suppress the aggregation text, but then you don't get a clear visual indication of what sort of aggregation is going on. Refer the example below, the balance column with - an average + an average +- `groupingShowCounts`: set to false if you don't like the counts against the groupHeaders @example @@ -68,9 +69,9 @@ we can't easily see that it's an average. $scope.gridOptions = { enableFiltering: true, columnDefs: [ - { name: 'state', grouping: { groupPriority: 1 }, sort: { direction: 'desc' }, width: '25%' }, + { name: 'state', grouping: { groupPriority: 1 }, sort: { direction: 'desc' }, width: '35%' }, { name: 'gender', grouping: { groupPriority: 2 }, sort: { direction: 'asc' }, width: '20%' }, - { name: 'name', grouping: { aggregation: uiGridGroupingConstants.aggregation.COUNT }, width: '30%' }, + { name: 'name' }, { name: 'age', grouping: { aggregation: uiGridGroupingConstants.aggregation.MAX }, width: '20%' }, { name: 'company', width: '25%' }, { name: 'balance', width: '25%', cellFilter: 'currency', groupingSuppressAggregationText: true, grouping: { aggregation: uiGridGroupingConstants.aggregation.AVG } } diff --git a/src/features/grouping/js/grouping.js b/src/features/grouping/js/grouping.js index 071307600e..291e8a39b4 100644 --- a/src/features/grouping/js/grouping.js +++ b/src/features/grouping/js/grouping.js @@ -89,7 +89,8 @@ SUM: 'sum', MAX: 'max', MIN: 'min', - AVG: 'avg' + AVG: 'avg', + FIELD: '##@@aggregation_running_count@@##' } }); @@ -312,7 +313,16 @@ * may be preferable to having the groupHeader come and go *
    Defaults to false */ - gridOptions.groupingRowHeaderAlwaysVisible = gridOptions.groupingRowHeaderAlwaysVisible !== false; + gridOptions.groupingRowHeaderAlwaysVisible = gridOptions.groupingRowHeaderAlwaysVisible === true; + + /** + * @ngdoc object + * @name groupingShowCounts + * @propertyOf ui.grid.grouping.api:GridOptions + * @description shows counts on the groupHeader rows + *
    Defaults to true + */ + gridOptions.groupingShowCounts = gridOptions.groupingShowCounts !== false; }, @@ -487,7 +497,7 @@ groupingColumnProcessor: function( columns ) { angular.forEach(columns, function(column, index){ if (column.name === uiGridGroupingConstants.groupingRowHeaderColName) { - if (typeof(column.grid.options.groupingRowHeaderAlwaysVisible) === 'undefined' || column.grid.options.groupingRowHeaderAlwaysVisible === true) { + if (typeof(column.grid.options.groupingRowHeaderAlwaysVisible) === 'undefined' || column.grid.options.groupingRowHeaderAlwaysVisible === false) { var groupingConfig = service.getGrouping(column.grid); if (groupingConfig.grouping.length > 0){ column.visible = true; @@ -945,6 +955,10 @@ // get the aggregation config to copy in - do this multiple times as shallow copying it // was harder than it looked, and as much work as just creating it again var aggregations = []; + if (grid.options.groupingShowCounts){ + aggregations.push({type: uiGridGroupingConstants.aggregation.COUNT, fieldName: uiGridGroupingConstants.aggregation.FIELD, value: null }); + } else { + } angular.forEach(columnSettings.aggregations, function(aggregation, index){ if (aggregation.aggregation === uiGridGroupingConstants.aggregation.AVG){ @@ -1087,17 +1101,23 @@ writeOutAggregation: function( grid, processingState ) { if ( processingState.currentGroupHeader ){ angular.forEach(processingState.runningAggregations, function( aggregation, index ){ - if (aggregation.col.groupingSuppressAggregationText){ - processingState.currentGroupHeader.entity[aggregation.fieldName] = aggregation.value; + if (aggregation.fieldName === uiGridGroupingConstants.aggregation.FIELD){ + // running total to include in the groupHeader + processingState.currentGroupHeader.entity[processingState.fieldName] = processingState.currentValue + ' (' + aggregation.value + ')'; + aggregation.value = null; } else { - processingState.currentGroupHeader.entity[aggregation.fieldName] = i18nService.get().aggregation[aggregation.type] + aggregation.value; - } - aggregation.value = null; - if ( aggregation.sum ){ - aggregation.sum = null; - } - if ( aggregation.count ){ - aggregation.count = null; + if (aggregation.col.groupingSuppressAggregationText){ + processingState.currentGroupHeader.entity[aggregation.fieldName] = aggregation.value; + } else { + processingState.currentGroupHeader.entity[aggregation.fieldName] = i18nService.get().aggregation[aggregation.type] + aggregation.value; + } + aggregation.value = null; + if ( aggregation.sum ){ + aggregation.sum = null; + } + if ( aggregation.count ){ + aggregation.count = null; + } } }); } @@ -1182,34 +1202,36 @@ aggregate: function( grid, row, groupFieldState ){ // TODO: check data types, cast as necessary, all that jazz angular.forEach( groupFieldState.runningAggregations, function( aggregation, index ){ - var fieldValue = grid.getCellValue(row, aggregation.col); - var numValue = Number(fieldValue); - switch (aggregation.type) { - case uiGridGroupingConstants.aggregation.COUNT: - aggregation.value++; - break; - case uiGridGroupingConstants.aggregation.SUM: - if (!isNaN(numValue)){ - aggregation.value += numValue; - } - break; - case uiGridGroupingConstants.aggregation.MIN: - if (fieldValue !== null && (fieldValue < aggregation.value || aggregation.value === null)){ - aggregation.value = fieldValue; - } - break; - case uiGridGroupingConstants.aggregation.MAX: - if (fieldValue > aggregation.value){ - aggregation.value = fieldValue; - } - break; - case uiGridGroupingConstants.aggregation.AVG: - aggregation.count++; - if (!isNaN(numValue)){ - aggregation.sum += numValue; - } - aggregation.value = aggregation.sum / aggregation.count; - break; + if (aggregation.type === uiGridGroupingConstants.aggregation.COUNT){ + // don't need getCellValue for counting, and column isn't present sometimes + aggregation.value++; + } else { + var fieldValue = grid.getCellValue(row, aggregation.col); + var numValue = Number(fieldValue); + switch (aggregation.type) { + case uiGridGroupingConstants.aggregation.SUM: + if (!isNaN(numValue)){ + aggregation.value += numValue; + } + break; + case uiGridGroupingConstants.aggregation.MIN: + if (fieldValue !== null && (fieldValue < aggregation.value || aggregation.value === null)){ + aggregation.value = fieldValue; + } + break; + case uiGridGroupingConstants.aggregation.MAX: + if (fieldValue > aggregation.value){ + aggregation.value = fieldValue; + } + break; + case uiGridGroupingConstants.aggregation.AVG: + aggregation.count++; + if (!isNaN(numValue)){ + aggregation.sum += numValue; + } + aggregation.value = aggregation.sum / aggregation.count; + break; + } } }); } diff --git a/src/features/grouping/test/grouping.spec.js b/src/features/grouping/test/grouping.spec.js index 696c1cb25d..a1d5693af6 100644 --- a/src/features/grouping/test/grouping.spec.js +++ b/src/features/grouping/test/grouping.spec.js @@ -105,9 +105,10 @@ describe('ui.grid.grouping uiGridGroupingService', function () { ]); }); - it('no aggregation', function() { + it('no aggregation, but groupingShowCounts', function() { grid.columns[1].grouping = {groupPriority: 3}; grid.columns[3].grouping = {groupPriority: 2}; + grid.options.groupingShowCounts = true; var result = uiGridGroupingService.initialiseProcessingState(grid); expect(result[0].col).toEqual(grid.columns[3]); @@ -116,8 +117,29 @@ describe('ui.grid.grouping uiGridGroupingService', function () { delete result[1].col; expect(result).toEqual([ - { fieldName: 'col3', initialised: false, currentValue: null, currentGroupHeader: null, runningAggregations: {} }, - { fieldName: 'col1', initialised: false, currentValue: null, currentGroupHeader: null, runningAggregations: {} } + { fieldName: 'col3', initialised: false, currentValue: null, currentGroupHeader: null, runningAggregations: [ + { type : uiGridGroupingConstants.aggregation.COUNT, fieldName : uiGridGroupingConstants.aggregation.FIELD, value : null } + ] }, + { fieldName: 'col1', initialised: false, currentValue: null, currentGroupHeader: null, runningAggregations: [ + { type : uiGridGroupingConstants.aggregation.COUNT, fieldName : uiGridGroupingConstants.aggregation.FIELD, value : null } + ] } + ]); + }); + + it('no aggregation, without groupingShowCounts', function() { + grid.columns[1].grouping = {groupPriority: 3}; + grid.columns[3].grouping = {groupPriority: 2}; + grid.options.groupingShowCounts = false; + + var result = uiGridGroupingService.initialiseProcessingState(grid); + expect(result[0].col).toEqual(grid.columns[3]); + delete result[0].col; + expect(result[1].col).toEqual(grid.columns[1]); + delete result[1].col; + + expect(result).toEqual([ + { fieldName: 'col3', initialised: false, currentValue: null, currentGroupHeader: null, runningAggregations: [] }, + { fieldName: 'col1', initialised: false, currentValue: null, currentGroupHeader: null, runningAggregations: [] } ]); }); @@ -126,27 +148,30 @@ describe('ui.grid.grouping uiGridGroupingService', function () { grid.columns[1].grouping = {groupPriority: 3}; grid.columns[2].grouping = {aggregation: uiGridGroupingConstants.aggregation.SUM}; grid.columns[3].grouping = {groupPriority: 2}; + grid.options.groupingShowCounts = true; // when expected results go wrong the messages suck if columns are in the results...so check them individually then delete them out var result = uiGridGroupingService.initialiseProcessingState(grid); expect(result[0].col).toEqual(grid.columns[3]); delete result[0].col; - expect(result[0].runningAggregations[0].col).toEqual(grid.columns[0]); - delete result[0].runningAggregations[0].col; - expect(result[0].runningAggregations[1].col).toEqual(grid.columns[2]); + expect(result[0].runningAggregations[1].col).toEqual(grid.columns[0]); delete result[0].runningAggregations[1].col; + expect(result[0].runningAggregations[2].col).toEqual(grid.columns[2]); + delete result[0].runningAggregations[2].col; expect(result[1].col).toEqual(grid.columns[1]); delete result[1].col; - expect(result[1].runningAggregations[0].col).toEqual(grid.columns[0]); - delete result[1].runningAggregations[0].col; - expect(result[1].runningAggregations[1].col).toEqual(grid.columns[2]); + expect(result[1].runningAggregations[1].col).toEqual(grid.columns[0]); delete result[1].runningAggregations[1].col; + expect(result[1].runningAggregations[2].col).toEqual(grid.columns[2]); + delete result[1].runningAggregations[2].col; expect(result).toEqual([ { fieldName: 'col3', initialised: false, currentValue: null, currentGroupHeader: null, runningAggregations: [ + { type : uiGridGroupingConstants.aggregation.COUNT, fieldName : uiGridGroupingConstants.aggregation.FIELD, value : null }, { type: uiGridGroupingConstants.aggregation.COUNT, fieldName: 'col0', value: null }, { type: uiGridGroupingConstants.aggregation.SUM, fieldName: 'col2', value: null } ] }, { fieldName: 'col1', initialised: false, currentValue: null, currentGroupHeader: null, runningAggregations: [ + { type : uiGridGroupingConstants.aggregation.COUNT, fieldName : uiGridGroupingConstants.aggregation.FIELD, value : null }, { type: uiGridGroupingConstants.aggregation.COUNT, fieldName: 'col0', value: null }, { type: uiGridGroupingConstants.aggregation.SUM, fieldName: 'col2', value: null } ] } From 93b3e3c0d83aebeef1c11e97c729ca46e78e5965 Mon Sep 17 00:00:00 2001 From: Paul Lambert Date: Thu, 12 Mar 2015 06:41:40 +1300 Subject: [PATCH 006/173] Fix(grouping): fix #2976 stable sort for grouping columns --- misc/tutorial/209_grouping.ngdoc | 4 +- src/features/grouping/js/grouping.js | 129 ++++++++++++++++----------- src/js/core/factories/Grid.js | 11 ++- 3 files changed, 84 insertions(+), 60 deletions(-) diff --git a/misc/tutorial/209_grouping.ngdoc b/misc/tutorial/209_grouping.ngdoc index f9434bbc2f..3d0a7f2c9f 100644 --- a/misc/tutorial/209_grouping.ngdoc +++ b/misc/tutorial/209_grouping.ngdoc @@ -69,11 +69,11 @@ we can't easily see that it's an average. $scope.gridOptions = { enableFiltering: true, columnDefs: [ - { name: 'state', grouping: { groupPriority: 1 }, sort: { direction: 'desc' }, width: '35%' }, + { name: 'name', width: '30%' }, { name: 'gender', grouping: { groupPriority: 2 }, sort: { direction: 'asc' }, width: '20%' }, - { name: 'name' }, { name: 'age', grouping: { aggregation: uiGridGroupingConstants.aggregation.MAX }, width: '20%' }, { name: 'company', width: '25%' }, + { name: 'state', sort: { direction: 'desc' }, width: '35%' }, { name: 'balance', width: '25%', cellFilter: 'currency', groupingSuppressAggregationText: true, grouping: { aggregation: uiGridGroupingConstants.aggregation.AVG } } ], onRegisterApi: function( gridApi ) { diff --git a/src/features/grouping/js/grouping.js b/src/features/grouping/js/grouping.js index 291e8a39b4..607693b212 100644 --- a/src/features/grouping/js/grouping.js +++ b/src/features/grouping/js/grouping.js @@ -488,16 +488,20 @@ * @methodOf ui.grid.grouping.service:uiGridGroupingService * @description Updates the visibility of the groupingRowHeader based on whether or not * there are any grouped columns - * - * @param {object} colDef columnDef we're basing on - * @param {GridCol} col the column we're to update - * @param {object} gridOptions the options we should use - * @returns {promise} promise for the builder - actually we do it all inline so it's immediately resolved + * + * @param {array} columns the columns to consider rendering + * @param {array} rows the grid rows, which we don't use but are passed to us + * @returns {array} updated columns array */ - groupingColumnProcessor: function( columns ) { + groupingColumnProcessor: function( columns, rows ) { + var grid = this; angular.forEach(columns, function(column, index){ + // position used to make stable sort in moveGroupColumns + column.groupingPosition = index; + + // find groupingRowHeader and decide whether to make it visible if (column.name === uiGridGroupingConstants.groupingRowHeaderColName) { - if (typeof(column.grid.options.groupingRowHeaderAlwaysVisible) === 'undefined' || column.grid.options.groupingRowHeaderAlwaysVisible === false) { + if (typeof(grid.options.groupingRowHeaderAlwaysVisible) === 'undefined' || grid.options.groupingRowHeaderAlwaysVisible === false) { var groupingConfig = service.getGrouping(column.grid); if (groupingConfig.grouping.length > 0){ column.visible = true; @@ -507,10 +511,70 @@ } } }); + + columns = service.moveGroupColumns(this, columns, rows); return columns; }, + /** + * @ngdoc function + * @name moveGroupColumns + * @methodOf ui.grid.grouping.service:uiGridGroupingService + * @description Moves the column order so that the grouped columns are lined up + * to the left (well, unless you're RTL, then it's the right). By doing this in + * the columnsProcessor, we make it transient - when the column is ungrouped it'll + * go back to where it was. + * + * Does nothing if the option `moveGroupColumns` is set to false. + * + * @param {Grid} grid grid object + * @param {array} columns the columns that we should process/move + * @param {array} rows the grid rows + * @returns {array} updated columns + */ + moveGroupColumns: function( grid, columns, rows ){ + if ( grid.options.moveGroupColumns === false){ + return; + } + + // optimisation - this can be done in the groupingColumnProcessor since we were already + // iterating. But commented out and left here to make the code a little more understandable + // + // angular.forEach(columns, function(column, index) { + // column.groupingPosition = index; + // }); + + columns.sort(function(a, b){ + var a_group, b_group; + if ( typeof(a.grouping) === 'undefined' || typeof(a.grouping.groupPriority) === 'undefined' || a.grouping.groupPriority < 0){ + a_group = null; + } else { + a_group = a.grouping.groupPriority; + } + + if ( typeof(b.grouping) === 'undefined' || typeof(b.grouping.groupPriority) === 'undefined' || b.grouping.groupPriority < 0){ + b_group = null; + } else { + b_group = b.grouping.groupPriority; + } + + // groups get sorted to the top + if ( a_group !== null && b_group === null) { return -1; } + if ( b_group !== null && a_group === null) { return 1; } + if ( a_group !== null && b_group !== null) {return a_group - b_group; } + + return a.groupingPosition - b.groupingPosition; + }); + + angular.forEach(columns, function(column, index) { + delete column.groupingPosition; + }); + + return columns; + }, + + /** * @ngdoc function * @name groupColumn @@ -518,8 +582,8 @@ * @description Adds this column to the existing grouping, at the end of the priority order. * If the column doesn't have a sort, adds one, by default ASC * - * If the option `groupMoveColumns` hasn't been set to false, moves the column to the left - * to make things look tidier. + * This column will move to the left of any non-group columns, the + * move is handled in a columnProcessor, so gets called as part of refresh * * @param {Grid} grid grid object * @param {GridCol} column the column we want to group @@ -541,7 +605,6 @@ } service.tidyPriorities( grid ); - service.moveGroupColumns( grid ); grid.queueGridRefresh(); }, @@ -555,7 +618,8 @@ * column was previously aggregated the aggregation will come back. * The sort will remain. * - * This column will move to the right of any other group columns. + * This column will move to the right of any other group columns, the + * move is handled in a columnProcessor, so gets called as part of refresh * * @param {Grid} grid grid object * @param {GridCol} column the column we want to ungroup @@ -568,7 +632,6 @@ delete column.grouping.groupPriority; service.tidyPriorities( grid ); - service.moveGroupColumns( grid ); grid.queueGridRefresh(); }, @@ -644,48 +707,6 @@ }, - /** - * @ngdoc function - * @name moveGroupColumns - * @methodOf ui.grid.grouping.service:uiGridGroupingService - * @description Moves the column order so that the grouped columns are lined up - * to the left (well, unless you're RTL, then it's the right). Doesn't change - * the columnDefs, just the columns array. (check move columns that this is how it works) - * - * All other columns retain their relative position, after the group columns - * - * Does nothing if the option `moveGroupColumns` is set to false. - * - * @param {Grid} grid grid object - */ - moveGroupColumns: function( grid ){ - if ( grid.options.moveGroupColumns === false){ - return; - } - - grid.columns.sort(function(a, b){ - var a_group, b_group; - if ( typeof(a.grouping) === 'undefined' || typeof(a.grouping.groupPriority) === 'undefined' || a.grouping.groupPriority < 0){ - a_group = null; - } else { - a_group = a.grouping.groupPriority; - } - - if ( typeof(b.grouping) === 'undefined' || typeof(b.grouping.groupPriority) === 'undefined' || b.grouping.groupPriority < 0){ - b_group = null; - } else { - b_group = b.grouping.groupPriority; - } - - // groups get sorted to the top - if ( a_group !== null && b_group === null) { return -1; } - if ( b_group !== null && a_group === null) { return 1; } - if ( a_group !== null && b_group !== null) {return a_group - b_group; } - }); - grid.api.core.notifyDataChange( uiGridConstants.dataChange.COLUMN ); - }, - - /** * @ngdoc function * @name expandAllRows diff --git a/src/js/core/factories/Grid.js b/src/js/core/factories/Grid.js index 29da52d0aa..7df193d977 100644 --- a/src/js/core/factories/Grid.js +++ b/src/js/core/factories/Grid.js @@ -1171,8 +1171,9 @@ angular.module('ui.grid') * @ngdoc function * @name registerRowsProcessor * @methodOf ui.grid.class:Grid - * @param {function(renderableRows)} rows processor function - * @returns {Array[GridRow]} Updated renderable rows + * @param {function(renderedRowsToProcess, columns )} processorFunction rows processor function, which + * is run in the context of the grid (i.e. this for the function will be the grid), and must + * return the updated rows list, which is passed to the next processor in the chain * @description Register a "rows processor" function. When the rows are updated, @@ -1308,8 +1309,10 @@ angular.module('ui.grid') * @ngdoc function * @name registerColumnsProcessor * @methodOf ui.grid.class:Grid - * @param {function(renderableColumns)} rows processor function - * @returns {Array[GridColumn]} Updated renderable columns + * @param {function(renderedColumnsToProcess, rows)} columnProcessor column processor function, which + * is run in the context of the grid (i.e. this for the function will be the grid), and + * which must return an updated renderedColumnsToProcess which can be passed to the next processor + * in the chain * @description Register a "columns processor" function. When the columns are updated, From 97b60607e739935198cc605897d74ae8da52a9a3 Mon Sep 17 00:00:00 2001 From: Paul Lambert Date: Thu, 12 Mar 2015 08:39:15 +1300 Subject: [PATCH 007/173] Fix(cellNav): Fix #2898 cellNav tab key not working --- misc/tutorial/209_grouping.ngdoc | 4 ++-- src/features/cellnav/js/cellnav.js | 8 +++++++- src/features/exporter/js/exporter.js | 2 +- 3 files changed, 10 insertions(+), 4 deletions(-) diff --git a/misc/tutorial/209_grouping.ngdoc b/misc/tutorial/209_grouping.ngdoc index 3d0a7f2c9f..98f6b3e287 100644 --- a/misc/tutorial/209_grouping.ngdoc +++ b/misc/tutorial/209_grouping.ngdoc @@ -70,10 +70,10 @@ we can't easily see that it's an average. enableFiltering: true, columnDefs: [ { name: 'name', width: '30%' }, - { name: 'gender', grouping: { groupPriority: 2 }, sort: { direction: 'asc' }, width: '20%' }, + { name: 'gender', grouping: { groupPriority: 1 }, sort: { direction: 'asc' }, width: '20%' }, { name: 'age', grouping: { aggregation: uiGridGroupingConstants.aggregation.MAX }, width: '20%' }, { name: 'company', width: '25%' }, - { name: 'state', sort: { direction: 'desc' }, width: '35%' }, + { name: 'state', grouping: { groupPriority: 0 }, sort: { direction: 'desc' }, width: '35%' }, { name: 'balance', width: '25%', cellFilter: 'currency', groupingSuppressAggregationText: true, grouping: { aggregation: uiGridGroupingConstants.aggregation.AVG } } ], onRegisterApi: function( gridApi ) { diff --git a/src/features/cellnav/js/cellnav.js b/src/features/cellnav/js/cellnav.js index c708af4aff..5670793aea 100644 --- a/src/features/cellnav/js/cellnav.js +++ b/src/features/cellnav/js/cellnav.js @@ -878,12 +878,15 @@ if (lastRowCol) { // Figure out which new row+combo we're navigating to var rowCol = uiGridCtrl.grid.renderContainers[containerId].cellNav.getNextRowCol(direction, lastRowCol.row, lastRowCol.col); + var focusableCols = uiGridCtrl.grid.renderContainers[containerId].cellNav.getFocusableCols(); // Shift+tab on top-left cell should exit cellnav on render container if ( // Navigating left direction === uiGridCellNavConstants.direction.LEFT && - // Trying to stay on same row + // New col is last col (i.e. wrap around) + rowCol.col === focusableCols[focusableCols.length - 1] && + // Staying on same row, which means we're at first row rowCol.row === lastRowCol.row && evt.keyCode === uiGridConstants.keymap.TAB && evt.shiftKey @@ -894,6 +897,9 @@ // Tab on bottom-right cell should exit cellnav on render container else if ( direction === uiGridCellNavConstants.direction.RIGHT && + // New col is first col (i.e. wrap around) + rowCol.col === focusableCols[0] && + // Staying on same row, which means we're at first row rowCol.row === lastRowCol.row && evt.keyCode === uiGridConstants.keymap.TAB && !evt.shiftKey diff --git a/src/features/exporter/js/exporter.js b/src/features/exporter/js/exporter.js index 7d095ff198..1e75a4f8b2 100644 --- a/src/features/exporter/js/exporter.js +++ b/src/features/exporter/js/exporter.js @@ -850,7 +850,7 @@ var docDefinition = this.prepareAsPdf(grid, exportColumnHeaders, exportData); if (this.isIE()) { - pdfMake.createPdf(docDefinition).download(); + var pdf = pdfMake.createPdf(docDefinition).download(); } else { pdfMake.createPdf(docDefinition).open(); } From 4acbdc1a58d8043d60e3a62d1126b0f69bc6ee86 Mon Sep 17 00:00:00 2001 From: Brian Hann Date: Wed, 11 Mar 2015 15:24:35 -0500 Subject: [PATCH 008/173] fix(RTL): Use Math.abs for normalizing negatives This fixes RTL horizontal scrolling in FireFox. --- src/js/core/services/ui-grid-util.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/js/core/services/ui-grid-util.js b/src/js/core/services/ui-grid-util.js index 04106be9c3..73dfafcb4c 100644 --- a/src/js/core/services/ui-grid-util.js +++ b/src/js/core/services/ui-grid-util.js @@ -933,7 +933,7 @@ module.service('gridUtil', ['$log', '$window', '$document', '$http', '$templateC case 'default': return element.scrollWidth - scrollLeft - element.clientWidth; case 'negative': - return scrollLeft + element.scrollWidth - element.clientWidth; + return Math.abs(scrollLeft); case 'reverse': return scrollLeft; } From 266168e03806dbdcdf7a7897413e78e0d69b5caf Mon Sep 17 00:00:00 2001 From: Michael Scharp Date: Wed, 11 Mar 2015 14:49:40 -0600 Subject: [PATCH 009/173] Add helper methods to gridTestUtils. Add getGrid, getRow, and selectRow helper methods to utils file. Modified existing methods to use these new helper methods where appropriate. In reference to issue #2693. --- test/e2e/gridTestUtils.spec.js | 1272 ++++++++++++++++---------------- 1 file changed, 652 insertions(+), 620 deletions(-) diff --git a/test/e2e/gridTestUtils.spec.js b/test/e2e/gridTestUtils.spec.js index b7e5e05e57..7bafeee35f 100644 --- a/test/e2e/gridTestUtils.spec.js +++ b/test/e2e/gridTestUtils.spec.js @@ -5,7 +5,7 @@ * @description * End to end test functions. Whenever these are updated, it may also be necessary * to update the associated tutorial. - * + * */ /** @@ -14,629 +14,661 @@ * @description * End to end test functions. Whenever these are updated, it may also be necessary * to update the associated tutorial. - * + * */ module.exports = { - /** - * @ngdoc method - * @methodOf ui.grid.e2eTestLibrary.api:gridTest - * @name expectRowCount - * @description Checks that a grid has the specified number of rows. Note - * that this only returns the number of rendered rows, and the grid does - * row virtualisation - that is that the browser can only see the rendered - * rows, not all the rows in the dataset. This method is useful when doing - * functional tests with small numbers of data, but typically with numbers - * greater than about 10 you'll find that some of the rows are not rendered - * and therefore an error is given. - * @param {string} gridId the id of the grid that you want to inspect - * @param {integer} expectedNumRows the number of visible rows you expect the - * grid to have - * - * @example - *
    -     *   gridTestUtils.expectRowCount('myGrid', 2);
    -     * 
    - * - */ - expectRowCount: function( gridId, expectedNumRows ) { - - var rows = element( by.id( gridId ) ).all( by.repeater('(rowRenderIndex, row) in rowContainer.renderedRows track by $index') ); - expect(rows.count()).toEqual(expectedNumRows); - }, - - - /** - * @ngdoc method - * @methodOf ui.grid.e2eTestLibrary.api:gridTest - * @name expectHeaderColumnCount - * @description Checks that a grid header body render container (the default render container) - * has the specified number of columns. If you are using pinned columns then you may also want - * to check expectHeaderLeftColumnCount - * @param {string} gridId the id of the grid that you want to inspect - * @param {integer} expectedNumCols the number of visible columns you expect the - * body to have - * - * @example - *
    -     *   gridTestUtils.expectHeaderColumnCount('myGrid', 2);
    -     * 
    - * - */ - expectHeaderColumnCount: function( gridId, expectedNumCols ) { - var headerCols = element( by.id( gridId ) ).element( by.css('.ui-grid-render-container-body')).element( by.css('.ui-grid-header') ).all( by.repeater('col in colContainer.renderedColumns track by col.colDef.name') ); - expect(headerCols.count()).toEqual(expectedNumCols); - }, - - - /** - * @ngdoc method - * @methodOf ui.grid.e2eTestLibrary.api:gridTest - * @name expectHeaderLeftColumnCount - * @description Checks that a grid header left render container has the specified number of columns. - * @param {string} gridId the id of the grid that you want to inspect - * @param {integer} expectedNumCols the number of visible columns you expect the - * left render container to have - * - * @example - *
    -     *   gridTestUtils.expectHeaderLeftColumnCount('myGrid', 2);
    -     * 
    - * - */ - expectHeaderLeftColumnCount: function( gridId, expectedNumCols ) { - var headerCols = element( by.id( gridId ) ).element( by.css('.ui-grid-render-container-left')).element( by.css('.ui-grid-header') ).all( by.repeater('col in colContainer.renderedColumns track by col.colDef.name') ); - expect(headerCols.count()).toEqual(expectedNumCols); - }, - /** - * @ngdoc method - * @methodOf ui.grid.e2eTestLibrary.api:gridTest - * @name expectFooterColumnCount - * @description Checks that a grid footer has the specified number of rows. - * @param {string} gridId the id of the grid that you want to inspect - * @param {integer} expectedNumCols the number of visible columns you expect the - * grid to have - * - * @example - *
    -     *   gridTestUtils.expectColumnCount('myGrid', 2);
    -     * 
    - * - */ - expectFooterColumnCount: function( gridId, expectedNumCols ) { - var footerCols = element( by.id( gridId ) ).element( by.css('.ui-grid-footer') ).all( by.repeater('col in colContainer.renderedColumns track by col.colDef.name') ); - expect(footerCols.count()).toEqual(expectedNumCols); - }, - - - /** - * @ngdoc method - * @methodOf ui.grid.e2eTestLibrary.api:gridTest - * @name headerCell - * @description Internal method used to return a headerCell element - * given the grid and column - * @param {string} gridId the id of the grid that you want to inspect - * @param {integer} col the number of the column (within the visible columns) - * that you want to return - * - * @example - *
    -     *   gridTestUtils.headerCell('myGrid', 2);
    -     * 
    - * - */ - headerCell: function( gridId, expectedCol, expectedValue ) { - return element( by.id( gridId ) ).element( by.css('.ui-grid-render-container-body')).element( by.css('.ui-grid-header') ).element( by.repeater('col in colContainer.renderedColumns track by col.colDef.name').row( expectedCol) ); - }, - - - /** - * @ngdoc method - * @methodOf ui.grid.e2eTestLibrary.api:gridTest - * @name footerCell - * @description Internal method used to return a footerCell element - * given the grid and column - * @param {string} gridId the id of the grid that you want to inspect - * @param {integer} col the number of the column (within the visible columns) - * that you want to return - * - * @example - *
    -     *   gridTestUtils.headerCell('myGrid', 2);
    -     * 
    - * - */ - footerCell: function( gridId, expectedCol, expectedValue ) { - return element( by.id( gridId ) ).element( by.css('.ui-grid-footer') ).element( by.repeater('col in colContainer.renderedColumns track by col.colDef.name').row( expectedCol) ); - }, - - - /** - * @ngdoc method - * @methodOf ui.grid.e2eTestLibrary.api:gridTest - * @name dataCell - * @description Internal method used to return a dataCell element - * given the grid and column, note it only returns from the 'body' - * render container - * @param {string} gridId the id of the grid that you want to inspect - * @param {integer} fetchRow the number of the row (within the visible rows) - * that you want to return - * @param {integer} fetchCol the number of the col (within the visible cols) - * that you want to return - * - * @example - *
    -     *   myElement = gridTestUtils.dataCell('myGrid', 2, 2);
    -     * 
    - * - */ - dataCell: function( gridId, fetchRow, fetchCol ) { - var row = element( by.id( gridId ) ).element( by.css('.ui-grid-render-container-body')).element( by.repeater('(rowRenderIndex, row) in rowContainer.renderedRows track by $index').row( fetchRow ) ); - return row.element( by.repeater('(colRenderIndex, col) in colContainer.renderedColumns track by col.colDef.name').row( fetchCol )); - }, - - - /** - * @ngdoc method - * @methodOf ui.grid.e2eTestLibrary.api:gridTest - * @name expectHeaderCellValueMatch - * @description Checks that a header cell matches the specified value, - * takes a regEx or a simple string. - * @param {string} gridId the id of the grid that you want to inspect - * @param {integer} expectedCol the number of the column (within the visible columns) - * that you want to check the value of - * @param {string} expectedValue a regex or string of the value you expect in that header - * - * @example - *
    -     *   gridTestUtils.expectHeaderCellValueMatch('myGrid', 2, 'HeaderValue');
    -     * 
    - * - */ - expectHeaderCellValueMatch: function( gridId, expectedCol, expectedValue ) { - var headerCell = this.headerCell( gridId, expectedCol); - expect(headerCell.getText()).toMatch(expectedValue); - }, - - - /** - * @ngdoc method - * @methodOf ui.grid.e2eTestLibrary.api:gridTest - * @name expectFooterCellValueMatch - * @description Checks that a footer cell matches the specified value, - * takes a regEx or a simple string. - * @param {string} gridId the id of the grid that you want to inspect - * @param {integer} expectedCol the number of the column (within the visible columns) - * that you want to check the value of - * @param {string} expectedValue a regex or string of the value you expect in that footer - * - * @example - *
    -     *   gridTestUtils.expectFooterCellValueMatch('myGrid', 2, 'FooterValue');
    -     * 
    - * - */ - expectFooterCellValueMatch: function( gridId, expectedCol, expectedValue ) { - var footerCell = this.footerCell( gridId, expectedCol); - expect(footerCell.getText()).toMatch(expectedValue); - }, - - - /** - * @ngdoc method - * @methodOf ui.grid.e2eTestLibrary.api:gridTest - * @name expectCellValueMatch - * @description Checks that a cell matches the specified value, - * takes a regEx or a simple string. - * @param {string} gridId the id of the grid that you want to inspect - * @param {integer} expectedRow the number of the row (within the visible rows) - * that you want to check the value of - * @param {integer} expectedCol the number of the column (within the visible columns) - * that you want to check the value of - * @param {string} expectedValue a regex or string of the value you expect in that cell - * - * @example - *
    -     *   gridTestUtils.expectCellValueMatch('myGrid', 0, 2, 'CellValue');
    -     * 
    - * - */ - expectCellValueMatch: function( gridId, expectedRow, expectedCol, expectedValue ) { - var dataCell = this.dataCell( gridId, expectedRow, expectedCol ); - expect(dataCell.getText()).toMatch(expectedValue); - }, - - - /** - * @ngdoc method - * @methodOf ui.grid.e2eTestLibrary.api:gridTest - * @name expectRowValuesMatch - * @description Checks that a row matches the specified values, - * takes an array of regExes or simple strings. - * @param {string} gridId the id of the grid that you want to inspect - * @param {integer} expectedRow the number of the row (within the visible rows) - * that you want to check the value of - * @param {array} expectedValueArray an array of regexes or strings of the values you expect in that row - * - * @example - *
    -     *   gridTestUtils.expectRowValuesMatch('myGrid', 0, [ 'CellValue1', '^cellvalue2', 'cellValue3$' ]);
    -     * 
    - * - */ - expectRowValuesMatch: function( gridId, expectedRow, expectedValueArray ) { - var row = element( by.id( gridId ) ).element( by.repeater('(rowRenderIndex, row) in rowContainer.renderedRows track by $index').row( expectedRow ) ); - - for ( var i = 0; i < expectedValueArray.length; i++){ - expect(row.element( by.repeater('(colRenderIndex, col) in colContainer.renderedColumns track by col.colDef.name').row(i)).getText()).toMatch(expectedValueArray[i], 'Expected to match: ' + expectedValueArray[i] + ' in column: ' + i); - } - }, - - - /** - * @ngdoc method - * @methodOf ui.grid.e2eTestLibrary.api:gridTest - * @name clickHeaderCell - * @description Clicks on the header of the specified column, - * which would usually result in a sort - * @param {string} gridId the id of the grid that you want to inspect - * @param {integer} colNumber the number of the column (within the visible columns) - * that you want to click on - * - * @example - *
    -     *   gridTestUtils.clickHeaderCell('myGrid', 0);
    -     * 
    - * - */ - clickHeaderCell: function( gridId, colNumber ) { - var headerCell = this.headerCell( gridId, colNumber); - - headerCell.click(); - }, + /** + * Helper function for returning a grid element. + * @param gridId Id of grid to return. + * + * @returns {ElementFinder|Grid} Grid wrapped in an ElementFinder + * + * @example + *
    +  *   var grid = gridTestUtils.getGrid( 'myGrid' ); //or internally
    +  *   var row = this.getGrid( gridId );
    +  * 
    + */ + getGrid: function( gridId ) { + return element( by.id( gridId ) ); + }, + + /** + * Helper function for returning a row. + * + * @param gridId {string} + * @param rowNum {integer} + * + * @returns {ElementFinder|*} + * + * @example + *
    +  *   var row = gridTestUtils.getRow( 'myGrid', 0); //or internally
    +  *   var row = this.getRow( gridId, rowNum );
    +  * 
    + */ + getRow: function( gridId, rowNum ) { + return this.getGrid( gridId ).element( by.repeater('(rowRenderIndex, row) in rowContainer.renderedRows track by $index').row( rowNum ) ); + }, + + /** + * Helper function to select a row. + * + * @param gridId {string} + * @param rowNum {integer} + * + * + * @example + *
    +  *   var row = gridTestUtils.selectRow( 'myGrid', 0 );
    +  * 
    + */ + selectRow: function( gridId, rowNum ) { + this.getRow( gridId, rowNum ).click(); + }, + + /** + * @ngdoc method + * @methodOf ui.grid.e2eTestLibrary.api:gridTest + * @name expectRowCount + * @description Checks that a grid has the specified number of rows. Note + * that this only returns the number of rendered rows, and the grid does + * row virtualisation - that is that the browser can only see the rendered + * rows, not all the rows in the dataset. This method is useful when doing + * functional tests with small numbers of data, but typically with numbers + * greater than about 10 you'll find that some of the rows are not rendered + * and therefore an error is given. + * @param {string} gridId the id of the grid that you want to inspect + * @param {integer} expectedNumRows the number of visible rows you expect the + * grid to have + * + * @example + *
    +  *   gridTestUtils.expectRowCount('myGrid', 2);
    +  * 
    + * + */ + expectRowCount: function( gridId, expectedNumRows ) { + + var rows = this.getGrid( gridId ).all( by.repeater('(rowRenderIndex, row) in rowContainer.renderedRows track by $index') ); + expect(rows.count()).toEqual(expectedNumRows); + }, + + /** + * @ngdoc method + * @methodOf ui.grid.e2eTestLibrary.api:gridTest + * @name expectHeaderColumnCount + * @description Checks that a grid header body render container (the default render container) + * has the specified number of columns. If you are using pinned columns then you may also want + * to check expectHeaderLeftColumnCount + * @param {string} gridId the id of the grid that you want to inspect + * @param {integer} expectedNumCols the number of visible columns you expect the + * body to have + * + * @example + *
    +  *   gridTestUtils.expectHeaderColumnCount('myGrid', 2);
    +  * 
    + * + */ + expectHeaderColumnCount: function( gridId, expectedNumCols ) { + var headerCols = this.getGrid( gridId ).element( by.css('.ui-grid-render-container-body')).element( by.css('.ui-grid-header') ).all( by.repeater('col in colContainer.renderedColumns track by col.colDef.name') ); + expect(headerCols.count()).toEqual(expectedNumCols); + }, + + /** + * @ngdoc method + * @methodOf ui.grid.e2eTestLibrary.api:gridTest + * @name expectHeaderLeftColumnCount + * @description Checks that a grid header left render container has the specified number of columns. + * @param {string} gridId the id of the grid that you want to inspect + * @param {integer} expectedNumCols the number of visible columns you expect the + * left render container to have + * + * @example + *
    +  *   gridTestUtils.expectHeaderLeftColumnCount('myGrid', 2);
    +  * 
    + * + */ + expectHeaderLeftColumnCount: function( gridId, expectedNumCols ) { + var headerCols = this.getGrid( gridId ).element( by.css('.ui-grid-render-container-left')).element( by.css('.ui-grid-header') ).all( by.repeater('col in colContainer.renderedColumns track by col.colDef.name') ); + expect(headerCols.count()).toEqual(expectedNumCols); + }, + + /** + * @ngdoc method + * @methodOf ui.grid.e2eTestLibrary.api:gridTest + * @name expectFooterColumnCount + * @description Checks that a grid footer has the specified number of rows. + * @param {string} gridId the id of the grid that you want to inspect + * @param {integer} expectedNumCols the number of visible columns you expect the + * grid to have + * + * @example + *
    +  *   gridTestUtils.expectColumnCount('myGrid', 2);
    +  * 
    + * + */ + expectFooterColumnCount: function( gridId, expectedNumCols ) { + var footerCols = this.getGrid( gridId ).element( by.css('.ui-grid-footer') ).all( by.repeater('col in colContainer.renderedColumns track by col.colDef.name') ); + expect(footerCols.count()).toEqual(expectedNumCols); + }, + + /** + * @ngdoc method + * @methodOf ui.grid.e2eTestLibrary.api:gridTest + * @name headerCell + * @description Internal method used to return a headerCell element + * given the grid and column + * @param {string} gridId the id of the grid that you want to inspect + * @param {integer} col the number of the column (within the visible columns) + * that you want to return + * + * @example + *
    +  *   gridTestUtils.headerCell('myGrid', 2);
    +  * 
    + * + */ + headerCell: function( gridId, expectedCol, expectedValue ) { + return this.getGrid( gridId ).element( by.css('.ui-grid-render-container-body')).element( by.css('.ui-grid-header') ).element( by.repeater('col in colContainer.renderedColumns track by col.colDef.name').row( expectedCol) ); + }, + + /** + * @ngdoc method + * @methodOf ui.grid.e2eTestLibrary.api:gridTest + * @name footerCell + * @description Internal method used to return a footerCell element + * given the grid and column + * @param {string} gridId the id of the grid that you want to inspect + * @param {integer} col the number of the column (within the visible columns) + * that you want to return + * + * @example + *
    +  *   gridTestUtils.headerCell('myGrid', 2);
    +  * 
    + * + */ + footerCell: function( gridId, expectedCol, expectedValue ) { + return this.getGrid( gridId ).element( by.css('.ui-grid-footer') ).element( by.repeater('col in colContainer.renderedColumns track by col.colDef.name').row( expectedCol) ); + }, + + /** + * @ngdoc method + * @methodOf ui.grid.e2eTestLibrary.api:gridTest + * @name dataCell + * @description Internal method used to return a dataCell element + * given the grid and column, note it only returns from the 'body' + * render container + * @param {string} gridId the id of the grid that you want to inspect + * @param {integer} fetchRow the number of the row (within the visible rows) + * that you want to return + * @param {integer} fetchCol the number of the col (within the visible cols) + * that you want to return + * + * @example + *
    +  *   myElement = gridTestUtils.dataCell('myGrid', 2, 2);
    +  * 
    + * + */ + dataCell: function( gridId, fetchRow, fetchCol ) { + var row = this.getGrid( gridId ).element( by.css('.ui-grid-render-container-body')).element( by.repeater('(rowRenderIndex, row) in rowContainer.renderedRows track by $index').row( fetchRow ) ); + return row.element( by.repeater('(colRenderIndex, col) in colContainer.renderedColumns track by col.colDef.name').row( fetchCol )); + }, + + /** + * @ngdoc method + * @methodOf ui.grid.e2eTestLibrary.api:gridTest + * @name expectHeaderCellValueMatch + * @description Checks that a header cell matches the specified value, + * takes a regEx or a simple string. + * @param {string} gridId the id of the grid that you want to inspect + * @param {integer} expectedCol the number of the column (within the visible columns) + * that you want to check the value of + * @param {string} expectedValue a regex or string of the value you expect in that header + * + * @example + *
    +  *   gridTestUtils.expectHeaderCellValueMatch('myGrid', 2, 'HeaderValue');
    +  * 
    + * + */ + expectHeaderCellValueMatch: function( gridId, expectedCol, expectedValue ) { + var headerCell = this.headerCell( gridId, expectedCol); + expect(headerCell.getText()).toMatch(expectedValue); + }, + + /** + * @ngdoc method + * @methodOf ui.grid.e2eTestLibrary.api:gridTest + * @name expectFooterCellValueMatch + * @description Checks that a footer cell matches the specified value, + * takes a regEx or a simple string. + * @param {string} gridId the id of the grid that you want to inspect + * @param {integer} expectedCol the number of the column (within the visible columns) + * that you want to check the value of + * @param {string} expectedValue a regex or string of the value you expect in that footer + * + * @example + *
    +  *   gridTestUtils.expectFooterCellValueMatch('myGrid', 2, 'FooterValue');
    +  * 
    + * + */ + expectFooterCellValueMatch: function( gridId, expectedCol, expectedValue ) { + var footerCell = this.footerCell( gridId, expectedCol); + expect(footerCell.getText()).toMatch(expectedValue); + }, + + /** + * @ngdoc method + * @methodOf ui.grid.e2eTestLibrary.api:gridTest + * @name expectCellValueMatch + * @description Checks that a cell matches the specified value, + * takes a regEx or a simple string. + * @param {string} gridId the id of the grid that you want to inspect + * @param {integer} expectedRow the number of the row (within the visible rows) + * that you want to check the value of + * @param {integer} expectedCol the number of the column (within the visible columns) + * that you want to check the value of + * @param {string} expectedValue a regex or string of the value you expect in that cell + * + * @example + *
    +  *   gridTestUtils.expectCellValueMatch('myGrid', 0, 2, 'CellValue');
    +  * 
    + * + */ + expectCellValueMatch: function( gridId, expectedRow, expectedCol, expectedValue ) { + var dataCell = this.dataCell( gridId, expectedRow, expectedCol ); + expect(dataCell.getText()).toMatch(expectedValue); + }, + + /** + * @ngdoc method + * @methodOf ui.grid.e2eTestLibrary.api:gridTest + * @name expectRowValuesMatch + * @description Checks that a row matches the specified values, + * takes an array of regExes or simple strings. + * @param {string} gridId the id of the grid that you want to inspect + * @param {integer} expectedRow the number of the row (within the visible rows) + * that you want to check the value of + * @param {array} expectedValueArray an array of regexes or strings of the values you expect in that row + * + * @example + *
    +  *   gridTestUtils.expectRowValuesMatch('myGrid', 0, [ 'CellValue1', '^cellvalue2', 'cellValue3$' ]);
    +  * 
    + * + */ + expectRowValuesMatch: function( gridId, expectedRow, expectedValueArray ) { + var row = this.getRow( gridId, expectedRow ); + + for ( var i = 0; i < expectedValueArray.length; i++){ + expect(row.element( by.repeater('(colRenderIndex, col) in colContainer.renderedColumns track by col.colDef.name').row(i)).getText()).toMatch(expectedValueArray[i], 'Expected to match: ' + expectedValueArray[i] + ' in column: ' + i); + } + }, /** - * @ngdoc method - * @methodOf ui.grid.e2eTestLibrary.api:gridTest - * @name resizeHeaderCell - * @description Drags the left resizer border towards the column menu button, - * which will perform a column resizing. - * @param {string} gridId the id of the grid that you want to adjust - * @param {integer} colNumber the number of the column (within the visible columns) - * which left resizer border you wish to drag (this will increase the size of colNumber-1). - * - * @example - *
    -   *   gridTestUtils.resizeHeaderCell('myGrid', 1);
    -   * 
    - * - */ - resizeHeaderCell: function( gridId, colNumber ) { - var headerCell = this.headerCell(gridId, colNumber); - - var resizer = headerCell.all( by.css( '.ui-grid-column-resizer' )).first(); - var menuButton = headerCell.element( by.css( '.ui-grid-column-menu-button' )); - - browser.actions() - .mouseDown(resizer) - .mouseMove(menuButton) - .mouseUp() - .perform(); - - }, - - - - /** - * @ngdoc method - * @methodOf ui.grid.e2eTestLibrary.api:gridTest - * @name shiftClickHeaderCell - * @description Shift-clicks on the header of the specified column, - * which would usually result in adding a second column to the sort - * @param {string} gridId the id of the grid that you want to inspect - * @param {integer} colNumber the number of the column (within the visible columns) - * that you want to click on - * - * @example - *
    -     *   gridTestUtils.shiftClickHeaderCell('myGrid', 0);
    -     * 
    - * - */ - shiftClickHeaderCell: function( gridId, colNumber ) { - var headerCell = this.headerCell( gridId, colNumber); - - browser.actions() - .keyDown(protractor.Key.SHIFT) - .click(headerCell) - .keyUp(protractor.Key.SHIFT) - .perform(); - }, - - /** - * @ngdoc method - * @methodOf ui.grid.e2eTestLibrary.api:gridTest - * @name clickColumnMenu - * @description Clicks on the specified option in the specified column - * menu. Using this method is fragile, as any change to menu ordering - * will break all the tests. For this reason it is recommended to wrap - * this into "clickColumnMenu" methods, each with a constant - * for the item you want to click on. You could alternatively develop - * a method that finds a menu item by text value, but given that - * ui-grid has i18n, the text values could also change easily - * @param {string} gridId the id of the grid that you want to inspect - * @param {integer} colNumber the number of the column (within the visible columns) - * that you want to sort on - * @param {integer} menuItemNumber the number of the item in the menu that - * you want to click on - * - * @example - *
    -     *   gridTestUtils.clickColumnMenu('myGrid', 0, 0);
    -     * 
    - * - */ - clickColumnMenu: function( gridId, colNumber, menuItemNumber ) { - var headerCell = this.headerCell( gridId, colNumber); - - headerCell.element( by.css( '.ui-grid-column-menu-button' ) ).click(); - - var columnMenu = element( by.id( gridId ) ).element( by.css( '.ui-grid-column-menu' )); - columnMenu.element( by.repeater('item in menuItems').row(menuItemNumber) ).click(); - }, - - - /** - * @ngdoc method - * @methodOf ui.grid.e2eTestLibrary.api:gridTest - * @name clickColumnMenuSortAsc - * @description Clicks on the sort ascending item within the specified - * column menu. Although it feels cumbersome to write lots of individual - * "click this menu item" helpers, it is quite useful if the column menus are - * changed to not have to go to every test script and change the menu item number - * @param {string} gridId the id of the grid that you want to inspect - * @param {integer} colNumber the number of the column (within the visible columns) - * that you want to sort on - * - * @example - *
    -     *   gridTestUtils.clickColumnMenuSortAsc('myGrid', 0);
    -     * 
    - * - */ - clickColumnMenuSortAsc: function( gridId, colNumber ) { - this.clickColumnMenu( gridId, colNumber, 0); - }, - - - /** - * @ngdoc method - * @methodOf ui.grid.e2eTestLibrary.api:gridTest - * @name clickColumnMenuSortDesc - * @description Clicks on the sort descending item within the specified - * column menu. Although it feels cumbersome to write lots of individual - * "click this menu item" helpers, it is quite useful if the column menus are - * changed to not have to go to every test script and change the menu item number - * @param {string} gridId the id of the grid that you want to inspect - * @param {integer} colNumber the number of the column (within the visible columns) - * that you want to sort on - * - * @example - *
    -     *   gridTestUtils.clickColumnMenuSortDesc('myGrid', 0);
    -     * 
    - * - */ - clickColumnMenuSortDesc: function( gridId, colNumber ) { - this.clickColumnMenu( gridId, colNumber, 1); - }, - - - /** - * @ngdoc method - * @methodOf ui.grid.e2eTestLibrary.api:gridTest - * @name clickColumnMenuRemoveSort - * @description Clicks on the remove sort item within the specified - * column menu. Although it feels cumbersome to write lots of individual - * "click this menu item" helpers, it is quite useful if the column menus are - * changed to not have to go to every test script and change the menu item number - * @param {string} gridId the id of the grid that you want to inspect - * @param {integer} colNumber the number of the column (within the visible columns) - * that you want to remove the sort from - * - * @example - *
    -     *   gridTestUtils.clickColumnMenuRemoveSort('myGrid', 0);
    -     * 
    - * - */ - clickColumnMenuRemoveSort: function( gridId, colNumber ) { - this.clickColumnMenu( gridId, colNumber, 2); - }, - - /** - * @ngdoc method - * @methodOf ui.grid.e2eTestLibrary.api:gridTest - * @name clickColumnMenuHide - * @description Clicks on the hide item within the specified - * column menu. Although it feels cumbersome to write lots of individual - * "click this menu item" helpers, it is quite useful if the column menus are - * changed to not have to go to every test script and change the menu item number - * @param {string} gridId the id of the grid that you want to inspect - * @param {integer} colNumber the number of the column (within the visible columns) - * that you want to hide - * - * @example - *
    -     *   gridTestUtils.clickColumnMenuHide('myGrid', 0);
    -     * 
    - * - */ - clickColumnMenuHide: function( gridId, colNumber ) { - this.clickColumnMenu( gridId, colNumber, 3); - }, - /** - * @ngdoc method - * @methodOf ui.grid.e2eTestLibrary.api:gridTest - * @name expectFilterBoxInColumn - * @description Checks that a filter box exists in the specified column - * @param {string} gridId the id of the grid that you want to inspect - * @param {integer} colNumber the number of the column (within the visible columns) - * that you want to verify the filter box is in - * @param {integer} count the number filter boxes you expect - 0 meaning none, 1 meaning - * a standard filter, 2 meaning a numerical filter with greater than / less than. - * - * @example - *
    -     *   gridTestUtils.expectFilterBoxInColumn('myGrid', 0, 0);
    -     * 
    - * - */ - expectFilterBoxInColumn: function( gridId, colNumber, count ) { - var headerCell = this.headerCell( gridId, colNumber); - - expect( headerCell.all( by.css( '.ui-grid-filter-input' ) ).count() ).toEqual(count); - }, - - - /** - * @ngdoc method - * @methodOf ui.grid.e2eTestLibrary.api:gridTest - * @name cancelFilterInColumn - * @description Cancels the filter in a column - * @param {string} gridId the id of the grid that you want to inspect - * @param {integer} colNumber the number of the column (within the visible columns) - * that you want to cancel the filter in - * - * @example - *
    -     *   gridTestUtils.cancelFilterInColumn('myGrid', 0);
    -     * 
    - * - */ - cancelFilterInColumn: function( gridId, colNumber ) { - var headerCell = this.headerCell( gridId, colNumber); - - headerCell.element( by.css( '.ui-grid-icon-cancel' ) ).click(); - }, - - - /** - * @ngdoc method - * @methodOf ui.grid.e2eTestLibrary.api:gridTest - * @name enterFilterInColumn - * @description Enters a filter in a column - * @param {string} gridId the id of the grid that you want to inspect - * @param {integer} colNumber the number of the column (within the visible columns) - * that you want to enter the filter in - * @param {string} filterValue the value you want to enter into the filter - * - * @example - *
    -     *   gridTestUtils.cancelFilterInColumn('myGrid', 0);
    -     * 
    - * - */ - enterFilterInColumn: function( gridId, colNumber, filterValue ) { - var headerCell = this.headerCell( gridId, colNumber); - - headerCell.element( by.css( '.ui-grid-filter-input' ) ).sendKeys(filterValue); - }, - - - /** - * @ngdoc method - * @methodOf ui.grid.e2eTestLibrary.api:gridTest - * @name expectVisibleColumnMenuItems - * @description Checks how many visible menu items there are in the column menu - * @param {string} gridId the id of the grid that you want to inspect - * @param {integer} colNumber the number of the column (within the visible columns) - * that you want to check the count for - * @param {integer} expectItems the number of visible items you expect - * - * @example - *
    -     *   gridTestUtils.visibleColumnMenuItems('myGrid', 0, 3);
    -     * 
    - * - */ - expectVisibleColumnMenuItems: function( gridId, colNumber, expectItems ) { - var headerCell = this.headerCell( gridId, colNumber ); - headerCell.element( by.css( '.ui-grid-column-menu-button' ) ).click(); - - var displayedCount = 0; - var columnMenu = element( by.id( gridId ) ).element( by.css( '.ui-grid-column-menu' )); - - var menuItems = columnMenu.all( by.css( '.ui-grid-menu-item' ) ); - - menuItems.map(function(elm) { - return elm.isDisplayed(); - }).then( function( displayedArray ){ - for ( var i = 0; i < displayedArray.length; i++ ){ - if ( displayedArray[i] ){ - displayedCount++; - } + * @ngdoc method + * @methodOf ui.grid.e2eTestLibrary.api:gridTest + * @name clickHeaderCell + * @description Clicks on the header of the specified column, + * which would usually result in a sort + * @param {string} gridId the id of the grid that you want to inspect + * @param {integer} colNumber the number of the column (within the visible columns) + * that you want to click on + * + * @example + *
    +  *   gridTestUtils.clickHeaderCell('myGrid', 0);
    +  * 
    + * + */ + clickHeaderCell: function( gridId, colNumber ) { + var headerCell = this.headerCell( gridId, colNumber); + + headerCell.click(); + }, + + /** + * @ngdoc method + * @methodOf ui.grid.e2eTestLibrary.api:gridTest + * @name resizeHeaderCell + * @description Drags the left resizer border towards the column menu button, + * which will perform a column resizing. + * @param {string} gridId the id of the grid that you want to adjust + * @param {integer} colNumber the number of the column (within the visible columns) + * which left resizer border you wish to drag (this will increase the size of colNumber-1). + * + * @example + *
    +  *   gridTestUtils.resizeHeaderCell('myGrid', 1);
    +  * 
    + * + */ + resizeHeaderCell: function( gridId, colNumber ) { + var headerCell = this.headerCell(gridId, colNumber); + + var resizer = headerCell.all( by.css( '.ui-grid-column-resizer' )).first(); + var menuButton = headerCell.element( by.css( '.ui-grid-column-menu-button' )); + + browser.actions() + .mouseDown(resizer) + .mouseMove(menuButton) + .mouseUp() + .perform(); + + }, + + /** + * @ngdoc method + * @methodOf ui.grid.e2eTestLibrary.api:gridTest + * @name shiftClickHeaderCell + * @description Shift-clicks on the header of the specified column, + * which would usually result in adding a second column to the sort + * @param {string} gridId the id of the grid that you want to inspect + * @param {integer} colNumber the number of the column (within the visible columns) + * that you want to click on + * + * @example + *
    +  *   gridTestUtils.shiftClickHeaderCell('myGrid', 0);
    +  * 
    + * + */ + shiftClickHeaderCell: function( gridId, colNumber ) { + var headerCell = this.headerCell( gridId, colNumber); + + browser.actions() + .keyDown(protractor.Key.SHIFT) + .click(headerCell) + .keyUp(protractor.Key.SHIFT) + .perform(); + }, + + /** + * @ngdoc method + * @methodOf ui.grid.e2eTestLibrary.api:gridTest + * @name clickColumnMenu + * @description Clicks on the specified option in the specified column + * menu. Using this method is fragile, as any change to menu ordering + * will break all the tests. For this reason it is recommended to wrap + * this into "clickColumnMenu" methods, each with a constant + * for the item you want to click on. You could alternatively develop + * a method that finds a menu item by text value, but given that + * ui-grid has i18n, the text values could also change easily + * @param {string} gridId the id of the grid that you want to inspect + * @param {integer} colNumber the number of the column (within the visible columns) + * that you want to sort on + * @param {integer} menuItemNumber the number of the item in the menu that + * you want to click on + * + * @example + *
    +  *   gridTestUtils.clickColumnMenu('myGrid', 0, 0);
    +  * 
    + * + */ + clickColumnMenu: function( gridId, colNumber, menuItemNumber ) { + var headerCell = this.headerCell( gridId, colNumber); + + headerCell.element( by.css( '.ui-grid-column-menu-button' ) ).click(); + + var columnMenu = this.getGrid( gridId ).element( by.css( '.ui-grid-column-menu' )); + columnMenu.element( by.repeater('item in menuItems').row(menuItemNumber) ).click(); + }, + + /** + * @ngdoc method + * @methodOf ui.grid.e2eTestLibrary.api:gridTest + * @name clickColumnMenuSortAsc + * @description Clicks on the sort ascending item within the specified + * column menu. Although it feels cumbersome to write lots of individual + * "click this menu item" helpers, it is quite useful if the column menus are + * changed to not have to go to every test script and change the menu item number + * @param {string} gridId the id of the grid that you want to inspect + * @param {integer} colNumber the number of the column (within the visible columns) + * that you want to sort on + * + * @example + *
    +  *   gridTestUtils.clickColumnMenuSortAsc('myGrid', 0);
    +  * 
    + * + */ + clickColumnMenuSortAsc: function( gridId, colNumber ) { + this.clickColumnMenu( gridId, colNumber, 0); + }, + + /** + * @ngdoc method + * @methodOf ui.grid.e2eTestLibrary.api:gridTest + * @name clickColumnMenuSortDesc + * @description Clicks on the sort descending item within the specified + * column menu. Although it feels cumbersome to write lots of individual + * "click this menu item" helpers, it is quite useful if the column menus are + * changed to not have to go to every test script and change the menu item number + * @param {string} gridId the id of the grid that you want to inspect + * @param {integer} colNumber the number of the column (within the visible columns) + * that you want to sort on + * + * @example + *
    +  *   gridTestUtils.clickColumnMenuSortDesc('myGrid', 0);
    +  * 
    + * + */ + clickColumnMenuSortDesc: function( gridId, colNumber ) { + this.clickColumnMenu( gridId, colNumber, 1); + }, + + /** + * @ngdoc method + * @methodOf ui.grid.e2eTestLibrary.api:gridTest + * @name clickColumnMenuRemoveSort + * @description Clicks on the remove sort item within the specified + * column menu. Although it feels cumbersome to write lots of individual + * "click this menu item" helpers, it is quite useful if the column menus are + * changed to not have to go to every test script and change the menu item number + * @param {string} gridId the id of the grid that you want to inspect + * @param {integer} colNumber the number of the column (within the visible columns) + * that you want to remove the sort from + * + * @example + *
    +  *   gridTestUtils.clickColumnMenuRemoveSort('myGrid', 0);
    +  * 
    + * + */ + clickColumnMenuRemoveSort: function( gridId, colNumber ) { + this.clickColumnMenu( gridId, colNumber, 2); + }, + + /** + * @ngdoc method + * @methodOf ui.grid.e2eTestLibrary.api:gridTest + * @name clickColumnMenuHide + * @description Clicks on the hide item within the specified + * column menu. Although it feels cumbersome to write lots of individual + * "click this menu item" helpers, it is quite useful if the column menus are + * changed to not have to go to every test script and change the menu item number + * @param {string} gridId the id of the grid that you want to inspect + * @param {integer} colNumber the number of the column (within the visible columns) + * that you want to hide + * + * @example + *
    +  *   gridTestUtils.clickColumnMenuHide('myGrid', 0);
    +  * 
    + * + */ + clickColumnMenuHide: function( gridId, colNumber ) { + this.clickColumnMenu( gridId, colNumber, 3); + }, + + /** + * @ngdoc method + * @methodOf ui.grid.e2eTestLibrary.api:gridTest + * @name expectFilterBoxInColumn + * @description Checks that a filter box exists in the specified column + * @param {string} gridId the id of the grid that you want to inspect + * @param {integer} colNumber the number of the column (within the visible columns) + * that you want to verify the filter box is in + * @param {integer} count the number filter boxes you expect - 0 meaning none, 1 meaning + * a standard filter, 2 meaning a numerical filter with greater than / less than. + * + * @example + *
    +  *   gridTestUtils.expectFilterBoxInColumn('myGrid', 0, 0);
    +  * 
    + * + */ + expectFilterBoxInColumn: function( gridId, colNumber, count ) { + var headerCell = this.headerCell( gridId, colNumber); + + expect( headerCell.all( by.css( '.ui-grid-filter-input' ) ).count() ).toEqual(count); + }, + + /** + * @ngdoc method + * @methodOf ui.grid.e2eTestLibrary.api:gridTest + * @name cancelFilterInColumn + * @description Cancels the filter in a column + * @param {string} gridId the id of the grid that you want to inspect + * @param {integer} colNumber the number of the column (within the visible columns) + * that you want to cancel the filter in + * + * @example + *
    +  *   gridTestUtils.cancelFilterInColumn('myGrid', 0);
    +  * 
    + * + */ + cancelFilterInColumn: function( gridId, colNumber ) { + var headerCell = this.headerCell( gridId, colNumber); + + headerCell.element( by.css( '.ui-grid-icon-cancel' ) ).click(); + }, + + /** + * @ngdoc method + * @methodOf ui.grid.e2eTestLibrary.api:gridTest + * @name enterFilterInColumn + * @description Enters a filter in a column + * @param {string} gridId the id of the grid that you want to inspect + * @param {integer} colNumber the number of the column (within the visible columns) + * that you want to enter the filter in + * @param {string} filterValue the value you want to enter into the filter + * + * @example + *
    +  *   gridTestUtils.cancelFilterInColumn('myGrid', 0);
    +  * 
    + * + */ + enterFilterInColumn: function( gridId, colNumber, filterValue ) { + var headerCell = this.headerCell( gridId, colNumber); + + headerCell.element( by.css( '.ui-grid-filter-input' ) ).sendKeys(filterValue); + }, + + /** + * @ngdoc method + * @methodOf ui.grid.e2eTestLibrary.api:gridTest + * @name expectVisibleColumnMenuItems + * @description Checks how many visible menu items there are in the column menu + * @param {string} gridId the id of the grid that you want to inspect + * @param {integer} colNumber the number of the column (within the visible columns) + * that you want to check the count for + * @param {integer} expectItems the number of visible items you expect + * + * @example + *
    +  *   gridTestUtils.visibleColumnMenuItems('myGrid', 0, 3);
    +  * 
    + * + */ + expectVisibleColumnMenuItems: function( gridId, colNumber, expectItems ) { + var headerCell = this.headerCell( gridId, colNumber ); + headerCell.element( by.css( '.ui-grid-column-menu-button' ) ).click(); + + var displayedCount = 0; + var columnMenu = this.getGrid( gridId ).element( by.css( '.ui-grid-column-menu' )); + + var menuItems = columnMenu.all( by.css( '.ui-grid-menu-item' ) ); + + menuItems.map(function(elm) { + return elm.isDisplayed(); + }).then( function( displayedArray ){ + for ( var i = 0; i < displayedArray.length; i++ ){ + if ( displayedArray[i] ){ + displayedCount++; } - expect(displayedCount).toEqual( expectItems ); - }); - }, - - - /** - * @ngdoc method - * @methodOf ui.grid.e2eTestLibrary.api:gridTest - * @name expectVisibleGridMenuItems - * @description Checks how many visible menu items there are in the grid menu - * @param {string} gridId the id of the grid that you want to inspect - * @param {integer} expectItems the number of visible items you expect - * - * @example - *
    -     *   gridTestUtils.expectVisibleGridMenuItems('myGrid', 3);
    -     * 
    - * - */ - expectVisibleGridMenuItems: function( gridId, expectItems ) { - var gridMenuButton = element( by.id( gridId ) ).element( by.css ( '.ui-grid-menu-button' ) ); - gridMenuButton.click(); - - var displayedCount = 0; - - var menuItems = gridMenuButton.all( by.css( '.ui-grid-menu-item' ) ); - - menuItems.map(function(elm) { - return elm.isDisplayed(); - }).then( function( displayedArray ){ - for ( var i = 0; i < displayedArray.length; i++ ){ - if ( displayedArray[i] ){ - displayedCount++; - } + } + expect(displayedCount).toEqual( expectItems ); + }); + }, + + /** + * @ngdoc method + * @methodOf ui.grid.e2eTestLibrary.api:gridTest + * @name expectVisibleGridMenuItems + * @description Checks how many visible menu items there are in the grid menu + * @param {string} gridId the id of the grid that you want to inspect + * @param {integer} expectItems the number of visible items you expect + * + * @example + *
    +  *   gridTestUtils.expectVisibleGridMenuItems('myGrid', 3);
    +  * 
    + * + */ + expectVisibleGridMenuItems: function( gridId, expectItems ) { + var gridMenuButton = this.getGrid( gridId ).element( by.css ( '.ui-grid-menu-button' ) ); + gridMenuButton.click(); + + var displayedCount = 0; + + var menuItems = gridMenuButton.all( by.css( '.ui-grid-menu-item' ) ); + + menuItems.map(function(elm) { + return elm.isDisplayed(); + }).then( function( displayedArray ){ + for ( var i = 0; i < displayedArray.length; i++ ){ + if ( displayedArray[i] ){ + displayedCount++; } - expect(displayedCount).toEqual( expectItems ); - }); - }, - - /** - * @ngdoc method - * @methodOf ui.grid.e2eTestLibrary.api:gridTest - * @name clickGridMenuItem - * @description Clicks on a numbered grid menu item. Note that it's clicking - * the item based on the underlying repeater - and that some of the items will - * not be visible. So the item you want to click on may not be the same - * as the item number when you look at the ui - * @param {string} gridId the id of the grid that you want to inspect - * @param {integer} itemNumber the number of visible items you expect - * - * @example - *
    -     *   gridTestUtils.clickVisibleGridMenuItem('myGrid', 9);
    -     * 
    - * - */ - clickGridMenuItem: function( gridId, itemNumber ) { - var gridMenuButton = element( by.id( gridId ) ).element( by.css ( '.ui-grid-menu-button' ) ); - gridMenuButton.click(); - - gridMenuButton.element( by.repeater('item in menuItems').row( itemNumber) ).click(); - } -}; + } + expect(displayedCount).toEqual( expectItems ); + }); + }, + /** + * @ngdoc method + * @methodOf ui.grid.e2eTestLibrary.api:gridTest + * @name clickGridMenuItem + * @description Clicks on a numbered grid menu item. Note that it's clicking + * the item based on the underlying repeater - and that some of the items will + * not be visible. So the item you want to click on may not be the same + * as the item number when you look at the ui + * @param {string} gridId the id of the grid that you want to inspect + * @param {integer} itemNumber the number of visible items you expect + * + * @example + *
    +  *   gridTestUtils.clickVisibleGridMenuItem('myGrid', 9);
    +  * 
    + * + */ + clickGridMenuItem: function( gridId, itemNumber ) { + var gridMenuButton = this.getGrid( gridId ).element( by.css ( '.ui-grid-menu-button' ) ); + gridMenuButton.click(); + + gridMenuButton.element( by.repeater('item in menuItems').row( itemNumber) ).click(); + } +}; \ No newline at end of file From 7ff9be728520c9cb93756dcbb0184c3d109fbd16 Mon Sep 17 00:00:00 2001 From: Alex Schechter Date: Wed, 11 Mar 2015 17:53:18 -0400 Subject: [PATCH 010/173] Stopping propagation on checkmark click event --- src/features/selection/js/selection.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/features/selection/js/selection.js b/src/features/selection/js/selection.js index 87ed99cc8a..7c37c0af9e 100644 --- a/src/features/selection/js/selection.js +++ b/src/features/selection/js/selection.js @@ -668,6 +668,7 @@ link: function($scope, $elm, $attrs, uiGridCtrl) { var self = uiGridCtrl.grid; $scope.selectButtonClick = function(row, evt) { + evt.stopPropagation(); if (evt.shiftKey) { uiGridSelectionService.shiftSelect(self, row, evt, self.options.multiSelect); } From 6f82e20665a931f5a214abbedf368d97b7545fd3 Mon Sep 17 00:00:00 2001 From: Paul Lambert Date: Thu, 12 Mar 2015 11:06:03 +1300 Subject: [PATCH 011/173] Fix(edit): edit on focus when tab. #2986 raised for solution being ugly --- src/features/cellnav/js/cellnav.js | 6 +++--- src/features/edit/js/gridEdit.js | 11 ++++++++--- 2 files changed, 11 insertions(+), 6 deletions(-) diff --git a/src/features/cellnav/js/cellnav.js b/src/features/cellnav/js/cellnav.js index 5670793aea..b6bbe16cee 100644 --- a/src/features/cellnav/js/cellnav.js +++ b/src/features/cellnav/js/cellnav.js @@ -911,12 +911,12 @@ rowCol.eventType = uiGridCellNavConstants.EVENT_TYPE.KEYDOWN; - // Broadcast the navigation - uiGridCtrl.cellNav.broadcastCellNav(rowCol); - // Scroll to the new cell, if it's not completely visible within the render container's viewport uiGridCellNavService.scrollToIfNecessary(grid, rowCol.row, rowCol.col); + // Broadcast the navigation + uiGridCtrl.cellNav.broadcastCellNav(rowCol); + evt.stopPropagation(); evt.preventDefault(); diff --git a/src/features/edit/js/gridEdit.js b/src/features/edit/js/gridEdit.js index 3a9e787bd6..78a73523ed 100644 --- a/src/features/edit/js/gridEdit.js +++ b/src/features/edit/js/gridEdit.js @@ -478,9 +478,14 @@ if ($scope.col.colDef.enableCellEditOnFocus) { $scope.$on(uiGridCellNavConstants.CELL_NAV_EVENT, function (evt, rowCol) { if (rowCol.row === $scope.row && rowCol.col === $scope.col) { - beginEdit(); - } - else { + // @PaulL: ugly nested timeout. Without this, this same scroll event ends the editing before it gets started + // Issue #2896 raised to fix this situation + $timeout(function() { + $timeout(function() { + beginEdit(); + }); + }); + } else { endEdit(); } }); From e8711a358489ab0efc86fc638d2d937f3a4a595a Mon Sep 17 00:00:00 2001 From: Paul Lambert Date: Thu, 12 Mar 2015 15:56:26 +1300 Subject: [PATCH 012/173] Enh(tooltip): provide the ability for a cell to have tooltips --- misc/tutorial/117_tooltips.ngdoc | 85 +++++++++++++++++++++++++++ src/js/core/constants.js | 1 + src/js/core/factories/Grid.js | 8 ++- src/js/core/factories/GridColumn.js | 22 +++++++ src/templates/ui-grid/uiGridCell.html | 2 +- 5 files changed, 116 insertions(+), 2 deletions(-) create mode 100644 misc/tutorial/117_tooltips.ngdoc diff --git a/misc/tutorial/117_tooltips.ngdoc b/misc/tutorial/117_tooltips.ngdoc new file mode 100644 index 0000000000..b6f430b7c2 --- /dev/null +++ b/misc/tutorial/117_tooltips.ngdoc @@ -0,0 +1,85 @@ +@ngdoc overview +@name Tutorial: 117 Tooltips +@description + +You can set a tooltip (actually, a title) to pop up when a user hovers over a cell. + +This tooltip can be simply the cell contents, in which case set the columnDef to have +`cellTooltip: true`. Or it can be a function that returns a value derived from the +current column and row - for example: +``` + cellTooltip: function(row, col) { + return 'Name: ' + row.entity.name + ' Company: ' + row.entity.company; + } +``` + +Note that turning on tooltips will create an extra watcher per cell, so it has an impact on overall grid +performance, it is not recommended to turn them on for every column, rather only for the columns likely to have +data that won't be displayable within the grid row (e.g. long description fields). + + + + var app = angular.module('app', ['ngAnimate', 'ngTouch', 'ui.grid']); + + app.controller('MainCtrl', ['$scope', '$http', 'uiGridConstants', function ($scope, $http, uiGridConstants) { + $scope.gridOptions = { + enableSorting: true, + columnDefs: [ + { field: 'name', cellTooltip: true }, + { field: 'company', cellTooltip: + function( row, col ) { + return 'Name: ' + row.entity.name + ' Company: ' + row.entity.company; + } + } + ], + onRegisterApi: function( gridApi ) { + $scope.gridApi = gridApi; + $scope.gridApi.core.on.sortChanged( $scope, function( grid, sort ) { + $scope.gridApi.core.notifyDataChange( uiGridConstants.dataChange.COLUMN ); + }) + } + }; + + $http.get('/data/100.json') + .success(function(data) { + $scope.gridOptions.data = data; + }); + }]); + + +
    +
    +
    +
    +
    +
    + + .grid { + width: 500px; + height: 200px; + } + .red { color: red; background-color: yellow !important; } + .blue { color: blue; } + + + var gridTestUtils = require('../../test/e2e/gridTestUtils.spec.js'); + describe( '115 header cell class', function() { + it('grid should have two visible columns', function () { + gridTestUtils.expectHeaderColumnCount( 'grid1', 2 ); + }); + + it('cell classes', function () { + // blue for header 0 + expect( gridTestUtils.headerCell( 'grid1', 0 ).getCssValue('color')).toEqual('rgba(0, 0, 255, 1)'); + + // header 2 starts with no coloring, but colors when sort is ASC + expect( gridTestUtils.headerCell( 'grid1', 1 ).getCssValue('color')).toEqual('rgba(44, 62, 80, 1)', 'normal foreground'); + + gridTestUtils.clickHeaderCell( 'grid1', 1 ); + expect( gridTestUtils.headerCell( 'grid1', 1 ).getCssValue('color')).toEqual('rgba(255, 0, 0, 1)', 'red highlight'); + + }); + }); + +
    + diff --git a/src/js/core/constants.js b/src/js/core/constants.js index 8905d33898..33a8ed8418 100644 --- a/src/js/core/constants.js +++ b/src/js/core/constants.js @@ -7,6 +7,7 @@ CUSTOM_FILTERS: /CUSTOM_FILTERS/g, COL_FIELD: /COL_FIELD/g, MODEL_COL_FIELD: /MODEL_COL_FIELD/g, + TOOLTIP: /title=\"TOOLTIP\"/g, DISPLAY_CELL_TEMPLATE: /DISPLAY_CELL_TEMPLATE/g, TEMPLATE_REGEXP: /<.+>/, FUNC_REGEXP: /(\([^)]*\))?$/, diff --git a/src/js/core/factories/Grid.js b/src/js/core/factories/Grid.js index 7df193d977..c412c8f4bc 100644 --- a/src/js/core/factories/Grid.js +++ b/src/js/core/factories/Grid.js @@ -726,7 +726,13 @@ angular.module('ui.grid') this.columns.forEach(function (col) { var html = col.cellTemplate.replace(uiGridConstants.MODEL_COL_FIELD, self.getQualifiedColField(col)); html = html.replace(uiGridConstants.COL_FIELD, 'grid.getCellValue(row, col)'); - + + if (col.cellTooltip === false){ + html = html.replace(uiGridConstants.TOOLTIP, ''); + } else { + // gridColumn will have made sure that the col either has false or a function for this value + html = html.replace(uiGridConstants.TOOLTIP, 'title="{{col.cellTooltip(row, col)}}"'); + } var compiledElementFn = $compile(html); col.compiledElementFn = compiledElementFn; diff --git a/src/js/core/factories/GridColumn.js b/src/js/core/factories/GridColumn.js index c2b46a57a1..b176fe33b4 100644 --- a/src/js/core/factories/GridColumn.js +++ b/src/js/core/factories/GridColumn.js @@ -381,6 +381,28 @@ angular.module('ui.grid') self.aggregationType = angular.isDefined(colDef.aggregationType) ? colDef.aggregationType : null; self.footerCellTemplate = angular.isDefined(colDef.footerCellTemplate) ? colDef.footerCellTemplate : null; + /** + * @ngdoc property + * @name tooltip + * @propertyOf ui.grid.class:GridOptions.columnDef + * @description Whether or not to show a tooltip when a user hovers over the cell. + * If set to false, no tooltip. If true, the cell value is shown in the tooltip (useful + * if you have long values in your cells), if a function then that function is called + * passing in the row and the col `cellTooltip( row, col )`, and the return value is shown in the tooltip. + * + * Defaults to false + * + */ + if ( typeof(colDef.cellTooltip) === 'undefined' || colDef.cellTooltip === false ) { + self.cellTooltip = false; + } else if ( colDef.cellTooltip === true ){ + self.cellTooltip = function(row, col) { + return self.grid.getCellValue( row, col ); + }; + } else { + self.cellTooltip = colDef.cellTooltip; + } + /** * @ngdoc property * @name footerCellClass diff --git a/src/templates/ui-grid/uiGridCell.html b/src/templates/ui-grid/uiGridCell.html index 25f5609c28..406c7deddc 100644 --- a/src/templates/ui-grid/uiGridCell.html +++ b/src/templates/ui-grid/uiGridCell.html @@ -1 +1 @@ -
    {{COL_FIELD CUSTOM_FILTERS}}
    \ No newline at end of file +
    {{COL_FIELD CUSTOM_FILTERS}}
    \ No newline at end of file From 23fec1743548c4a9cd87eedcdd81bb161ebcc130 Mon Sep 17 00:00:00 2001 From: Shane Walters Date: Thu, 12 Mar 2015 13:41:18 -0500 Subject: [PATCH 013/173] perf(core): optimize grid.getCellValue --- src/js/core/factories/Grid.js | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/src/js/core/factories/Grid.js b/src/js/core/factories/Grid.js index c412c8f4bc..15db6d1d7c 100644 --- a/src/js/core/factories/Grid.js +++ b/src/js/core/factories/Grid.js @@ -1703,13 +1703,11 @@ angular.module('ui.grid') * @param {GridColumn} col Column to access */ Grid.prototype.getCellValue = function getCellValue(row, col){ - var self = this; - - if (!self.cellValueGetterCache[col.colDef.name]) { - self.cellValueGetterCache[col.colDef.name] = $parse(row.getEntityQualifiedColField(col)); + if (!col.cellValueGetterCache) { + col.cellValueGetterCache = $parse(row.getEntityQualifiedColField(col)); } - return self.cellValueGetterCache[col.colDef.name](row); + return col.cellValueGetterCache(row); }; From abdb84b4afad1f7da6060a8697f107cf46d8cc55 Mon Sep 17 00:00:00 2001 From: Takayuki Tanaka Date: Fri, 13 Mar 2015 17:10:28 +0900 Subject: [PATCH 014/173] Fix editDropdownFilter change CUSTOM_FILSTERS position to enable filters and add a test for editDropdownFilter --- .../edit/templates/dropdownEditor.html | 2 +- .../test/uiGridCellWithDropdownFilter.spec.js | 84 +++++++++++++++++++ 2 files changed, 85 insertions(+), 1 deletion(-) create mode 100644 src/features/edit/test/uiGridCellWithDropdownFilter.spec.js diff --git a/src/features/edit/templates/dropdownEditor.html b/src/features/edit/templates/dropdownEditor.html index d7d6745d73..60ffe1dfff 100644 --- a/src/features/edit/templates/dropdownEditor.html +++ b/src/features/edit/templates/dropdownEditor.html @@ -1,5 +1,5 @@
    - +
    diff --git a/src/features/edit/test/uiGridCellWithDropdownFilter.spec.js b/src/features/edit/test/uiGridCellWithDropdownFilter.spec.js new file mode 100644 index 0000000000..6aa4e50653 --- /dev/null +++ b/src/features/edit/test/uiGridCellWithDropdownFilter.spec.js @@ -0,0 +1,84 @@ +describe('ui.grid.edit GridCellDirective - with dropdown filter', function () { + var gridUtil; + var scope; + var element; + var uiGridConstants; + var recompile; + var $timeout; + + beforeEach(module('ui.grid.edit')); + + beforeEach(inject(function ($rootScope, $compile, $controller, _gridUtil_, $templateCache, gridClassFactory, + uiGridEditService, _uiGridConstants_, _$timeout_) { + gridUtil = _gridUtil_; + uiGridConstants = _uiGridConstants_; + $timeout = _$timeout_; + + $templateCache.put('ui-grid/uiGridCell', '
    {{COL_FIELD CUSTOM_FILTERS}}
    '); + $templateCache.put('ui-grid/dropdownEditor', '
    '); + + scope = $rootScope.$new(); + var grid = gridClassFactory.createGrid(); + grid.options.columnDefs = [ + { + name: 'col1', + enableCellEdit: true, + editableCellTemplate: 'ui-grid/dropdownEditor', + editDropdownOptionsArray: [ + {id: 1, value: 'fred'}, + {id: 2, value: 'john'}, + {id: 3, value: 'ken'} + ], + editDropdownFilter: 'filter:{ id: 3 }:true' + } + ]; + grid.options.data = [ + {col1: 1} + ]; + uiGridEditService.initializeGrid(grid); + grid.buildColumns(); + grid.modifyRows(grid.options.data); + + scope.grid = grid; + scope.col = grid.columns[0]; + scope.row = grid.rows[0]; + + scope.getCellValue = function(row,col){return 'val';}; + + $timeout(function(){ + recompile = function () { + $compile(element)(scope); + $rootScope.$digest(); + }; + }); + $timeout.flush(); + + })); + + describe('ui.grid.edit uiGridCell and uiGridEditor with editDropdownFilter', function () { + var displayHtml; + beforeEach(function () { + element = angular.element('
    '); + recompile(); + + displayHtml = element.html(); + expect(element.text()).toBe('1'); + //invoke edit + element.dblclick(); + expect(element.find('select')).toBeDefined(); + + }); + + it('should have filtered options', function () { + + var options = element.find('select option'); + + // options should be 0 and ken + expect(options.length).toBe(2); + expect(options[1].text).toBe('ken'); + + }); + + }); + +}); From 1646bed18eadba681dbb721ffb5fbd58ddcf08c8 Mon Sep 17 00:00:00 2001 From: Paul Lambert Date: Fri, 13 Mar 2015 21:43:49 +1300 Subject: [PATCH 015/173] Fix(saveState): save focus where only row is focused --- src/features/saveState/js/saveState.js | 8 ++++++-- src/features/saveState/test/saveState.spec.js | 20 +++++++++++++++++++ 2 files changed, 26 insertions(+), 2 deletions(-) diff --git a/src/features/saveState/js/saveState.js b/src/features/saveState/js/saveState.js index 2aaf688a3b..bb588551c8 100644 --- a/src/features/saveState/js/saveState.js +++ b/src/features/saveState/js/saveState.js @@ -332,8 +332,12 @@ scrollFocus.focus = true; var rowCol = grid.api.cellNav.getFocusedCell(); if ( rowCol !== null ) { - scrollFocus.colName = rowCol.col.colDef.name; - scrollFocus.rowVal = service.getRowVal( grid, rowCol.row ); + if ( rowCol.col !== null ){ + scrollFocus.colName = rowCol.col.colDef.name; + } + if ( rowCol.row !== null ){ + scrollFocus.rowVal = service.getRowVal( grid, rowCol.row ); + } } } else if ( grid.options.saveScroll ) { scrollFocus.focus = false; diff --git a/src/features/saveState/test/saveState.spec.js b/src/features/saveState/test/saveState.spec.js index 9277f09ac1..abacd571df 100644 --- a/src/features/saveState/test/saveState.spec.js +++ b/src/features/saveState/test/saveState.spec.js @@ -135,6 +135,26 @@ describe('ui.grid.saveState uiGridSaveStateService', function () { expect( uiGridSaveStateService.saveScrollFocus( grid ) ).toEqual( { focus: true, colName: 'col4', rowVal: { identity: false, row: 1 } } ); }); + it('save focus, focus present, no col', function() { + uiGridCellNavService.initializeGrid(grid); + + spyOn( grid.api.cellNav, 'getFocusedCell' ).andCallFake( function() { + return { row: grid.rows[2], col: null }; + }); + + expect( uiGridSaveStateService.saveScrollFocus( grid ) ).toEqual( { focus: true, rowVal: { identity: false, row: 1 } } ); + }); + + it('save focus, focus present, no row', function() { + uiGridCellNavService.initializeGrid(grid); + + spyOn( grid.api.cellNav, 'getFocusedCell' ).andCallFake( function() { + return { row: null, col: grid.columns[3] }; + }); + + expect( uiGridSaveStateService.saveScrollFocus( grid ) ).toEqual( { focus: true, colName: 'col4' } ); + }); + it('save focus, focus present, row identity present', function() { uiGridCellNavService.initializeGrid(grid); From 19c0c41500407f6e79429057f85031b6caea9710 Mon Sep 17 00:00:00 2001 From: Paul Lambert Date: Fri, 13 Mar 2015 21:44:11 +1300 Subject: [PATCH 016/173] Doco(FAQ): fix #2995 document browser versions supported --- misc/tutorial/499_FAQ.ngdoc | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/misc/tutorial/499_FAQ.ngdoc b/misc/tutorial/499_FAQ.ngdoc index fdc3690dfa..5c9a8f4500 100644 --- a/misc/tutorial/499_FAQ.ngdoc +++ b/misc/tutorial/499_FAQ.ngdoc @@ -42,4 +42,14 @@ There are a number of common gotchas in using the grid, this FAQ aims to cover m ``` cellTemplate: '
    {{grid.renderContainers.body.visibleRowCache.indexOf(row)}}
    ' -``` \ No newline at end of file +``` + +### What browsers are supported by ui.grid + + Our current testing verifies against IE9+, Chrome, Firefox, Safari 5+, Opera and Android. We expect that the functionality + is compatible with any HTML5 compliant and Javascript enabled browser. + +## What angular versions are supported by ui.grid + + Our current testing uses 1.2.8, 1.2.14, 1.2.26, 1.3.0 and 1.3.6. We intend to remain compatible with all forward versions of 1.3. + \ No newline at end of file From 8047917993b7088f1b6551e555c9de85a72a9837 Mon Sep 17 00:00:00 2001 From: Takayuki Tanaka Date: Fri, 13 Mar 2015 19:12:09 +0900 Subject: [PATCH 017/173] Fix editDropdown test Change key of templateCache to use dropdownEditor in the test file. --- src/features/edit/test/uiGridCellWithDropdown.spec.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/features/edit/test/uiGridCellWithDropdown.spec.js b/src/features/edit/test/uiGridCellWithDropdown.spec.js index 77a9505f34..264783060e 100644 --- a/src/features/edit/test/uiGridCellWithDropdown.spec.js +++ b/src/features/edit/test/uiGridCellWithDropdown.spec.js @@ -15,7 +15,7 @@ describe('ui.grid.edit GridCellDirective - with dropdown', function () { $timeout = _$timeout_; $templateCache.put('ui-grid/uiGridCell', '
    {{COL_FIELD CUSTOM_FILTERS}}
    '); - $templateCache.put('ui-grid/cellEditor', '
    '); + $templateCache.put('ui-grid/dropdownEditor', '
    '); scope = $rootScope.$new(); var grid = gridClassFactory.createGrid(); From 84ddb09200324bd5e22652a386294d354b12bc73 Mon Sep 17 00:00:00 2001 From: Rachid Date: Fri, 13 Mar 2015 10:07:58 +0100 Subject: [PATCH 018/173] Incorrect handle offset when moving column on Safari On Safari, the handle to move columns has an offset, which increases the more you have horizontally scrolled (so not really visible when the table has not many columns) rework conditional to save a call to getBoundingClientRect() --- src/features/move-columns/js/column-movable.js | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/features/move-columns/js/column-movable.js b/src/features/move-columns/js/column-movable.js index 7b2b59f364..bd58abe879 100644 --- a/src/features/move-columns/js/column-movable.js +++ b/src/features/move-columns/js/column-movable.js @@ -276,7 +276,15 @@ //Left of cloned element should be aligned to original header cell. movingElm.addClass('movingColumn'); var movingElementStyles = {}; - var elmLeft = $elm[0].getBoundingClientRect().left; + var elmLeft; + if (gridUtil.detectBrowser() === 'safari') { + //Correction for Safari getBoundingClientRect, + //which does not correctly compute when there is an horizontal scroll + elmLeft = $elm[0].offsetLeft + $elm[0].offsetWidth - $elm[0].getBoundingClientRect().width; + } + else { + elmLeft = $elm[0].getBoundingClientRect().left; + } movingElementStyles.left = (elmLeft - gridLeft) + 'px'; var gridRight = $scope.grid.element[0].getBoundingClientRect().right; var elmRight = $elm[0].getBoundingClientRect().right; From 40cd53bd26e14b35fb8097012c5bde3fe1d73fff Mon Sep 17 00:00:00 2001 From: Paul Lambert Date: Sat, 14 Mar 2015 07:26:38 +1300 Subject: [PATCH 019/173] Doco(modal): fix #3008 add example to modal tutorial --- misc/tutorial/110_grid_in_modal.ngdoc | 22 +++++++++++++--------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/misc/tutorial/110_grid_in_modal.ngdoc b/misc/tutorial/110_grid_in_modal.ngdoc index b06a0983e5..cfde0ca5dd 100644 --- a/misc/tutorial/110_grid_in_modal.ngdoc +++ b/misc/tutorial/110_grid_in_modal.ngdoc @@ -5,15 +5,19 @@ Using a grid in a modal popup. In some cases, and in particular with the bootstrap modal, you may find that your grid renders smaller than the -available width. This is because the bootstrap modal animates the initial render and the grid renders whilst the -modal is still animating - the available space isn't as expected. You can correct this by calling `handleWindowResize`. +available width (or sometimes appears to render not at all. This is believed to be because the bootstrap modal +animates the initial render and the grid renders whilst the modal is still animating - the available space isn't +as expected. You can correct this by calling `handleWindowResize`. The animation time seems to be somewhat variable, +so the currently recommended approach is to use $interval, and to call every 500ms for the first 5s after modal opening. + +In a sense this is similar to what the auto-resize feature does, but it only does it for a short period after modal opening. @example var app = angular.module('app', ['ngTouch', 'ui.grid']); - app.controller('MainCtrl', ['$rootScope', '$scope', '$http', 'modal', '$timeout', function ($rootScope, $scope, $http, modal, $timeout) { + app.controller('MainCtrl', ['$rootScope', '$scope', '$http', 'modal', '$interval', function ($rootScope, $scope, $http, modal, $interval) { var myModal = new modal(); $scope.hideGrid = true; @@ -21,7 +25,12 @@ modal is still animating - the available space isn't as expected. You can corre $rootScope.gridOptions = { onRegisterApi: function (gridApi) { $scope.gridApi = gridApi; - } + + // call resize every 200 ms for 2 s after modal finishes opening - usually only necessary on a bootstrap modal + $interval( function() { + $scope.gridApi.core.handleWindowResize(); + }, 10, 500); + } }; $http.get('/data/100.json') @@ -31,11 +40,6 @@ modal is still animating - the available space isn't as expected. You can corre $scope.showModal = function() { myModal.open(); - - // call resize after modal finishes opening - usually only necessary on a bootstrap modal - $timeout( function() { - $scope.gridApi.core.handleWindowResize(); - }); }; }]); From 3daa522fd716aa9f229c565e9f18dac12bb52bf8 Mon Sep 17 00:00:00 2001 From: Paul Lambert Date: Sun, 15 Mar 2015 08:08:42 +1300 Subject: [PATCH 020/173] Fix(perf): Fix #2574, add flatEntityAccess option, 70% reduction in sort time --- .../404_large_data_sets_and_performance.ngdoc | 81 +++++++++++++++++++ src/js/core/factories/Grid.js | 12 ++- src/js/core/factories/GridOptions.js | 14 ++++ test/unit/core/factories/GridOptions.spec.js | 5 ++ 4 files changed, 108 insertions(+), 4 deletions(-) create mode 100644 misc/tutorial/404_large_data_sets_and_performance.ngdoc diff --git a/misc/tutorial/404_large_data_sets_and_performance.ngdoc b/misc/tutorial/404_large_data_sets_and_performance.ngdoc new file mode 100644 index 0000000000..8a357d9fcf --- /dev/null +++ b/misc/tutorial/404_large_data_sets_and_performance.ngdoc @@ -0,0 +1,81 @@ +@ngdoc overview +@name Tutorial: 404 Large Data Sets and Performance +@description + +The grid provides a richly featured component that allows very large data sets to be displayed without overloading +the browser. It virtualises the rows and columns actually displayed, what this means is that it provides the illusion +of displaying many rows and columns, whilst actually showing the browser only the visible cells plus one or two columns +and rows either side of those currently visible. The remainder of the grid canvas is white space, faked so as to have the +browser correctly show scroll bars that position you in the middle of this space. + +In turn, this means that each time anything changes, in particular scrolling, the grid needs to determine whether it needs +to render additional rows or columns. Rather than adding new rows or columns to the DOM (which is computationally expensive) +it instead shuffles which data elements the current rows and columns point to, and then adjusts the scroll to provide the illusion +that you are smoothly scrolling the grid. If it weren't for the fact that the values inside each of the visible cells were being +changed rapidly, what you'd actually see is the same DOM cells scrolling to the left, then jumping back to the right as another column +comes into view, then scrolling smoothly to the left again. + +As you might expect, this can be quite computationally expensive. Additionally, some operations such as sorting and filtering, must +work against the entire data set, not just those currently visible. + +A different area of complexity in the grid is the provision of complex binding methods (refer {@link 106_binding the binding tutorial}). +These binding methods add noticable overhead to every access to a cell value. The combination of large data sets and these +binding methods can result in noticeable performance degradation. + +All of this is a rather long winded way of saying that if we turn off those complex binding methods, many parts of the grid run +faster. + +So, if you have a data array that consists purely of flat objects - and each column in your grid is tied to a single field in the +entities in that array (i.e. the way we think 90% of people use the grid), then you can turn off the support for complex binding, +and the grid will run faster. + +The option for this is `flatEntityAccess`. The below grid provides a toggle on this value so you can see the difference it makes +with a data set of 640,000 rows. + +@example + + + var app = angular.module('app', ['ngAnimate', 'ngTouch', 'ui.grid']); + + app.controller('MainCtrl', ['$scope', '$http', 'uiGridConstants', function ($scope, $http, uiGridConstants) { + $scope.gridOptions = { + enableFiltering: true, + flatEntityAccess: true, + showGridFooter: true + }; + + $scope.gridOptions.columnDefs = [ + {name:'id'}, + {name:'name'}, + {name:'gender'}, + {field:'age'} + ]; + + $http.get('https://cdn.rawgit.com/angular-ui/ui-grid.info/gh-pages/data/10000_complex.json') + .success(function(data) { + for( var i=0; i<7; i++){ + data = data.concat(data); + } + $scope.gridOptions.data = data; + }); + + $scope.toggleFlat = function() { + $scope.gridOptions.flatEntityAccess = !$scope.gridOptions.flatEntityAccess; + } + }]); + + +
    + Current setting: {{gridOptions.flatEntityAccess}} +
    +
    +
    + + .grid { + width: 650px; + height: 400px; + } + + + +
    \ No newline at end of file diff --git a/src/js/core/factories/Grid.js b/src/js/core/factories/Grid.js index 15db6d1d7c..2f826322b9 100644 --- a/src/js/core/factories/Grid.js +++ b/src/js/core/factories/Grid.js @@ -1703,11 +1703,15 @@ angular.module('ui.grid') * @param {GridColumn} col Column to access */ Grid.prototype.getCellValue = function getCellValue(row, col){ - if (!col.cellValueGetterCache) { - col.cellValueGetterCache = $parse(row.getEntityQualifiedColField(col)); + if (this.options.flatEntityAccess && col.field){ + return row.entity[col.field]; + } else { + if (!col.cellValueGetterCache) { + col.cellValueGetterCache = $parse(row.getEntityQualifiedColField(col)); + } + + return col.cellValueGetterCache(row); } - - return col.cellValueGetterCache(row); }; diff --git a/src/js/core/factories/GridOptions.js b/src/js/core/factories/GridOptions.js index 4a983166f7..00c0414ebb 100644 --- a/src/js/core/factories/GridOptions.js +++ b/src/js/core/factories/GridOptions.js @@ -155,6 +155,20 @@ angular.module('ui.grid') return row.$$hashKey; }; + /** + * @ngdoc function + * @name flatEntityAccess + * @methodOf ui.grid.class:GridOptions + * @description Set to true if your columns are all related directly to fields in a flat object structure - i.e. + * each of your columns associate directly with a propery one each of the entities in your data array. + * + * In that situation we can avoid all the logic associated with complex binding to functions or to properties of sub-objects, + * which can provide a significant speed improvement with large data sets, with filtering and with sorting. + * + * By default false + */ + baseOptions.flatEntityAccess = baseOptions.flatEntityAccess === true; + /** * @ngdoc property * @name showHeader diff --git a/test/unit/core/factories/GridOptions.spec.js b/test/unit/core/factories/GridOptions.spec.js index 2fb711b562..5a71784e0b 100644 --- a/test/unit/core/factories/GridOptions.spec.js +++ b/test/unit/core/factories/GridOptions.spec.js @@ -19,6 +19,7 @@ describe('GridOptions factory', function () { enableRowHashing: true, rowIdentity: jasmine.any(Function), getRowIdentity: jasmine.any(Function), + flatEntityAccess: false, headerRowHeight: 30, rowHeight: 30, minRowsToShow: 10, @@ -60,6 +61,7 @@ describe('GridOptions factory', function () { enableRowHashing: true, rowIdentity: testFunction, getRowIdentity: testFunction, + flatEntityAccess: true, headerRowHeight: 40, rowHeight: 40, minRowsToShow: 15, @@ -98,6 +100,7 @@ describe('GridOptions factory', function () { enableRowHashing: true, rowIdentity: testFunction, getRowIdentity: testFunction, + flatEntityAccess: true, headerRowHeight: 40, rowHeight: 40, minRowsToShow: 15, @@ -140,6 +143,7 @@ describe('GridOptions factory', function () { enableRowHashing: false, rowIdentity: testFunction, getRowIdentity: testFunction, + flatEntityAccess: false, headerRowHeight: 40, rowHeight: 40, minRowsToShow: 15, @@ -177,6 +181,7 @@ describe('GridOptions factory', function () { enableRowHashing: false, rowIdentity: testFunction, getRowIdentity: testFunction, + flatEntityAccess: false, headerRowHeight: 0, // Because of showHeader: false rowHeight: 40, minRowsToShow: 15, From 4c1eefa470e5dd16f437b4c7f65b33d7cc53ccfe Mon Sep 17 00:00:00 2001 From: Paul Lambert Date: Sun, 15 Mar 2015 08:19:11 +1300 Subject: [PATCH 021/173] Fix(perf): Fix #2574 native forEach, function declared separately --- .../404_large_data_sets_and_performance.ngdoc | 2 +- src/js/core/services/rowSorter.js | 21 ++++++++++++------- 2 files changed, 14 insertions(+), 9 deletions(-) diff --git a/misc/tutorial/404_large_data_sets_and_performance.ngdoc b/misc/tutorial/404_large_data_sets_and_performance.ngdoc index 8a357d9fcf..3cce520a62 100644 --- a/misc/tutorial/404_large_data_sets_and_performance.ngdoc +++ b/misc/tutorial/404_large_data_sets_and_performance.ngdoc @@ -53,7 +53,7 @@ with a data set of 640,000 rows. $http.get('https://cdn.rawgit.com/angular-ui/ui-grid.info/gh-pages/data/10000_complex.json') .success(function(data) { - for( var i=0; i<7; i++){ + for( var i=0; i<6; i++){ data = data.concat(data); } $scope.gridOptions.data = data; diff --git a/src/js/core/services/rowSorter.js b/src/js/core/services/rowSorter.js index 966d24095a..1693694f5f 100644 --- a/src/js/core/services/rowSorter.js +++ b/src/js/core/services/rowSorter.js @@ -412,12 +412,13 @@ module.service('rowSorter', ['$parse', 'uiGridConstants', function ($parse, uiGr var r = rows.slice(0); // put a custom index field on each row, used to make a stable sort out of unstable sorts (e.g. Chrome) - angular.forEach( rows, function ( row, idx ) { + var setIndex = function( row, idx ){ row.entity.$uiGridIndex = idx; - }); + }; + rows.forEach(setIndex); // Now actually sort the data - var newRows = rows.sort(function rowSortFn(rowA, rowB) { + var rowSortFn = function (rowA, rowB) { var tem = 0, idx = 0, sortFn; @@ -438,7 +439,8 @@ module.service('rowSorter', ['$parse', 'uiGridConstants', function ($parse, uiGr } // Chrome doesn't implement a stable sort function. If our sort returns 0 - // (i.e. the items are equal), then return the previous order using our custom + // (i.e. the items are equal), and we're at the last sort column in the list, + // then return the previous order using our custom // index variable if (tem === 0 ) { return rowA.entity.$uiGridIndex - rowB.entity.$uiGridIndex; @@ -450,12 +452,15 @@ module.service('rowSorter', ['$parse', 'uiGridConstants', function ($parse, uiGr } else { return 0 - tem; } - }); + }; + + var newRows = rows.sort(rowSortFn); // remove the custom index field on each row, used to make a stable sort out of unstable sorts (e.g. Chrome) - angular.forEach( newRows, function ( row, idx ) { - delete row.entity.$uiGridIndex; - }); + var clearIndex = function( row, idx ){ + delete row.entity.$uiGridIndex; + }; + rows.forEach(setIndex); return newRows; }; From ab8da8e6ca84688589041d37db10da04e79d5db1 Mon Sep 17 00:00:00 2001 From: Paul Lambert Date: Sun, 15 Mar 2015 15:06:49 +1300 Subject: [PATCH 022/173] Refactor(modifyRows): simplify and standardise modifyRows code --- src/js/core/factories/Grid.js | 242 +++++++------------------- test/unit/core/factories/Grid.spec.js | 97 ++++++----- 2 files changed, 117 insertions(+), 222 deletions(-) diff --git a/src/js/core/factories/Grid.js b/src/js/core/factories/Grid.js index 2f826322b9..82a4da1b50 100644 --- a/src/js/core/factories/Grid.js +++ b/src/js/core/factories/Grid.js @@ -888,185 +888,85 @@ angular.module('ui.grid') return t; }; - /** - * @ngdoc function - * @name getRow - * @methodOf ui.grid.class:Grid - * @description returns the GridRow that contains the rowEntity - * @param {object} rowEntity the gridOptions.data array element instance - */ - Grid.prototype.getRow = function getRow(rowEntity) { - var self = this; - var rows = this.rows.filter(function (row) { - return self.options.rowEquality(row.entity, rowEntity); - }); - return rows.length > 0 ? rows[0] : null; - }; + /** + * @ngdoc function + * @name getRow + * @methodOf ui.grid.class:Grid + * @description returns the GridRow that contains the rowEntity + * @param {object} rowEntity the gridOptions.data array element instance + * @param {array} rows [optional] the rows to look in - if not provided then + * looks in grid.rows + */ + Grid.prototype.getRow = function getRow(rowEntity, lookInRows) { + var self = this; + + lookInRows = typeof(lookInRows) === 'undefined' ? this.rows : lookInRows; + + var rows = this.lookInRows.filter(function (row) { + return self.options.rowEquality(row.entity, rowEntity); + }); + return rows.length > 0 ? rows[0] : null; + }; - /** + /** * @ngdoc function * @name modifyRows * @methodOf ui.grid.class:Grid * @description creates or removes GridRow objects from the newRawData array. Calls each registered * rowBuilder to further process the row * - * Rows are identified using the gridOptions.rowEquality function + * This method aims to achieve three things: + * 1. the resulting rows array is in the same order as the newRawData, we'll call + * rowsProcessors immediately after to sort the data anyway + * 2. if we have row hashing available, we try to use the rowHash to find the row + * 3. no memory leaks - rows that are no longer in newRawData need to be garbage collected + * + * The basic logic flow makes use of the newRawData, oldRows and oldHash, and creates + * the newRows and newHash + * + * ``` + * newRawData.forEach newEntity + * if (hashing enabled) + * check oldHash for newEntity + * else + * look for old row directly in oldRows + * if !oldRowFound // must be a new row + * create newRow + * append to the newRows and add to newHash + * run the processors + * + * Rows are identified using the hashKey if configured. If not configured, then rows + * are identified using the gridOptions.rowEquality function */ Grid.prototype.modifyRows = function modifyRows(newRawData) { - var self = this, - i, - rowhash, - found, - newRow; - if ((self.options.useExternalSorting || self.getColumnSorting().length === 0) && newRawData.length > 0) { - var oldRowHash = self.rowHashMap; - if (!oldRowHash) { - oldRowHash = {get: function(){return null;}}; - } - self.createRowHashMap(); - rowhash = self.rowHashMap; - var wasEmpty = self.rows.length === 0; - self.rows.length = 0; - for (i = 0; i < newRawData.length; i++) { - var newRawRow = newRawData[i]; - found = oldRowHash.get(newRawRow); - if (found) { - newRow = found.row; - } - else { - newRow = self.processRowBuilders(new GridRow(newRawRow, i, self)); - } - self.rows.push(newRow); - rowhash.put(newRawRow, { - i: i, - entity: newRawRow, - row:newRow - }); - } - //now that we have data, it is save to assign types to colDefs -// if (wasEmpty) { - self.assignTypes(); -// } - } else { - if (self.rows.length === 0 && newRawData.length > 0) { - if (self.options.enableRowHashing) { - if (!self.rowHashMap) { - self.createRowHashMap(); - } - - for (i = 0; i < newRawData.length; i++) { - newRow = newRawData[i]; - - self.rowHashMap.put(newRow, { - i: i, - entity: newRow - }); - } - } - - self.addRows(newRawData); - //now that we have data, it is save to assign types to colDefs - self.assignTypes(); - } - else if (newRawData.length > 0) { - var unfoundNewRows, unfoundOldRows, unfoundNewRowsToFind; - - // If row hashing is turned on - if (self.options.enableRowHashing) { - // Array of new rows that haven't been found in the old rowset - unfoundNewRows = []; - // Array of new rows that we explicitly HAVE to search for manually in the old row set. They cannot be looked up by their identity (because it doesn't exist). - unfoundNewRowsToFind = []; - // Map of rows that have been found in the new rowset - var foundOldRows = {}; - // Array of old rows that have NOT been found in the new rowset - unfoundOldRows = []; - - // Create the row HashMap if it doesn't exist already - if (!self.rowHashMap) { - self.createRowHashMap(); - } - rowhash = self.rowHashMap; - - // Make sure every new row has a hash - for (i = 0; i < newRawData.length; i++) { - newRow = newRawData[i]; - - // Flag this row as needing to be manually found if it didn't come in with a $$hashKey - var mustFind = false; - if (!self.options.getRowIdentity(newRow)) { - mustFind = true; - } - - // See if the new row is already in the rowhash - found = rowhash.get(newRow); - // If so... - if (found) { - // See if it's already being used by as GridRow - if (found.row) { - // If so, mark this new row as being found - foundOldRows[self.options.rowIdentity(newRow)] = true; - } - } - else { - // Put the row in the hashmap with the index it corresponds to - rowhash.put(newRow, { - i: i, - entity: newRow - }); - - // This row has to be searched for manually in the old row set - if (mustFind) { - unfoundNewRowsToFind.push(newRow); - } - else { - unfoundNewRows.push(newRow); - } - } - } - - // Build the list of unfound old rows - for (i = 0; i < self.rows.length; i++) { - var row = self.rows[i]; - var hash = self.options.rowIdentity(row.entity); - if (!foundOldRows[hash]) { - unfoundOldRows.push(row); - } - } + var self = this; + var oldRows = self.rows.slice(0); + var oldRowHash = self.rowHashMap || self.createRowHashMap(); + self.rowHashMap = self.createRowHashMap(); + self.rows.length = 0; + + newRawData.forEach( function( newEntity, i ) { + var newRow; + if ( self.options.enableRowHashing ){ + // if hashing is enabled, then this row will be in the hash if we already know about it + newRow = oldRowHash.get( newEntity ); + } else { + // otherwise, manually search the oldRows to see if we can find this row + newRow = self.getRow(newEntity, oldRows); } - // Look for new rows - var newRows = unfoundNewRows || []; - - // The unfound new rows is either `unfoundNewRowsToFind`, if row hashing is turned on, or straight `newRawData` if it isn't - var unfoundNew = (unfoundNewRowsToFind || newRawData); - - // Search for real new rows in `unfoundNew` and concat them onto `newRows` - newRows = newRows.concat(self.newInN(self.rows, unfoundNew, 'entity')); - - self.addRows(newRows); - - var deletedRows = self.getDeletedRows((unfoundOldRows || self.rows), newRawData); - - for (i = 0; i < deletedRows.length; i++) { - if (self.options.enableRowHashing) { - self.rowHashMap.remove(deletedRows[i].entity); - } - - self.rows.splice( self.rows.indexOf(deletedRows[i]), 1 ); + // if we didn't find the row, it must be new, so create it + if ( !newRow ){ + newRow = self.processRowBuilders(new GridRow(newEntity, i, self)); } - } - // Empty data set - else { - // Reset the row HashMap - self.createRowHashMap(); - // Reset the rows length! - self.rows.length = 0; - } - } + self.rows.push( newRow ); + self.rowHashMap.put( newEntity, newRow ); + }); + self.assignTypes(); + var p1 = $q.when(self.processRowsProcessors(self.rows)) .then(function (renderableRows) { return self.setVisibleRows(renderableRows); @@ -1080,18 +980,6 @@ angular.module('ui.grid') return $q.all([p1, p2]); }; - Grid.prototype.getDeletedRows = function(oldRows, newRows) { - var self = this; - - var olds = oldRows.filter(function (oldRow) { - return !newRows.some(function (newItem) { - return self.options.rowEquality(newItem, oldRow.entity); - }); - }); - // var olds = self.newInN(newRows, oldRows, null, 'entity'); - // dump('olds', olds); - return olds; - }; /** * Private Undocumented Method @@ -1850,7 +1738,7 @@ angular.module('ui.grid') var hashMap = new RowHashMap(); hashMap.grid = self; - self.rowHashMap = hashMap; + return hashMap; }; diff --git a/test/unit/core/factories/Grid.spec.js b/test/unit/core/factories/Grid.spec.js index 38f78a6ef4..788f49fd3d 100644 --- a/test/unit/core/factories/Grid.spec.js +++ b/test/unit/core/factories/Grid.spec.js @@ -449,61 +449,68 @@ describe('Grid factory', function () { }); it('should swap', function() { - var dataRows = [{str:'abc'},{str:'cba'}]; - var grid = new Grid({ id: 1 }); + var dataRows = [{str:'abc'},{str:'cba'}]; + var grid = new Grid({ id: 1 }); - grid.modifyRows(dataRows); + grid.modifyRows(dataRows); - expect(grid.rows[0].entity.str).toBe('abc'); - expect(grid.rows[1].entity.str).toBe('cba'); + expect(grid.rows[0].entity.str).toBe('abc'); + expect(grid.rows[1].entity.str).toBe('cba'); - var tmpRow = dataRows[0]; - dataRows[0] = dataRows[1]; - dataRows[1] = tmpRow; - grid.modifyRows(dataRows); - - expect(grid.rows[0].entity.str).toBe('cba'); - expect(grid.rows[1].entity.str).toBe('abc'); - }); + var tmpRow = dataRows[0]; + dataRows[0] = dataRows[1]; + dataRows[1] = tmpRow; + grid.modifyRows(dataRows); + + expect(grid.rows[0].entity.str).toBe('cba'); + expect(grid.rows[1].entity.str).toBe('abc'); + }); + it('should delete and insert new in the middle', function() { - var dataRows = [{str:'abc'},{str:'cba'},{str:'bac'}]; - var grid = new Grid({ id: 1 }); + var dataRows = [{str:'abc'},{str:'cba'},{str:'bac'}]; + var grid = new Grid({ id: 1 }); - grid.modifyRows(dataRows); + grid.modifyRows(dataRows); - expect(grid.rows.length).toBe(3); - expect(grid.rows[0].entity.str).toBe('abc'); - expect(grid.rows[1].entity.str).toBe('cba'); - expect(grid.rows[2].entity.str).toBe('bac'); + expect(grid.rows.length).toBe(3); + expect(grid.rows[0].entity.str).toBe('abc'); + expect(grid.rows[1].entity.str).toBe('cba'); + expect(grid.rows[2].entity.str).toBe('bac'); - dataRows[1] = {str:'xyz'}; - grid.modifyRows(dataRows); - - expect(grid.rows.length).toBe(3); - expect(grid.rows[0].entity.str).toBe('abc'); - expect(grid.rows[1].entity.str).toBe('xyz'); - expect(grid.rows[2].entity.str).toBe('bac'); - }); + dataRows[1] = {str:'xyz'}; + grid.modifyRows(dataRows); + + expect(grid.rows.length).toBe(3); + expect(grid.rows[0].entity.str).toBe('abc'); + expect(grid.rows[1].entity.str).toBe('xyz'); + expect(grid.rows[2].entity.str).toBe('bac'); + }); + + /* + * No longer trying to keep order of sort - we run rowsProcessors + * immediately after anyway, which will resort. + * it('should keep the order of the sort', function() { - var dataRows = [{str:'abc'},{str:'cba'},{str:'bac'}]; - var grid = new Grid({ id: 1 }); - grid.options.columnDefs = [{name:'1',type:'string'}]; - grid.buildColumns(); - grid.modifyRows(dataRows); + var dataRows = [{str:'abc'},{str:'cba'},{str:'bac'}]; + var grid = new Grid({ id: 1 }); + grid.options.columnDefs = [{name:'1',type:'string'}]; + grid.buildColumns(); + grid.modifyRows(dataRows); - expect(grid.rows.length).toBe(3); - expect(grid.rows[0].entity.str).toBe('abc'); - expect(grid.rows[1].entity.str).toBe('cba'); - expect(grid.rows[2].entity.str).toBe('bac'); + expect(grid.rows.length).toBe(3); + expect(grid.rows[0].entity.str).toBe('abc'); + expect(grid.rows[1].entity.str).toBe('cba'); + expect(grid.rows[2].entity.str).toBe('bac'); - grid.sortColumn(grid.columns[0]); - - dataRows.splice(0,0,{str:'xyz'}); - grid.modifyRows(dataRows); - expect(grid.rows.length).toBe(4); - expect(grid.rows[0].entity.str).toBe('abc'); - expect(grid.rows[3].entity.str).toBe('xyz'); - }); + grid.sortColumn(grid.columns[0]); + + dataRows.splice(0,0,{str:'xyz'}); + grid.modifyRows(dataRows); + expect(grid.rows.length).toBe(4); + expect(grid.rows[0].entity.str).toBe('abc'); + expect(grid.rows[3].entity.str).toBe('xyz'); + }); + */ }); describe('binding', function() { From 554c71a817db716f340cbab86fd8273b02e58c5a Mon Sep 17 00:00:00 2001 From: Paul Lambert Date: Sun, 15 Mar 2015 16:07:46 +1300 Subject: [PATCH 023/173] Chore(stds): Fix #3009 replace angular.forEach with forEach --- misc/tutorial/105_footer.ngdoc | 2 +- .../tutorial/312_exporting_data_complex.ngdoc | 2 +- misc/tutorial/499_FAQ.ngdoc | 2 +- src/features/expandable/js/expandable.js | 6 ++-- src/features/exporter/js/exporter.js | 12 ++++---- src/features/grouping/js/grouping.js | 28 +++++++++--------- src/features/importer/js/importer.js | 29 +++++++++++-------- src/features/row-edit/js/gridRowEdit.js | 10 +++++-- src/features/saveState/js/saveState.js | 6 ++-- test/unit/core/directives/ui-grid.spec.js | 4 +-- test/unit/core/factories/Grid.spec.js | 2 +- test/unit/core/services/ui-grid-util.spec.js | 2 +- 12 files changed, 57 insertions(+), 48 deletions(-) diff --git a/misc/tutorial/105_footer.ngdoc b/misc/tutorial/105_footer.ngdoc index cdfcc33035..2d86c62170 100644 --- a/misc/tutorial/105_footer.ngdoc +++ b/misc/tutorial/105_footer.ngdoc @@ -48,7 +48,7 @@ You can override the default grid footer template with gridOptions.footerTemplat $http.get('/data/500_complex.json') .success(function(data) { - angular.forEach(data, function(row) { + data.forEach( function(row) { row.registered = Date.parse(row.registered); }); $scope.gridOptions.data = data; diff --git a/misc/tutorial/312_exporting_data_complex.ngdoc b/misc/tutorial/312_exporting_data_complex.ngdoc index c21328ae12..0f027336a0 100644 --- a/misc/tutorial/312_exporting_data_complex.ngdoc +++ b/misc/tutorial/312_exporting_data_complex.ngdoc @@ -70,7 +70,7 @@ We also right align the gender column. $http.get('/data/100.json') .success(function(data) { - angular.forEach( data, function( row, index ) { + data.forEach( function( row, index ) { if( row.gender === 'female' ){ row.gender = 1; } else { diff --git a/misc/tutorial/499_FAQ.ngdoc b/misc/tutorial/499_FAQ.ngdoc index 5c9a8f4500..c52abd09e9 100644 --- a/misc/tutorial/499_FAQ.ngdoc +++ b/misc/tutorial/499_FAQ.ngdoc @@ -27,7 +27,7 @@ There are a number of common gotchas in using the grid, this FAQ aims to cover m If the latter, then you can do it by just adding a counter column to your data: ``` - angular.forEach($scope.myData, function( row, index){ + $scope.myData.forEach( function( row, index){ row.sequence = index; }); ``` diff --git a/src/features/expandable/js/expandable.js b/src/features/expandable/js/expandable.js index c80f53d053..32e89d8a92 100644 --- a/src/features/expandable/js/expandable.js +++ b/src/features/expandable/js/expandable.js @@ -196,7 +196,7 @@ }, expandAllRows: function(grid, $scope) { - angular.forEach(grid.renderContainers.body.visibleRowCache, function(row) { + grid.renderContainers.body.visibleRowCache.forEach( function(row) { if (!row.isExpanded) { service.toggleRowExpansion(grid, row); } @@ -206,7 +206,7 @@ }, collapseAllRows: function(grid) { - angular.forEach(grid.renderContainers.body.visibleRowCache, function(row) { + grid.renderContainers.body.visibleRowCache.forEach( function(row) { if (row.isExpanded) { service.toggleRowExpansion(grid, row); } @@ -395,7 +395,7 @@ function updateRowContainerWidth() { var grid = $scope.grid; var colWidth = 0; - angular.forEach(grid.columns, function (column) { + grid.columns.forEach( function (column) { if (column.renderContainer === 'left') { colWidth += column.width; } diff --git a/src/features/exporter/js/exporter.js b/src/features/exporter/js/exporter.js index 1e75a4f8b2..da535b79be 100644 --- a/src/features/exporter/js/exporter.js +++ b/src/features/exporter/js/exporter.js @@ -573,7 +573,7 @@ */ getColumnHeaders: function (grid, colTypes) { var headers = []; - angular.forEach(grid.columns, function( gridCol, index ) { + grid.columns.forEach( function( gridCol, index ) { if ( (gridCol.visible || colTypes === uiGridExporterConstants.ALL ) && gridCol.colDef.exporterSuppressExport !== true && grid.options.exporterSuppressColumns.indexOf( gridCol.name ) === -1 ){ @@ -650,11 +650,11 @@ break; } - angular.forEach(rows, function( row, index ) { + rows.forEach( function( row, index ) { if (row.exporterEnableExporting !== false) { var extractedRow = []; - angular.forEach(grid.columns, function( gridCol, index ) { + grid.columns.forEach( function( gridCol, index ) { if ( (gridCol.visible || colTypes === uiGridExporterConstants.ALL ) && gridCol.colDef.exporterSuppressExport !== true && grid.options.exporterSuppressColumns.indexOf( gridCol.name ) === -1 ){ @@ -938,19 +938,19 @@ * for any column that is a % * * @param {Grid} grid the grid from which data should be exported - * @param {object} exportHeaders array of header information + * @param {array} exportHeaders array of header information * @returns {object} an array of header widths */ calculatePdfHeaderWidths: function ( grid, exportHeaders ) { var baseGridWidth = 0; - angular.forEach(exportHeaders, function(value){ + exportHeaders.forEach( function(value){ if (typeof(value.width) === 'number'){ baseGridWidth += value.width; } }); var extraColumns = 0; - angular.forEach(exportHeaders, function(value){ + exportHeaders.forEach( function(value){ if (value.width === '*'){ extraColumns += 100; } diff --git a/src/features/grouping/js/grouping.js b/src/features/grouping/js/grouping.js index 607693b212..18115d1d2e 100644 --- a/src/features/grouping/js/grouping.js +++ b/src/features/grouping/js/grouping.js @@ -495,7 +495,7 @@ */ groupingColumnProcessor: function( columns, rows ) { var grid = this; - angular.forEach(columns, function(column, index){ + columns.forEach( function(column, index){ // position used to make stable sort in moveGroupColumns column.groupingPosition = index; @@ -541,7 +541,7 @@ // optimisation - this can be done in the groupingColumnProcessor since we were already // iterating. But commented out and left here to make the code a little more understandable // - // angular.forEach(columns, function(column, index) { + // columns.forEach( function(column, index) { // column.groupingPosition = index; // }); @@ -567,7 +567,7 @@ return a.groupingPosition - b.groupingPosition; }); - angular.forEach(columns, function(column, index) { + columns.forEach( function(column, index) { delete column.groupingPosition; }); @@ -679,7 +679,7 @@ var groupArray = []; var sortArray = []; - angular.forEach(grid.columns, function(column, index){ + grid.columns.forEach( function(column, index){ if ( typeof(column.grouping) !== 'undefined' && typeof(column.grouping.groupPriority) !== 'undefined' && column.grouping.groupPriority >= 0){ groupArray.push(column); } else if ( typeof(column.sort) !== 'undefined' && typeof(column.sort.priority) !== 'undefined' && column.sort.priority >= 0){ @@ -688,7 +688,7 @@ }); groupArray.sort(function(a, b){ return a.grouping.groupPriority - b.grouping.groupPriority; }); - angular.forEach(groupArray, function(column, index){ + groupArray.forEach( function(column, index){ column.grouping.groupPriority = index; column.suppressRemoveSort = true; if ( typeof(column.sort) === 'undefined'){ @@ -699,7 +699,7 @@ var i = groupArray.length; sortArray.sort(function(a, b){ return a.sort.priority - b.sort.priority; }); - angular.forEach(sortArray, function(column, index){ + sortArray.forEach( function(column, index){ column.sort.priority = i; column.suppressRemoveSort = column.colDef.suppressRemoveSort; i++; @@ -752,7 +752,7 @@ expandedStatesSubset.state = targetState; // set all child nodes - angular.forEach(expandedStatesSubset, function( childNode, key){ + expandedStatesSubset.forEach( function( childNode, key){ if (key !== 'state'){ service.setAllNodes(childNode, targetState); } @@ -944,7 +944,7 @@ for (var i = 0; i < renderableRows.length; i++ ){ var row = renderableRows[i]; - angular.forEach(groupingProcessingState, updateProcessingState); + groupingProcessingState.forEach( updateProcessingState); service.setVisibility( grid, row, groupingProcessingState ); } @@ -972,7 +972,7 @@ var processingState = []; var columnSettings = service.getGrouping( grid ); - angular.forEach(columnSettings.grouping, function( groupItem, index){ + columnSettings.grouping.forEach( function( groupItem, index){ // get the aggregation config to copy in - do this multiple times as shallow copying it // was harder than it looked, and as much work as just creating it again var aggregations = []; @@ -980,7 +980,7 @@ aggregations.push({type: uiGridGroupingConstants.aggregation.COUNT, fieldName: uiGridGroupingConstants.aggregation.FIELD, value: null }); } else { } - angular.forEach(columnSettings.aggregations, function(aggregation, index){ + columnSettings.aggregations.forEach( function(aggregation, index){ if (aggregation.aggregation === uiGridGroupingConstants.aggregation.AVG){ aggregations.push({ type: aggregation.aggregation, fieldName: aggregation.field, col: aggregation.col, value: null, sum: null, count: null }); @@ -1017,7 +1017,7 @@ var aggregateArray = []; // get all the grouping - angular.forEach(grid.columns, function(column, columnIndex){ + grid.columns.forEach( function(column, columnIndex){ if ( column.grouping ){ if ( typeof(column.grouping.groupPriority) !== 'undefined' && column.grouping.groupPriority >= 0){ groupArray.push({ field: column.field, col: column, groupPriority: column.grouping.groupPriority, grouping: column.grouping }); @@ -1033,7 +1033,7 @@ }); // renumber the priority in case it was somewhat messed up, then remove the grouping reference - angular.forEach( groupArray, function( group, index) { + groupArray.forEach( function( group, index) { group.grouping.groupPriority = index; group.groupPriority = index; delete group.grouping; @@ -1121,7 +1121,7 @@ */ writeOutAggregation: function( grid, processingState ) { if ( processingState.currentGroupHeader ){ - angular.forEach(processingState.runningAggregations, function( aggregation, index ){ + processingState.runningAggregations.forEach( function( aggregation, index ){ if (aggregation.fieldName === uiGridGroupingConstants.aggregation.FIELD){ // running total to include in the groupHeader processingState.currentGroupHeader.entity[processingState.fieldName] = processingState.currentValue + ' (' + aggregation.value + ')'; @@ -1222,7 +1222,7 @@ */ aggregate: function( grid, row, groupFieldState ){ // TODO: check data types, cast as necessary, all that jazz - angular.forEach( groupFieldState.runningAggregations, function( aggregation, index ){ + groupFieldState.runningAggregations.forEach( function( aggregation, index ){ if (aggregation.type === uiGridGroupingConstants.aggregation.COUNT){ // don't need getCellValue for counting, and column isn't present sometimes aggregation.value++; diff --git a/src/features/importer/js/importer.js b/src/features/importer/js/importer.js index 972de40f8c..c622a32a0d 100644 --- a/src/features/importer/js/importer.js +++ b/src/features/importer/js/importer.js @@ -384,7 +384,11 @@ var newObjects = []; var newObject; - angular.forEach( service.parseJson( grid, importFile ), function( value, index ) { + var importArray = service.parseJson( grid, importFile ); + if (importArray === null){ + return; + } + importArray.forEach( function( value, index ) { newObject = service.newObject( grid ); angular.extend( newObject, value ); newObject = grid.options.importerObjectCallback( grid, newObject ); @@ -488,8 +492,7 @@ * the columns in the column defs. The resulting objects will have attributes * that are named based on the column.field or column.name, in that order. * @param {Grid} grid the grid that we want to import into - * @param {FileObject} importFile the file that we want to import, as a - * file object + * @param {Array} importArray the data that we want to import, as an array */ createCsvObjects: function( grid, importArray ){ // pull off header row and turn into headers @@ -501,13 +504,15 @@ var newObjects = []; var newObject; - angular.forEach( importArray, function( row, index ) { + importArray.forEach( function( row, index ) { newObject = service.newObject( grid ); - angular.forEach( row, function( field, index ){ - if ( headerMapping[index] !== null ){ - newObject[ headerMapping[index] ] = field; - } - }); + if ( row !== null ){ + row.forEach( function( field, index ){ + if ( headerMapping[index] !== null ){ + newObject[ headerMapping[index] ] = field; + } + }); + } newObject = grid.options.importerObjectCallback( grid, newObject ); newObjects.push( newObject ); }); @@ -534,13 +539,13 @@ if ( !grid.options.columnDefs || grid.options.columnDefs.length === 0 ){ // we are going to create new columnDefs for all these columns, so just remove // spaces from the names to create fields - angular.forEach( headerRow, function( value, index ) { + headerRow.forEach( function( value, index ) { headers.push( value.replace( /[^0-9a-zA-Z\-_]/g, '_' ) ); }); return headers; } else { var lookupHash = service.flattenColumnDefs( grid, grid.options.columnDefs ); - angular.forEach( headerRow, function( value, index ) { + headerRow.forEach( function( value, index ) { if ( lookupHash[value] ) { headers.push( lookupHash[value] ); } else if ( lookupHash[ value.toLowerCase() ] ) { @@ -569,7 +574,7 @@ */ flattenColumnDefs: function( grid, columnDefs ){ var flattenedHash = {}; - angular.forEach( columnDefs, function( columnDef, index) { + columnDefs.forEach( function( columnDef, index) { if ( columnDef.name ){ flattenedHash[ columnDef.name ] = columnDef.field || columnDef.name; flattenedHash[ columnDef.name.toLowerCase() ] = columnDef.field || columnDef.name; diff --git a/src/features/row-edit/js/gridRowEdit.js b/src/features/row-edit/js/gridRowEdit.js index 99bd861362..4d7e5df4c7 100644 --- a/src/features/row-edit/js/gridRowEdit.js +++ b/src/features/row-edit/js/gridRowEdit.js @@ -314,7 +314,11 @@ * @param {GridRow} gridRow the row that should be removed */ removeRow: function( rowArray, removeGridRow ){ - angular.forEach( rowArray, function( gridRow, index ){ + if (typeof(rowArray) === 'undefined' || rowArray === null){ + return; + } + + rowArray.forEach( function( gridRow, index ){ if ( gridRow.uid === removeGridRow.uid ){ rowArray.splice( index, 1); } @@ -333,7 +337,7 @@ */ isRowPresent: function( rowArray, removeGridRow ){ var present = false; - angular.forEach( rowArray, function( gridRow, index ){ + rowArray.forEach( function( gridRow, index ){ if ( gridRow.uid === removeGridRow.uid ){ present = true; } @@ -359,7 +363,7 @@ */ flushDirtyRows: function(grid){ var promises = []; - angular.forEach(grid.rowEdit.dirtyRows, function( gridRow ){ + grid.rowEdit.dirtyRows.forEach( function( gridRow ){ service.saveRow( grid, gridRow )(); promises.push( gridRow.rowEditSavePromise ); }); diff --git a/src/features/saveState/js/saveState.js b/src/features/saveState/js/saveState.js index bb588551c8..fbcc839c5a 100644 --- a/src/features/saveState/js/saveState.js +++ b/src/features/saveState/js/saveState.js @@ -287,7 +287,7 @@ */ saveColumns: function( grid ) { var columns = []; - angular.forEach( grid.columns, function( column ) { + grid.columns.forEach( function( column ) { var savedColumn = {}; savedColumn.name = column.name; savedColumn.visible = column.visible; @@ -414,7 +414,7 @@ * @param {object} columnsState the list of columns we had before, with their state */ restoreColumns: function( grid, columnsState ){ - angular.forEach( columnsState, function( columnState, index ) { + columnsState.forEach( function( columnState, index ) { var currentCol = grid.columns.filter( function( column ) { return column.name === columnState.name; }); @@ -513,7 +513,7 @@ grid.api.selection.clearSelectedRows(); - angular.forEach( selectionState, function( rowVal ) { + selectionState.forEach( function( rowVal ) { if ( rowVal.identity ){ var foundRow = service.findRowByIdentity( grid, rowVal ); diff --git a/test/unit/core/directives/ui-grid.spec.js b/test/unit/core/directives/ui-grid.spec.js index 9f59a7bfeb..573bd0589c 100644 --- a/test/unit/core/directives/ui-grid.spec.js +++ b/test/unit/core/directives/ui-grid.spec.js @@ -102,7 +102,7 @@ describe('ui-grid', function() { afterEach(function() { element.remove(); - angular.forEach(columnDefs, function (c) { + columnDefs.forEach( function (c) { delete c.width; }); }); @@ -114,7 +114,7 @@ describe('ui-grid', function() { it('should distribute extra width', function () { var renderWidth = 0; - angular.forEach(gridApi.grid.columns, function (c) { + gridApi.grid.columns.forEach( function (c) { renderWidth += c.drawnWidth; }); diff --git a/test/unit/core/factories/Grid.spec.js b/test/unit/core/factories/Grid.spec.js index 38f78a6ef4..5eebf1c7ee 100644 --- a/test/unit/core/factories/Grid.spec.js +++ b/test/unit/core/factories/Grid.spec.js @@ -753,7 +753,7 @@ describe('Grid factory', function () { it( 'register then deregister data change callback', function() { var countCallbacks = function(){ var i = 0; - angular.forEach(grid.dataChangeCallbacks, function(callback){ + angular.forEach(grid.dataChangeCallbacks, function(callback, key){ i++; }); return i; diff --git a/test/unit/core/services/ui-grid-util.spec.js b/test/unit/core/services/ui-grid-util.spec.js index f14e43e899..2f3cdf5086 100644 --- a/test/unit/core/services/ui-grid-util.spec.js +++ b/test/unit/core/services/ui-grid-util.spec.js @@ -102,7 +102,7 @@ describe('ui.grid.utilService', function() { ['address.city', 'Address.City'] ]; - angular.forEach(translationExpects, function (set) { + translationExpects.forEach( function (set) { var strIn = set[0]; var strOut = set[1]; From 729f257b93c4f8bb449902eaa59346ca9089e545 Mon Sep 17 00:00:00 2001 From: Paul Lambert Date: Sun, 15 Mar 2015 16:12:26 +1300 Subject: [PATCH 024/173] Fix(e2e): 117 tooltip had a failing e2e test --- misc/tutorial/117_tooltips.ngdoc | 17 ----------------- 1 file changed, 17 deletions(-) diff --git a/misc/tutorial/117_tooltips.ngdoc b/misc/tutorial/117_tooltips.ngdoc index b6f430b7c2..bc900e8d5a 100644 --- a/misc/tutorial/117_tooltips.ngdoc +++ b/misc/tutorial/117_tooltips.ngdoc @@ -63,23 +63,6 @@ data that won't be displayable within the grid row (e.g. long description fields
    var gridTestUtils = require('../../test/e2e/gridTestUtils.spec.js'); - describe( '115 header cell class', function() { - it('grid should have two visible columns', function () { - gridTestUtils.expectHeaderColumnCount( 'grid1', 2 ); - }); - - it('cell classes', function () { - // blue for header 0 - expect( gridTestUtils.headerCell( 'grid1', 0 ).getCssValue('color')).toEqual('rgba(0, 0, 255, 1)'); - - // header 2 starts with no coloring, but colors when sort is ASC - expect( gridTestUtils.headerCell( 'grid1', 1 ).getCssValue('color')).toEqual('rgba(44, 62, 80, 1)', 'normal foreground'); - - gridTestUtils.clickHeaderCell( 'grid1', 1 ); - expect( gridTestUtils.headerCell( 'grid1', 1 ).getCssValue('color')).toEqual('rgba(255, 0, 0, 1)', 'red highlight'); - - }); - });
    From 5b6bcf5a29a81f2eb805d15bc09dc195e9b898d9 Mon Sep 17 00:00:00 2001 From: Paul Lambert Date: Sun, 15 Mar 2015 16:30:42 +1300 Subject: [PATCH 025/173] Fix(grouping): fix #2989 dynamic width for grouping column --- src/features/grouping/js/grouping.js | 20 +++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/src/features/grouping/js/grouping.js b/src/features/grouping/js/grouping.js index 18115d1d2e..16fd4cc0a3 100644 --- a/src/features/grouping/js/grouping.js +++ b/src/features/grouping/js/grouping.js @@ -289,18 +289,18 @@ * @ngdoc object * @name groupingRowHeaderWidth * @propertyOf ui.grid.grouping.api:GridOptions - * @description Width of the grouping header, if your nested grouping is too - * deep you may need to increase this - *
    Defaults to 40 + * @description Base width of the grouping header, provides for a single level of grouping. This + * is incremented by `groupingIndent` for each extra level + *
    Defaults to 30 */ - gridOptions.groupingRowHeaderWidth = gridOptions.groupingRowHeaderWidth || 40; + gridOptions.groupingRowHeaderBaseWidth = gridOptions.groupingRowHeaderBaseWidth || 30; /** * @ngdoc object * @name groupingIndent * @propertyOf ui.grid.grouping.api:GridOptions * @description Number of pixels of indent for the icon at each grouping level, wider indents are visually more pleasing, - * but may result in you having to make the group row header wider + * but will make the group row header wider *
    Defaults to 10 */ gridOptions.groupingIndent = gridOptions.groupingIndent || 10; @@ -499,17 +499,23 @@ // position used to make stable sort in moveGroupColumns column.groupingPosition = index; - // find groupingRowHeader and decide whether to make it visible + // find groupingRowHeader if (column.name === uiGridGroupingConstants.groupingRowHeaderColName) { + var groupingConfig = service.getGrouping(column.grid); + // decide whether to make it visible if (typeof(grid.options.groupingRowHeaderAlwaysVisible) === 'undefined' || grid.options.groupingRowHeaderAlwaysVisible === false) { - var groupingConfig = service.getGrouping(column.grid); if (groupingConfig.grouping.length > 0){ column.visible = true; } else { column.visible = false; } } + // set the width based on the depth of grouping + var indent = ( groupingConfig.grouping.length - 1 ) * grid.options.groupingIndent; + indent = indent > 0 ? indent : 0; + column.width = grid.options.groupingRowHeaderBaseWidth + indent; } + }); columns = service.moveGroupColumns(this, columns, rows); From be5c0ba1ccef1d185dfcfac394cbb2318bb65bee Mon Sep 17 00:00:00 2001 From: Paul Lambert Date: Sun, 15 Mar 2015 16:32:56 +1300 Subject: [PATCH 026/173] Fix(exporter): fix #2921 IE detection for higher versions - untested --- src/features/exporter/js/exporter.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/features/exporter/js/exporter.js b/src/features/exporter/js/exporter.js index da535b79be..16cfee8d75 100644 --- a/src/features/exporter/js/exporter.js +++ b/src/features/exporter/js/exporter.js @@ -749,8 +749,8 @@ * @description Checks whether current browser is IE and returns it's version if it is */ isIE: function () { - var myNav = navigator.userAgent.toLowerCase(); - return (myNav.indexOf('msie') !== -1) ? parseInt(myNav.split('msie')[1]) : false; + var match = navigator.userAgent.match(/(?:MSIE |Trident\/.*; rv:)(\d+)/); + return match ? parseInt(match[1]) : false; }, From accf72c94c67ee4b9e7ba0f6c320c3787a02bd71 Mon Sep 17 00:00:00 2001 From: Paul Lambert Date: Sun, 15 Mar 2015 16:41:47 +1300 Subject: [PATCH 027/173] Fix(cellNav): fix #2962 take footer height into account in scrollToIfNecessary --- misc/tutorial/202_cellnav.ngdoc | 6 ++++-- src/features/cellnav/js/cellnav.js | 4 ++-- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/misc/tutorial/202_cellnav.ngdoc b/misc/tutorial/202_cellnav.ngdoc index e26b8dbf74..acac65a9cf 100644 --- a/misc/tutorial/202_cellnav.ngdoc +++ b/misc/tutorial/202_cellnav.ngdoc @@ -26,8 +26,10 @@ extract values of selected cells. var app = angular.module('app', ['ngTouch', 'ui.grid', 'ui.grid.cellNav', 'ui.grid.pinning']); app.controller('MainCtrl', ['$scope', '$http', '$log', function ($scope, $http, $log) { - $scope.gridOptions = {}; - $scope.gridOptions.modifierKeysToMultiSelectCells = true; + $scope.gridOptions = { + modifierKeysToMultiSelectCells: true, + showGridFooter: true + }; $scope.gridOptions.columnDefs = [ { name: 'id', width:'150' }, { name: 'name', width:'200' }, diff --git a/src/features/cellnav/js/cellnav.js b/src/features/cellnav/js/cellnav.js index b6bbe16cee..8a74da1064 100644 --- a/src/features/cellnav/js/cellnav.js +++ b/src/features/cellnav/js/cellnav.js @@ -614,9 +614,9 @@ // The left boundary is the current X scroll position var leftBound = grid.renderContainers.body.prevScrollLeft; - // The bottom boundary is the current Y scroll position, plus the height of the grid, but minus the header height. + // The bottom boundary is the current Y scroll position, plus the height of the grid, but minus the header height and minus the footerHeight. // Basically this is the viewport height added on to the scroll position - var bottomBound = grid.renderContainers.body.prevScrollTop + grid.gridHeight - grid.headerHeight; + var bottomBound = grid.renderContainers.body.prevScrollTop + grid.gridHeight - grid.headerHeight - grid.footerHeight; // If there's a horizontal scrollbar, remove its height from the bottom boundary, otherwise we'll be letting it obscure rows //if (grid.horizontalScrollbarHeight) { From 8efcd621985c6565da04a85b4c87593b9c9a32b4 Mon Sep 17 00:00:00 2001 From: Paul Lambert Date: Sun, 15 Mar 2015 17:22:32 +1300 Subject: [PATCH 028/173] Doco(design): draft rendering design doco --- misc/api/design-rendering-cycle.ngdoc | 60 +++++++++++++++++++++++++++ 1 file changed, 60 insertions(+) create mode 100644 misc/api/design-rendering-cycle.ngdoc diff --git a/misc/api/design-rendering-cycle.ngdoc b/misc/api/design-rendering-cycle.ngdoc new file mode 100644 index 0000000000..c780ac475f --- /dev/null +++ b/misc/api/design-rendering-cycle.ngdoc @@ -0,0 +1,60 @@ +@ngdoc overview +@name Rendering Cycle +@module +@description + +The core grid rendering cycle is executed whenever the grid needs re-rendering. There are a number of core methods, +with some of those methods able to be called individually. + +### Current +The key method is `grid.refresh`. This method updates the rows and columns in the grid, then redraws and resizes the grid. + + - grid.refresh + - rowsProcessors + - setVisibleRows + - columnsProcessors + - setVisibleColumns + - redrawInPlace + - refreshCanvas + +By preference grid.refresh is called through a debounce function - grid.queueGridRefresh. If you use this method you are +telling the grid that you want a refresh, but you're allowing the grid to consolidate all refreshes from the current digest cycle +and process just once. + +A similar method, `grid.refreshRows` also exists, this is the same as grid.refresh except that it doesn't run `columnsProcessors` +or `setVisibleColumns`. + +The rows and columns processors are focused on ordering and determining the visibility of columns and rows. They include functions +such as sorting and filtering (impacting order and visibility of rows), grouping rows (which adds extra rows, and changes the ordering +and widths of columns), and pinning, which changes which render container particular columns are in. + +`redrawInPlace` determines the correct scroll percentage in the grid, and therefore which of the rows and columns should currently +be visible in the viewport. + +`refreshCanvas` is a complicated method that determines the sizing of each of the grid elements. In some cases it is currently iterative, +for example it determines header height by rendering each of the column headers, and determining the maximum rendered height. This largely +appears to be to accomodate filters. + + - refreshCanvas + - buildStyles + - $timeout - calcHeaders (this is inline - should it be a style computation? It isn't a promise, and doesn't wait on the buildStyles + promise, but it does run in a timeout. Conversely, it creates a promise that it resolves - but it doesn't wait for the header calc to + complete before resolving the promise) + - may call buildStyles again if it decides headerHeight has changed + + +### Vision +The vision is to make the style calculations more deterministic, and remove any iteration or other dependencies. A single pass through +refreshCanvas should return a correctly sized grid. + +To achieve this, we really need to: + +- calculate all sizing in code, without reference to the sizing of rendered grid elements. We already do most of this for rows + and columns, the main gap seems to be grid header +- we could reference rendered size to determine the grid's available size (if we want to), which could allow the grid to be more responsive +- layout all the columns first - what render container they're in etc, before we start any sizing +- calculate the column widths and element heights +- calculate the overall grid sizing based on all the elements (headerHeight, footerHeight, container widths etc) +- render + +Thinking only at this stage!! From 0c8410ce5c2e0f65e1808393318db6463550adcb Mon Sep 17 00:00:00 2001 From: Paul Lambert Date: Sun, 15 Mar 2015 17:25:10 +1300 Subject: [PATCH 029/173] Fix(modifyRows): stupid error --- src/js/core/factories/Grid.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/js/core/factories/Grid.js b/src/js/core/factories/Grid.js index 82a4da1b50..e95e83e402 100644 --- a/src/js/core/factories/Grid.js +++ b/src/js/core/factories/Grid.js @@ -900,9 +900,9 @@ angular.module('ui.grid') Grid.prototype.getRow = function getRow(rowEntity, lookInRows) { var self = this; - lookInRows = typeof(lookInRows) === 'undefined' ? this.rows : lookInRows; + lookInRows = typeof(lookInRows) === 'undefined' ? self.rows : lookInRows; - var rows = this.lookInRows.filter(function (row) { + var rows = lookInRows.filter(function (row) { return self.options.rowEquality(row.entity, rowEntity); }); return rows.length > 0 ? rows[0] : null; From ba304b298f7de451d97ff12d71751fac4649e693 Mon Sep 17 00:00:00 2001 From: Paul Lambert Date: Mon, 16 Mar 2015 15:43:20 +1300 Subject: [PATCH 030/173] Enh(filtering): Fix #2067 allow enabling filtering dynamically --- misc/api/design-rendering-cycle.ngdoc | 17 +++++- misc/tutorial/102_sorting.ngdoc | 2 +- misc/tutorial/103_filtering.ngdoc | 9 +++ src/js/core/directives/ui-grid-header-cell.js | 55 +++++++++---------- src/js/core/factories/GridColumn.js | 2 +- 5 files changed, 53 insertions(+), 32 deletions(-) diff --git a/misc/api/design-rendering-cycle.ngdoc b/misc/api/design-rendering-cycle.ngdoc index c780ac475f..25a5cc14d4 100644 --- a/misc/api/design-rendering-cycle.ngdoc +++ b/misc/api/design-rendering-cycle.ngdoc @@ -42,6 +42,18 @@ appears to be to accomodate filters. complete before resolving the promise) - may call buildStyles again if it decides headerHeight has changed +The style builders include: + +- `GridRenderContainer`, which appears to currently apply no styles +- `uiGrid.updateColumnWidths`, which calculates column widths based on the defined settings, including resolving * and ** etc. No rendering + is involved - all based on the availableWidth. This may be the source of some of the iteration - because availableWidth must in some way be + based on columnWidth - the canvas doesn't really have an available width. I also have question on why we calculate widths on the grid + and not on the renderContainer, that may be another source of iteration. Having said that, things like % and * probably apply to the + full grid width, not to just a container, and we wouldn't expect a column to change width when it changed container (e.g. when we pinned it) +- `uiGridRenderContainer.update()`, which is called for each renderContainer. It determines the +width of each column in the render container, and the width of the overall render container. +- `Grid.prototype.getFooterStyles()`, sets the columnFooterHeight and the gridFooterHeight based on fixed values declared in the options +- when there are multiple renderContainers (e.g. a left container), the non-body render containers appear to execute first ### Vision The vision is to make the style calculations more deterministic, and remove any iteration or other dependencies. A single pass through @@ -51,9 +63,10 @@ To achieve this, we really need to: - calculate all sizing in code, without reference to the sizing of rendered grid elements. We already do most of this for rows and columns, the main gap seems to be grid header -- we could reference rendered size to determine the grid's available size (if we want to), which could allow the grid to be more responsive -- layout all the columns first - what render container they're in etc, before we start any sizing +- we could reference rendered size to determine the grid's available size (if we want to), which could allow the grid to be more + responsive. Probably we already do this. - calculate the column widths and element heights +- layout all the columns - what render container they're in etc, then size the render containers - calculate the overall grid sizing based on all the elements (headerHeight, footerHeight, container widths etc) - render diff --git a/misc/tutorial/102_sorting.ngdoc b/misc/tutorial/102_sorting.ngdoc index 0884a113e3..bbee4d046a 100644 --- a/misc/tutorial/102_sorting.ngdoc +++ b/misc/tutorial/102_sorting.ngdoc @@ -115,7 +115,7 @@ for that column. This will let the user change the direction of the sort, but t

    - +

    diff --git a/misc/tutorial/103_filtering.ngdoc b/misc/tutorial/103_filtering.ngdoc index 6cba3f5afb..d51410c70f 100644 --- a/misc/tutorial/103_filtering.ngdoc +++ b/misc/tutorial/103_filtering.ngdoc @@ -56,6 +56,9 @@ for `filters: [{ term: 'xxx' }]`. See the "age" column below for an example. app.controller('MainCtrl', ['$scope', '$http', 'uiGridConstants', function ($scope, $http, uiGridConstants) { $scope.gridOptions = { enableFiltering: true, + onRegisterApi: function(gridApi){ + $scope.gridApi = gridApi; + }, columnDefs: [ // default { field: 'name' }, @@ -106,6 +109,11 @@ for `filters: [{ term: 'xxx' }]`. See the "age" column below for an example. $scope.gridOptions.data = data; $scope.gridOptions.data[0].age = -5; }); + + $scope.toggleFiltering = function(){ + $scope.gridOptions.enableFiltering = !$scope.gridOptions.enableFiltering; + $scope.gridApi.core.notifyDataChange( uiGridConstants.dataChange.COLUMN ); + }; }]); @@ -116,6 +124,7 @@ for `filters: [{ term: 'xxx' }]`. See the "age" column below for an example. Note: The third column has the filter input disabled, but actually has a filter set in code that requires every company to have an 'a' in their name.

    +
    diff --git a/src/js/core/directives/ui-grid-header-cell.js b/src/js/core/directives/ui-grid-header-cell.js index 3697c99bf4..f9a3280631 100644 --- a/src/js/core/directives/ui-grid-header-cell.js +++ b/src/js/core/directives/ui-grid-header-cell.js @@ -48,7 +48,7 @@ // apply any headerCellClass var classAdded; - var updateClass = function( grid ){ + var updateHeaderOptions = function( grid ){ var contents = $elm; if ( classAdded ){ contents.removeClass( classAdded ); @@ -65,6 +65,30 @@ var rightMostContainer = $scope.grid.renderContainers['right'] ? $scope.grid.renderContainers['right'] : $scope.grid.renderContainers['body']; $scope.isLastCol = ( $scope.col === rightMostContainer.visibleColumnCache[ rightMostContainer.visibleColumnCache.length - 1 ] ); + + // Figure out whether this column is sortable or not + if (uiGridCtrl.grid.options.enableSorting && $scope.col.enableSorting) { + $scope.sortable = true; + } + else { + $scope.sortable = false; + } + + // Figure out whether this column is filterable or not + if (uiGridCtrl.grid.options.enableFiltering && $scope.col.enableFiltering) { + $scope.filterable = true; + } + else { + $scope.filterable = false; + } + + // figure out whether we support column menus + if ($scope.col.grid.options && $scope.col.grid.options.enableColumnMenus !== false && + $scope.col.colDef && $scope.col.colDef.enableColumnMenu !== false){ + $scope.colMenu = true; + } else { + $scope.colMenu = false; + } }; $scope.$watch('col', function (n, o) { @@ -79,38 +103,13 @@ } }); - updateClass(); + updateHeaderOptions(); // Register a data change watch that would get triggered whenever someone edits a cell or modifies column defs - var dataChangeDereg = $scope.grid.registerDataChangeCallback( updateClass, [uiGridConstants.dataChange.COLUMN]); + var dataChangeDereg = $scope.grid.registerDataChangeCallback( updateHeaderOptions, [uiGridConstants.dataChange.COLUMN]); $scope.$on( '$destroy', dataChangeDereg ); - - // Figure out whether this column is sortable or not - if (uiGridCtrl.grid.options.enableSorting && $scope.col.enableSorting) { - $scope.sortable = true; - } - else { - $scope.sortable = false; - } - - // Figure out whether this column is filterable or not - if (uiGridCtrl.grid.options.enableFiltering && $scope.col.enableFiltering) { - $scope.filterable = true; - } - else { - $scope.filterable = false; - } - - // figure out whether we support column menus - if ($scope.col.grid.options && $scope.col.grid.options.enableColumnMenus !== false && - $scope.col.colDef && $scope.col.colDef.enableColumnMenu !== false){ - $scope.colMenu = true; - } else { - $scope.colMenu = false; - } - function handleClick(event) { // If the shift key is being held down, add this column to the sort var add = false; diff --git a/src/js/core/factories/GridColumn.js b/src/js/core/factories/GridColumn.js index b176fe33b4..7cc1c2bbde 100644 --- a/src/js/core/factories/GridColumn.js +++ b/src/js/core/factories/GridColumn.js @@ -524,7 +524,7 @@ angular.module('ui.grid') if (colDef.filter) { defaultFilters.push(colDef.filter); } - else if (self.enableFiltering && self.grid.options.enableFiltering) { + else { // Add an empty filter definition object, which will // translate to a guessed condition and no pre-populated // value for the filter . From 5fb48f498fad4fa91bebe02aadcd14ad952e007a Mon Sep 17 00:00:00 2001 From: AlphaHinex Date: Tue, 17 Mar 2015 09:23:56 +0800 Subject: [PATCH 031/173] add exporterMenuAllData property --- src/features/exporter/js/exporter.js | 12 ++++++++++-- src/features/exporter/test/exporter.spec.js | 3 +++ 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/src/features/exporter/js/exporter.js b/src/features/exporter/js/exporter.js index 16cfee8d75..f8b580bed9 100644 --- a/src/features/exporter/js/exporter.js +++ b/src/features/exporter/js/exporter.js @@ -350,6 +350,14 @@ *
    Defaults to null, which means no layout */ + /** + * @ngdoc object + * @name exporterMenuAllData + * @porpertyOf ui.grid.exporter.api:GridOptions + * @description Add export all data as cvs/pdf menu items to the ui-grid grid menu, if it's present. Defaults to true. + */ + gridOptions.exporterMenuAllData = gridOptions.exporterMenuAllData !== undefined ? gridOptions.exporterMenuAllData : true; + /** * @ngdoc object * @name exporterMenuCsv @@ -470,7 +478,7 @@ this.grid.api.exporter.csvExport( uiGridExporterConstants.ALL, uiGridExporterConstants.ALL ); }, shown: function() { - return this.grid.options.exporterMenuCsv; + return this.grid.options.exporterMenuCsv && this.grid.options.exporterMenuAllData; } }, { @@ -498,7 +506,7 @@ this.grid.api.exporter.pdfExport( uiGridExporterConstants.ALL, uiGridExporterConstants.ALL ); }, shown: function() { - return this.grid.options.exporterMenuPdf; + return this.grid.options.exporterMenuPdf && this.grid.options.exporterMenuAllData; } }, { diff --git a/src/features/exporter/test/exporter.spec.js b/src/features/exporter/test/exporter.spec.js index 96bcb76053..325799a559 100644 --- a/src/features/exporter/test/exporter.spec.js +++ b/src/features/exporter/test/exporter.spec.js @@ -80,6 +80,7 @@ describe('ui.grid.exporter uiGridExporterService', function () { exporterPdfMaxGridWidth : 720, exporterPdfCustomFormatter: jasmine.any(Function), exporterHeaderFilterUseName: false, + exporterMenuAllData: true, exporterMenuCsv: true, exporterMenuPdf: true, exporterFieldCallback: jasmine.any(Function), @@ -105,6 +106,7 @@ describe('ui.grid.exporter uiGridExporterService', function () { exporterPdfMaxGridWidth : 670, exporterPdfCustomFormatter: callback, exporterHeaderFilterUseName: true, + exporterMenuAllData: false, exporterMenuCsv: false, exporterMenuPdf: false, exporterFieldCallback: callback, @@ -127,6 +129,7 @@ describe('ui.grid.exporter uiGridExporterService', function () { exporterPdfMaxGridWidth : 670, exporterPdfCustomFormatter: callback, exporterHeaderFilterUseName: true, + exporterMenuAllData: false, exporterMenuCsv: false, exporterMenuPdf: false, exporterFieldCallback: callback, From 0bda31aaaa5e1af9c7a4e491b82d3691cdcea711 Mon Sep 17 00:00:00 2001 From: AlphaHinex Date: Tue, 17 Mar 2015 10:24:50 +0800 Subject: [PATCH 032/173] ordered json data --- misc/site/data/100_ASC.json | 478 +++++++++++++++++------------------ misc/site/data/100_DESC.json | 462 ++++++++++++++++----------------- 2 files changed, 470 insertions(+), 470 deletions(-) diff --git a/misc/site/data/100_ASC.json b/misc/site/data/100_ASC.json index 2d61b62597..023e4eae99 100644 --- a/misc/site/data/100_ASC.json +++ b/misc/site/data/100_ASC.json @@ -1,348 +1,353 @@ [ { - "name": "Beryl Rice", + "name": "Alexander Foley", + "gender": "male", + "company": "Geekosis" + }, + { + "name": "Alisha Myers", "gender": "female", - "company": "Velity" + "company": "Intradisk" }, { - "name": "Bruce Strong", + "name": "Anthony Joyner", "gender": "male", - "company": "Xyqag" + "company": "Senmei" }, { - "name": "Carroll Buchanan", + "name": "Atkins Dunlap", "gender": "male", - "company": "Ecosys" + "company": "Comveyor" }, { - "name": "Claudine Neal", - "gender": "female", - "company": "Sealoud" + "name": "Ayers Hood", + "gender": "male", + "company": "Accuprint" }, { - "name": "Dawson Barber", + "name": "Baird Ryan", "gender": "male", - "company": "Dymi" + "company": "Aquasseur" }, { - "name": "Ethel Price", - "gender": "female", - "company": "Enersol" + "name": "Barnett Case", + "gender": "male", + "company": "Norali" }, { - "name": "Evans Hickman", + "name": "Barr Page", "gender": "male", - "company": "Parleynet" + "company": "Apex" }, { - "name": "Georgina Schultz", - "gender": "female", - "company": "Suretech" + "name": "Bean Donovan", + "gender": "male", + "company": "Mantro" }, { - "name": "Jackson Macias", - "gender": "male", - "company": "Aquamate" + "name": "Beryl Rice", + "gender": "female", + "company": "Velity" }, { - "name": "Lynda Mendoza", + "name": "Betsy Horton", "gender": "female", - "company": "Dogspa" + "company": "Zilla" }, { - "name": "Nellie Whitfield", + "name": "Billie Rowe", "gender": "female", - "company": "Exospace" + "company": "Cemention" }, { - "name": "Robles Boyle", + "name": "Blackburn Drake", "gender": "male", - "company": "Comtract" + "company": "Frenex" }, { - "name": "Sarah Massey", + "name": "Blanche Conley", "gender": "female", - "company": "Bisba" + "company": "Imkan" }, { - "name": "Schroeder Mathews", + "name": "Bruce Strong", "gender": "male", - "company": "Polarium" + "company": "Xyqag" }, { - "name": "Valarie Atkinson", - "gender": "female", - "company": "Hopeli" + "name": "Carroll Buchanan", + "gender": "male", + "company": "Ecosys" }, { - "name": "Wilder Gonzales", + "name": "Chaney Roach", "gender": "male", - "company": "Geekko" + "company": "Qualitern" }, { - "name": "Pena Pena", - "gender": "male", - "company": "Quarx" + "name": "Christine Compton", + "gender": "female", + "company": "Bleeko" }, { - "name": "Lelia Gates", + "name": "Claudine Neal", "gender": "female", - "company": "Proxsoft" + "company": "Sealoud" }, { - "name": "Letitia Vasquez", + "name": "Consuelo Dickson", "gender": "female", - "company": "Slumberia" + "company": "Poshome" }, { - "name": "Trevino Moreno", - "gender": "male", - "company": "Conjurica" + "name": "Cora Chase", + "gender": "female", + "company": "Isonus" }, { - "name": "Barr Page", - "gender": "male", - "company": "Apex" + "name": "Dale Byrd", + "gender": "female", + "company": "Kneedles" }, { - "name": "Kirkland Merrill", + "name": "Dawson Barber", "gender": "male", - "company": "Utara" + "company": "Dymi" }, { - "name": "Blanche Conley", + "name": "Deann Bridges", "gender": "female", - "company": "Imkan" + "company": "Equitox" }, { - "name": "Atkins Dunlap", - "gender": "male", - "company": "Comveyor" + "name": "Ericka Alvarado", + "gender": "female", + "company": "Lyrichord" }, { - "name": "Everett Foreman", + "name": "Ethel Price", + "gender": "female", + "company": "Enersol" + }, + { + "name": "Evans Hickman", "gender": "male", - "company": "Maineland" + "company": "Parleynet" }, { - "name": "Gould Randolph", + "name": "Everett Foreman", "gender": "male", - "company": "Intergeek" + "company": "Maineland" }, { - "name": "Kelli Leon", + "name": "Felecia Smith", "gender": "female", - "company": "Verbus" + "company": "Futurity" }, { - "name": "Freda Mason", + "name": "Francesca Elliott", "gender": "female", - "company": "Accidency" + "company": "Nspire" }, { - "name": "Tucker Maxwell", + "name": "Franco Hunter", "gender": "male", - "company": "Lumbrex" + "company": "Rockabye" }, { - "name": "Yvonne Parsons", + "name": "Freda Mason", "gender": "female", - "company": "Zolar" + "company": "Accidency" }, { - "name": "Woods Key", + "name": "Frye Sharpe", "gender": "male", - "company": "Bedder" + "company": "Eplode" }, { - "name": "Stephens Reilly", + "name": "Gaines Beck", "gender": "male", - "company": "Acusage" + "company": "Sequitur" }, { - "name": "Mcfarland Sparks", + "name": "Garrett Brennan", "gender": "male", - "company": "Comvey" + "company": "Bluegrain" }, { - "name": "Jocelyn Sawyer", + "name": "Georgia Mercer", "gender": "female", - "company": "Fortean" + "company": "Skyplex" }, { - "name": "Renee Barr", + "name": "Georgina Schultz", "gender": "female", - "company": "Kiggle" + "company": "Suretech" }, { - "name": "Gaines Beck", + "name": "Gould Randolph", "gender": "male", - "company": "Sequitur" + "company": "Intergeek" }, { - "name": "Luisa Farrell", - "gender": "female", - "company": "Cinesanct" + "name": "Graham Marsh", + "gender": "male", + "company": "Medifax" }, { - "name": "Robyn Strickland", - "gender": "female", - "company": "Obones" + "name": "Hale Boone", + "gender": "male", + "company": "Digial" }, { - "name": "Roseann Jarvis", + "name": "Hattie Mullen", "gender": "female", - "company": "Aquazure" + "company": "Zilencio" }, { - "name": "Johnston Park", + "name": "Herring Pierce", "gender": "male", - "company": "Netur" + "company": "Geeketron" }, { - "name": "Wong Craft", - "gender": "male", - "company": "Opticall" + "name": "Hilda Crane", + "gender": "female", + "company": "Jumpstack" }, { - "name": "Merritt Cole", + "name": "Humphrey Curtis", "gender": "male", - "company": "Techtrix" + "company": "Corepan" }, { - "name": "Dale Byrd", - "gender": "female", - "company": "Kneedles" + "name": "Jackson Macias", + "gender": "male", + "company": "Aquamate" }, { - "name": "Sara Delgado", + "name": "Jeanne Lindsay", "gender": "female", - "company": "Netagy" + "company": "Genesynk" }, { - "name": "Alisha Myers", + "name": "Jerri King", "gender": "female", - "company": "Intradisk" + "company": "Eventex" }, { - "name": "Felecia Smith", + "name": "Jocelyn Sawyer", "gender": "female", - "company": "Futurity" + "company": "Fortean" }, { - "name": "Neal Harvey", + "name": "Johnston Park", "gender": "male", - "company": "Pyramax" + "company": "Netur" }, { - "name": "Nola Miles", + "name": "Kelli Leon", "gender": "female", - "company": "Sonique" + "company": "Verbus" }, { - "name": "Herring Pierce", + "name": "Kirk Cross", "gender": "male", - "company": "Geeketron" + "company": "Portico" }, { - "name": "Shelley Rodriquez", + "name": "Kirkland Merrill", + "gender": "male", + "company": "Utara" + }, + { + "name": "Kristi Brewer", "gender": "female", - "company": "Bostonic" + "company": "Oronoko" }, { - "name": "Cora Chase", + "name": "Lakisha Huber", "gender": "female", - "company": "Isonus" + "company": "Insource" }, { - "name": "Mckay Santos", + "name": "Lancaster Patel", "gender": "male", - "company": "Amtas" + "company": "Krog" }, { - "name": "Hilda Crane", + "name": "Lelia Gates", "gender": "female", - "company": "Jumpstack" + "company": "Proxsoft" }, { - "name": "Jeanne Lindsay", + "name": "Letitia Vasquez", "gender": "female", - "company": "Genesynk" - }, - { - "name": "Frye Sharpe", - "gender": "male", - "company": "Eplode" + "company": "Slumberia" }, { - "name": "Velma Fry", + "name": "Lindsay Avery", "gender": "female", - "company": "Ronelon" + "company": "Unq" }, { - "name": "Reyna Espinoza", + "name": "Luisa Farrell", "gender": "female", - "company": "Prismatic" - }, - { - "name": "Spencer Sloan", - "gender": "male", - "company": "Comverges" + "company": "Cinesanct" }, { - "name": "Graham Marsh", - "gender": "male", - "company": "Medifax" + "name": "Lynda Mendoza", + "gender": "female", + "company": "Dogspa" }, { - "name": "Hale Boone", - "gender": "male", - "company": "Digial" + "name": "Lynette Stein", + "gender": "female", + "company": "Macronaut" }, { - "name": "Wiley Hubbard", + "name": "Lyons Peters", "gender": "male", - "company": "Zensus" + "company": "Quinex" }, { - "name": "Blackburn Drake", - "gender": "male", - "company": "Frenex" + "name": "Marcy Green", + "gender": "female", + "company": "Pharmex" }, { - "name": "Franco Hunter", + "name": "Mcfarland Sparks", "gender": "male", - "company": "Rockabye" + "company": "Comvey" }, { - "name": "Barnett Case", + "name": "Mckay Santos", "gender": "male", - "company": "Norali" + "company": "Amtas" }, { - "name": "Alexander Foley", + "name": "Merritt Cole", "gender": "male", - "company": "Geekosis" + "company": "Techtrix" }, { - "name": "Lynette Stein", + "name": "Milagros Finch", "gender": "female", - "company": "Macronaut" + "company": "Handshake" }, { - "name": "Anthony Joyner", + "name": "Neal Harvey", "gender": "male", - "company": "Senmei" + "company": "Pyramax" }, { - "name": "Garrett Brennan", - "gender": "male", - "company": "Bluegrain" + "name": "Nellie Whitfield", + "gender": "female", + "company": "Exospace" }, { - "name": "Betsy Horton", + "name": "Nola Miles", "gender": "female", - "company": "Zilla" + "company": "Sonique" }, { "name": "Patton Small", @@ -350,153 +355,148 @@ "company": "Genmex" }, { - "name": "Lakisha Huber", - "gender": "female", - "company": "Insource" + "name": "Pena Pena", + "gender": "male", + "company": "Quarx" }, { - "name": "Lindsay Avery", + "name": "Renee Barr", "gender": "female", - "company": "Unq" + "company": "Kiggle" }, { - "name": "Ayers Hood", - "gender": "male", - "company": "Accuprint" + "name": "Reyna Espinoza", + "gender": "female", + "company": "Prismatic" }, { - "name": "Torres Durham", + "name": "Robles Boyle", "gender": "male", - "company": "Uplinx" + "company": "Comtract" }, { - "name": "Vincent Hernandez", - "gender": "male", - "company": "Talendula" + "name": "Robyn Strickland", + "gender": "female", + "company": "Obones" }, { - "name": "Baird Ryan", + "name": "Rocha Meadows", "gender": "male", - "company": "Aquasseur" + "company": "Goko" }, { - "name": "Georgia Mercer", + "name": "Rosa Dyer", "gender": "female", - "company": "Skyplex" + "company": "Netility" }, { - "name": "Francesca Elliott", + "name": "Roseann Jarvis", "gender": "female", - "company": "Nspire" - }, - { - "name": "Lyons Peters", - "gender": "male", - "company": "Quinex" + "company": "Aquazure" }, { - "name": "Kristi Brewer", + "name": "Sara Delgado", "gender": "female", - "company": "Oronoko" + "company": "Netagy" }, { - "name": "Tonya Bray", + "name": "Sarah Massey", "gender": "female", - "company": "Insuron" + "company": "Bisba" }, { - "name": "Valenzuela Huff", + "name": "Schroeder Mathews", "gender": "male", - "company": "Applideck" + "company": "Polarium" }, { - "name": "Tiffany Anderson", + "name": "Shelley Rodriquez", "gender": "female", - "company": "Zanymax" + "company": "Bostonic" }, { - "name": "Jerri King", - "gender": "female", - "company": "Eventex" + "name": "Spencer Sloan", + "gender": "male", + "company": "Comverges" }, { - "name": "Rocha Meadows", + "name": "Stephens Reilly", "gender": "male", - "company": "Goko" + "company": "Acusage" }, { - "name": "Marcy Green", + "name": "Sylvia Sosa", "gender": "female", - "company": "Pharmex" - }, - { - "name": "Kirk Cross", - "gender": "male", - "company": "Portico" + "company": "Circum" }, { - "name": "Hattie Mullen", + "name": "Tiffany Anderson", "gender": "female", - "company": "Zilencio" + "company": "Zanymax" }, { - "name": "Deann Bridges", + "name": "Tonya Bray", "gender": "female", - "company": "Equitox" + "company": "Insuron" }, { - "name": "Chaney Roach", + "name": "Torres Durham", "gender": "male", - "company": "Qualitern" + "company": "Uplinx" }, { - "name": "Consuelo Dickson", - "gender": "female", - "company": "Poshome" + "name": "Trevino Moreno", + "gender": "male", + "company": "Conjurica" }, { - "name": "Billie Rowe", - "gender": "female", - "company": "Cemention" + "name": "Tucker Maxwell", + "gender": "male", + "company": "Lumbrex" }, { - "name": "Bean Donovan", - "gender": "male", - "company": "Mantro" + "name": "Valarie Atkinson", + "gender": "female", + "company": "Hopeli" }, { - "name": "Lancaster Patel", + "name": "Valenzuela Huff", "gender": "male", - "company": "Krog" + "company": "Applideck" }, { - "name": "Rosa Dyer", + "name": "Velma Fry", "gender": "female", - "company": "Netility" + "company": "Ronelon" }, { - "name": "Christine Compton", - "gender": "female", - "company": "Bleeko" + "name": "Vincent Hernandez", + "gender": "male", + "company": "Talendula" }, { - "name": "Milagros Finch", - "gender": "female", - "company": "Handshake" + "name": "Wilder Gonzales", + "gender": "male", + "company": "Geekko" }, { - "name": "Ericka Alvarado", - "gender": "female", - "company": "Lyrichord" + "name": "Wiley Hubbard", + "gender": "male", + "company": "Zensus" }, { - "name": "Sylvia Sosa", - "gender": "female", - "company": "Circum" + "name": "Wong Craft", + "gender": "male", + "company": "Opticall" }, { - "name": "Humphrey Curtis", + "name": "Woods Key", "gender": "male", - "company": "Corepan" + "company": "Bedder" + }, + { + "name": "Yvonne Parsons", + "gender": "female", + "company": "Zolar" } -] \ No newline at end of file +] diff --git a/misc/site/data/100_DESC.json b/misc/site/data/100_DESC.json index 159ea94821..dbd1e0fe7a 100644 --- a/misc/site/data/100_DESC.json +++ b/misc/site/data/100_DESC.json @@ -1,163 +1,183 @@ [ { - "name": "Wilder Gonzales", + "name": "Yvonne Parsons", + "gender": "female", + "company": "Zolar" + }, + { + "name": "Woods Key", "gender": "male", - "company": "Geekko" + "company": "Bedder" }, { - "name": "Valarie Atkinson", - "gender": "female", - "company": "Hopeli" + "name": "Wong Craft", + "gender": "male", + "company": "Opticall" }, { - "name": "Schroeder Mathews", + "name": "Wiley Hubbard", "gender": "male", - "company": "Polarium" + "company": "Zensus" }, { - "name": "Sarah Massey", - "gender": "female", - "company": "Bisba" + "name": "Wilder Gonzales", + "gender": "male", + "company": "Geekko" }, { - "name": "Robles Boyle", + "name": "Vincent Hernandez", "gender": "male", - "company": "Comtract" + "company": "Talendula" }, { - "name": "Nellie Whitfield", + "name": "Velma Fry", "gender": "female", - "company": "Exospace" + "company": "Ronelon" }, { - "name": "Lynda Mendoza", + "name": "Valenzuela Huff", + "gender": "male", + "company": "Applideck" + }, + { + "name": "Valarie Atkinson", "gender": "female", - "company": "Dogspa" + "company": "Hopeli" }, { - "name": "Jackson Macias", + "name": "Tucker Maxwell", "gender": "male", - "company": "Aquamate" + "company": "Lumbrex" }, { - "name": "Georgina Schultz", - "gender": "female", - "company": "Suretech" + "name": "Trevino Moreno", + "gender": "male", + "company": "Conjurica" }, { - "name": "Evans Hickman", + "name": "Torres Durham", "gender": "male", - "company": "Parleynet" + "company": "Uplinx" }, { - "name": "Ethel Price", + "name": "Tonya Bray", "gender": "female", - "company": "Enersol" + "company": "Insuron" }, { - "name": "Dawson Barber", - "gender": "male", - "company": "Dymi" + "name": "Tiffany Anderson", + "gender": "female", + "company": "Zanymax" }, { - "name": "Claudine Neal", + "name": "Sylvia Sosa", "gender": "female", - "company": "Sealoud" + "company": "Circum" }, { - "name": "Carroll Buchanan", + "name": "Stephens Reilly", "gender": "male", - "company": "Ecosys" + "company": "Acusage" }, { - "name": "Bruce Strong", + "name": "Spencer Sloan", "gender": "male", - "company": "Xyqag" + "company": "Comverges" }, { - "name": "Beryl Rice", + "name": "Shelley Rodriquez", "gender": "female", - "company": "Velity" + "company": "Bostonic" }, { - "name": "Pena Pena", + "name": "Schroeder Mathews", "gender": "male", - "company": "Quarx" + "company": "Polarium" }, { - "name": "Lelia Gates", + "name": "Sarah Massey", "gender": "female", - "company": "Proxsoft" + "company": "Bisba" }, { - "name": "Letitia Vasquez", + "name": "Sara Delgado", "gender": "female", - "company": "Slumberia" + "company": "Netagy" }, { - "name": "Trevino Moreno", - "gender": "male", - "company": "Conjurica" + "name": "Roseann Jarvis", + "gender": "female", + "company": "Aquazure" }, { - "name": "Barr Page", - "gender": "male", - "company": "Apex" + "name": "Rosa Dyer", + "gender": "female", + "company": "Netility" }, { - "name": "Kirkland Merrill", + "name": "Rocha Meadows", "gender": "male", - "company": "Utara" + "company": "Goko" }, { - "name": "Blanche Conley", + "name": "Robyn Strickland", "gender": "female", - "company": "Imkan" + "company": "Obones" }, { - "name": "Atkins Dunlap", + "name": "Robles Boyle", "gender": "male", - "company": "Comveyor" + "company": "Comtract" }, { - "name": "Everett Foreman", + "name": "Reyna Espinoza", + "gender": "female", + "company": "Prismatic" + }, + { + "name": "Renee Barr", + "gender": "female", + "company": "Kiggle" + }, + { + "name": "Pena Pena", "gender": "male", - "company": "Maineland" + "company": "Quarx" }, { - "name": "Gould Randolph", + "name": "Patton Small", "gender": "male", - "company": "Intergeek" + "company": "Genmex" }, { - "name": "Kelli Leon", + "name": "Nola Miles", "gender": "female", - "company": "Verbus" + "company": "Sonique" }, { - "name": "Freda Mason", + "name": "Nellie Whitfield", "gender": "female", - "company": "Accidency" + "company": "Exospace" }, { - "name": "Tucker Maxwell", + "name": "Neal Harvey", "gender": "male", - "company": "Lumbrex" + "company": "Pyramax" }, { - "name": "Yvonne Parsons", + "name": "Milagros Finch", "gender": "female", - "company": "Zolar" + "company": "Handshake" }, { - "name": "Woods Key", + "name": "Merritt Cole", "gender": "male", - "company": "Bedder" + "company": "Techtrix" }, { - "name": "Stephens Reilly", + "name": "Mckay Santos", "gender": "male", - "company": "Acusage" + "company": "Amtas" }, { "name": "Mcfarland Sparks", @@ -165,19 +185,24 @@ "company": "Comvey" }, { - "name": "Jocelyn Sawyer", + "name": "Marcy Green", "gender": "female", - "company": "Fortean" + "company": "Pharmex" }, { - "name": "Renee Barr", + "name": "Lyons Peters", + "gender": "male", + "company": "Quinex" + }, + { + "name": "Lynette Stein", "gender": "female", - "company": "Kiggle" + "company": "Macronaut" }, { - "name": "Gaines Beck", - "gender": "male", - "company": "Sequitur" + "name": "Lynda Mendoza", + "gender": "female", + "company": "Dogspa" }, { "name": "Luisa Farrell", @@ -185,84 +210,64 @@ "company": "Cinesanct" }, { - "name": "Robyn Strickland", + "name": "Lindsay Avery", "gender": "female", - "company": "Obones" + "company": "Unq" }, { - "name": "Roseann Jarvis", + "name": "Letitia Vasquez", "gender": "female", - "company": "Aquazure" - }, - { - "name": "Johnston Park", - "gender": "male", - "company": "Netur" + "company": "Slumberia" }, { - "name": "Wong Craft", - "gender": "male", - "company": "Opticall" + "name": "Lelia Gates", + "gender": "female", + "company": "Proxsoft" }, { - "name": "Merritt Cole", + "name": "Lancaster Patel", "gender": "male", - "company": "Techtrix" - }, - { - "name": "Dale Byrd", - "gender": "female", - "company": "Kneedles" + "company": "Krog" }, { - "name": "Sara Delgado", + "name": "Lakisha Huber", "gender": "female", - "company": "Netagy" + "company": "Insource" }, { - "name": "Alisha Myers", + "name": "Kristi Brewer", "gender": "female", - "company": "Intradisk" + "company": "Oronoko" }, { - "name": "Felecia Smith", - "gender": "female", - "company": "Futurity" + "name": "Kirkland Merrill", + "gender": "male", + "company": "Utara" }, { - "name": "Neal Harvey", + "name": "Kirk Cross", "gender": "male", - "company": "Pyramax" + "company": "Portico" }, { - "name": "Nola Miles", + "name": "Kelli Leon", "gender": "female", - "company": "Sonique" + "company": "Verbus" }, { - "name": "Herring Pierce", + "name": "Johnston Park", "gender": "male", - "company": "Geeketron" - }, - { - "name": "Shelley Rodriquez", - "gender": "female", - "company": "Bostonic" + "company": "Netur" }, { - "name": "Cora Chase", + "name": "Jocelyn Sawyer", "gender": "female", - "company": "Isonus" - }, - { - "name": "Mckay Santos", - "gender": "male", - "company": "Amtas" + "company": "Fortean" }, { - "name": "Hilda Crane", + "name": "Jerri King", "gender": "female", - "company": "Jumpstack" + "company": "Eventex" }, { "name": "Jeanne Lindsay", @@ -270,29 +275,29 @@ "company": "Genesynk" }, { - "name": "Frye Sharpe", + "name": "Jackson Macias", "gender": "male", - "company": "Eplode" + "company": "Aquamate" }, { - "name": "Velma Fry", - "gender": "female", - "company": "Ronelon" + "name": "Humphrey Curtis", + "gender": "male", + "company": "Corepan" }, { - "name": "Reyna Espinoza", + "name": "Hilda Crane", "gender": "female", - "company": "Prismatic" + "company": "Jumpstack" }, { - "name": "Spencer Sloan", + "name": "Herring Pierce", "gender": "male", - "company": "Comverges" + "company": "Geeketron" }, { - "name": "Graham Marsh", - "gender": "male", - "company": "Medifax" + "name": "Hattie Mullen", + "gender": "female", + "company": "Zilencio" }, { "name": "Hale Boone", @@ -300,164 +305,154 @@ "company": "Digial" }, { - "name": "Wiley Hubbard", + "name": "Graham Marsh", "gender": "male", - "company": "Zensus" + "company": "Medifax" }, { - "name": "Blackburn Drake", + "name": "Gould Randolph", "gender": "male", - "company": "Frenex" + "company": "Intergeek" }, { - "name": "Franco Hunter", - "gender": "male", - "company": "Rockabye" + "name": "Georgina Schultz", + "gender": "female", + "company": "Suretech" }, { - "name": "Barnett Case", - "gender": "male", - "company": "Norali" + "name": "Georgia Mercer", + "gender": "female", + "company": "Skyplex" }, { - "name": "Alexander Foley", + "name": "Garrett Brennan", "gender": "male", - "company": "Geekosis" - }, - { - "name": "Lynette Stein", - "gender": "female", - "company": "Macronaut" + "company": "Bluegrain" }, { - "name": "Anthony Joyner", + "name": "Gaines Beck", "gender": "male", - "company": "Senmei" + "company": "Sequitur" }, { - "name": "Garrett Brennan", + "name": "Frye Sharpe", "gender": "male", - "company": "Bluegrain" + "company": "Eplode" }, { - "name": "Betsy Horton", + "name": "Freda Mason", "gender": "female", - "company": "Zilla" + "company": "Accidency" }, { - "name": "Patton Small", + "name": "Franco Hunter", "gender": "male", - "company": "Genmex" + "company": "Rockabye" }, { - "name": "Lakisha Huber", + "name": "Francesca Elliott", "gender": "female", - "company": "Insource" + "company": "Nspire" }, { - "name": "Lindsay Avery", + "name": "Felecia Smith", "gender": "female", - "company": "Unq" - }, - { - "name": "Ayers Hood", - "gender": "male", - "company": "Accuprint" + "company": "Futurity" }, { - "name": "Torres Durham", + "name": "Everett Foreman", "gender": "male", - "company": "Uplinx" + "company": "Maineland" }, { - "name": "Vincent Hernandez", + "name": "Evans Hickman", "gender": "male", - "company": "Talendula" + "company": "Parleynet" }, { - "name": "Baird Ryan", - "gender": "male", - "company": "Aquasseur" + "name": "Ethel Price", + "gender": "female", + "company": "Enersol" }, { - "name": "Georgia Mercer", + "name": "Ericka Alvarado", "gender": "female", - "company": "Skyplex" + "company": "Lyrichord" }, { - "name": "Francesca Elliott", + "name": "Deann Bridges", "gender": "female", - "company": "Nspire" + "company": "Equitox" }, { - "name": "Lyons Peters", + "name": "Dawson Barber", "gender": "male", - "company": "Quinex" + "company": "Dymi" }, { - "name": "Kristi Brewer", + "name": "Dale Byrd", "gender": "female", - "company": "Oronoko" + "company": "Kneedles" }, { - "name": "Tonya Bray", + "name": "Cora Chase", "gender": "female", - "company": "Insuron" + "company": "Isonus" }, { - "name": "Valenzuela Huff", - "gender": "male", - "company": "Applideck" + "name": "Consuelo Dickson", + "gender": "female", + "company": "Poshome" }, { - "name": "Tiffany Anderson", + "name": "Claudine Neal", "gender": "female", - "company": "Zanymax" + "company": "Sealoud" }, { - "name": "Jerri King", + "name": "Christine Compton", "gender": "female", - "company": "Eventex" + "company": "Bleeko" }, { - "name": "Rocha Meadows", + "name": "Chaney Roach", "gender": "male", - "company": "Goko" + "company": "Qualitern" }, { - "name": "Marcy Green", - "gender": "female", - "company": "Pharmex" + "name": "Carroll Buchanan", + "gender": "male", + "company": "Ecosys" }, { - "name": "Kirk Cross", + "name": "Bruce Strong", "gender": "male", - "company": "Portico" + "company": "Xyqag" }, { - "name": "Hattie Mullen", + "name": "Blanche Conley", "gender": "female", - "company": "Zilencio" + "company": "Imkan" }, { - "name": "Deann Bridges", - "gender": "female", - "company": "Equitox" + "name": "Blackburn Drake", + "gender": "male", + "company": "Frenex" }, { - "name": "Chaney Roach", - "gender": "male", - "company": "Qualitern" + "name": "Billie Rowe", + "gender": "female", + "company": "Cemention" }, { - "name": "Consuelo Dickson", + "name": "Betsy Horton", "gender": "female", - "company": "Poshome" + "company": "Zilla" }, { - "name": "Billie Rowe", + "name": "Beryl Rice", "gender": "female", - "company": "Cemention" + "company": "Velity" }, { "name": "Bean Donovan", @@ -465,38 +460,43 @@ "company": "Mantro" }, { - "name": "Lancaster Patel", + "name": "Barr Page", "gender": "male", - "company": "Krog" + "company": "Apex" }, { - "name": "Rosa Dyer", - "gender": "female", - "company": "Netility" + "name": "Barnett Case", + "gender": "male", + "company": "Norali" }, { - "name": "Christine Compton", - "gender": "female", - "company": "Bleeko" + "name": "Baird Ryan", + "gender": "male", + "company": "Aquasseur" }, { - "name": "Milagros Finch", - "gender": "female", - "company": "Handshake" + "name": "Ayers Hood", + "gender": "male", + "company": "Accuprint" }, { - "name": "Ericka Alvarado", - "gender": "female", - "company": "Lyrichord" + "name": "Atkins Dunlap", + "gender": "male", + "company": "Comveyor" }, { - "name": "Sylvia Sosa", + "name": "Anthony Joyner", + "gender": "male", + "company": "Senmei" + }, + { + "name": "Alisha Myers", "gender": "female", - "company": "Circum" + "company": "Intradisk" }, { - "name": "Humphrey Curtis", + "name": "Alexander Foley", "gender": "male", - "company": "Corepan" + "company": "Geekosis" } -] \ No newline at end of file +] From 9cfe606e40756afb68c61e861d3b3c2e8863f4ff Mon Sep 17 00:00:00 2001 From: Umer Farooq Date: Tue, 17 Mar 2015 16:33:21 -0400 Subject: [PATCH 033/173] Fix angular-u/ng-grid #3036: Update column sorting priority to not increment when sort is toggled, remove when unsorted, and to respect priority of columns with suppressRemoveSort by taking their priority in to account when a new column is sorted. --- src/js/core/factories/Grid.js | 6 +++-- test/unit/core/factories/Grid.spec.js | 39 ++++++++++++++++----------- 2 files changed, 28 insertions(+), 17 deletions(-) diff --git a/src/js/core/factories/Grid.js b/src/js/core/factories/Grid.js index 2f826322b9..8d18a90701 100644 --- a/src/js/core/factories/Grid.js +++ b/src/js/core/factories/Grid.js @@ -1804,8 +1804,10 @@ angular.module('ui.grid') if (!add) { self.resetColumnSorting(column); column.sort.priority = 0; + // Get the actual priority since there may be columns which have suppressRemoveSort set + column.sort.priority = self.getNextColumnSortPriority(); } - else { + else if (!column.sort.priority){ column.sort.priority = self.getNextColumnSortPriority(); } @@ -1818,7 +1820,7 @@ angular.module('ui.grid') if ( column.colDef && column.suppressRemoveSort ){ column.sort.direction = uiGridConstants.ASC; } else { - column.sort.direction = null; + column.sort = {}; } } else { diff --git a/test/unit/core/factories/Grid.spec.js b/test/unit/core/factories/Grid.spec.js index 5eebf1c7ee..b4c902210e 100644 --- a/test/unit/core/factories/Grid.spec.js +++ b/test/unit/core/factories/Grid.spec.js @@ -675,46 +675,52 @@ describe('Grid factory', function () { } }); - it( 'if sort is currently null, then should toggle to ASC', function() { + it( 'if sort is currently null, then should toggle to ASC, and reset priority', function() { grid.sortColumn( column, false ); expect( column.sort.direction ).toEqual(uiGridConstants.ASC); + expect( column.sort.priority ).toEqual(1); }); - it( 'if sort is currently ASC, then should toggle to DESC', function() { - column.sort = {direction: uiGridConstants.ASC}; + it( 'if sort is currently ASC, then should toggle to DESC, and reset priortiy', function() { + column.sort = {direction: uiGridConstants.ASC, priority: 2}; grid.sortColumn( column, false ); expect( column.sort.direction ).toEqual(uiGridConstants.DESC); + expect( column.sort.priority ).toEqual(1); }); - it( 'if sort is currently DESC, and suppressRemoveSort is undefined, then should toggle to null', function() { - column.sort = {direction: uiGridConstants.DESC}; + it( 'if sort is currently DESC, and suppressRemoveSort is undefined, then should toggle to null, and remove priority', function() { + column.sort = {direction: uiGridConstants.DESC, priority: 1}; grid.sortColumn( column, false ); expect( column.sort.direction ).toEqual(null); + expect( column.sort.priority ).toEqual(null); }); - it( 'if sort is currently DESC, and suppressRemoveSort is null, then should toggle to null', function() { - column.sort = {direction: uiGridConstants.DESC, suppressRemoveSort: null}; + it( 'if sort is currently DESC, and suppressRemoveSort is null, then should toggle to null, and remove priority', function() { + column.sort = {direction: uiGridConstants.DESC, priority: 1, suppressRemoveSort: null}; grid.sortColumn( column, false ); expect( column.sort.direction ).toEqual(null); + expect( column.sort.priority ).toEqual(null); }); - it( 'if sort is currently DESC, and suppressRemoveSort is false, then should toggle to null', function() { - column.sort = {direction: uiGridConstants.DESC, suppressRemoveSort: false}; + it( 'if sort is currently DESC, and suppressRemoveSort is false, then should toggle to null, and remove priority', function() { + column.sort = {direction: uiGridConstants.DESC, priority: 1, suppressRemoveSort: false}; grid.sortColumn( column, false ); expect( column.sort.direction ).toEqual(null); + expect( column.sort.priority ).toEqual(null); }); - it( 'if sort is currently DESC, and suppressRemoveSort is true, then should toggle to ASC', function() { - column.sort = {direction: uiGridConstants.DESC}; + it( 'if sort is currently DESC, and suppressRemoveSort is true, then should toggle to ASC, and reset priority', function() { + column.sort = {direction: uiGridConstants.DESC, priority: 2}; column.suppressRemoveSort = true; grid.sortColumn( column, false ); expect( column.sort.direction ).toEqual(uiGridConstants.ASC); + expect( column.sort.priority ).toEqual(1); }); it( 'if another column has a sort, that sort should be removed', function() { @@ -724,27 +730,30 @@ describe('Grid factory', function () { grid.sortColumn( column, false ); expect( column.sort.direction ).toEqual(uiGridConstants.ASC); + expect( column.sort.priority ).toEqual(1); expect( priorColumn.sort ).toEqual({}); }); it( 'if another column has a sort, and add is set to true, then that sort should not be removed', function() { - var priorColumn = new GridColumn({ name: 'b', sort: { direction: uiGridConstants.ASC } }, 0, grid); + var priorColumn = new GridColumn({ name: 'b', sort: { direction: uiGridConstants.ASC, priority: 1 } }, 0, grid); grid.columns.push( priorColumn ); grid.sortColumn( column, true ); expect( column.sort.direction ).toEqual(uiGridConstants.ASC); - expect( priorColumn.sort ).toEqual({ direction: uiGridConstants.ASC }); + expect( column.sort.priority ).toEqual(2); + expect( priorColumn.sort ).toEqual({ direction: uiGridConstants.ASC, priority: 1}); }); it( 'if another column has a sort, and add is set to false, but that other column has suppressRemoveSort, then it shouldn\'t be removed', function() { - var priorColumn = new GridColumn({ name: 'b', sort: { direction: uiGridConstants.ASC }, suppressRemoveSort: true }, 0, grid); + var priorColumn = new GridColumn({ name: 'b', sort: { direction: uiGridConstants.ASC, priority: 1 }, suppressRemoveSort: true }, 0, grid); grid.columns.push( priorColumn ); grid.sortColumn( column, false ); expect( column.sort.direction ).toEqual(uiGridConstants.ASC); - expect( priorColumn.sort ).toEqual({ direction: uiGridConstants.ASC }); + expect( column.sort.priority ).toEqual(2); + expect( priorColumn.sort ).toEqual({ direction: uiGridConstants.ASC, priority: 1}); }); }); From 007137b57640647ab1e91ba1c2a0d63f77ba5f29 Mon Sep 17 00:00:00 2001 From: Paul Lambert Date: Wed, 18 Mar 2015 09:37:25 +1300 Subject: [PATCH 034/173] Doco(tuts): cellTemplate and date filters --- misc/tutorial/103_filtering.ngdoc | 24 ++++- misc/tutorial/301_custom_row_template.ngdoc | 63 ------------ misc/tutorial/317_custom_templates.ngdoc | 101 ++++++++++++++++++++ 3 files changed, 122 insertions(+), 66 deletions(-) delete mode 100644 misc/tutorial/301_custom_row_template.ngdoc create mode 100644 misc/tutorial/317_custom_templates.ngdoc diff --git a/misc/tutorial/103_filtering.ngdoc b/misc/tutorial/103_filtering.ngdoc index d51410c70f..78774817e4 100644 --- a/misc/tutorial/103_filtering.ngdoc +++ b/misc/tutorial/103_filtering.ngdoc @@ -46,7 +46,9 @@ Occasionally, you may want to provide two or more filters for a single column. T The elements of this array are the same as the contents of the `filter` object in all the previous examples. In fact, `filter: { term: 'xxx' }` is just an alias for `filters: [{ term: 'xxx' }]`. See the "age" column below for an example. - +### Date filters +The example also includes date filters. These work, however there isn't a date chooser in the filter widget - so you may need to implement a custom field +if you want to filter dates in this way. @example @@ -54,6 +56,10 @@ for `filters: [{ term: 'xxx' }]`. See the "age" column below for an example. var app = angular.module('app', ['ngAnimate', 'ngTouch', 'ui.grid']); app.controller('MainCtrl', ['$scope', '$http', 'uiGridConstants', function ($scope, $http, uiGridConstants) { + var today = new Date(); + var nextWeek = new Date(); + nextWeek.setDate(nextWeek.getDate() + 7); + $scope.gridOptions = { enableFiltering: true, onRegisterApi: function(gridApi){ @@ -100,7 +106,13 @@ for `filters: [{ term: 'xxx' }]`. See the "age" column below for an example. condition: uiGridConstants.filter.LESS_THAN, placeholder: 'less than' } - ]} + ]}, + // date filter + { field: 'mixedDate', cellFilter: 'date', width: '15%', filter: { + condition: uiGridConstants.filter.LESS_THAN, + placeholder: 'less than', + term: nextWeek + }} ] }; @@ -108,6 +120,11 @@ for `filters: [{ term: 'xxx' }]`. See the "age" column below for an example. .success(function(data) { $scope.gridOptions.data = data; $scope.gridOptions.data[0].age = -5; + + data.forEach( function addDates( row, index ){ + row.mixedDate = new Date(); + row.mixedDate.setDate(today.getDate() + ( index % 14 ) ); + }); }); $scope.toggleFiltering = function(){ @@ -142,13 +159,14 @@ for `filters: [{ term: 'xxx' }]`. See the "age" column below for an example. gridTestUtils.expectHeaderColumnCount( 'grid1', 6 ); }); - it('filter on 4 columns, filter with greater than/less than on one, one with no filter', function () { + it('filter on 4 columns, filter with greater than/less than on one, one with no filter, then one with one filter', function () { gridTestUtils.expectFilterBoxInColumn( 'grid1', 0, 1 ); gridTestUtils.expectFilterBoxInColumn( 'grid1', 1, 1 ); gridTestUtils.expectFilterBoxInColumn( 'grid1', 2, 0 ); gridTestUtils.expectFilterBoxInColumn( 'grid1', 3, 1 ); gridTestUtils.expectFilterBoxInColumn( 'grid1', 4, 1 ); gridTestUtils.expectFilterBoxInColumn( 'grid1', 5, 2 ); + gridTestUtils.expectFilterBoxInColumn( 'grid1', 6, 1 ); }); it('third row should be Hatfield Hudson - will be Terry Clay if filtering broken', function () { diff --git a/misc/tutorial/301_custom_row_template.ngdoc b/misc/tutorial/301_custom_row_template.ngdoc deleted file mode 100644 index 9058d41693..0000000000 --- a/misc/tutorial/301_custom_row_template.ngdoc +++ /dev/null @@ -1,63 +0,0 @@ -@ngdoc overview -@name Tutorial: 301 Custom Row Template -@description - -Create a grid almost the same as the most basic one, but with a custom row template. - -You can use [grid.appScope](/docs/#/tutorial/305_appScope) in your row template to access -elements in your controller's scope. More details are on -the [external scopes](/docs/#/tutorial/305_appScope) tutorial. - -@example - - - var app = angular.module('app', ['ngTouch', 'ui.grid']); - - app.controller('MainCtrl', ['$scope', '$http', '$timeout', '$interval', function ($scope, $http, $timeout, $interval) { - var start = new Date(); - var sec = $interval(function () { - var wait = parseInt(((new Date()) - start) / 1000, 10); - $scope.wait = wait + 's'; - }, 1000); - - function rowTemplate() { - return $timeout(function() { - $scope.waiting = 'Done!'; - $interval.cancel(sec); - $scope.wait = ''; - return '
    '; - }, 6000); - } - - // Access outside scope functions from row template - $scope.fnOne = function(row) { - console.log(row); - }; - - $scope.waiting = 'Waiting for row template...'; - - $http.get('/data/100.json') - .success(function (data) { - $scope.data = data; - }); - - $scope.gridOptions = { - rowTemplate: rowTemplate(), - data: 'data' - }; - }]); -
    - -
    - {{ wait }} -
    -
    -
    -
    - - .grid { - width: 500px; - height: 300px; - } - -
    \ No newline at end of file diff --git a/misc/tutorial/317_custom_templates.ngdoc b/misc/tutorial/317_custom_templates.ngdoc new file mode 100644 index 0000000000..975997a348 --- /dev/null +++ b/misc/tutorial/317_custom_templates.ngdoc @@ -0,0 +1,101 @@ +@ngdoc overview +@name Tutorial: 317 Custom Templates +@description + +The grid allows you to override most of the templates, including cellTemplate, headerCellTemplate, rowTemplate +and others. You would typically do this to inject functionality like buttons or to get a very different look and +feel that you couldn't achieve through cell classes and other settings. + +It is generally good practice to at least review the standard template in https://github.com/angular-ui/ng-grid/tree/master/src/templates/ui-grid +to make sure there isn't functionality that you are overriding that you needed to keep. In many cases it is desirable to +use the standard template as a starting point, and add your customisations on top. Also remember that new features +or code changes may mean that you need to upgrade your custom template (if the standard template has been modified). + +In this example we create a grid almost the same as the most basic one, but with a custom row template, and +with a cellTemplate that totals all the items above it in the grid. This template continues to work when the data is +filtered or sorted. + +You can use [grid.appScope](/docs/#/tutorial/305_appScope) in your row template to access +elements in your controller's scope. More details are on +the [scopes](/docs/#/tutorial/305_appScope) tutorial. + +In the cellTemplate you have access to `grid`, `row` and `column`, which allows you to write any of a range of functions. + +@example + + + var app = angular.module('app', ['ngTouch', 'ui.grid']); + + app.controller('MainCtrl', ['$scope', '$http', '$timeout', '$interval', function ($scope, $http, $timeout, $interval) { + var start = new Date(); + var sec = $interval(function () { + var wait = parseInt(((new Date()) - start) / 1000, 10); + $scope.wait = wait + 's'; + }, 1000); + + function rowTemplate() { + return $timeout(function() { + $scope.waiting = 'Done!'; + $interval.cancel(sec); + $scope.wait = ''; + return '
    '; + }, 6000); + } + + // Access outside scope functions from row template + $scope.fnOne = function(row) { + console.log(row); + }; + + $scope.waiting = 'Waiting for row template...'; + + $http.get('/data/100.json') + .success(function (data) { + data.forEach( function(row, index) { + row.widgets = index % 10; + }); + $scope.data = data; + }); + + $scope.gridOptions = { + enableFiltering: true, + rowTemplate: rowTemplate(), + data: 'data', + columnDefs: [ + { name: 'name' }, + { name: 'gender' }, + { name: 'company' }, + { name: 'widgets' }, + { name: 'cumulativeWidgets', field: 'widgets', cellTemplate: '
    {{grid.appScope.cumulative(grid, row)}}
    ' } + ] + }; + + $scope.cumulative = function( grid, myRow ) { + var myRowFound = false; + var cumulativeTotal = 0; + grid.renderContainers.body.visibleRowCache.forEach( function( row, index ) { + if( !myRowFound ) { + cumulativeTotal += row.entity.widgets; + if( row === myRow ) { + myRowFound = true; + } + } + }); + return cumulativeTotal; + }; + }]); +
    + +
    + {{ wait }} +
    +
    +
    +
    + + .grid { + width: 500px; + height: 300px; + } + +
    \ No newline at end of file From d69648489a3ff3788759ee8d7a77788ddf9f078f Mon Sep 17 00:00:00 2001 From: Paul Lambert Date: Wed, 18 Mar 2015 10:02:05 +1300 Subject: [PATCH 035/173] Fix(filtering): dynamic filtering wasn't quite there, watch and rowsProcessor too --- src/features/saveState/test/saveState.spec.js | 20 +- src/js/core/directives/ui-grid-header-cell.js | 215 +++++++++--------- src/js/core/services/gridClassFactory.js | 5 +- src/js/core/services/rowSearcher.js | 5 + test/unit/core/row-sorting.spec.js | 5 +- 5 files changed, 131 insertions(+), 119 deletions(-) diff --git a/src/features/saveState/test/saveState.spec.js b/src/features/saveState/test/saveState.spec.js index abacd571df..d8324c35c7 100644 --- a/src/features/saveState/test/saveState.spec.js +++ b/src/features/saveState/test/saveState.spec.js @@ -105,10 +105,10 @@ describe('ui.grid.saveState uiGridSaveStateService', function () { describe('saveColumns', function() { it('save columns', function() { expect( uiGridSaveStateService.saveColumns( grid ) ).toEqual([ - { name: 'col1', visible: true, width: 50, sort: [], filters: [] }, - { name: 'col2', visible: true, width: '*', sort: [], filters: [] }, - { name: 'col3', visible: false, width: 100, sort: [], filters: [] }, - { name: 'col4', visible: true, width: 200, sort: [], filters: [] } + { name: 'col1', visible: true, width: 50, sort: [], filters: [ {} ] }, + { name: 'col2', visible: true, width: '*', sort: [], filters: [ {} ] }, + { name: 'col3', visible: false, width: 100, sort: [], filters: [ {} ] }, + { name: 'col4', visible: true, width: 200, sort: [], filters: [ {} ] } ]); }); }); @@ -287,10 +287,10 @@ describe('ui.grid.saveState uiGridSaveStateService', function () { }); uiGridSaveStateService.restoreColumns( grid, [ - { name: 'col2', visible: false, width: 90, sort: [ {blah: 'blah'} ], filters: [] }, + { name: 'col2', visible: false, width: 90, sort: [ {blah: 'blah'} ], filters: [ {} ] }, { name: 'col1', visible: true, width: '*', sort: [], filters: [ {'blah': 'blah'} ] }, - { name: 'col4', visible: false, width: 120, sort: [], filters: [] }, - { name: 'col3', visible: true, width: 220, sort: [], filters: [] } + { name: 'col4', visible: false, width: 120, sort: [], filters: [ {} ] }, + { name: 'col3', visible: true, width: 220, sort: [], filters: [ {} ] } ]); expect( grid.columns[0].name ).toEqual('col2', 'column 0 name should be col2'); @@ -318,10 +318,10 @@ describe('ui.grid.saveState uiGridSaveStateService', function () { expect( grid.columns[2].sort ).toEqual([]); expect( grid.columns[3].sort ).toEqual([]); - expect( grid.columns[0].filters ).toEqual([]); + expect( grid.columns[0].filters ).toEqual([ {} ]); expect( grid.columns[1].filters ).toEqual([ { blah: 'blah' } ]); - expect( grid.columns[2].filters ).toEqual([]); - expect( grid.columns[3].filters ).toEqual([]); + expect( grid.columns[2].filters ).toEqual([ {} ]); + expect( grid.columns[3].filters ).toEqual([ {} ]); expect( colVisChangeCount ).toEqual( 4, '4 columns changed visibility'); expect( colFilterChangeCount ).toEqual( 1, '1 columns changed filter'); diff --git a/src/js/core/directives/ui-grid-header-cell.js b/src/js/core/directives/ui-grid-header-cell.js index f9a3280631..225cfea709 100644 --- a/src/js/core/directives/ui-grid-header-cell.js +++ b/src/js/core/directives/ui-grid-header-cell.js @@ -89,6 +89,118 @@ } else { $scope.colMenu = false; } + + /** + * @ngdoc property + * @name enableColumnMenu + * @propertyOf ui.grid.class:GridOptions.columnDef + * @description if column menus are enabled, controls the column menus for this specific + * column (i.e. if gridOptions.enableColumnMenus, then you can control column menus + * using this option. If gridOptions.enableColumnMenus === false then you get no column + * menus irrespective of the value of this option ). Defaults to true. + * + */ + /** + * @ngdoc property + * @name enableColumnMenus + * @propertyOf ui.grid.class:GridOptions.columnDef + * @description Override for column menus everywhere - if set to false then you get no + * column menus. Defaults to true. + * + */ + + var downEvent = gridUtil.isTouchEnabled() ? 'touchstart' : 'mousedown'; + if ($scope.sortable || $scope.colMenu) { + // Long-click (for mobile) + var cancelMousedownTimeout; + var mousedownStartTime = 0; + + $contentsElm.on(downEvent, function(event) { + event.stopPropagation(); + + if (typeof(event.originalEvent) !== 'undefined' && event.originalEvent !== undefined) { + event = event.originalEvent; + } + + // Don't show the menu if it's not the left button + if (event.button && event.button !== 0) { + return; + } + + mousedownStartTime = (new Date()).getTime(); + + cancelMousedownTimeout = $timeout(function() { }, mousedownTimeout); + + cancelMousedownTimeout.then(function () { + if ( $scope.colMenu ) { + uiGridCtrl.columnMenuScope.showMenu($scope.col, $elm, event); + } + }); + + uiGridCtrl.fireEvent(uiGridConstants.events.COLUMN_HEADER_CLICK, {event: event, columnName: $scope.col.colDef.name}); + }); + + var upEvent = gridUtil.isTouchEnabled() ? 'touchend' : 'mouseup'; + $contentsElm.on(upEvent, function () { + $timeout.cancel(cancelMousedownTimeout); + }); + + $scope.$on('$destroy', function () { + $contentsElm.off('mousedown touchstart'); + }); + } else { + $contentsElm.off(downEvent); + } + + // If this column is sortable, add a click event handler + var clickEvent = gridUtil.isTouchEnabled() ? 'touchend' : 'click'; + if ($scope.sortable) { + $contentsElm.on(clickEvent, function(event) { + event.stopPropagation(); + + $timeout.cancel(cancelMousedownTimeout); + + var mousedownEndTime = (new Date()).getTime(); + var mousedownTime = mousedownEndTime - mousedownStartTime; + + if (mousedownTime > mousedownTimeout) { + // long click, handled above with mousedown + } + else { + // short click + handleClick(event); + } + }); + + $scope.$on('$destroy', function () { + // Cancel any pending long-click timeout + $timeout.cancel(cancelMousedownTimeout); + }); + } else { + $contentsElm.off(clickEvent); + } + + // if column is filterable add a filter watcher + var filterDeregisters = []; + if ($scope.filterable) { + $scope.col.filters.forEach( function(filter, i) { + filterDeregisters.push($scope.$watch('col.filters[' + i + '].term', function(n, o) { + if (n !== o) { + uiGridCtrl.grid.api.core.raise.filterChanged(); + uiGridCtrl.grid.refresh(true); + } + })); + }); + $scope.$on('$destroy', function() { + filterDeregisters.forEach( function(filterDeregister) { + filterDeregister(); + }); + }); + } else { + filterDeregisters.forEach( function(filterDeregister) { + filterDeregister(); + }); + } }; $scope.$watch('col', function (n, o) { @@ -125,66 +237,6 @@ }); } - /** - * @ngdoc property - * @name enableColumnMenu - * @propertyOf ui.grid.class:GridOptions.columnDef - * @description if column menus are enabled, controls the column menus for this specific - * column (i.e. if gridOptions.enableColumnMenus, then you can control column menus - * using this option. If gridOptions.enableColumnMenus === false then you get no column - * menus irrespective of the value of this option ). Defaults to true. - * - */ - /** - * @ngdoc property - * @name enableColumnMenus - * @propertyOf ui.grid.class:GridOptions.columnDef - * @description Override for column menus everywhere - if set to false then you get no - * column menus. Defaults to true. - * - */ - - if ($scope.sortable || $scope.colMenu) { - // Long-click (for mobile) - var cancelMousedownTimeout; - var mousedownStartTime = 0; - - var downEvent = gridUtil.isTouchEnabled() ? 'touchstart' : 'mousedown'; - $contentsElm.on(downEvent, function(event) { - event.stopPropagation(); - - if (typeof(event.originalEvent) !== 'undefined' && event.originalEvent !== undefined) { - event = event.originalEvent; - } - - // Don't show the menu if it's not the left button - if (event.button && event.button !== 0) { - return; - } - - mousedownStartTime = (new Date()).getTime(); - - cancelMousedownTimeout = $timeout(function() { }, mousedownTimeout); - - cancelMousedownTimeout.then(function () { - if ( $scope.colMenu ) { - uiGridCtrl.columnMenuScope.showMenu($scope.col, $elm, event); - } - }); - - uiGridCtrl.fireEvent(uiGridConstants.events.COLUMN_HEADER_CLICK, {event: event, columnName: $scope.col.colDef.name}); - }); - - var upEvent = gridUtil.isTouchEnabled() ? 'touchend' : 'mouseup'; - $contentsElm.on(upEvent, function () { - $timeout.cancel(cancelMousedownTimeout); - }); - - $scope.$on('$destroy', function () { - $contentsElm.off('mousedown touchstart'); - }); - } - $scope.toggleMenu = function(event) { event.stopPropagation(); @@ -208,49 +260,6 @@ uiGridCtrl.columnMenuScope.showMenu($scope.col, $elm); } }; - - // If this column is sortable, add a click event handler - if ($scope.sortable) { - var clickEvent = gridUtil.isTouchEnabled() ? 'touchend' : 'click'; - $contentsElm.on(clickEvent, function(event) { - event.stopPropagation(); - - $timeout.cancel(cancelMousedownTimeout); - - var mousedownEndTime = (new Date()).getTime(); - var mousedownTime = mousedownEndTime - mousedownStartTime; - - if (mousedownTime > mousedownTimeout) { - // long click, handled above with mousedown - } - else { - // short click - handleClick(event); - } - }); - - $scope.$on('$destroy', function () { - // Cancel any pending long-click timeout - $timeout.cancel(cancelMousedownTimeout); - }); - } - - if ($scope.filterable) { - var filterDeregisters = []; - angular.forEach($scope.col.filters, function(filter, i) { - filterDeregisters.push($scope.$watch('col.filters[' + i + '].term', function(n, o) { - if (n !== o) { - uiGridCtrl.grid.api.core.raise.filterChanged(); - uiGridCtrl.grid.refresh(true); - } - })); - }); - $scope.$on('$destroy', function() { - angular.forEach(filterDeregisters, function(filterDeregister) { - filterDeregister(); - }); - }); - } } }; } diff --git a/src/js/core/services/gridClassFactory.js b/src/js/core/services/gridClassFactory.js index 1a68e51900..2a5cf2b7c6 100644 --- a/src/js/core/services/gridClassFactory.js +++ b/src/js/core/services/gridClassFactory.js @@ -74,10 +74,7 @@ }); - - if (grid.options.enableFiltering) { - grid.registerRowsProcessor(grid.searchRows); - } + grid.registerRowsProcessor(grid.searchRows); // Register the default row processor, it sorts rows by selected columns if (grid.options.externalSort && angular.isFunction(grid.options.externalSort)) { diff --git a/src/js/core/services/rowSearcher.js b/src/js/core/services/rowSearcher.js index 7475ed2327..0e6bc89131 100644 --- a/src/js/core/services/rowSearcher.js +++ b/src/js/core/services/rowSearcher.js @@ -311,6 +311,11 @@ module.service('rowSearcher', ['gridUtil', 'uiGridConstants', function (gridUtil if (!rows) { return; } + + // don't filter if filtering currently disabled + if (!grid.options.enableFiltering){ + return rows; + } // Build list of filters to apply var filterData = []; diff --git a/test/unit/core/row-sorting.spec.js b/test/unit/core/row-sorting.spec.js index c37306b861..8893e692cf 100644 --- a/test/unit/core/row-sorting.spec.js +++ b/test/unit/core/row-sorting.spec.js @@ -261,8 +261,9 @@ describe('rowSorter', function() { returnedRows = newRows; }); - // Have to flush $timeout once per processor, as they run consecutively - for (var i = 0; i < grid.rowsProcessors.length; i++) { + // Have to flush $timeout once per processor, as they run consecutively. Magic value - reduce by one + // since filter processor is enabled but does nothing + for (var i = 0; i < grid.rowsProcessors.length - 1; i++) { $timeout.flush(); } From 54d13a14fabc30ad8f87b53491271fbff1eafe2e Mon Sep 17 00:00:00 2001 From: Paul Lambert Date: Wed, 18 Mar 2015 16:14:51 +1300 Subject: [PATCH 036/173] Fix(edit): revert #3001, broke i18n of dropdown values --- .../edit/templates/dropdownEditor.html | 2 +- .../test/uiGridCellWithDropdownFilter.spec.js | 84 ------------------- 2 files changed, 1 insertion(+), 85 deletions(-) delete mode 100644 src/features/edit/test/uiGridCellWithDropdownFilter.spec.js diff --git a/src/features/edit/templates/dropdownEditor.html b/src/features/edit/templates/dropdownEditor.html index 60ffe1dfff..d7d6745d73 100644 --- a/src/features/edit/templates/dropdownEditor.html +++ b/src/features/edit/templates/dropdownEditor.html @@ -1,5 +1,5 @@
    - +
    diff --git a/src/features/edit/test/uiGridCellWithDropdownFilter.spec.js b/src/features/edit/test/uiGridCellWithDropdownFilter.spec.js deleted file mode 100644 index 6aa4e50653..0000000000 --- a/src/features/edit/test/uiGridCellWithDropdownFilter.spec.js +++ /dev/null @@ -1,84 +0,0 @@ -describe('ui.grid.edit GridCellDirective - with dropdown filter', function () { - var gridUtil; - var scope; - var element; - var uiGridConstants; - var recompile; - var $timeout; - - beforeEach(module('ui.grid.edit')); - - beforeEach(inject(function ($rootScope, $compile, $controller, _gridUtil_, $templateCache, gridClassFactory, - uiGridEditService, _uiGridConstants_, _$timeout_) { - gridUtil = _gridUtil_; - uiGridConstants = _uiGridConstants_; - $timeout = _$timeout_; - - $templateCache.put('ui-grid/uiGridCell', '
    {{COL_FIELD CUSTOM_FILTERS}}
    '); - $templateCache.put('ui-grid/dropdownEditor', '
    '); - - scope = $rootScope.$new(); - var grid = gridClassFactory.createGrid(); - grid.options.columnDefs = [ - { - name: 'col1', - enableCellEdit: true, - editableCellTemplate: 'ui-grid/dropdownEditor', - editDropdownOptionsArray: [ - {id: 1, value: 'fred'}, - {id: 2, value: 'john'}, - {id: 3, value: 'ken'} - ], - editDropdownFilter: 'filter:{ id: 3 }:true' - } - ]; - grid.options.data = [ - {col1: 1} - ]; - uiGridEditService.initializeGrid(grid); - grid.buildColumns(); - grid.modifyRows(grid.options.data); - - scope.grid = grid; - scope.col = grid.columns[0]; - scope.row = grid.rows[0]; - - scope.getCellValue = function(row,col){return 'val';}; - - $timeout(function(){ - recompile = function () { - $compile(element)(scope); - $rootScope.$digest(); - }; - }); - $timeout.flush(); - - })); - - describe('ui.grid.edit uiGridCell and uiGridEditor with editDropdownFilter', function () { - var displayHtml; - beforeEach(function () { - element = angular.element('
    '); - recompile(); - - displayHtml = element.html(); - expect(element.text()).toBe('1'); - //invoke edit - element.dblclick(); - expect(element.find('select')).toBeDefined(); - - }); - - it('should have filtered options', function () { - - var options = element.find('select option'); - - // options should be 0 and ken - expect(options.length).toBe(2); - expect(options[1].text).toBe('ken'); - - }); - - }); - -}); From 5bc37903e5eb2c10586afb0e086e85a66f4a4a5a Mon Sep 17 00:00:00 2001 From: Paul Lambert Date: Wed, 18 Mar 2015 16:33:13 +1300 Subject: [PATCH 037/173] Fix(rowEdit): fix #3045 rowEdit can call save twice --- src/features/row-edit/js/gridRowEdit.js | 8 ++++ .../test/uiGridRowEditService.spec.js | 47 +++++++++++++++++++ 2 files changed, 55 insertions(+) diff --git a/src/features/row-edit/js/gridRowEdit.js b/src/features/row-edit/js/gridRowEdit.js index 4d7e5df4c7..5912084c08 100644 --- a/src/features/row-edit/js/gridRowEdit.js +++ b/src/features/row-edit/js/gridRowEdit.js @@ -218,9 +218,15 @@ return function() { gridRow.isSaving = true; + if ( gridRow.rowEditSavePromise ){ + // don't save the row again if it's already saving - that causes stale object exceptions + return gridRow.rowEditSavePromise; + } + var promise = grid.api.rowEdit.raise.saveRow( gridRow.entity ); if ( gridRow.rowEditSavePromise ){ + console.log(gridRow.rowEditSavePromise); gridRow.rowEditSavePromise.then( self.processSuccessPromise( grid, gridRow ), self.processErrorPromise( grid, gridRow )); } else { gridUtil.logError( 'A promise was not returned when saveRow event was raised, either nobody is listening to event, or event handler did not return a promise' ); @@ -270,6 +276,7 @@ delete gridRow.isDirty; delete gridRow.isError; delete gridRow.rowEditSaveTimer; + delete gridRow.rowEditSavePromise; self.removeRow( grid.rowEdit.errorRows, gridRow ); self.removeRow( grid.rowEdit.dirtyRows, gridRow ); }; @@ -290,6 +297,7 @@ return function() { delete gridRow.isSaving; delete gridRow.rowEditSaveTimer; + delete gridRow.rowEditSavePromise; gridRow.isError = true; diff --git a/src/features/row-edit/test/uiGridRowEditService.spec.js b/src/features/row-edit/test/uiGridRowEditService.spec.js index 1fe2305f14..2a61fd1bf2 100644 --- a/src/features/row-edit/test/uiGridRowEditService.spec.js +++ b/src/features/row-edit/test/uiGridRowEditService.spec.js @@ -341,6 +341,9 @@ describe('ui.grid.edit uiGridRowEditService', function () { expect( grid.rows[0].isError ).toEqual(true); expect( grid.rowEdit.dirtyRows.length ).toEqual(1); expect( grid.rowEdit.errorRows.length ).toEqual(1); + + $rootScope.$apply(); + expect( grid.rows[0].rowEditSavePromise ).not.toEqual(undefined, 'save promise should be set'); promise.resolve(1); $rootScope.$apply(); @@ -350,6 +353,7 @@ describe('ui.grid.edit uiGridRowEditService', function () { expect( grid.rows[0].isError ).toEqual(undefined); expect( grid.rowEdit.dirtyRows.length ).toEqual(0); expect( grid.rowEdit.errorRows.length ).toEqual(0); + expect( grid.rowEdit.rowEditSavePromise ).toEqual(undefined); }); it( 'saveRow on dirty row, promise rejected so goes to error state', function() { @@ -367,6 +371,9 @@ describe('ui.grid.edit uiGridRowEditService', function () { expect( grid.rows[0].isError ).toEqual(undefined); expect( grid.rowEdit.dirtyRows.length ).toEqual(1); expect( grid.rowEdit.errorRows ).toEqual(undefined); + + $rootScope.$apply(); + expect( grid.rows[0].rowEditSavePromise ).not.toEqual(undefined, 'save promise should be set'); promise.reject(); $rootScope.$apply(); @@ -376,6 +383,7 @@ describe('ui.grid.edit uiGridRowEditService', function () { expect( grid.rows[0].isError ).toEqual(true); expect( grid.rowEdit.dirtyRows.length ).toEqual(1); expect( grid.rowEdit.errorRows.length ).toEqual(1); + expect( grid.rowEdit.rowEditSavePromise ).toEqual(undefined); }); }); @@ -434,6 +442,45 @@ describe('ui.grid.edit uiGridRowEditService', function () { expect( failure ).toEqual(false); }); + it( 'one dirty rows, already saving, doesn\'t call save again', function() { + var promises = [$q.defer()]; + var promiseCounter = 0; + var success = false; + var failure = false; + + grid.rows[0].isDirty = true; + + grid.rowEdit.dirtyRows = [ grid.rows[0] ]; + + grid.api.rowEdit.on.saveRow( $scope, function( rowEntity ){ + grid.api.rowEdit.setSavePromise( rowEntity, promises[promiseCounter].promise); + promiseCounter++; + }); + + // set row saving + uiGridRowEditService.saveRow( grid, grid.rows[0] )(); + + expect( grid.rows[0].isSaving ).toEqual(true); + expect( grid.rowEdit.dirtyRows.length ).toEqual(1); + expect( promiseCounter ).toEqual(1); + + // flush dirty rows, expect no new promise + var overallPromise = uiGridRowEditService.flushDirtyRows( grid ); + overallPromise.then( function() { success = true; }, function() { failure = true; }); + + expect( grid.rows[0].isSaving ).toEqual(true); + expect( grid.rowEdit.dirtyRows.length ).toEqual(1); + expect( promiseCounter ).toEqual(1); + + promises[0].resolve(1); + $rootScope.$apply(); + + expect( grid.rows[0].isSaving ).toEqual(undefined); + expect( grid.rows[0].isDirty ).toEqual(undefined); + expect( grid.rowEdit.dirtyRows.length ).toEqual(0); + expect( success ).toEqual(true); + }); + it( 'three dirty rows, one save fails', function() { var promises = [$q.defer(), $q.defer(), $q.defer()]; var promiseCounter = 0; From 7c0a0dc0a9f8901100ed302319f490e017cdbcb7 Mon Sep 17 00:00:00 2001 From: Paul Lambert Date: Thu, 19 Mar 2015 13:42:34 +1300 Subject: [PATCH 038/173] Enh(saveState): allow save of grouping, tidy up options to work Also fixes minor defects in functionality. --- src/features/grouping/js/grouping.js | 102 ++++++++++++++- src/features/grouping/test/grouping.spec.js | 113 +++++++++++++++- src/features/saveState/js/saveState.js | 113 +++++++++++++--- src/features/saveState/test/saveState.spec.js | 122 +++++++++++++++++- 4 files changed, 424 insertions(+), 26 deletions(-) diff --git a/src/features/grouping/js/grouping.js b/src/features/grouping/js/grouping.js index 16fd4cc0a3..98f0ae6e3b 100644 --- a/src/features/grouping/js/grouping.js +++ b/src/features/grouping/js/grouping.js @@ -253,6 +253,58 @@ */ collapseRow: function ( row ) { service.collapseRow(grid, row); + }, + + /** + * @ngdoc function + * @name getGrouping + * @methodOf ui.grid.grouping.api:PublicApi + * @description Get the grouping configuration for this grid, + * used by the saveState feature + * Returned grouping is an object + * `{ grouping: groupArray, aggregations: aggregateArray, expandedState: hash }` + * where grouping contains an array of objects: + * `{ field: column.field, colName: column.name, groupPriority: column.grouping.groupPriority }` + * and aggregations contains an array of objects: + * `{ field: column.field, colName: column.name, aggregation: column.grouping.aggregation }` + * and expandedState is a hash of the currently expanded nodes + * + * The groupArray will be sorted by groupPriority. + * + * @param {boolean} getExpanded whether or not to return the expanded state + * @returns {object} grouping configuration + */ + getGrouping: function ( getExpanded ) { + var grouping = service.getGrouping(grid); + + grouping.grouping.forEach( function( group ) { + group.colName = group.col.name; + delete group.col; + }); + + grouping.aggregations.forEach( function( aggregation ) { + aggregation.colName = aggregation.col.name; + delete aggregation.col; + }); + + if ( getExpanded ){ + grouping.rowExpandedStates = grid.grouping.rowExpandedStates; + } + + return grouping; + }, + + /** + * @ngdoc function + * @name setGrouping + * @methodOf ui.grid.grouping.api:PublicApi + * @description Set the grouping configuration for this grid, + * used by the saveState feature + * @param {object} config the config you want to apply, in the format + * provided out by getGrouping + */ + setGrouping: function ( config ) { + service.setGrouping(grid, config); } } } @@ -682,6 +734,11 @@ * @param {Grid} grid grid object */ tidyPriorities: function( grid ){ + // if we're called from sortChanged, grid is in this, not passed as param + if ( typeof(grid) === 'undefined' && typeof(this.grid) !== 'undefined' ) { + grid = this.grid; + } + var groupArray = []; var sortArray = []; @@ -1032,7 +1089,7 @@ } } }); - + // sort grouping into priority order groupArray.sort( function(a, b){ return a.groupPriority - b.groupPriority; @@ -1261,7 +1318,48 @@ } } }); - } + }, + + + /** + * @ngdoc function + * @name setGrouping + * @methodOf ui.grid.grouping.service:uiGridGroupingService + * @description Set the grouping based on a config object, used by the save state feature + * (more specifically, by the restore function in that feature ) + * + * @param {Grid} grid grid object + * @param {object} config the config we want to set, same format as that returned by getGrouping + */ + setGrouping: function ( grid, config ){ + if ( typeof(config) === 'undefined' ){ + return; + } + + if ( config.grouping && config.grouping.length && config.grouping.length > 0 ){ + config.grouping.forEach( function( group ) { + var col = grid.getColumn(group.colName); + + if ( col ) { + service.groupColumn( grid, col ); + } + }); + } + + if ( config.aggregations && config.aggregations.length && config.aggregations.length > 0 ){ + config.aggregations.forEach( function( aggregation ) { + var col = grid.getColumn(aggregation.colName); + + if ( col ) { + service.aggregateColumn( grid, col, aggregation.aggregation ); + } + }); + } + + if ( config.rowExpandedStates ){ + grid.grouping.rowExpandedStates = config.rowExpandedStates; + } + } }; diff --git a/src/features/grouping/test/grouping.spec.js b/src/features/grouping/test/grouping.spec.js index a1d5693af6..8927f28d8d 100644 --- a/src/features/grouping/test/grouping.spec.js +++ b/src/features/grouping/test/grouping.spec.js @@ -263,7 +263,118 @@ describe('ui.grid.grouping uiGridGroupingService', function () { }); }); }); - + + + describe('getGrouping via api (returns colName)', function() { + it('should find no grouping', function() { + expect(grid.api.grouping.getGrouping( true )).toEqual({ + grouping: [], + aggregations: [], + rowExpandedStates: {} + }); + }); + + it('should find no grouping, no expanded states', function() { + expect(grid.api.grouping.getGrouping( false )).toEqual({ + grouping: [], + aggregations: [] + }); + }); + + it('should find no grouping, expanded states present', function() { + grid.grouping.rowExpandedStates = { male: { state: 'expanded' } }; + expect(grid.api.grouping.getGrouping( true )).toEqual({ + grouping: [], + aggregations: [], + rowExpandedStates: { male: { state: 'expanded' } } + }); + }); + + it('finds one grouping', function() { + grid.columns[1].grouping = {groupPriority: 0}; + expect(grid.api.grouping.getGrouping(true)).toEqual({ + grouping: [{ field: 'col1', colName: 'col1', groupPriority: 0 }], + aggregations: [], + rowExpandedStates: {} + }); + }); + + it('finds one aggregation, has no priority', function() { + grid.columns[1].grouping = {aggregation: uiGridGroupingConstants.aggregation.COUNT}; + expect(grid.api.grouping.getGrouping(false)).toEqual({ + grouping: [], + aggregations: [{ field: 'col1', colName: 'col1', aggregation: uiGridGroupingConstants.aggregation.COUNT} ] + }); + }); + + it('finds one aggregation, has a priority, aggregation is ignored', function() { + grid.columns[1].grouping = {groupPriority: 0, aggregation: uiGridGroupingConstants.aggregation.COUNT}; + expect(grid.api.grouping.getGrouping(false)).toEqual({ + grouping: [{ field: 'col1', colName: 'col1', groupPriority: 0 }], + aggregations: [] + }); + }); + + it('finds one aggregation, has no priority, aggregation is stored', function() { + grid.columns[1].grouping = {groupPriority: -1, aggregation: uiGridGroupingConstants.aggregation.COUNT}; + expect(grid.api.grouping.getGrouping(false)).toEqual({ + grouping: [], + aggregations: [ { field: 'col1', colName: 'col1', aggregation: uiGridGroupingConstants.aggregation.COUNT } ] + }); + }); + + it('multiple finds, sorts correctly', function() { + grid.columns[1].grouping = {aggregation: uiGridGroupingConstants.aggregation.COUNT}; + grid.columns[2].grouping = {groupPriority: 1}; + grid.columns[3].grouping = {groupPriority: 0, aggregation: uiGridGroupingConstants.aggregation.COUNT}; + expect(grid.api.grouping.getGrouping(false)).toEqual({ + grouping: [ + { field: 'col3', colName: 'col3', groupPriority: 0 }, + { field: 'col2', colName: 'col2', groupPriority: 1 } + ], + aggregations: [ + { field: 'col1', colName: 'col1', aggregation: uiGridGroupingConstants.aggregation.COUNT} + ] + }); + }); + }); + + + describe('setGrouping', function() { + it('no grouping', function() { + grid.api.grouping.setGrouping( + {} + ); + expect(grid.api.grouping.getGrouping( true )).toEqual( + { grouping: [], aggregations: [], rowExpandedStates: {} } + ); + }); + + it('grouping, aggregations and rowExpandedStates', function() { + grid.api.grouping.setGrouping({ + grouping: [ + { field: 'col3', colName: 'col3', groupPriority: 0 }, + { field: 'col2', colName: 'col2', groupPriority: 1 } + ], + aggregations: [ + { field: 'col1', colName: 'col1', aggregation: uiGridGroupingConstants.aggregation.COUNT} + ], + rowExpandedStates: { male: { state: 'expanded' } } + }); + expect(grid.api.grouping.getGrouping(true)).toEqual({ + grouping: [ + { field: 'col3', colName: 'col3', groupPriority: 0 }, + { field: 'col2', colName: 'col2', groupPriority: 1 } + ], + aggregations: [ + { field: 'col1', colName: 'col1', aggregation: uiGridGroupingConstants.aggregation.COUNT} + ], + rowExpandedStates: { male: { state: 'expanded' } } + }); + }); + + }); + describe('insertGroupHeader', function() { it('inserts a header in the middle', function() { diff --git a/src/features/saveState/js/saveState.js b/src/features/saveState/js/saveState.js index fbcc839c5a..0789171762 100644 --- a/src/features/saveState/js/saveState.js +++ b/src/features/saveState/js/saveState.js @@ -20,7 +20,7 @@ *
    */ - var module = angular.module('ui.grid.saveState', ['ui.grid', 'ui.grid.selection', 'ui.grid.cellNav']); + var module = angular.module('ui.grid.saveState', ['ui.grid', 'ui.grid.selection', 'ui.grid.cellNav', 'ui.grid.grouping']); /** * @ngdoc object @@ -118,7 +118,7 @@ * @ngdoc object * @name saveOrder * @propertyOf ui.grid.saveState.api:GridOptions - * @description Save the current column order. Note that unless + * @description Restore the current column order. Note that unless * you've provided the user with some way to reorder their columns (for * example the move columns feature), this makes little sense. *
    Defaults to true @@ -221,6 +221,29 @@ *
    Defaults to true */ gridOptions.saveSelection = gridOptions.saveSelection !== false; + /** + * @ngdoc object + * @name saveGrouping + * @propertyOf ui.grid.saveState.api:GridOptions + * @description Save the grouping configuration. If set to true and the + * grouping feature is not enabled then does nothing. + * + *
    Defaults to true + */ + gridOptions.saveGrouping = gridOptions.saveGrouping !== false; + /** + * @ngdoc object + * @name saveGroupingExpandedStates + * @propertyOf ui.grid.saveState.api:GridOptions + * @description Save the grouping row expanded states. If set to true and the + * grouping feature is not enabled then does nothing. + * + * This can be quite a bit of data, in many cases you wouldn't want to save this + * information. + * + *
    Defaults to false + */ + gridOptions.saveGroupingExpandedStates = gridOptions.saveGroupingExpandedStates === true; }, @@ -240,6 +263,7 @@ savedState.columns = service.saveColumns( grid ); savedState.scrollFocus = service.saveScrollFocus( grid ); savedState.selection = service.saveSelection( grid ); + savedState.grouping = service.saveGrouping( grid ); return savedState; }, @@ -268,6 +292,10 @@ service.restoreSelection( grid, state.selection ); } + if ( state.grouping ){ + service.restoreGrouping( grid, state.grouping ); + } + grid.queueGridRefresh(); }, @@ -290,12 +318,24 @@ grid.columns.forEach( function( column ) { var savedColumn = {}; savedColumn.name = column.name; - savedColumn.visible = column.visible; - savedColumn.width = column.width; + + if ( grid.options.saveVisible ){ + savedColumn.visible = column.visible; + } + + if ( grid.options.saveWidths ){ + savedColumn.width = column.width; + } // these two must be copied, not just pointed too - otherwise our saved state is pointing to the same object as current state - savedColumn.sort = angular.copy( column.sort ); - savedColumn.filters = angular.copy ( column.filters ); + if ( grid.options.saveSort ){ + savedColumn.sort = angular.copy( column.sort ); + } + + if ( grid.options.saveFilter ){ + savedColumn.filters = angular.copy ( column.filters ); + } + columns.push( savedColumn ); }); @@ -375,6 +415,23 @@ }, + /** + * @ngdoc function + * @name saveGrouping + * @methodOf ui.grid.saveState.service:uiGridSaveStateService + * @description Saves the grouping state, if the grouping feature is enabled + * @param {Grid} grid the grid whose state we'd like to save + * @returns {object} the grouping state ready to be saved + */ + saveGrouping: function( grid ){ + if ( !grid.api.grouping || !grid.options.saveGrouping ){ + return {}; + } + + return grid.api.grouping.getGrouping( grid.options.saveGroupingExpandedStates ); + }, + + /** * @ngdoc function * @name getRowVal @@ -415,34 +472,37 @@ */ restoreColumns: function( grid, columnsState ){ columnsState.forEach( function( columnState, index ) { - var currentCol = grid.columns.filter( function( column ) { - return column.name === columnState.name; - }); + var currentCol = grid.getColumn( columnState.name ); - if ( currentCol.length > 0 ){ - var currentIndex = grid.columns.indexOf( currentCol[0] ); + if ( currentCol ){ + var currentIndex = grid.columns.indexOf( currentCol ); - if ( grid.columns[currentIndex].visible !== columnState.visible || - grid.columns[currentIndex].colDef.visible !== columnState.visible ){ + if ( grid.options.saveVisible && + ( grid.columns[currentIndex].visible !== columnState.visible || + grid.columns[currentIndex].colDef.visible !== columnState.visible ) ){ grid.columns[currentIndex].visible = columnState.visible; grid.columns[currentIndex].colDef.visible = columnState.visible; grid.api.core.raise.columnVisibilityChanged( grid.columns[currentIndex]); } - grid.columns[currentIndex].width = columnState.width; + if ( grid.options.saveWidths ){ + grid.columns[currentIndex].width = columnState.width; + } - if ( !angular.equals(grid.columns[currentIndex].sort, columnState.sort) && + if ( grid.options.saveSort && + !angular.equals(grid.columns[currentIndex].sort, columnState.sort) && !( grid.columns[currentIndex].sort === undefined && angular.isEmpty(columnState.sort) ) ){ grid.columns[currentIndex].sort = angular.copy( columnState.sort ); grid.api.core.raise.sortChanged(); } - if ( !angular.equals(grid.columns[currentIndex].filters, columnState.filters ) ){ + if ( grid.options.saveFilter && + !angular.equals(grid.columns[currentIndex].filters, columnState.filters ) ){ grid.columns[currentIndex].filters = angular.copy( columnState.filters ); grid.api.core.raise.filterChanged(); } - if ( currentIndex !== index ){ + if ( grid.options.saveOrder && currentIndex !== index ){ var column = grid.columns.splice( currentIndex, 1 )[0]; grid.columns.splice( index, 0, column ); } @@ -526,7 +586,24 @@ } }); }, - + + + /** + * @ngdoc function + * @name restoreGrouping + * @methodOf ui.grid.saveState.service:uiGridSaveStateService + * @description Restores the grouping configuration, if the grouping feature + * is enabled. + * @param {Grid} grid the grid whose state we'd like to restore + * @param {object} groupingState the grouping state ready to be restored + */ + restoreGrouping: function( grid, groupingState ){ + if ( !grid.api.grouping || typeof(groupingState) === 'undefined' || groupingState === null || angular.equals(groupingState, {}) ){ + return; + } + + grid.api.grouping.setGrouping( groupingState ); + }, /** * @ngdoc function diff --git a/src/features/saveState/test/saveState.spec.js b/src/features/saveState/test/saveState.spec.js index d8324c35c7..0513cba311 100644 --- a/src/features/saveState/test/saveState.spec.js +++ b/src/features/saveState/test/saveState.spec.js @@ -3,6 +3,7 @@ describe('ui.grid.saveState uiGridSaveStateService', function () { var uiGridSaveStateConstants; var uiGridSelectionService; var uiGridCellNavService; + var uiGridGroupingService; var gridClassFactory; var grid; var $compile; @@ -14,11 +15,12 @@ describe('ui.grid.saveState uiGridSaveStateService', function () { beforeEach(inject(function (_uiGridSaveStateService_, _gridClassFactory_, _uiGridSaveStateConstants_, _$compile_, _$rootScope_, _$document_, _uiGridSelectionService_, - _uiGridCellNavService_ ) { + _uiGridCellNavService_, _uiGridGroupingService_ ) { uiGridSaveStateService = _uiGridSaveStateService_; uiGridSaveStateConstants = _uiGridSaveStateConstants_; uiGridSelectionService = _uiGridSelectionService_; uiGridCellNavService = _uiGridCellNavService_; + uiGridGroupingService = _uiGridGroupingService_; gridClassFactory = _gridClassFactory_; $compile = _$compile_; $scope = _$rootScope_.$new(); @@ -71,7 +73,9 @@ describe('ui.grid.saveState uiGridSaveStateService', function () { saveVisible: true, saveSort: true, saveFilter: true, - saveSelection: true + saveSelection: true, + saveGrouping: true, + saveGroupingExpandedStates: false }); }); @@ -85,7 +89,9 @@ describe('ui.grid.saveState uiGridSaveStateService', function () { saveVisible: false, saveSort: false, saveFilter: false, - saveSelection: false + saveSelection: false, + saveGrouping: false, + saveGroupingExpandedStates: true }; uiGridSaveStateService.defaultGridOptions(options); expect( options ).toEqual({ @@ -96,7 +102,9 @@ describe('ui.grid.saveState uiGridSaveStateService', function () { saveVisible: false, saveSort: false, saveFilter: false, - saveSelection: false + saveSelection: false, + saveGrouping: false, + saveGroupingExpandedStates: true }); }); }); @@ -111,6 +119,20 @@ describe('ui.grid.saveState uiGridSaveStateService', function () { { name: 'col4', visible: true, width: 200, sort: [], filters: [ {} ] } ]); }); + + it('save columns with most options turned off', function() { + grid.options.saveWidths = false; + grid.options.saveVisible = false; + grid.options.saveSort = false; + grid.options.saveFilter = false; + + expect( uiGridSaveStateService.saveColumns( grid ) ).toEqual([ + { name: 'col1' }, + { name: 'col2' }, + { name: 'col3' }, + { name: 'col4' } + ]); + }); }); @@ -269,7 +291,13 @@ describe('ui.grid.saveState uiGridSaveStateService', function () { describe('restoreColumns', function() { - it('restore columns', function() { + it('restore columns, all options turned on', function() { + grid.options.saveWidths = true; + grid.options.saveOrder = true; + grid.options.saveVisible = true; + grid.options.saveSort = true; + grid.options.saveFilter = true; + var colVisChangeCount = 0; var colFilterChangeCount = 0; var colSortChangeCount = 0; @@ -327,6 +355,71 @@ describe('ui.grid.saveState uiGridSaveStateService', function () { expect( colFilterChangeCount ).toEqual( 1, '1 columns changed filter'); expect( colSortChangeCount ).toEqual( 4, '4 columns changed sort'); }); + + it('restore columns, all options turned off', function() { + grid.options.saveWidths = false; + grid.options.saveOrder = false; + grid.options.saveVisible = false; + grid.options.saveSort = false; + grid.options.saveFilter = false; + + var colVisChangeCount = 0; + var colFilterChangeCount = 0; + var colSortChangeCount = 0; + + grid.api.core.on.columnVisibilityChanged( $scope, function( column ) { + colVisChangeCount++; + }); + + grid.api.core.on.filterChanged( $scope, function() { + colFilterChangeCount++; + }); + + grid.api.core.on.sortChanged( $scope, function() { + colSortChangeCount++; + }); + + uiGridSaveStateService.restoreColumns( grid, [ + { name: 'col2', visible: false, width: 90, sort: [ {blah: 'blah'} ], filters: [ {} ] }, + { name: 'col1', visible: true, width: '*', sort: [], filters: [ {'blah': 'blah'} ] }, + { name: 'col4', visible: false, width: 120, sort: [], filters: [ {} ] }, + { name: 'col3', visible: true, width: 220, sort: [], filters: [ {} ] } + ]); + + expect( grid.columns[0].name ).toEqual('col1', 'column 0 name should be col1'); + expect( grid.columns[1].name ).toEqual('col2', 'column 1 name should be col2'); + expect( grid.columns[2].name ).toEqual('col3', 'column 2 name should be col3'); + expect( grid.columns[3].name ).toEqual('col4', 'column 3 name should be col4'); + + expect( grid.columns[0].visible ).toEqual(true, 'column 0 visible should be true'); + expect( grid.columns[1].visible ).toEqual(true, 'column 1 visible should be true'); + expect( grid.columns[2].visible ).toEqual(false, 'column 2 visible should be false'); + expect( grid.columns[3].visible ).toEqual(true, 'column 3 visible should be true'); + + expect( grid.columns[0].colDef.visible ).toEqual(undefined, 'coldef 0 visible should be undefined'); + expect( grid.columns[1].colDef.visible ).toEqual(undefined, 'coldef 1 visible should be undefined'); + expect( grid.columns[2].colDef.visible ).toEqual(undefined, 'coldef 2 visible should be undefined'); + expect( grid.columns[3].colDef.visible ).toEqual(undefined, 'coldef 3 visible should be undefined'); + + expect( grid.columns[0].width ).toEqual(50); + expect( grid.columns[1].width ).toEqual('*'); + expect( grid.columns[2].width ).toEqual(100); + expect( grid.columns[3].width ).toEqual(200); + + expect( grid.columns[0].sort ).toEqual([]); + expect( grid.columns[1].sort ).toEqual([]); + expect( grid.columns[2].sort ).toEqual([]); + expect( grid.columns[3].sort ).toEqual([]); + + expect( grid.columns[0].filters ).toEqual([ {} ]); + expect( grid.columns[1].filters ).toEqual([ {} ]); + expect( grid.columns[2].filters ).toEqual([ {} ]); + expect( grid.columns[3].filters ).toEqual([ {} ]); + + expect( colVisChangeCount ).toEqual( 0, '0 columns changed visibility'); + expect( colFilterChangeCount ).toEqual( 0, '0 columns changed filter'); + expect( colSortChangeCount ).toEqual( 0, '0 columns changed sort'); + }); }); @@ -500,6 +593,25 @@ describe('ui.grid.saveState uiGridSaveStateService', function () { expect( grid.api.selection.getSelectedGridRows().length ).toEqual( 0 ); }); }); + + describe('restoreGrouping', function() { + beforeEach( function() { + grid.api.grouping = { setGrouping: function() {}}; + spyOn( grid.api.grouping, 'setGrouping' ).andCallFake(function() {}); + }); + + it( 'calls setGrouping with config', function() { + uiGridSaveStateService.restoreGrouping( grid, { grouping: [], aggregations: [] }); + + expect(grid.api.grouping.setGrouping).toHaveBeenCalledWith( { grouping: [], aggregations: [] }); + }); + + it( 'doesn\'t call setGrouping when config missing', function() { + uiGridSaveStateService.restoreGrouping( grid, undefined); + + expect(grid.api.grouping.setGrouping).not.toHaveBeenCalled(); + }); + }); describe('findRowByIdentity', function() { From 69c1718841b90b65a4d5c3b43dc13365b4400bb3 Mon Sep 17 00:00:00 2001 From: Paul Lambert Date: Thu, 19 Mar 2015 14:38:22 +1300 Subject: [PATCH 039/173] Fix(grouping) fix #3051 angular.forEach needed, not array.forEach --- src/features/grouping/js/grouping.js | 2 +- src/templates/ui-grid/uiGridHeaderCell.html | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/features/grouping/js/grouping.js b/src/features/grouping/js/grouping.js index 98f0ae6e3b..d519ea92d7 100644 --- a/src/features/grouping/js/grouping.js +++ b/src/features/grouping/js/grouping.js @@ -815,7 +815,7 @@ expandedStatesSubset.state = targetState; // set all child nodes - expandedStatesSubset.forEach( function( childNode, key){ + angular.forEach(expandedStatesSubset, function( childNode, key){ if (key !== 'state'){ service.setAllNodes(childNode, targetState); } diff --git a/src/templates/ui-grid/uiGridHeaderCell.html b/src/templates/ui-grid/uiGridHeaderCell.html index bd220cfcc3..e7ecc67585 100644 --- a/src/templates/ui-grid/uiGridHeaderCell.html +++ b/src/templates/ui-grid/uiGridHeaderCell.html @@ -13,7 +13,8 @@
    - + +
      From 1cad03b8d13faee39e8fba0fe6384cecdaf40676 Mon Sep 17 00:00:00 2001 From: Paul Lambert Date: Thu, 19 Mar 2015 16:52:24 +1300 Subject: [PATCH 040/173] Enh(filtering): added capability for dropdowns --- misc/tutorial/103_filtering.ngdoc | 32 +++++++++++++++-- src/js/core/constants.js | 4 ++- src/js/core/factories/GridColumn.js | 18 +++++++--- src/less/header.less | 34 +++++++++++++++++++ src/templates/ui-grid/uiGridColumnFilter.html | 15 -------- src/templates/ui-grid/uiGridHeaderCell.html | 18 +++++++--- 6 files changed, 95 insertions(+), 26 deletions(-) delete mode 100644 src/templates/ui-grid/uiGridColumnFilter.html diff --git a/misc/tutorial/103_filtering.ngdoc b/misc/tutorial/103_filtering.ngdoc index 78774817e4..93912a0a05 100644 --- a/misc/tutorial/103_filtering.ngdoc +++ b/misc/tutorial/103_filtering.ngdoc @@ -50,6 +50,14 @@ for `filters: [{ term: 'xxx' }]`. See the "age" column below for an example. The example also includes date filters. These work, however there isn't a date chooser in the filter widget - so you may need to implement a custom field if you want to filter dates in this way. +### Dropdowns +Filtering supports dropdowns, in order to set a particular column to use a dropdown you should set: + `type: uiGridConstants.filter.SELECT` +and + `selectOptions: [ { value: 'x', label: 'decode of x' } , .... ]` + +If you need to internationalize the labels you'll need to complete that before providing the selectOptions array. + @example @@ -69,7 +77,12 @@ if you want to filter dates in this way. // default { field: 'name' }, // pre-populated search field - { field: 'gender', filter: { term: 'male' } }, + { field: 'gender', filter: { + term: '1', + type: uiGridConstants.filter.SELECT, + selectOptions: [ { value: '1', label: 'male' }, { value: '2', label: 'female' }, { value: '3', label: 'unknown'}, { value: '4', label: 'not stated' }, { value: '5', label: 'a really long value that extends things' } ] + }, + cellFilter: 'mapGender' }, // no filter input { field: 'company', enableFiltering: false, filter: { noTerm: true, @@ -124,6 +137,7 @@ if you want to filter dates in this way. data.forEach( function addDates( row, index ){ row.mixedDate = new Date(); row.mixedDate.setDate(today.getDate() + ( index % 14 ) ); + row.gender = row.gender==='male' ? '1' : '2'; }); }); @@ -131,7 +145,21 @@ if you want to filter dates in this way. $scope.gridOptions.enableFiltering = !$scope.gridOptions.enableFiltering; $scope.gridApi.core.notifyDataChange( uiGridConstants.dataChange.COLUMN ); }; - }]); + }]) + .filter('mapGender', function() { + var genderHash = { + 1: 'male', + 2: 'female' + }; + + return function(input) { + if (!input){ + return ''; + } else { + return genderHash[input]; + } + }; + });
    diff --git a/src/js/core/constants.js b/src/js/core/constants.js index 33a8ed8418..90c0b25d53 100644 --- a/src/js/core/constants.js +++ b/src/js/core/constants.js @@ -72,7 +72,9 @@ GREATER_THAN_OR_EQUAL: 64, LESS_THAN: 128, LESS_THAN_OR_EQUAL: 256, - NOT_EQUAL: 512 + NOT_EQUAL: 512, + SELECT: 'select', + INPUT: 'input' }, aggregationTypes: { diff --git a/src/js/core/factories/GridColumn.js b/src/js/core/factories/GridColumn.js index 7cc1c2bbde..8e818967ba 100644 --- a/src/js/core/factories/GridColumn.js +++ b/src/js/core/factories/GridColumn.js @@ -86,7 +86,7 @@ angular.module('ui.grid') * @propertyOf ui.grid.class:GridColumn * @description Filter on this column. * @example - *
    { term: 'text', condition: uiGridConstants.filter.STARTS_WITH, placeholder: 'type to filter...', flags: { caseSensitive: false } }
    + *
    { term: 'text', condition: uiGridConstants.filter.STARTS_WITH, placeholder: 'type to filter...', flags: { caseSensitive: false }, type: uiGridConstants.filter.SELECT, [ { value: 1, label: 'male' }, { value: 2, label: 'female' } ] }
    * */ @@ -227,7 +227,9 @@ angular.module('ui.grid') * term: 'aa', * condition: uiGridConstants.filter.STARTS_WITH, * placeholder: 'starts with...', - * flags: { caseSensitive: false } + * flags: { caseSensitive: false }, + * type: uiGridConstants.filter.SELECT, + * selectOptions: [ { value: 1, label: 'male' }, { value: 2, label: 'female' } ] * }, * { * condition: uiGridConstants.filter.ENDS_WITH, @@ -251,7 +253,9 @@ angular.module('ui.grid') * term: 'foo', // ngModel for * condition: uiGridConstants.filter.STARTS_WITH, * placeholder: 'starts with...', - * flags: { caseSensitive: false } + * flags: { caseSensitive: false }, + * type: uiGridConstants.filter.SELECT, + * selectOptions: [ { value: 1, label: 'male' }, { value: 2, label: 'female' } ] * }, * { * term: 'baz', @@ -549,6 +553,10 @@ angular.module('ui.grid') * your custom function doesn't require a term (so it can run even when the term is null) * - flags: only flag currently available is `caseSensitive`, set to false if you don't want * case sensitive matching + * - type: defaults to uiGridConstants.filter.INPUT, which gives a text box. If set to uiGridConstants.filter.SELECT + * then a select box will be shown with options selectOptions + * - selectOptions: options in the format `[ { value: 1, label: 'male' }]`. No i18n filter is provided, you need + * to perform the i18n on the values before you provide them * @example *
    $scope.gridOptions.columnDefs = [ 
          *   {
    @@ -557,7 +565,9 @@ angular.module('ui.grid')
          *       term: 'xx',
          *       condition: uiGridConstants.filter.STARTS_WITH,
          *       placeholder: 'starts with...',
    -     *       flags: { caseSensitive: false }
    +     *       flags: { caseSensitive: false },
    +     *       type: uiGridConstants.filter.SELECT,
    +     *       selectOptions: [ { value: 1, label: 'male' }, { value: 2, label: 'female' } ]
          *     }
          *   }
          * ]; 
    diff --git a/src/less/header.less b/src/less/header.less index efab7a319b..8afba48bd8 100644 --- a/src/less/header.less +++ b/src/less/header.less @@ -166,6 +166,26 @@ } } } + + .ui-grid-filter-button-select { + position: absolute; + top: 0; + bottom: 0; + right: 0; + + [class^="ui-grid-icon"] { + position: absolute; + top: 50%; + line-height: 32px; + margin-top: -16px; + right: 0px; + opacity: 0.66; + + &:hover { + opacity: 1; + } + } + } } input[type="text"].ui-grid-filter-input { @@ -177,6 +197,20 @@ input[type="text"].ui-grid-filter-input { border: @gridBorderWidth solid @borderColor; .border-radius(@gridBorderRadius); + &:hover { + border: @gridBorderWidth solid @borderColor; + } +} + +select.ui-grid-filter-select { + padding: 0; + margin: 0; + border: 0; + width: 90%; + + border: @gridBorderWidth solid @borderColor; + .border-radius(@gridBorderRadius); + &:hover { border: @gridBorderWidth solid @borderColor; } diff --git a/src/templates/ui-grid/uiGridColumnFilter.html b/src/templates/ui-grid/uiGridColumnFilter.html deleted file mode 100644 index 569797a56e..0000000000 --- a/src/templates/ui-grid/uiGridColumnFilter.html +++ /dev/null @@ -1,15 +0,0 @@ -
  • -
    - - -
    -   -
    -
    - -
    -
    -   -
    -
    -
  • \ No newline at end of file diff --git a/src/templates/ui-grid/uiGridHeaderCell.html b/src/templates/ui-grid/uiGridHeaderCell.html index e7ecc67585..d55070e328 100644 --- a/src/templates/ui-grid/uiGridHeaderCell.html +++ b/src/templates/ui-grid/uiGridHeaderCell.html @@ -13,11 +13,21 @@
    - - +
    + -
    -   +
    +   +
    + +
    + + +
    +   +
    +
    +
    From 707dc53bc05dedd31d2f223e8c495d1ae9f850b8 Mon Sep 17 00:00:00 2001 From: AlphaHinex Date: Tue, 17 Mar 2015 11:14:38 +0800 Subject: [PATCH 041/173] load all data before export all if needed add comments fix bugs correct tutorial number fix a bug change getPage function show the first page on load use promise way clean code change property name add comments fix a bug --- .../405_exporting_all_data_complex.ngdoc | 107 ++++++++++++++++++ src/features/exporter/js/exporter.js | 94 ++++++++++++--- src/features/exporter/test/exporter.spec.js | 3 + 3 files changed, 186 insertions(+), 18 deletions(-) create mode 100644 misc/tutorial/405_exporting_all_data_complex.ngdoc diff --git a/misc/tutorial/405_exporting_all_data_complex.ngdoc b/misc/tutorial/405_exporting_all_data_complex.ngdoc new file mode 100644 index 0000000000..4c93181685 --- /dev/null +++ b/misc/tutorial/405_exporting_all_data_complex.ngdoc @@ -0,0 +1,107 @@ +@ngdoc overview +@name Tutorial: 405 Exporting All Data With External Pagination +@description + +When using build in pagination, the data is fully loaded before export. + +For external pagination, use the `exportAll` event to load all grid data and then disable external pagination. + +@example +This shows combined external pagination and sorting. + + + var app = angular.module('app', ['ngTouch', 'ui.grid', 'ui.grid.pagination', 'ui.grid.selection', 'ui.grid.exporter']); + + app.controller('MainCtrl', [ + '$scope', '$http', 'uiGridConstants', '$q', function($scope, $http, uiGridConstants, $q) { + + var paginationOptions = { + sort: null + }; + + $scope.gridOptions = { + paginationPageSizes: [25, 50, 75], + paginationPageSize: 25, + useExternalPagination: true, + useExternalSorting: true, + enableGridMenu: true, + columnDefs: [ + { name: 'name' }, + { name: 'gender', enableSorting: false }, + { name: 'company', enableSorting: false } + ], + exporterAllDataPromise: function() { + return getPage(1, $scope.gridOptions.totalItems, paginationOptions.sort) + .then(getPageSuccess) + .then(function() { + $scope.gridOptions.useExternalPagination = false; + $scope.gridOptions.useExternalSorting = false; + getPage = null; + }); + }, + onRegisterApi: function(gridApi) { + $scope.gridApi = gridApi; + $scope.gridApi.core.on.sortChanged($scope, function(grid, sortColumns) { + if(getPage) { + if (sortColumns.length > 0) { + paginationOptions.sort = sortColumns[0].sort.direction; + } else { + paginationOptions.sort = null; + } + getPage(grid.options.paginationCurrentPage, grid.options.paginationPageSize, paginationOptions.sort).then(getPageSuccess); + } + }); + gridApi.pagination.on.paginationChanged($scope, function (newPage, pageSize) { + if(getPage) { + getPage(newPage, pageSize, paginationOptions.sort).then(getPageSuccess); + } + }); + } + }; + + var getPage = function(curPage, pageSize, sort) { + var url; + switch(sort) { + case uiGridConstants.ASC: + url = '/data/100_ASC.json'; + break; + case uiGridConstants.DESC: + url = '/data/100_DESC.json'; + break; + default: + url = '/data/100.json'; + break; + } + + var deferred = $q.defer(); + $http.get(url) + .success(function (data) { + var firstRow = (curPage - 1) * pageSize; + deferred.resolve({ + totalItems: 100, + data: data.slice(firstRow, firstRow + pageSize) + }); + }); + return deferred.promise; + }; + + var getPageSuccess = function(result) { + $scope.gridOptions.totalItems = result.totalItems; + $scope.gridOptions.data = result.data; + }; + + getPage(1, $scope.gridOptions.paginationPageSize).then(getPageSuccess); + } + ]); + + +
    +
    +
    +
    + + .grid { + width: 600px; + } + +
    diff --git a/src/features/exporter/js/exporter.js b/src/features/exporter/js/exporter.js index 16cfee8d75..0297b66082 100644 --- a/src/features/exporter/js/exporter.js +++ b/src/features/exporter/js/exporter.js @@ -76,6 +76,8 @@ var service = { + delay: 100, + initializeGrid: function (grid) { //add feature namespace and any properties to grid for needed state @@ -144,7 +146,7 @@ if (grid.api.core.addToGridMenu){ service.addToMenu( grid ); } - }, 100, 1); + }, this.delay, 1); } }, @@ -451,6 +453,28 @@ * */ gridOptions.exporterFieldCallback = gridOptions.exporterFieldCallback ? gridOptions.exporterFieldCallback : function( grid, row, col, value ) { return value; }; + + /** + * @ngdoc function + * @name exporterAllDataPromise + * @propertyOf ui.grid.exporter.api:GridOptions + * @description This promise is needed when exporting all rows, + * and the data need to be provided by server side. Default is null. + * @returns {Promise} a promise to load all data from server + * + * @example + *
    +           *   gridOptions.exporterAllDataPromise = function () {
    +           *     var deferred = $q.defer();
    +           *     $http.get('/data/100.json')
    +           *       .success(function ( data ) {
    +           *         deferred.resolve( data );
    +           *       });
    +           *     return deferred.promise;
    +           *   }
    +           * 
    + */ + gridOptions.exporterAllDataPromise = gridOptions.exporterAllDataPromise ? gridOptions.exporterAllDataPromise : null; }, @@ -539,14 +563,46 @@ * uiGridExporterConstants.SELECTED */ csvExport: function (grid, rowTypes, colTypes) { - var exportColumnHeaders = this.getColumnHeaders(grid, colTypes); - var exportData = this.getData(grid, rowTypes, colTypes); - var csvContent = this.formatAsCsv(exportColumnHeaders, exportData, grid.options.exporterCsvColumnSeparator); - - this.downloadFile (grid.options.exporterCsvFilename, csvContent, grid.options.exporterOlderExcelCompatibility); + this.loadAllDataIfNeeded(this, grid, rowTypes, colTypes, function(ref, grid, rowTypes, colTypes) { + var exportColumnHeaders = ref.getColumnHeaders(grid, colTypes); + var exportData = ref.getData(grid, rowTypes, colTypes); + var csvContent = ref.formatAsCsv(exportColumnHeaders, exportData, grid.options.exporterCsvColumnSeparator); + + ref.downloadFile (grid.options.exporterCsvFilename, csvContent, grid.options.exporterOlderExcelCompatibility); + }); }, - - + + /** + * @ngdoc function + * @name loadAllDataIfNeeded + * @methodOf ui.grid.exporter.service:uiGridExporterService + * @description When using server side pagination, raise exportAll event to load all data, + * and call callback function till all data loaded. + * When using client side pagination, call callback function directly + * @param {object} ref reference used by callback + * @param {Grid} grid the grid from which data should be exported + * @param {string} rowTypes which rows to export, valid values are + * uiGridExporterConstants.ALL, uiGridExporterConstants.VISIBLE, + * uiGridExporterConstants.SELECTED + * @param {string} colTypes which columns to export, valid values are + * uiGridExporterConstants.ALL, uiGridExporterConstants.VISIBLE, + * uiGridExporterConstants.SELECTED + * @param {object} callback callback function + */ + loadAllDataIfNeeded: function (ref, grid, rowTypes, colTypes, callback) { + if ( rowTypes === uiGridExporterConstants.ALL && grid.rows.length !== grid.options.totalItems && grid.options.exporterAllDataPromise) { + grid.options.exporterAllDataPromise() + .then(function() { + grid.modifyRows(grid.options.data); + }) + .then(function() { + callback(ref, grid, rowTypes, colTypes); + }); + } else { + callback(ref, grid, rowTypes, colTypes); + } + }, + /** * @ngdoc property * @propertyOf ui.grid.exporter.api:ColumnDef @@ -824,7 +880,7 @@ } D.body.removeChild(a); - }, 100); + }, this.delay); }, /** @@ -845,15 +901,17 @@ * uiGridExporterConstants.SELECTED */ pdfExport: function (grid, rowTypes, colTypes) { - var exportColumnHeaders = this.getColumnHeaders(grid, colTypes); - var exportData = this.getData(grid, rowTypes, colTypes); - var docDefinition = this.prepareAsPdf(grid, exportColumnHeaders, exportData); - - if (this.isIE()) { - var pdf = pdfMake.createPdf(docDefinition).download(); - } else { - pdfMake.createPdf(docDefinition).open(); - } + this.loadAllDataIfNeeded(this, grid, rowTypes, colTypes, function(ref, grid, rowTypes, colTypes) { + var exportColumnHeaders = ref.getColumnHeaders(grid, colTypes); + var exportData = ref.getData(grid, rowTypes, colTypes); + var docDefinition = ref.prepareAsPdf(grid, exportColumnHeaders, exportData); + + if (ref.isIE()) { + var pdf = pdfMake.createPdf(docDefinition).download(); + } else { + pdfMake.createPdf(docDefinition).open(); + } + }); }, diff --git a/src/features/exporter/test/exporter.spec.js b/src/features/exporter/test/exporter.spec.js index 96bcb76053..15ac09eb2b 100644 --- a/src/features/exporter/test/exporter.spec.js +++ b/src/features/exporter/test/exporter.spec.js @@ -83,6 +83,7 @@ describe('ui.grid.exporter uiGridExporterService', function () { exporterMenuCsv: true, exporterMenuPdf: true, exporterFieldCallback: jasmine.any(Function), + exporterAllDataPromise: null, exporterSuppressColumns: [] }); }); @@ -108,6 +109,7 @@ describe('ui.grid.exporter uiGridExporterService', function () { exporterMenuCsv: false, exporterMenuPdf: false, exporterFieldCallback: callback, + exporterAllDataPromise: callback, exporterSuppressColumns: [ 'buttons' ] }; uiGridExporterService.defaultGridOptions(options); @@ -130,6 +132,7 @@ describe('ui.grid.exporter uiGridExporterService', function () { exporterMenuCsv: false, exporterMenuPdf: false, exporterFieldCallback: callback, + exporterAllDataPromise: callback, exporterSuppressColumns: [ 'buttons' ] }); }); From ceb69c76dc00a7b13523e54bd5a7bca528ba6640 Mon Sep 17 00:00:00 2001 From: Paul Lambert Date: Fri, 20 Mar 2015 10:47:45 +1300 Subject: [PATCH 042/173] Enh(filters): allow changes to colDef.filter other than term --- src/js/core/factories/GridColumn.js | 20 ++++++++++++++++- test/unit/core/factories/GridColumn.spec.js | 24 +++++++++++++++++++++ 2 files changed, 43 insertions(+), 1 deletion(-) diff --git a/src/js/core/factories/GridColumn.js b/src/js/core/factories/GridColumn.js index 8e818967ba..b1aa56faf1 100644 --- a/src/js/core/factories/GridColumn.js +++ b/src/js/core/factories/GridColumn.js @@ -528,7 +528,9 @@ angular.module('ui.grid') if (colDef.filter) { defaultFilters.push(colDef.filter); } - else { + else if ( colDef.filters ){ + defaultFilters = colDef.filters; + } else { // Add an empty filter definition object, which will // translate to a guessed condition and no pre-populated // value for the filter . @@ -592,9 +594,25 @@ angular.module('ui.grid') // Only set filter if this is a newly added column, if we're updating an existing // column then we don't want to put the default filter back if the user may have already // removed it. + // However, we do want to keep the settings if they change, just not the term if ( isNew ) { self.setPropertyOrDefault(colDef, 'filter'); self.setPropertyOrDefault(colDef, 'filters', defaultFilters); + } else if ( self.filters.length === defaultFilters.length ) { + self.filters.forEach( function( filter, index ){ + if (typeof(filter.placeholder) !== 'undefined') { + filter.placeholder = defaultFilters[index].placeholder; + } + if (typeof(filter.flags) !== 'undefined') { + filter.flags = defaultFilters[index].flags; + } + if (typeof(filter.type) !== 'undefined') { + filter.type = defaultFilters[index].type; + } + if (typeof(filter.selectOptions) !== 'undefined') { + filter.selectOptions = defaultFilters[index].selectOptions; + } + }); } // Remove this column from the grid sorting, include inside build columns so has diff --git a/test/unit/core/factories/GridColumn.spec.js b/test/unit/core/factories/GridColumn.spec.js index fc2417dd8a..c17391f9bc 100644 --- a/test/unit/core/factories/GridColumn.spec.js +++ b/test/unit/core/factories/GridColumn.spec.js @@ -67,6 +67,30 @@ describe('GridColumn factory', function () { }); }); + it('should update everything but term when updating filters', function () { + var filter = { term: 'x', placeholder: 'placeholder', type: uiGridConstants.filter.SELECT, selectOptions: [ { value: 1, label: "male" } ] }; + grid.options.columnDefs[0].filter = filter; + + runs(buildCols); + + runs(function () { + expect(grid.columns[0].filters).toEqual([ { placeholder: 'placeholder', type: uiGridConstants.filter.SELECT, selectOptions: [ { value: 1, label: "male" } ] } ] ); + }); + }); + + + it('should update everything but term when updating filters', function () { + var filters = [{ term: 'x', placeholder: 'placeholder', type: uiGridConstants.filter.SELECT, selectOptions: [ { value: 1, label: "male" } ] }]; + grid.options.columnDefs[0].filters = filters; + + runs(buildCols); + + runs(function () { + expect(grid.columns[0].filters).toEqual([ { placeholder: 'placeholder', type: uiGridConstants.filter.SELECT, selectOptions: [ { value: 1, label: "male" } ] } ] ); + }); + }); + + it('should obey columnDef sort spec', function () { // ... TODO(c0bra) From 1356171a8bdad03c1a4e852c421fce981f28cd9e Mon Sep 17 00:00:00 2001 From: priceld Date: Fri, 20 Mar 2015 09:21:22 -0400 Subject: [PATCH 043/173] Fix #3061: Add option to opt out of initial grid resize --- src/js/core/directives/ui-grid.js | 2 +- src/js/core/factories/GridOptions.js | 12 +++++++++++- test/unit/core/factories/GridOptions.spec.js | 5 +++++ 3 files changed, 17 insertions(+), 2 deletions(-) diff --git a/src/js/core/directives/ui-grid.js b/src/js/core/directives/ui-grid.js index 97ce1fe9b7..4608370b2c 100644 --- a/src/js/core/directives/ui-grid.js +++ b/src/js/core/directives/ui-grid.js @@ -236,7 +236,7 @@ angular.module('ui.grid').directive('uiGrid', grid.gridHeight = $scope.gridHeight = gridUtil.elementHeight($elm); // If the grid isn't tall enough to fit a single row, it's kind of useless. Resize it to fit a minimum number of rows - if (grid.gridHeight < grid.options.rowHeight) { + if (grid.gridHeight < grid.options.rowHeight && grid.options.enableMinHeightCheck) { // Figure out the new height var contentHeight = grid.options.minRowsToShow * grid.options.rowHeight; var headerHeight = grid.options.showHeader ? grid.options.headerRowHeight : 0; diff --git a/src/js/core/factories/GridOptions.js b/src/js/core/factories/GridOptions.js index 00c0414ebb..137ad47328 100644 --- a/src/js/core/factories/GridOptions.js +++ b/src/js/core/factories/GridOptions.js @@ -352,7 +352,17 @@ angular.module('ui.grid') * Supported values: uiGridConstants.scrollbars.ALWAYS, uiGridConstants.scrollbars.NEVER */ baseOptions.enableHorizontalScrollbar = typeof(baseOptions.enableHorizontalScrollbar) !== "undefined" ? baseOptions.enableHorizontalScrollbar : uiGridConstants.scrollbars.ALWAYS; - + + /** + * @ngdoc boolean + * @name enableMinHeightCheck + * @propertyOf ui.grid.class:GridOptions + * @description True by default. When enabled, a newly initialized grid will check to see if it is tall enough to display + * at least one row of data. If the grid is not tall enough, it will resize the DOM element to display minRowsToShow number + * of rows. + */ + baseOptions.enableMinHeightCheck = baseOptions.enableMinHeightCheck !== false; + /** * @ngdoc boolean * @name minimumColumnSize diff --git a/test/unit/core/factories/GridOptions.spec.js b/test/unit/core/factories/GridOptions.spec.js index 5a71784e0b..f8363e3b27 100644 --- a/test/unit/core/factories/GridOptions.spec.js +++ b/test/unit/core/factories/GridOptions.spec.js @@ -42,6 +42,7 @@ describe('GridOptions factory', function () { enableColumnMenus: true, enableVerticalScrollbar: 1, enableHorizontalScrollbar: 1, + enableMinHeightCheck: true, minimumColumnSize: 10, rowEquality: jasmine.any(Function), headerTemplate: null, @@ -84,6 +85,7 @@ describe('GridOptions factory', function () { enableColumnMenus: true, enableVerticalScrollbar: 1, enableHorizontalScrollbar: 1, + enableMinHeightCheck: true, minimumColumnSize: 20, rowEquality: testFunction, headerTemplate: 'testHeader', @@ -123,6 +125,7 @@ describe('GridOptions factory', function () { enableColumnMenus: true, enableVerticalScrollbar: 1, enableHorizontalScrollbar: 1, + enableMinHeightCheck: true, minimumColumnSize: 20, rowEquality: testFunction, headerTemplate: 'testHeader', @@ -166,6 +169,7 @@ describe('GridOptions factory', function () { enableColumnMenus: false, enableVerticalScrollbar: 0, enableHorizontalScrollbar: 0, + enableMinHeightCheck: false, minimumColumnSize: 10, rowEquality: testFunction, headerTemplate: 'testHeader', @@ -204,6 +208,7 @@ describe('GridOptions factory', function () { enableColumnMenus: false, enableVerticalScrollbar: 0, enableHorizontalScrollbar: 0, + enableMinHeightCheck: false, minimumColumnSize: 10, rowEquality: testFunction, headerTemplate: 'testHeader', From 96505bc20fc48c435707463e4746c7a695531d20 Mon Sep 17 00:00:00 2001 From: Charles King Date: Fri, 20 Mar 2015 13:33:55 -0700 Subject: [PATCH 044/173] Update rowSorter.js Updated rowSorter to use an object instead of an array. This fixes an edge case where, you have a column named "sort", and the code would basically overwrite the default Array.sort function. --- src/js/core/services/rowSorter.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/js/core/services/rowSorter.js b/src/js/core/services/rowSorter.js index 1693694f5f..609bb0072d 100644 --- a/src/js/core/services/rowSorter.js +++ b/src/js/core/services/rowSorter.js @@ -26,7 +26,7 @@ module.service('rowSorter', ['$parse', 'uiGridConstants', function ($parse, uiGr // Cache of sorting functions. Once we create them, we don't want to keep re-doing it // this takes a piece of data from the cell and tries to determine its type and what sorting // function to use for it - colSortFnCache: [] + colSortFnCache: {} }; @@ -468,4 +468,4 @@ module.service('rowSorter', ['$parse', 'uiGridConstants', function ($parse, uiGr return rowSorter; }]); -})(); \ No newline at end of file +})(); From bbf2e33629d2253ca3e0a8d63431bad778478be2 Mon Sep 17 00:00:00 2001 From: Paul Lambert Date: Sat, 21 Mar 2015 10:41:04 +1300 Subject: [PATCH 045/173] Fix(rowSorter): fix #3074 1177uiGridIndex rather than --- src/js/core/services/rowSorter.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/js/core/services/rowSorter.js b/src/js/core/services/rowSorter.js index 1693694f5f..4d784cb082 100644 --- a/src/js/core/services/rowSorter.js +++ b/src/js/core/services/rowSorter.js @@ -413,7 +413,7 @@ module.service('rowSorter', ['$parse', 'uiGridConstants', function ($parse, uiGr // put a custom index field on each row, used to make a stable sort out of unstable sorts (e.g. Chrome) var setIndex = function( row, idx ){ - row.entity.$uiGridIndex = idx; + row.entity.$$uiGridIndex = idx; }; rows.forEach(setIndex); @@ -443,7 +443,7 @@ module.service('rowSorter', ['$parse', 'uiGridConstants', function ($parse, uiGr // then return the previous order using our custom // index variable if (tem === 0 ) { - return rowA.entity.$uiGridIndex - rowB.entity.$uiGridIndex; + return rowA.entity.$$uiGridIndex - rowB.entity.$$uiGridIndex; } // Made it this far, we don't have to worry about null & undefined @@ -458,9 +458,9 @@ module.service('rowSorter', ['$parse', 'uiGridConstants', function ($parse, uiGr // remove the custom index field on each row, used to make a stable sort out of unstable sorts (e.g. Chrome) var clearIndex = function( row, idx ){ - delete row.entity.$uiGridIndex; + delete row.entity.$$uiGridIndex; }; - rows.forEach(setIndex); + rows.forEach(clearIndex); return newRows; }; From 5e165815f55f0818ee442558ba549c072888b134 Mon Sep 17 00:00:00 2001 From: Paul Lambert Date: Sat, 21 Mar 2015 11:28:27 +1300 Subject: [PATCH 046/173] Fix(grouping, rowEdit): remove console.logs that had crept in --- src/features/grouping/test/grouping.spec.js | 11 ----------- src/features/row-edit/js/gridRowEdit.js | 1 - 2 files changed, 12 deletions(-) diff --git a/src/features/grouping/test/grouping.spec.js b/src/features/grouping/test/grouping.spec.js index 8927f28d8d..5d5003635b 100644 --- a/src/features/grouping/test/grouping.spec.js +++ b/src/features/grouping/test/grouping.spec.js @@ -81,17 +81,6 @@ describe('ui.grid.grouping uiGridGroupingService', function () { var groupedRows = uiGridGroupingService.groupRows.call( grid, grid.rows ); -/* - console.log('data'); - for (var i = 0; i < 10; i++) { - console.log(grid.options.data[i]); - } - - console.log('results'); - for (i = 0; i < 18; i++) { - console.log(grid.rows[i].entity); - } -*/ expect( groupedRows.length ).toEqual( 18, 'we\'ve added 3 col0 headers, and 5 col2 headers' ); }); }); diff --git a/src/features/row-edit/js/gridRowEdit.js b/src/features/row-edit/js/gridRowEdit.js index 5912084c08..b2f453355d 100644 --- a/src/features/row-edit/js/gridRowEdit.js +++ b/src/features/row-edit/js/gridRowEdit.js @@ -226,7 +226,6 @@ var promise = grid.api.rowEdit.raise.saveRow( gridRow.entity ); if ( gridRow.rowEditSavePromise ){ - console.log(gridRow.rowEditSavePromise); gridRow.rowEditSavePromise.then( self.processSuccessPromise( grid, gridRow ), self.processErrorPromise( grid, gridRow )); } else { gridUtil.logError( 'A promise was not returned when saveRow event was raised, either nobody is listening to event, or event handler did not return a promise' ); From bf3472d3a877e92a2fd6949f08022bfd61770285 Mon Sep 17 00:00:00 2001 From: Paul Lambert Date: Sat, 21 Mar 2015 11:29:50 +1300 Subject: [PATCH 047/173] Fix(filtering): allow change to programmatic filter --- src/js/core/factories/GridColumn.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/js/core/factories/GridColumn.js b/src/js/core/factories/GridColumn.js index b1aa56faf1..3b39194914 100644 --- a/src/js/core/factories/GridColumn.js +++ b/src/js/core/factories/GridColumn.js @@ -600,16 +600,16 @@ angular.module('ui.grid') self.setPropertyOrDefault(colDef, 'filters', defaultFilters); } else if ( self.filters.length === defaultFilters.length ) { self.filters.forEach( function( filter, index ){ - if (typeof(filter.placeholder) !== 'undefined') { + if (typeof(defaultFilters[index].placeholder) !== 'undefined') { filter.placeholder = defaultFilters[index].placeholder; } - if (typeof(filter.flags) !== 'undefined') { + if (typeof(defaultFilters[index].flags) !== 'undefined') { filter.flags = defaultFilters[index].flags; } - if (typeof(filter.type) !== 'undefined') { + if (typeof(defaultFilters[index].type) !== 'undefined') { filter.type = defaultFilters[index].type; } - if (typeof(filter.selectOptions) !== 'undefined') { + if (typeof(defaultFilters[index].selectOptions) !== 'undefined') { filter.selectOptions = defaultFilters[index].selectOptions; } }); From 4cc4b02cc2943d4b50c9215af34bea895b1c2de4 Mon Sep 17 00:00:00 2001 From: Paul Lambert Date: Sat, 21 Mar 2015 11:30:09 +1300 Subject: [PATCH 048/173] Fix(gridFactory): forceInvisible is no longer an option --- src/js/core/services/gridClassFactory.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/js/core/services/gridClassFactory.js b/src/js/core/services/gridClassFactory.js index 2a5cf2b7c6..86f090a2ff 100644 --- a/src/js/core/services/gridClassFactory.js +++ b/src/js/core/services/gridClassFactory.js @@ -49,7 +49,7 @@ // Reset all rows to visible initially grid.registerRowsProcessor(function allRowsVisible(rows) { rows.forEach(function (row) { - row.visible = !row.forceInvisible; + row.visible = true; }); return rows; From a394f02f7285c2cf1461466efbf3f516481f3bef Mon Sep 17 00:00:00 2001 From: Paul Lambert Date: Sat, 21 Mar 2015 11:30:45 +1300 Subject: [PATCH 049/173] Enh(selection): fix #3054 allow isRowSelectable to be dynamically changed, and allow data update --- misc/tutorial/210_selection.ngdoc | 24 ++++++++++++++++++++- src/features/selection/js/selection.js | 29 +++++++++++++++++++------- 2 files changed, 44 insertions(+), 9 deletions(-) diff --git a/misc/tutorial/210_selection.ngdoc b/misc/tutorial/210_selection.ngdoc index 90c532be29..d38249c240 100644 --- a/misc/tutorial/210_selection.ngdoc +++ b/misc/tutorial/210_selection.ngdoc @@ -36,6 +36,11 @@ The selectAll box can be disabled by setting `enableSelectAll` to false. You can set the selection row header column width by setting 'selectionRowHeaderWidth' option. +You can use an `isRowSelectable` function to determine which rows are selectable. If you set this function in the options +after grid initialisation you need to call `gridApi.core.notifyDataChange(uiGridConstants.dataChange.OPTIONS)` to enable +the option. In the grid below pressing the button to "set selectable" will set any rows that have an age > 30 to not be +selectable, and also set the age of the first row to 31. + @example Two examples are provided, the first with rowHeaderSelection and multi-select, the second without. The first example auto-selects the first row once the data is loaded. @@ -44,7 +49,7 @@ auto-selects the first row once the data is loaded. var app = angular.module('app', ['ngTouch', 'ui.grid', 'ui.grid.selection']); - app.controller('MainCtrl', ['$scope', '$http', '$log', '$timeout', function ($scope, $http, $log, $timeout) { + app.controller('MainCtrl', ['$scope', '$http', '$log', '$timeout', 'uiGridConstants', function ($scope, $http, $log, $timeout, uiGridConstants) { $scope.gridOptions = { enableRowSelection: true, enableSelectAll: true, @@ -93,6 +98,22 @@ auto-selects the first row once the data is loaded. $scope.toggleRow1 = function() { $scope.gridApi.selection.toggleRowSelection($scope.gridOptions.data[0]); }; + + $scope.setSelectable = function() { + $scope.gridApi.selection.clearSelectedRows(); + + $scope.gridOptions.isRowSelectable = function(row){ + if(row.entity.age > 30){ + return false; + } else { + return true; + } + }; + $scope.gridApi.core.notifyDataChange(uiGridConstants.dataChange.OPTIONS); + + $scope.gridOptions.data[0].age = 31; + $scope.gridApi.core.notifyDataChange(uiGridConstants.dataChange.EDIT); + }; $scope.gridOptions.onRegisterApi = function(gridApi){ //set gridApi on scope @@ -152,6 +173,7 @@ auto-selects the first row once the data is loaded.
    +
    diff --git a/src/features/selection/js/selection.js b/src/features/selection/js/selection.js index 7c37c0af9e..e1c8904f69 100644 --- a/src/features/selection/js/selection.js +++ b/src/features/selection/js/selection.js @@ -430,7 +430,7 @@ * @ngdoc object * @name isRowSelectable * @propertyOf ui.grid.selection.api:GridOptions - * @description Makes it possible to specify a method that evaluates for each and sets its "enableSelection" property. + * @description Makes it possible to specify a method that evaluates for each row and sets its "enableSelection" property. */ gridOptions.isRowSelectable = angular.isDefined(gridOptions.isRowSelectable) ? gridOptions.isRowSelectable : angular.noop; @@ -615,8 +615,8 @@
    */ - module.directive('uiGridSelection', ['uiGridSelectionConstants', 'uiGridSelectionService', '$templateCache', - function (uiGridSelectionConstants, uiGridSelectionService, $templateCache) { + module.directive('uiGridSelection', ['uiGridSelectionConstants', 'uiGridSelectionService', '$templateCache', 'uiGridConstants', + function (uiGridSelectionConstants, uiGridSelectionService, $templateCache, uiGridConstants) { return { replace: true, priority: 0, @@ -643,11 +643,24 @@ uiGridCtrl.grid.addRowHeaderColumn(selectionRowHeaderDef); } - if (uiGridCtrl.grid.options.isRowSelectable !== angular.noop) { - uiGridCtrl.grid.registerRowBuilder(function(row, options) { - row.enableSelection = uiGridCtrl.grid.options.isRowSelectable(row); - }); - } + var processorSet = false; + var updateOptions = function(){ + if (uiGridCtrl.grid.options.isRowSelectable !== angular.noop && processorSet !== true) { + uiGridCtrl.grid.registerRowsProcessor(function(rows) { + rows.forEach(function(row){ + row.enableSelection = uiGridCtrl.grid.options.isRowSelectable(row); + }); + return rows; + }); + processorSet = true; + } + }; + + updateOptions(); + + var dataChangeDereg = uiGridCtrl.grid.registerDataChangeCallback( updateOptions, [uiGridConstants.dataChange.OPTIONS] ); + + $scope.$on( '$destroy', dataChangeDereg); }, post: function ($scope, $elm, $attrs, uiGridCtrl) { From 111871450b029733946516a79e4b2f0ebf351181 Mon Sep 17 00:00:00 2001 From: Paul Lambert Date: Sat, 21 Mar 2015 11:57:21 +1300 Subject: [PATCH 050/173] Doco(grouping): update tut to address fix #3052, suppress data in columns that are grouped --- misc/tutorial/209_grouping.ngdoc | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/misc/tutorial/209_grouping.ngdoc b/misc/tutorial/209_grouping.ngdoc index 98f6b3e287..0de721466d 100644 --- a/misc/tutorial/209_grouping.ngdoc +++ b/misc/tutorial/209_grouping.ngdoc @@ -40,7 +40,6 @@ to allow people to start using it. Notable outstandings are: - enhancement: allow a limit on number of columns grouped - consideration of RTL - not sure whether the indent/outdent should get reversed? - special formatting for header rows in exporter? -- add grouping options to saveState Options to watch out for include: @@ -54,6 +53,12 @@ Options to watch out for include: an average - `groupingShowCounts`: set to false if you don't like the counts against the groupHeaders + If you would like to suppress the data in a grouped column (so it only shows in the groupHeader rows) this can + be done by overriding the cellTemplate for any of the columns you allow grouping on as follows: + `cellTemplate: '
    {{COL_FIELD CUSTOM_FILTERS}}
    '` + + In the example below this has been done on the state column only. This isn't included in the base code as it + could potentially interact with people's custom templates. @example In this example we group by the state column then the gender column, and we count the names (a proxy for @@ -73,7 +78,7 @@ we can't easily see that it's an average. { name: 'gender', grouping: { groupPriority: 1 }, sort: { direction: 'asc' }, width: '20%' }, { name: 'age', grouping: { aggregation: uiGridGroupingConstants.aggregation.MAX }, width: '20%' }, { name: 'company', width: '25%' }, - { name: 'state', grouping: { groupPriority: 0 }, sort: { direction: 'desc' }, width: '35%' }, + { name: 'state', grouping: { groupPriority: 0 }, sort: { direction: 'desc' }, width: '35%', cellTemplate: '
    {{COL_FIELD CUSTOM_FILTERS}}
    ' }, { name: 'balance', width: '25%', cellFilter: 'currency', groupingSuppressAggregationText: true, grouping: { aggregation: uiGridGroupingConstants.aggregation.AVG } } ], onRegisterApi: function( gridApi ) { From 8c9e3876cd4c47d1bff9ed310a58664e206c4d42 Mon Sep 17 00:00:00 2001 From: Paul Lambert Date: Sat, 21 Mar 2015 13:23:01 +1300 Subject: [PATCH 051/173] Enh(grouping): fix #2996 allow programmatic get/set grouping --- misc/tutorial/209_grouping.ngdoc | 44 +++-- src/features/grouping/js/grouping.js | 195 +++++++++++++++----- src/features/grouping/test/grouping.spec.js | 35 ++++ 3 files changed, 217 insertions(+), 57 deletions(-) diff --git a/misc/tutorial/209_grouping.ngdoc b/misc/tutorial/209_grouping.ngdoc index 0de721466d..8bfe7a1201 100644 --- a/misc/tutorial/209_grouping.ngdoc +++ b/misc/tutorial/209_grouping.ngdoc @@ -29,13 +29,24 @@ The group rowHeader by default is only visible when one or more columns are grou to have no visual impact when grouping is turned on but unused. If you'd like the groupRowHeader permanently present then set the `groupingRowHeaderAlwaysVisible: true` gridOption. +If you want to change the grouping programmatically after grid initialisation, you do this through calling the +provided methods: + + - `groupColumn`: groups an individual column. Adds it to the end of the current grouping - so you need to remove + existing grouped columns first if you wanted this to be the only grouping. Adds a sort ASC if there isn't one + - `ungroupColumn`: ungroups an individual column + - `aggregateColumn`: sets aggregation on a column, including setting the aggregation off. Automatically removes + any sort first. + - `setGrouping`: sets all the grouping in one go, removing existing grouping + - `getGrouping`: gets the grouping config for the grid + - `clearGrouping`: clears all current grouping settings + Grouping is still alpha, and under development, however it is included in the distribution files to allow people to start using it. Notable outstandings are: - does not correctly handle columns that are based on functions or complex objects. The groupHeader rows create a fake row.entity, and then set the appropriate fields in that entity. This doesn't work well with complex column definitions at present -- notify data change capability is needed for when people programmatically change the grouping - some more unit testing - enhancement: allow a limit on number of columns grouped - consideration of RTL - not sure whether the indent/outdent should get reversed? @@ -52,13 +63,16 @@ Options to watch out for include: visual indication of what sort of aggregation is going on. Refer the example below, the balance column with an average - `groupingShowCounts`: set to false if you don't like the counts against the groupHeaders + + +If you would like to suppress the data in a grouped column (so it only shows in the groupHeader rows) this can +be done by overriding the cellTemplate for any of the columns you allow grouping on as follows: + + `cellTemplate: '
    {{COL_FIELD CUSTOM_FILTERS}}
    '` - If you would like to suppress the data in a grouped column (so it only shows in the groupHeader rows) this can - be done by overriding the cellTemplate for any of the columns you allow grouping on as follows: - `cellTemplate: '
    {{COL_FIELD CUSTOM_FILTERS}}
    '` - - In the example below this has been done on the state column only. This isn't included in the base code as it - could potentially interact with people's custom templates. +In the example below this has been done on the state column only. This isn't included in the base code as it +could potentially interact with people's custom templates. + @example In this example we group by the state column then the gender column, and we count the names (a proxy for @@ -75,10 +89,10 @@ we can't easily see that it's an average. enableFiltering: true, columnDefs: [ { name: 'name', width: '30%' }, - { name: 'gender', grouping: { groupPriority: 1 }, sort: { direction: 'asc' }, width: '20%' }, + { name: 'gender', grouping: { groupPriority: 1 }, sort: { priority: 1, direction: 'asc' }, width: '20%' }, { name: 'age', grouping: { aggregation: uiGridGroupingConstants.aggregation.MAX }, width: '20%' }, { name: 'company', width: '25%' }, - { name: 'state', grouping: { groupPriority: 0 }, sort: { direction: 'desc' }, width: '35%', cellTemplate: '
    {{COL_FIELD CUSTOM_FILTERS}}
    ' }, + { name: 'state', grouping: { groupPriority: 0 }, sort: { priority: 0, direction: 'desc' }, width: '35%', cellTemplate: '
    {{COL_FIELD CUSTOM_FILTERS}}
    ' }, { name: 'balance', width: '25%', cellFilter: 'currency', groupingSuppressAggregationText: true, grouping: { aggregation: uiGridGroupingConstants.aggregation.AVG } } ], onRegisterApi: function( gridApi ) { @@ -92,6 +106,7 @@ we can't easily see that it's an average. data[i].state = data[i].address.state; data[i].balance = Number( data[i].balance.slice(1).replace(/,/,'') ); } + delete data[2].age; $scope.gridOptions.data = data; }); @@ -101,16 +116,23 @@ we can't easily see that it's an average. $scope.toggleRow = function( rowNum ){ $scope.gridApi.grouping.toggleRowGroupingState($scope.gridApi.grid.renderContainers.body.visibleRowCache[rowNum]); - } + }; + + $scope.changeGrouping = function() { + $scope.gridApi.grouping.clearGrouping(); + $scope.gridApi.grouping.groupColumn('age'); + $scope.gridApi.grouping.aggregateColumn('state', uiGridGroupingConstants.aggregation.COUNT); + }; }]);
    -
    + +
    diff --git a/src/features/grouping/js/grouping.js b/src/features/grouping/js/grouping.js index d519ea92d7..aa0cccce9b 100644 --- a/src/features/grouping/js/grouping.js +++ b/src/features/grouping/js/grouping.js @@ -299,12 +299,78 @@ * @name setGrouping * @methodOf ui.grid.grouping.api:PublicApi * @description Set the grouping configuration for this grid, - * used by the saveState feature + * used by the saveState feature, but can also be used by any + * user to specify a combined grouping and aggregation configuration * @param {object} config the config you want to apply, in the format * provided out by getGrouping */ setGrouping: function ( config ) { service.setGrouping(grid, config); + }, + + /** + * @ngdoc function + * @name groupColumn + * @methodOf ui.grid.grouping.api:PublicApi + * @description Adds this column to the existing grouping, at the end of the priority order. + * If the column doesn't have a sort, adds one, by default ASC + * + * This column will move to the left of any non-group columns, the + * move is handled in a columnProcessor, so gets called as part of refresh + * + * @param {string} columnName the name of the column we want to group + */ + groupColumn: function( columnName ) { + var column = grid.getColumn(columnName); + service.groupColumn(grid, column); + }, + + /** + * @ngdoc function + * @name ungroupColumn + * @methodOf ui.grid.grouping.api:PublicApi + * @description Removes the groupPriority from this column. If the + * column was previously aggregated the aggregation will come back. + * The sort will remain. + * + * This column will move to the right of any other group columns, the + * move is handled in a columnProcessor, so gets called as part of refresh + * + * @param {string} columnName the name of the column we want to ungroup + */ + ungroupColumn: function( columnName ) { + var column = grid.getColumn(columnName); + service.ungroupColumn(grid, column); + }, + + /** + * @ngdoc function + * @name clearGrouping + * @methodOf ui.grid.grouping.api:PublicApi + * @description Clear any grouped columns and any aggregations. Doesn't remove sorting, + * as we don't know whether that sorting was added by grouping or was there beforehand + * + */ + clearGrouping: function() { + service.clearGrouping(grid); + }, + + /** + * @ngdoc function + * @name aggregateColumn + * @methodOf ui.grid.grouping.api:PublicApi + * @description Sets the aggregation type on a column, if the + * column is currently grouped then it removes the grouping first. + * If the aggregationType is null then will result in the aggregation + * being removed + * + * @param {string} columnName the column we want to aggregate + * @param {string} aggregationType one of the recognised types + * from uiGridGroupingConstants + */ + aggregateColumn: function( columnName, aggregationType){ + var column = grid.getColumn(columnName); + service.aggregateColumn( grid, column, aggregationType ); } } } @@ -668,7 +734,7 @@ }, - /** + /** * @ngdoc function * @name ungroupColumn * @methodOf ui.grid.grouping.service:uiGridGroupingService @@ -721,6 +787,85 @@ grid.queueGridRefresh(); }, + + /** + * @ngdoc function + * @name setGrouping + * @methodOf ui.grid.grouping.service:uiGridGroupingService + * @description Set the grouping based on a config object, used by the save state feature + * (more specifically, by the restore function in that feature ) + * + * @param {Grid} grid grid object + * @param {object} config the config we want to set, same format as that returned by getGrouping + */ + setGrouping: function ( grid, config ){ + if ( typeof(config) === 'undefined' ){ + return; + } + + // first remove any existing grouping + service.clearGrouping(grid); + + if ( config.grouping && config.grouping.length && config.grouping.length > 0 ){ + config.grouping.forEach( function( group ) { + var col = grid.getColumn(group.colName); + + if ( col ) { + service.groupColumn( grid, col ); + } + }); + } + + if ( config.aggregations && config.aggregations.length && config.aggregations.length > 0 ){ + config.aggregations.forEach( function( aggregation ) { + var col = grid.getColumn(aggregation.colName); + + if ( col ) { + service.aggregateColumn( grid, col, aggregation.aggregation ); + } + }); + } + + if ( config.rowExpandedStates ){ + grid.grouping.rowExpandedStates = config.rowExpandedStates; + } + }, + + + /** + * @ngdoc function + * @name clearGrouping + * @methodOf ui.grid.grouping.service:uiGridGroupingService + * @description Clear any grouped columns and any aggregations. Doesn't remove sorting, + * as we don't know whether that sorting was added by grouping or was there beforehand + * + * @param {Grid} grid grid object + */ + clearGrouping: function( grid ) { + var currentGrouping = service.getGrouping(grid); + + if ( currentGrouping.grouping.length > 0 ){ + currentGrouping.grouping.forEach( function( group ) { + if (!group.col){ + // should have a group.colName if there's no col + group.col = grid.getColumn(group.colName); + } + service.ungroupColumn(grid, group.col); + }); + } + + if ( currentGrouping.aggregations.length > 0 ){ + currentGrouping.aggregations.forEach( function( aggregation ){ + if (!aggregation.col){ + // should have a group.colName if there's no col + aggregation.col = grid.getColumn(aggregation.colName); + } + service.aggregateColumn(grid, aggregation.col, null); + }); + } + + }, + /** * @ngdoc function @@ -1299,12 +1444,12 @@ } break; case uiGridGroupingConstants.aggregation.MIN: - if (fieldValue !== null && (fieldValue < aggregation.value || aggregation.value === null)){ + if (fieldValue !== undefined && fieldValue !== null && (fieldValue < aggregation.value || aggregation.value === null)){ aggregation.value = fieldValue; } break; case uiGridGroupingConstants.aggregation.MAX: - if (fieldValue > aggregation.value){ + if (fieldValue !== undefined && fieldValue > aggregation.value){ aggregation.value = fieldValue; } break; @@ -1318,49 +1463,7 @@ } } }); - }, - - - /** - * @ngdoc function - * @name setGrouping - * @methodOf ui.grid.grouping.service:uiGridGroupingService - * @description Set the grouping based on a config object, used by the save state feature - * (more specifically, by the restore function in that feature ) - * - * @param {Grid} grid grid object - * @param {object} config the config we want to set, same format as that returned by getGrouping - */ - setGrouping: function ( grid, config ){ - if ( typeof(config) === 'undefined' ){ - return; - } - - if ( config.grouping && config.grouping.length && config.grouping.length > 0 ){ - config.grouping.forEach( function( group ) { - var col = grid.getColumn(group.colName); - - if ( col ) { - service.groupColumn( grid, col ); - } - }); - } - - if ( config.aggregations && config.aggregations.length && config.aggregations.length > 0 ){ - config.aggregations.forEach( function( aggregation ) { - var col = grid.getColumn(aggregation.colName); - - if ( col ) { - service.aggregateColumn( grid, col, aggregation.aggregation ); - } - }); - } - - if ( config.rowExpandedStates ){ - grid.grouping.rowExpandedStates = config.rowExpandedStates; - } } - }; return service; diff --git a/src/features/grouping/test/grouping.spec.js b/src/features/grouping/test/grouping.spec.js index 5d5003635b..8611e27738 100644 --- a/src/features/grouping/test/grouping.spec.js +++ b/src/features/grouping/test/grouping.spec.js @@ -363,6 +363,41 @@ describe('ui.grid.grouping uiGridGroupingService', function () { }); }); + + + describe('clearGrouping', function() { + it('no grouping', function() { + grid.api.grouping.setGrouping( + {} + ); + + // really just checking there are no errors, it should do nothing + grid.api.grouping.clearGrouping(); + + expect(grid.api.grouping.getGrouping( true )).toEqual( + { grouping: [], aggregations: [], rowExpandedStates: {} } + ); + }); + + it('clear grouping, aggregations and rowExpandedStates', function() { + grid.api.grouping.setGrouping({ + grouping: [ + { field: 'col3', colName: 'col3', groupPriority: 0 }, + { field: 'col2', colName: 'col2', groupPriority: 1 } + ], + aggregations: [ + { field: 'col1', colName: 'col1', aggregation: uiGridGroupingConstants.aggregation.COUNT} + ], + rowExpandedStates: { male: { state: 'expanded' } } + }); + grid.api.grouping.clearGrouping(); + + expect(grid.api.grouping.getGrouping( true )).toEqual( + { grouping: [], aggregations: [], rowExpandedStates: { male : { state : 'expanded' } } } + ); + }); + + }); describe('insertGroupHeader', function() { From 48e594308a1441e0b36fc0b8eee27854261c3c76 Mon Sep 17 00:00:00 2001 From: Paul Lambert Date: Sun, 22 Mar 2015 13:19:35 +1300 Subject: [PATCH 052/173] Enh(menu): fix #2662 leave menu open when change column visibility --- misc/tutorial/304_grid_menu.ngdoc | 1 + src/js/core/directives/ui-grid-menu-button.js | 6 ++++-- src/js/core/directives/ui-grid-menu.js | 7 +++++-- src/js/core/factories/GridColumn.js | 3 ++- src/templates/ui-grid/uiGridMenu.html | 6 ++++-- 5 files changed, 16 insertions(+), 7 deletions(-) diff --git a/misc/tutorial/304_grid_menu.ngdoc b/misc/tutorial/304_grid_menu.ngdoc index 49eb9751c1..4aa34eef10 100644 --- a/misc/tutorial/304_grid_menu.ngdoc +++ b/misc/tutorial/304_grid_menu.ngdoc @@ -16,6 +16,7 @@ use i18n on this through the `gridMenuTitleFilter` setting) - `context`: by default, the `action`, `shown` and `active`'s' contexts will have a reference to the grid added as the property `grid` (accessible through `this.grid`. You can pass in your own context by supplying the `context` property to your menu item. It will be accessible through `this.context`. +- `leaveOpen`: by default false, if set to true the menu will be left open after the action The exporter feature also adds menu items to this menu. The `exporterMenuCsv` option is set to false, which suppresses csv export. The 'export selected rows' option is only available diff --git a/src/js/core/directives/ui-grid-menu-button.js b/src/js/core/directives/ui-grid-menu-button.js index efba5a502f..7837dab955 100644 --- a/src/js/core/directives/ui-grid-menu-button.js +++ b/src/js/core/directives/ui-grid-menu-button.js @@ -253,7 +253,8 @@ angular.module('ui.grid') shown: function() { return this.context.gridCol.colDef.visible === true || this.context.gridCol.colDef.visible === undefined; }, - context: { gridCol: $scope.grid.getColumn(colDef.name || colDef.field) } + context: { gridCol: $scope.grid.getColumn(colDef.name || colDef.field) }, + leaveOpen: true }; service.setMenuItemTitle( menuItem, colDef, $scope.grid ); showHideColumns.push( menuItem ); @@ -268,7 +269,8 @@ angular.module('ui.grid') shown: function() { return !(this.context.gridCol.colDef.visible === true || this.context.gridCol.colDef.visible === undefined); }, - context: { gridCol: $scope.grid.getColumn(colDef.name || colDef.field) } + context: { gridCol: $scope.grid.getColumn(colDef.name || colDef.field) }, + leaveOpen: true }; service.setMenuItemTitle( menuItem, colDef, $scope.grid ); showHideColumns.push( menuItem ); diff --git a/src/js/core/directives/ui-grid-menu.js b/src/js/core/directives/ui-grid-menu.js index 1dcf86dc75..e79c95f657 100644 --- a/src/js/core/directives/ui-grid-menu.js +++ b/src/js/core/directives/ui-grid-menu.js @@ -173,7 +173,8 @@ function ($compile, $timeout, $window, $document, gridUtil, uiGridConstants) { icon: '=', shown: '=', context: '=', - templateUrl: '=' + templateUrl: '=', + leaveOpen: '=' }, require: ['?^uiGrid', '^uiGridMenu'], templateUrl: 'ui-grid/uiGridMenuItem', @@ -237,7 +238,9 @@ function ($compile, $timeout, $window, $document, gridUtil, uiGridConstants) { $scope.action.call(context, $event, title); - $scope.$emit('hide-menu'); + if ( !$scope.leaveOpen ){ + $scope.$emit('hide-menu'); + } } }; diff --git a/src/js/core/factories/GridColumn.js b/src/js/core/factories/GridColumn.js index 3b39194914..95a4bee845 100644 --- a/src/js/core/factories/GridColumn.js +++ b/src/js/core/factories/GridColumn.js @@ -279,7 +279,8 @@ angular.module('ui.grid') * - action: the method to call when the menu is clicked * - shown: a function to evaluate to determine whether or not to show the item * - active: a function to evaluate to determine whether or not the item is currently selected - * - context: context to pass to the action function?? + * - context: context to pass to the action function, available in this.context in your handler + * - leaveOpen: if set to true, the menu should stay open after the action, defaults to false * @example *
      $scope.gridOptions.columnDefs = [ 
        *   { field: 'field1', menuItems: [
    diff --git a/src/templates/ui-grid/uiGridMenu.html b/src/templates/ui-grid/uiGridMenu.html
    index 76e6daa743..bb72ed341f 100644
    --- a/src/templates/ui-grid/uiGridMenu.html
    +++ b/src/templates/ui-grid/uiGridMenu.html
    @@ -4,13 +4,15 @@
           
    • + template-url="item.templateUrl" + leave-open="item.leaveOpen">
    From 7dabf0941bb074ca16f531acebd1ac007abe551f Mon Sep 17 00:00:00 2001 From: Paul Lambert Date: Sun, 22 Mar 2015 16:18:08 +1300 Subject: [PATCH 053/173] Doco(templates): update tut to fix #2448 - example of rowTemplate that formats row --- misc/tutorial/317_custom_templates.ngdoc | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/misc/tutorial/317_custom_templates.ngdoc b/misc/tutorial/317_custom_templates.ngdoc index 975997a348..5d55d15fe3 100644 --- a/misc/tutorial/317_custom_templates.ngdoc +++ b/misc/tutorial/317_custom_templates.ngdoc @@ -33,19 +33,22 @@ In the cellTemplate you have access to `grid`, `row` and `column`, which allows $scope.wait = wait + 's'; }, 1000); + // you could of course just include the template inline in your code, this example shows a template being returned from a function function rowTemplate() { return $timeout(function() { $scope.waiting = 'Done!'; $interval.cancel(sec); $scope.wait = ''; - return '
    '; + return '
    ' + + '
    ' + + '
    '; }, 6000); } // Access outside scope functions from row template - $scope.fnOne = function(row) { - console.log(row); - }; + $scope.rowFormatter = function( row ) { + return row.entity.gender === 'male'; + }; $scope.waiting = 'Waiting for row template...'; @@ -97,5 +100,6 @@ In the cellTemplate you have access to `grid`, `row` and `column`, which allows width: 500px; height: 300px; } + .my-css-class { color: blue }
    \ No newline at end of file From ea09b1609ac17d6a84fdd0a4d4ad399ba9ba3bfd Mon Sep 17 00:00:00 2001 From: Paul Lambert Date: Mon, 23 Mar 2015 16:37:09 +1300 Subject: [PATCH 054/173] Fix(infiniteScroll): fix #2730, fix #2827, BREAKING changes Substantial refactor of infinite scroll. Provides new functionality, a much more detailed tutorial, and removes the code that crept into core in #2730 (which I merged when I shouldn't have). Clarifies the handling, removes a viewPort directive that was unnecessary, makes the handling for scroll up and down consistent. Strictly speaking the API hasn't changed, but there are some new settings and the default behaviour has subtly changed in that scrollUp isn't enabled by default. This should be more logical to users, but probably does require a review of the tutorial and API. --- misc/tutorial/212_infinite_scroll.ngdoc | 143 ++++++----- .../infinite-scroll/js/infinite-scroll.js | 240 ++++++++++++++---- .../test/infiniteScroll.spec.js | 138 ++++++---- src/js/core/directives/ui-grid.js | 35 +-- src/js/core/factories/GridRenderContainer.js | 37 +-- 5 files changed, 360 insertions(+), 233 deletions(-) diff --git a/misc/tutorial/212_infinite_scroll.ngdoc b/misc/tutorial/212_infinite_scroll.ngdoc index d8f184d9f4..44d26ed239 100644 --- a/misc/tutorial/212_infinite_scroll.ngdoc +++ b/misc/tutorial/212_infinite_scroll.ngdoc @@ -2,85 +2,104 @@ @name Tutorial: 212 Infinite scroll @description -The infinite scroll feature allows the user to lazy load their data to gridOptions.data +The infinite scroll feature allows the user to lazy load their data to gridOptions.data. -Specify percentage when lazy load should trigger: -
    -  $scope.gridOptions.infiniteScroll = 20;
    -
    +Once you reach the top (or bottom) of your real data set, you can notify that no more pages exist +up (or down), and infinite scroll will stop triggering events in that direction. You can also +optionally tell us up-front that there are no more pages up through `infiniteScrollUp = true` or down through +`infiniteScrollDown = true`, and we will never trigger +pages in that direction. By default we assume you have pages down but not up. + +You can specify the percentage of the grid at which the infinite scroll will trigger a request for +more data `infiniteScrollPercentage = 20`. By default we trigger when you are 20% away from the end of +the grid (in either direction). + +We will raise a `needMoreData` or `needMoreDataTop` event, which you must listen to and respond to if +you have told us that you have more data available. Once you have retrieved the data and added it to your +data array (at the top if the event was `needMoreDataTop`), you need to call `dataLoaded` to tell us +that you have loaded your data. Optionally, you can tell us that there is no more data, and we won't trigger +further requests for more data in that direction. + +When you have loaded your data we will attempt to adjust the grid scroll to give the appearance of continuous +scrolling. This is a little jumpy at present, largely because we work of percentages instead of a number of +rows from the end. We basically assume that your user will have reached the end of the scroll (upwards or downwards) +by the time the data comes back, and scroll the user to the beginning of the newly added data to reflect that. If +your user has already scrolled a lot of pages, then they may not be at the end of the data (20% can be a long way). +Ideally the API would change to a number of rows. + +Finally, we suppress the normal grid behaviour of propagating the scroll to the parent container when you reach the end +if infinite scroll is enabled and there is still data in that direction. @example +In this example we have a data set that starts at page 2 of a 5 page data set. Each page is 100 records, so we start at +record 200, and we can scroll up 2 pages, and scroll down 2 pages. You should see smooth scrolling as you move up, when +you hit record zero a touchpad scroll should propagate to the parent page. You should also see smooth scrolling as you +move down, and when you hit record 499 a touchpad scroll should propagate to the parent page. + var app = angular.module('app', ['ngTouch', 'ui.grid', 'ui.grid.infiniteScroll']); - app.controller('MainCtrl', ['$scope', '$http', '$log', function ($scope, $http, $log) { - $scope.gridOptions = {}; - - /** - * @ngdoc property - * @name infiniteScrollPercentage - * @propertyOf ui.grid.class:GridOptions - * @description This setting controls at what percentage of the scroll more data - * is requested by the infinite scroll - */ - $scope.gridOptions.infiniteScrollPercentage = 15; - - $scope.gridOptions.columnDefs = [ - { name:'id'}, - { name:'name' }, - { name:'age' } - ]; - var page = 0; - var pageUp = 0; - var getData = function(data, page) { - var res = []; - for (var i = (page * 100); i < (page + 1) * 100 && i < data.length; ++i) { - res.push(data[i]); + app.controller('MainCtrl', ['$scope', '$http', function ($scope, $http) { + $scope.gridOptions = { + infiniteScrollPercentage: 15, + infiniteScrollUp: true, + infiniteScrollDown: true, + columnDefs: [ + { name:'id'}, + { name:'name' }, + { name:'age' } + ], + data: 'data', + onRegisterApi: function(gridApi){ + gridApi.infiniteScroll.on.needLoadMoreData($scope, $scope.getDataDown); + gridApi.infiniteScroll.on.needLoadMoreDataTop($scope, $scope.getDataUp); + $scope.gridApi = gridApi; } - return res; }; - var getDataUp = function(data, page) { - var res = []; - for (var i = data.length - (page * 100) - 1; (data.length - i) < ((page + 1) * 100) && (data.length - i) > 0; --i) { - data[i].id = -(data.length - data[i].id) - res.push(data[i]); - } - return res; + $scope.data = []; + + var firstPage = 2; + var lastPage = 1; + + $scope.getDataDown = function() { + $http.get('/data/10000_complex.json') + .success(function(data) { + lastPage++; + var newData = $scope.getPage(data, lastPage); + $scope.data = $scope.data.concat(newData); + $scope.gridApi.infiniteScroll.dataLoaded(null, lastPage === 4); + }) + .error(function(error) { + $scope.gridApi.infiniteScroll.dataLoaded(); + }); }; - $http.get('/data/10000_complex.json') + $scope.getDataUp = function() { + $http.get('/data/10000_complex.json') .success(function(data) { - $scope.gridOptions.data = getData(data, page); - ++page; + firstPage--; + var newData = $scope.getPage(data, firstPage); + $scope.data = newData.concat($scope.data); + $scope.gridApi.infiniteScroll.dataLoaded(firstPage === 0, null); + }) + .error(function(error) { + $scope.gridApi.infiniteScroll.dataLoaded(); }); + }; - $scope.gridOptions.onRegisterApi = function(gridApi){ - gridApi.infiniteScroll.on.needLoadMoreData($scope,function(){ - $http.get('/data/10000_complex.json') - .success(function(data) { - $scope.gridOptions.data = $scope.gridOptions.data.concat(getData(data, page)); - ++page; - gridApi.infiniteScroll.dataLoaded(); - }) - .error(function() { - gridApi.infiniteScroll.dataLoaded(); - }); - }); - gridApi.infiniteScroll.on.needLoadMoreDataTop($scope,function(){ - $http.get('/data/10000_complex.json') - .success(function(data) { - $scope.gridOptions.data = getDataUp(data, pageUp).reverse().concat($scope.gridOptions.data); - ++pageUp; - gridApi.infiniteScroll.dataLoaded(); - }) - .error(function() { - gridApi.infiniteScroll.dataLoaded(); - }); - }); + + $scope.getPage = function(data, page) { + var res = []; + for (var i = (page * 100); i < (page + 1) * 100 && i < data.length; ++i) { + res.push(data[i]); + } + return res; }; + + $scope.getDataDown(); }]); diff --git a/src/features/infinite-scroll/js/infinite-scroll.js b/src/features/infinite-scroll/js/infinite-scroll.js index bd90ae8747..06ba5b6e3a 100644 --- a/src/features/infinite-scroll/js/infinite-scroll.js +++ b/src/features/infinite-scroll/js/infinite-scroll.js @@ -17,7 +17,7 @@ * * @description Service for infinite scroll features */ - module.service('uiGridInfiniteScrollService', ['gridUtil', '$compile', '$timeout', 'uiGridConstants', function (gridUtil, $compile, $timeout, uiGridConstants) { + module.service('uiGridInfiniteScrollService', ['gridUtil', '$compile', '$timeout', 'uiGridConstants', 'ScrollEvent', function (gridUtil, $compile, $timeout, uiGridConstants, ScrollEvent) { var service = { @@ -28,8 +28,24 @@ * @description This method register events and methods into grid public API */ - initializeGrid: function(grid) { + initializeGrid: function(grid, $scope) { service.defaultGridOptions(grid.options); + grid.infiniteScroll = { dataLoading: false, scrollUp: grid.options.infiniteScrollUp, scrollDown: grid.options.infiniteScrollDown }; + + if ( grid.options.infiniteScrollUp){ + grid.suppressParentScrollUp = true; + } + + if ( grid.options.infiniteScrollDown){ + grid.suppressParentScrollDown = true; + } + + if (grid.options.enableInfiniteScroll) { + grid.api.core.on.scrollEvent($scope, service.handleScroll); + } + + // tweak the scroll for infinite scroll up (if enabled) + service.adjustScroll(grid); /** * @ngdoc object @@ -45,7 +61,7 @@ * @ngdoc event * @name needLoadMoreData * @eventOf ui.grid.infiniteScroll.api:PublicAPI - * @description This event fires when scroll reached bottom percentage of grid + * @description This event fires when scroll reaches bottom percentage of grid * and needs to load data */ @@ -56,7 +72,7 @@ * @ngdoc event * @name needLoadMoreDataTop * @eventOf ui.grid.infiniteScroll.api:PublicAPI - * @description This event fires when scroll reached top percentage of grid + * @description This event fires when scroll reaches top percentage of grid * and needs to load data */ @@ -71,20 +87,40 @@ * @ngdoc function * @name dataLoaded * @methodOf ui.grid.infiniteScroll.api:PublicAPI - * @description This function is used as a promise when data finished loading. - * See infinite_scroll ngdoc for example of usage + * @description Call this function when you have loaded the additional data + * requested. You can set noMoreDataTop or noMoreDataBottom to indicate + * that we've reached the end of your data set, we won't fire any more events + * for scroll in that direction. + * See infinite_scroll tutorial for example of usage + * @param {boolean} noMoreDataTop flag that there are no more pages upwards, so don't fire + * any more infinite scroll events upward + * @param {boolean} noMoreDataBottom flag that there are no more pages downwards, so don't + * fire any more infinite scroll events downward */ - dataLoaded: function() { - grid.options.loadTimout = false; + dataLoaded: function( noMoreDataTop, noMoreDataBottom ) { + grid.infiniteScroll.dataLoading = false; + + if ( noMoreDataTop === true ){ + grid.infiniteScroll.scrollUp = false; + grid.suppressParentScrollUp = false; + } + + if ( noMoreDataBottom === true ){ + grid.infiniteScroll.scrollDown = false; + grid.suppressParentScrollDown = false; + } + + service.adjustScroll(grid); } } } }; - grid.options.loadTimout = false; grid.api.registerEventsFromObject(publicApi.events); grid.api.registerMethodsFromObject(publicApi.methods); }, + + defaultGridOptions: function (gridOptions) { //default option to true unless it was explicitly set to false /** @@ -103,6 +139,73 @@ *
    Defaults to true */ gridOptions.enableInfiniteScroll = gridOptions.enableInfiniteScroll !== false; + + /** + * @ngdoc property + * @name infiniteScrollPercentage + * @propertyOf ui.grid.class:GridOptions + * @description This setting controls at what percentage remaining more data + * is requested by the infinite scroll, whether scrolling up or down. + * + * TODO: it would be nice if this were percentage of a page, not percentage of the + * total scroll - as you get more and more data, the needMoreData event is triggered + * further and further away from the end (in terms of number of rows) + *
    Defaults to 20 + */ + gridOptions.infiniteScrollPercentage = gridOptions.infiniteScrollPercentage || 20; + + /** + * @ngdoc property + * @name infiniteScrollUp + * @propertyOf ui.grid.class:GridOptions + * @description Whether you allow infinite scroll up, implying that the first page of data + * you have displayed is in the middle of your data set. If set to true then we trigger the + * needMoreDataTop event when the user hits the top of the scrollbar. + *
    Defaults to false + */ + gridOptions.infiniteScrollUp = gridOptions.infiniteScrollUp === true; + + /** + * @ngdoc property + * @name infiniteScrollDown + * @propertyOf ui.grid.class:GridOptions + * @description Whether you allow infinite scroll down, implying that the first page of data + * you have displayed is in the middle of your data set. If set to true then we trigger the + * needMoreData event when the user hits the bottom of the scrollbar. + *
    Defaults to true + */ + gridOptions.infiniteScrollDown = gridOptions.infiniteScrollDown !== false; + }, + + + /** + * @ngdoc function + * @name handleScroll + * @methodOf ui.grid.infiniteScroll.service:uiGridInfiniteScrollService + * @description Called whenever the grid scrolls, determines whether the scroll should + * trigger an infinite scroll request for more data + * @param {object} args the args from the event + */ + handleScroll: function (args) { + // don't request data if already waiting for data, or if source is coming from ui.grid.adjustInfiniteScrollPosition() function + if ( args.grid.infiniteScroll && args.grid.infiniteScroll.dataLoading || args.source === 'ui.grid.adjustInfiniteScrollPosition' ){ + return; + } + + if (args.y) { + var percentage; + if (args.grid.scrollDirection === uiGridConstants.scrollDirection.UP ) { + percentage = args.y.percentage; + if (percentage <= args.grid.options.infiniteScrollPercentage / 100){ + service.loadData(args.grid); + } + } else if (args.grid.scrollDirection === uiGridConstants.scrollDirection.DOWN) { + percentage = 1 - args.y.percentage; + if (percentage <= args.grid.options.infiniteScrollPercentage / 100){ + service.loadData(args.grid); + } + } + } }, @@ -111,43 +214,92 @@ * @name loadData * @methodOf ui.grid.infiniteScroll.service:uiGridInfiniteScrollService * @description This function fires 'needLoadMoreData' or 'needLoadMoreDataTop' event based on scrollDirection + * and whether there are more pages upwards or downwards + * @param {Grid} grid the grid we're working on */ - loadData: function (grid) { - grid.options.loadTimout = true; - if (grid.scrollDirection === uiGridConstants.scrollDirection.UP) { + // save number of currently visible rows to calculate new scroll position later - we know that we want + // to be at approximately the row we're currently at + grid.infiniteScroll.previousVisibleRows = grid.renderContainers.body.visibleRowCache.length; + grid.infiniteScroll.direction = grid.scrollDirection; + + if (grid.scrollDirection === uiGridConstants.scrollDirection.UP && grid.infiniteScroll.scrollUp ) { + grid.infiniteScroll.dataLoading = true; grid.api.infiniteScroll.raise.needLoadMoreDataTop(); - return; + } else if (grid.scrollDirection === uiGridConstants.scrollDirection.DOWN && grid.infiniteScroll.scrollDown ) { + grid.infiniteScroll.dataLoading = true; + grid.api.infiniteScroll.raise.needLoadMoreData(); } - grid.api.infiniteScroll.raise.needLoadMoreData(); }, - + + /** * @ngdoc function - * @name checkScroll + * @name adjustScroll * @methodOf ui.grid.infiniteScroll.service:uiGridInfiniteScrollService - * @description This function checks scroll position inside grid and - * calls 'loadData' function when scroll reaches 'infiniteScrollPercentage' + * @description Once we are informed that data has been loaded, adjust the scroll position to account for that + * addition and to make things look clean. + * + * If we're scrolling up we scroll to the first row of the old data set - + * so we're assuming that you would have gotten to the top of the grid (from the 20% need more data trigger) by + * the time the data comes back. If we're scrolling down we scoll to the last row of the old data set - so we're + * assuming that you would have gotten to the bottom of the grid (from the 80% need more data trigger) by the time + * the data comes back. + * + * Neither of these are good assumptions, but making this a smoother experience really requires + * that trigger to not be a percentage, and to be much closer to the end of the data (say, 5 rows off the end). Even then + * it'd be better still to actually run into the end. But if the data takes a while to come back, they may have scrolled + * somewhere else in the mean-time, in which case they'll get a jump back to the new data. Anyway, this will do for + * now, until someone wants to do better. + * @param {Grid} grid the grid we're working on */ + adjustScroll: function(grid){ + $timeout(function () { + var percentage; + + if ( grid.infiniteScroll.direction === undefined ){ + // called from initialize, tweak our scroll up a little + service.adjustInfiniteScrollPosition(grid, 0); + } - checkScroll: function(grid, scrollTop) { + var newVisibleRows = grid.renderContainers.body.visibleRowCache.length; + if ( grid.infiniteScroll.direction === uiGridConstants.scrollDirection.UP ){ + percentage = ( newVisibleRows - grid.infiniteScroll.previousVisibleRows ) / newVisibleRows; + service.adjustInfiniteScrollPosition(grid, percentage); + } - /* Take infiniteScrollPercentage value or use 20% as default */ - var infiniteScrollPercentage = grid.options.infiniteScrollPercentage ? grid.options.infiniteScrollPercentage : 20; + if ( grid.infiniteScroll.direction === uiGridConstants.scrollDirection.DOWN ){ + percentage = grid.infiniteScroll.previousVisibleRows / newVisibleRows; + service.adjustInfiniteScrollPosition(grid, percentage); + } + }, 0); + }, + + + /** + * @ngdoc function + * @name adjustInfiniteScrollPosition + * @methodOf ui.grid.infiniteScroll.service:uiGridInfiniteScrollService + * @description This function fires 'needLoadMoreData' or 'needLoadMoreDataTop' event based on scrollDirection + * @param {Grid} grid the grid we're working on + * @param {number} percentage the percentage through the grid that we want to scroll to + */ + adjustInfiniteScrollPosition: function (grid, percentage) { + var scrollEvent = new ScrollEvent(grid, null, null, 'ui.grid.adjustInfiniteScrollPosition'); - if (!grid.options.loadTimout && scrollTop <= infiniteScrollPercentage) { - this.loadData(grid); - return true; + //for infinite scroll, if there are pages upwards then never allow it to be at the zero position so the up button can be active + if ( percentage === 0 && grid.infiniteScroll.scrollUp ) { + scrollEvent.y = {pixels: 1}; } - return false; + else { + scrollEvent.y = {percentage: percentage}; + } + scrollEvent.fireScrollingEvent(); } - /** - * @ngdoc property - * @name infiniteScrollPercentage - * @propertyOf ui.grid.class:GridOptions - * @description This setting controls at what percentage of the scroll more data - * is requested by the infinite scroll - */ + + + + }; return service; }]); @@ -193,7 +345,7 @@ compile: function($scope, $elm, $attr){ return { pre: function($scope, $elm, $attr, uiGridCtrl) { - uiGridInfiniteScrollService.initializeGrid(uiGridCtrl.grid); + uiGridInfiniteScrollService.initializeGrid(uiGridCtrl.grid, $scope); }, post: function($scope, $elm, $attr) { } @@ -202,26 +354,4 @@ }; }]); - module.directive('uiGridViewport', - ['$compile', 'gridUtil', 'uiGridInfiniteScrollService', 'uiGridConstants', - function ($compile, gridUtil, uiGridInfiniteScrollService, uiGridConstants) { - return { - priority: -200, - scope: false, - link: function ($scope, $elm, $attr){ - if ($scope.grid.options.enableInfiniteScroll) { - $scope.grid.api.core.on.scrollEvent($scope, function (args) { - //Prevent circular scroll references, if source is coming from ui.grid.adjustInfiniteScrollPosition() function - if (args.y && (args.source !== 'ui.grid.adjustInfiniteScrollPosition')) { - var percentage = 100 - (args.y.percentage * 100); - if ($scope.grid.scrollDirection === uiGridConstants.scrollDirection.UP) { - percentage = (args.y.percentage * 100); - } - uiGridInfiniteScrollService.checkScroll($scope.grid, percentage); - } - }); - } - } - }; - }]); })(); \ No newline at end of file diff --git a/src/features/infinite-scroll/test/infiniteScroll.spec.js b/src/features/infinite-scroll/test/infiniteScroll.spec.js index dd9af5c09e..b978ad5f99 100644 --- a/src/features/infinite-scroll/test/infiniteScroll.spec.js +++ b/src/features/infinite-scroll/test/infiniteScroll.spec.js @@ -1,74 +1,112 @@ /* global _ */ (function () { - 'use strict'; - describe('ui.grid.infiniteScroll uiGridInfiniteScrollService', function () { + 'use strict'; + describe('ui.grid.infiniteScroll uiGridInfiniteScrollService', function () { - var uiGridInfiniteScrollService; - var grid; - var gridClassFactory; + var uiGridInfiniteScrollService; + var grid; + var gridClassFactory; var uiGridConstants; + var $rootScope; + var $scope; - beforeEach(module('ui.grid.infiniteScroll')); + beforeEach(module('ui.grid.infiniteScroll')); - beforeEach(inject(function (_uiGridInfiniteScrollService_, _gridClassFactory_, _uiGridConstants_) { - uiGridInfiniteScrollService = _uiGridInfiniteScrollService_; - gridClassFactory = _gridClassFactory_; + beforeEach(inject(function (_uiGridInfiniteScrollService_, _gridClassFactory_, _uiGridConstants_, _$rootScope_) { + uiGridInfiniteScrollService = _uiGridInfiniteScrollService_; + gridClassFactory = _gridClassFactory_; uiGridConstants = _uiGridConstants_; - - grid = gridClassFactory.createGrid({}); - - grid.options.columnDefs = [ - {field: 'col1'} - ]; - grid.options.infiniteScroll = 20; - - grid.options.onRegisterApi = function (gridApi) { - gridApi.infiniteScroll.on.needLoadMoreData(function(){ - return []; - }); - gridApi.infiniteScroll.on.needLoadMoreDataTop(function(){ - return []; - }); - - }; - - uiGridInfiniteScrollService.initializeGrid(grid); + $rootScope = _$rootScope_; + $scope = $rootScope.$new(); + + grid = gridClassFactory.createGrid({}); + + grid.options.columnDefs = [ + {field: 'col1'} + ]; + grid.options.infiniteScrollPercentage = 20; + + uiGridInfiniteScrollService.initializeGrid(grid, $scope); spyOn(grid.api.infiniteScroll.raise, 'needLoadMoreData'); spyOn(grid.api.infiniteScroll.raise, 'needLoadMoreDataTop'); - grid.options.data = [{col1:'a'},{col1:'b'}]; + grid.options.data = [{col1:'a'},{col1:'b'}]; - grid.buildColumns(); + grid.buildColumns(); - })); + })); - describe('event handling', function () { - it('should return false if scrollTop is positioned more than 20% of scrollHeight', function() { - var scrollHeight = 100; - var scrollTop = 80; - var callResult = uiGridInfiniteScrollService.checkScroll(grid, scrollTop); - expect(callResult).toBe(false); - }); + describe('event handling', function () { + beforeEach(function() { + spyOn(uiGridInfiniteScrollService, 'loadData').andCallFake(function() {}); + }); + + it('should not request more data if scroll up to 21%', function() { + grid.scrollDirection = uiGridConstants.scrollDirection.UP; + uiGridInfiniteScrollService.handleScroll( { grid: grid, y: { percentage: 0.21 }}); + expect(uiGridInfiniteScrollService.loadData).not.toHaveBeenCalled(); + }); - it('should return false if scrollTop is positioned less than 20% of scrollHeight', function() { - var scrollHeight = 100; - var scrollTop = 19; - var callResult = uiGridInfiniteScrollService.checkScroll(grid, scrollTop); - expect(callResult).toBe(true); - }); + it('should request more data if scroll up to 20%', function() { + grid.scrollDirection = uiGridConstants.scrollDirection.UP; + uiGridInfiniteScrollService.handleScroll( { grid: grid, y: { percentage: 0.20 }}); + expect(uiGridInfiniteScrollService.loadData).toHaveBeenCalled(); + }); - it('should call load data function on grid event raise', function () { - uiGridInfiniteScrollService.loadData(grid); - expect(grid.api.infiniteScroll.raise.needLoadMoreData).toHaveBeenCalled(); - }); + it('should not request more data if scroll down to 79%', function() { + grid.scrollDirection = uiGridConstants.scrollDirection.DOWN; + uiGridInfiniteScrollService.handleScroll( {grid: grid, y: { percentage: 0.79 }}); + expect(uiGridInfiniteScrollService.loadData).not.toHaveBeenCalled(); + }); - it('should call load data top function on grid event raise', function () { + it('should request more data if scroll down to 80%', function() { + grid.scrollDirection = uiGridConstants.scrollDirection.DOWN; + uiGridInfiniteScrollService.handleScroll( { grid: grid, y: { percentage: 0.80 }}); + expect(uiGridInfiniteScrollService.loadData).toHaveBeenCalled(); + }); + }); + + describe('loadData', function() { + it('scroll up and there is data up', function() { grid.scrollDirection = uiGridConstants.scrollDirection.UP; + grid.infiniteScroll.scrollUp = true; + uiGridInfiniteScrollService.loadData(grid); + expect(grid.api.infiniteScroll.raise.needLoadMoreDataTop).toHaveBeenCalled(); + expect(grid.infiniteScroll.previousVisibleRows).toEqual(0); + expect(grid.infiniteScroll.direction).toEqual(uiGridConstants.scrollDirection.UP); }); + it('scroll up and there isn\'t data up', function() { + grid.scrollDirection = uiGridConstants.scrollDirection.UP; + grid.infiniteScroll.scrollUp = false; + + uiGridInfiniteScrollService.loadData(grid); + + expect(grid.api.infiniteScroll.raise.needLoadMoreDataTop).not.toHaveBeenCalled(); + }); + + it('scroll down and there is data down', function() { + grid.scrollDirection = uiGridConstants.scrollDirection.DOWN; + grid.infiniteScroll.scrollDown = true; + + uiGridInfiniteScrollService.loadData(grid); + + expect(grid.api.infiniteScroll.raise.needLoadMoreData).toHaveBeenCalled(); + expect(grid.infiniteScroll.previousVisibleRows).toEqual(0); + expect(grid.infiniteScroll.direction).toEqual(uiGridConstants.scrollDirection.DOWN); + }); + + it('scroll down and there isn\'t data down', function() { + grid.scrollDirection = uiGridConstants.scrollDirection.DOWN; + grid.infiniteScroll.scrollDown = false; + + uiGridInfiniteScrollService.loadData(grid); + + expect(grid.api.infiniteScroll.raise.needLoadMoreData).not.toHaveBeenCalled(); + }); }); - }); + }); })(); \ No newline at end of file diff --git a/src/js/core/directives/ui-grid.js b/src/js/core/directives/ui-grid.js index 97ce1fe9b7..2e30f86169 100644 --- a/src/js/core/directives/ui-grid.js +++ b/src/js/core/directives/ui-grid.js @@ -2,9 +2,9 @@ 'use strict'; angular.module('ui.grid').controller('uiGridController', ['$scope', '$element', '$attrs', 'gridUtil', '$q', 'uiGridConstants', - '$templateCache', 'gridClassFactory', '$timeout', '$parse', '$compile', 'ScrollEvent', + '$templateCache', 'gridClassFactory', '$timeout', '$parse', '$compile', function ($scope, $elm, $attrs, gridUtil, $q, uiGridConstants, - $templateCache, gridClassFactory, $timeout, $parse, $compile, ScrollEvent) { + $templateCache, gridClassFactory, $timeout, $parse, $compile) { // gridUtil.logDebug('ui-grid controller'); var self = this; @@ -59,23 +59,6 @@ } } - function adjustInfiniteScrollPosition (scrollToRow) { - - var scrollEvent = new ScrollEvent(self.grid, null, null, 'ui.grid.adjustInfiniteScrollPosition'); - var totalRows = self.grid.renderContainers.body.visibleRowCache.length; - var percentage = ( scrollToRow + ( scrollToRow / ( totalRows - 1 ) ) ) / totalRows; - - //for infinite scroll, never allow it to be at the zero position so the up button can be active - if ( percentage === 0 ) { - scrollEvent.y = {pixels: 1}; - } - else { - scrollEvent.y = {percentage: percentage}; - } - scrollEvent.fireScrollingEvent(); - - } - function dataWatchFunction(newData) { // gridUtil.logDebug('dataWatch fired'); var promises = []; @@ -114,20 +97,6 @@ $scope.$evalAsync(function() { self.grid.refreshCanvas(true); self.grid.callDataChangeCallbacks(uiGridConstants.dataChange.ROW); - - $timeout(function () { - //Process post load scroll events if using infinite scroll - if ( self.grid.options.enableInfiniteScroll ) { - //If first load, seed the scrollbar down a little to activate the button - if ( self.grid.renderContainers.body.prevRowScrollIndex === 0 ) { - adjustInfiniteScrollPosition(0); - } - //If we are scrolling up, we need to reseed the grid. - if (self.grid.scrollDirection === uiGridConstants.scrollDirection.UP) { - adjustInfiniteScrollPosition(self.grid.renderContainers.body.prevRowScrollIndex + 1 + self.grid.options.excessRows); - } - } - }, 0); }); }); }); diff --git a/src/js/core/factories/GridRenderContainer.js b/src/js/core/factories/GridRenderContainer.js index 495b3eeaa4..7c39c73545 100644 --- a/src/js/core/factories/GridRenderContainer.js +++ b/src/js/core/factories/GridRenderContainer.js @@ -364,48 +364,19 @@ angular.module('ui.grid') if (rowCache.length > self.grid.options.virtualizationThreshold) { if (!(typeof(scrollTop) === 'undefined' || scrollTop === null)) { // Have we hit the threshold going down? - if (!self.grid.options.enableInfiniteScroll && self.prevScrollTop < scrollTop && rowIndex < self.prevRowScrollIndex + self.grid.options.scrollThreshold && rowIndex < maxRowIndex) { + if ( !self.grid.suppressParentScrollDown && self.prevScrollTop < scrollTop && rowIndex < self.prevRowScrollIndex + self.grid.options.scrollThreshold && rowIndex < maxRowIndex) { return; } //Have we hit the threshold going up? - if (!self.grid.options.enableInfiniteScroll && self.prevScrollTop > scrollTop && rowIndex > self.prevRowScrollIndex - self.grid.options.scrollThreshold && rowIndex < maxRowIndex) { + if ( !self.grid.suppressParentScrollUp && self.prevScrollTop > scrollTop && rowIndex > self.prevRowScrollIndex - self.grid.options.scrollThreshold && rowIndex < maxRowIndex) { return; } } var rangeStart = {}; var rangeEnd = {}; - //If infinite scroll is enabled, and we loaded more data coming from redrawInPlace, then recalculate the range and set rowIndex to proper place to scroll to - if ( self.grid.options.enableInfiniteScroll && self.grid.scrollDirection !== uiGridConstants.scrollDirection.NONE && postDataLoaded ) { - var findIndex = null; - var i = null; - if ( self.grid.scrollDirection === uiGridConstants.scrollDirection.UP ) { - findIndex = rowIndex > 0 ? self.grid.options.excessRows : 0; - for ( i = 0; i < rowCache.length; i++) { - if (self.grid.options.rowIdentity(rowCache[i].entity) === self.grid.options.rowIdentity(self.renderedRows[findIndex].entity)) { - rowIndex = i; - break; - } - } - rangeStart = Math.max(0, rowIndex); - rangeEnd = Math.min(rowCache.length, rangeStart + self.grid.options.excessRows + minRows); - } - else if ( self.grid.scrollDirection === uiGridConstants.scrollDirection.DOWN ) { - findIndex = minRows; - for ( i = 0; i < rowCache.length; i++) { - if (self.grid.options.rowIdentity(rowCache[i].entity) === self.grid.options.rowIdentity(self.renderedRows[findIndex].entity)) { - rowIndex = i; - break; - } - } - rangeStart = Math.max(0, rowIndex - self.grid.options.excessRows - minRows); - rangeEnd = Math.min(rowCache.length, rowIndex + minRows + self.grid.options.excessRows); - } - } - else { - rangeStart = Math.max(0, rowIndex - self.grid.options.excessRows); - rangeEnd = Math.min(rowCache.length, rowIndex + minRows + self.grid.options.excessRows); - } + rangeStart = Math.max(0, rowIndex - self.grid.options.excessRows); + rangeEnd = Math.min(rowCache.length, rowIndex + minRows + self.grid.options.excessRows); newRange = [rangeStart, rangeEnd]; } From f1f82faac3753f42746b76498aa02443e85a3640 Mon Sep 17 00:00:00 2001 From: Raul Teixeira Date: Tue, 24 Mar 2015 09:54:48 -0300 Subject: [PATCH 055/173] Added missing translations for ptBR --- src/js/i18n/pt-br.js | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/src/js/i18n/pt-br.js b/src/js/i18n/pt-br.js index 2d30349439..0ec6e30883 100644 --- a/src/js/i18n/pt-br.js +++ b/src/js/i18n/pt-br.js @@ -1,3 +1,6 @@ +/** + * Altered by raul on 24/03/2015 (leaving theses lines makes it easier to compare with en.js) + */ (function () { angular.module('ui.grid').config(['$provide', function($provide) { $provide.decorator('i18nService', ['$delegate', function($delegate) { @@ -44,6 +47,7 @@ }, gridMenu: { columns: 'Colunas:', + importerTitle: 'Importar arquivo', exporterAllAsCsv: 'Exportar todos os dados como csv', exporterVisibleAsCsv: 'Exportar dados visíveis como csv', exporterSelectedAsCsv: 'Exportar dados selecionados como csv', @@ -57,9 +61,23 @@ invalidCsv: 'Arquivo não pode ser processado. É um CSV válido?', invalidJson: 'Arquivo não pode ser processado. É um Json válido?', jsonNotArray: 'Arquivo json importado tem que conter um array. Abortando.' + }, + pagination: { + sizes: 'itens por página', + totalItems: 'itens' + }, + grouping: { + group: 'Agrupar', + ungroup: 'Desagrupar', + aggregate_count: 'Agr: Contar', + aggregate_sum: 'Agr: Soma', + aggregate_max: 'Agr: Max', + aggregate_min: 'Agr: Min', + aggregate_avg: 'Agr: Med', + aggregate_remove: 'Agr: Remover' } }); return $delegate; }]); }]); -})(); \ No newline at end of file +})(); From 36315463599ef5feabcc8f8cf8a8f9ec370244d5 Mon Sep 17 00:00:00 2001 From: Martin Kluska Date: Thu, 26 Mar 2015 13:54:31 +0100 Subject: [PATCH 056/173] Added czech localization --- src/js/i18n/cs.js | 89 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 89 insertions(+) create mode 100644 src/js/i18n/cs.js diff --git a/src/js/i18n/cs.js b/src/js/i18n/cs.js new file mode 100644 index 0000000000..8d925d4f35 --- /dev/null +++ b/src/js/i18n/cs.js @@ -0,0 +1,89 @@ +/** + * Created by pion on 26/4/15. + */ +(function () { + angular.module('ui.grid').config(['$provide', function($provide) { + $provide.decorator('i18nService', ['$delegate', function($delegate) { + var lang = { + aggregate: { + label: 'položky' + }, + groupPanel: { + description: 'Přesuntě záhlaví zde pro vytvoření skupiny dle sloupce.' + }, + search: { + placeholder: 'Hledat...', + showingItems: 'Zobrazuji položky:', + selectedItems: 'Vybrané položky:', + totalItems: 'Celkem položek:', + size: 'Velikost strany:', + first: 'První strana', + next: 'Další strana', + previous: 'Předchozí strana', + last: 'Poslední strana' + }, + menu: { + text: 'Vyberte sloupec:' + }, + sort: { + ascending: 'Seřadit od A-Z', + descending: 'Seřadit od Z-A', + remove: 'Odebrat seřazení' + }, + column: { + hide: 'Schovat sloupec' + }, + aggregation: { + count: 'celkem řádků: ', + sum: 'celkem: ', + avg: 'avg: ', + min: 'min.: ', + max: 'max.: ' + }, + pinning: { + pinLeft: 'Zamknout v levo', + pinRight: 'Zamknout v pravo', + unpin: 'Odemknout' + }, + gridMenu: { + columns: 'Sloupce:', + importerTitle: 'Importovat soubor', + exporterAllAsCsv: 'Exportovat všechny data do csv', + exporterVisibleAsCsv: 'Exportovat viditelné data do csv', + exporterSelectedAsCsv: 'Exportovat vybranné data do csv', + exporterAllAsPdf: 'Exportovat všechny data do pdf', + exporterVisibleAsPdf: 'Exportovat viditelné data do pdf', + exporterSelectedAsPdf: 'Exportovat vybranné data do pdf' + }, + importer: { + noHeaders: 'Názvy sloupců se nepodařilo získat, obsahuje soubor záhlaví?', + noObjects: 'Data se nepodařilo zpracovat, obsahuje soubor řádky mimo záhlaví?', + invalidCsv: 'Soubor nelze zpracovat, jedná se CSV?', + invalidJson: 'Soubor nelze zpracovat, je to JSON?', + jsonNotArray: 'Soubor musí obsahovat json. Ukončuji..' + }, + pagination: { + sizes: 'položek na stránku', + totalItems: 'položek' + }, + grouping: { + group: 'Seskupit', + ungroup: 'Odebrat seskupení', + aggregate_count: 'Agregace: Count', + aggregate_sum: 'Agregace: Sum', + aggregate_max: 'Agregace: Max', + aggregate_min: 'Agregace: Min', + aggregate_avg: 'Agregace: Avg', + aggregate_remove: 'Agregace: Odebrat' + } + }; + + // support varianty of different czech keys. + $delegate.add('cs', lang); + $delegate.add('cz', lang); + $delegate.add('cs-cz', lang); + $delegate.add('cs-CZ', lang); + return $delegate; + }]); + }]); +})(); From 052c2321f97b37f860c769dcbd2e8d9094cf2bbf Mon Sep 17 00:00:00 2001 From: swalters Date: Thu, 26 Mar 2015 11:24:32 -0500 Subject: [PATCH 057/173] refactor(core): Change scrolling events. Various performance improvements in scrolling Before this change, scrolling Events were being fire for each render container. Now, a ScrollBegin and ScrollEnd event is fired once for the grid. ScrollTo methods were moved from cellNav into core and also changed to return a promise. BREAKING CHANGE: Two events are now emitted on scroll: grid.api.core.ScrollBegin grid.api.core.ScrollEnd Before: grid.api.core.ScrollEvent After: grid.api.core.ScrollBegin ScrollToIfNecessary and ScrollTo moved from cellNav to core and grid removed from arguments Before: grid.api.cellNav.ScrollToIfNecessary(grid, gridRow, gridCol) grid.api.cellNav.ScrollTo(grid, rowEntity, colDef) After: grid.api.core.ScrollToIfNecessary(gridRow, gridCol) grid.api.core.ScrollTo(rowEntity, colDef) GridEdit/cellNav When using cellNav, a cell no longer receives focus. Instead the viewport always receives focus. This eliminated many bugs associated with scrolling and focus. If you have a custom editor, you will no longer receive keyDown/Up events from the readonly cell. Use the cellNav api viewPortKeyDown to capture any needed keydown events. see GridEdit.js for an example --- misc/tutorial/202_cellnav.ngdoc | 2 +- misc/tutorial/211_two_grids.ngdoc | 2 +- src/features/cellnav/js/cellnav.js | 433 ++++++------------ .../test/uiGridCellNavDirective.spec.js | 2 +- .../cellnav/test/uiGridCellNavService.spec.js | 70 ++- src/features/edit/js/gridEdit.js | 150 +++--- src/features/edit/test/uiGridCell.spec.js | 2 +- .../edit/test/uiGridCellWithDropdown.spec.js | 2 +- .../move-columns/js/column-movable.js | 2 +- src/features/saveState/js/saveState.js | 2 +- src/features/saveState/test/saveState.spec.js | 32 +- src/features/selection/js/selection.js | 34 +- src/js/core/directives/ui-grid-menu.js | 2 +- .../directives/ui-grid-render-container.js | 101 ++-- src/js/core/directives/ui-grid-viewport.js | 101 ++-- src/js/core/factories/Grid.js | 349 +++++++++++++- src/js/core/factories/GridApi.js | 26 +- src/js/core/factories/GridColumn.js | 174 ++++--- src/js/core/factories/GridOptions.js | 24 +- src/js/core/factories/GridRenderContainer.js | 50 ++ src/js/core/factories/ScrollEvent.js | 28 +- src/js/core/services/ui-grid-util.js | 12 +- src/less/body.less | 5 +- test/unit/core/factories/GridOptions.spec.js | 18 +- 24 files changed, 1006 insertions(+), 617 deletions(-) diff --git a/misc/tutorial/202_cellnav.ngdoc b/misc/tutorial/202_cellnav.ngdoc index acac65a9cf..303d58bda7 100644 --- a/misc/tutorial/202_cellnav.ngdoc +++ b/misc/tutorial/202_cellnav.ngdoc @@ -68,7 +68,7 @@ extract values of selected cells. }; $scope.scrollTo = function( rowIndex, colIndex ) { - $scope.gridApi.cellNav.scrollTo( $scope.gridOptions.data[rowIndex], $scope.gridOptions.columnDefs[colIndex]); + $scope.gridApi.core.scrollTo( $scope.gridOptions.data[rowIndex], $scope.gridOptions.columnDefs[colIndex]); }; $scope.scrollToFocus = function( rowIndex, colIndex ) { diff --git a/misc/tutorial/211_two_grids.ngdoc b/misc/tutorial/211_two_grids.ngdoc index adb93879f3..311c8759ab 100644 --- a/misc/tutorial/211_two_grids.ngdoc +++ b/misc/tutorial/211_two_grids.ngdoc @@ -18,7 +18,7 @@ each other. }; $scope.scrollTo = function( rowIndex, colIndex ) { - $scope.gridApi.cellNav.scrollTo( $scope.gridOptions.data[rowIndex], $scope.gridOptions.columnDefs[colIndex]); + $scope.gridApi.core.scrollTo( $scope.gridOptions.data[rowIndex], $scope.gridOptions.columnDefs[colIndex]); }; $http.get('/data/100.json') diff --git a/src/features/cellnav/js/cellnav.js b/src/features/cellnav/js/cellnav.js index 8a74da1064..931faaa396 100644 --- a/src/features/cellnav/js/cellnav.js +++ b/src/features/cellnav/js/cellnav.js @@ -251,7 +251,12 @@ initializeGrid: function (grid) { grid.registerColumnBuilder(service.cellNavColumnBuilder); - //create variables for state + + /** + * @ngdoc object + * @name ui.grid.cellNav:Grid.cellNav + * @description cellNav properties added to grid class + */ grid.cellNav = {}; grid.cellNav.lastRowCol = null; grid.cellNav.focusedCells = []; @@ -278,24 +283,23 @@ * @param {object} newRowCol new position * @param {object} oldRowCol old position */ - navigate: function (newRowCol, oldRowCol) { - } + navigate: function (newRowCol, oldRowCol) {}, + /** + * @ngdoc event + * @name viewPortKeyDown + * @eventOf ui.grid.cellNav.api:PublicApi + * @description is raised when the viewPort receives a keyDown event. Cells never get focus in uiGrid + * due to the difficulties of setting focus on a cell that is not visible in the viewport. Use this + * event whenever you need a keydown event on a cell + *
    + * @param {object} event keydown event + * @param {object} rowCol current rowCol position + */ + viewPortKeyDown: function (event, rowCol) {} } }, methods: { cellNav: { - /** - * @ngdoc function - * @name scrollTo - * @methodOf ui.grid.cellNav.api:PublicApi - * @description brings the specified row and column into view - * @param {object} rowEntity gridOptions.data[] array instance to make visible - * @param {object} colDef to make visible - */ - scrollTo: function (rowEntity, colDef) { - service.scrollTo(grid, rowEntity, colDef); - }, - /** * @ngdoc function * @name scrollToFocus @@ -304,21 +308,10 @@ * to that cell * @param {object} rowEntity gridOptions.data[] array instance to make visible and set focus * @param {object} colDef to make visible and set focus + * @returns {promise} a promise that is resolved after any scrolling is finished */ scrollToFocus: function (rowEntity, colDef) { - service.scrollToFocus(grid, rowEntity, colDef); - }, - - /** - * @ngdoc function - * @name scrollToIfNecessary - * @methodOf ui.grid.cellNav.api:PublicApi - * @description brings the specified row and column fully into view if it isn't already - * @param {GridRow} row grid row that we should make fully visible - * @param {GridCol} col grid col to make fully visible - */ - scrollToIfNecessary: function (row, col) { - service.scrollToIfNecessary(grid, row, col); + return service.scrollToFocus(grid, rowEntity, colDef); }, /** @@ -482,30 +475,6 @@ return $q.all(promises); }, - /** - * @ngdoc method - * @methodOf ui.grid.cellNav.service:uiGridCellNavService - * @name scrollTo - * @description Scroll the grid such that the specified - * row and column is in view - * @param {Grid} grid the grid you'd like to act upon, usually available - * from gridApi.grid - * @param {object} rowEntity gridOptions.data[] array instance to make visible - * @param {object} colDef to make visible - */ - scrollTo: function (grid, rowEntity, colDef) { - var gridRow = null, gridCol = null; - - if (rowEntity !== null && typeof(rowEntity) !== 'undefined' ) { - gridRow = grid.getRow(rowEntity); - } - - if (colDef !== null && typeof(colDef) !== 'undefined' ) { - gridCol = grid.getColumn(colDef.name ? colDef.name : colDef.field); - } - this.scrollToIfNecessary(grid, gridRow, gridCol); - }, - /** * @ngdoc method * @methodOf ui.grid.cellNav.service:uiGridCellNavService @@ -516,6 +485,7 @@ * from gridApi.grid * @param {object} rowEntity gridOptions.data[] array instance to make visible and set focus to * @param {object} colDef to make visible and set focus to + * @returns {promise} a promise that is resolved after any scrolling is finished */ scrollToFocus: function (grid, rowEntity, colDef) { var gridRow = null, gridCol = null; @@ -527,12 +497,14 @@ if (typeof(colDef) !== 'undefined' && colDef !== null) { gridCol = grid.getColumn(colDef.name ? colDef.name : colDef.field); } - this.scrollToIfNecessary(grid, gridRow, gridCol); + return grid.api.core.scrollToIfNecessary(gridRow, gridCol).then(function () { + var rowCol = { row: gridRow, col: gridCol }; + + // Broadcast the navigation + grid.cellNav.broadcastCellNav(rowCol); + }); - var rowCol = { row: gridRow, col: gridCol }; - // Broadcast the navigation - grid.cellNav.broadcastCellNav(rowCol); }, @@ -581,156 +553,10 @@ } if (scrollEvent.y || scrollEvent.x) { - scrollEvent.fireScrollingEvent(); + grid.scrollContainers('', scrollEvent); } }, - /** - * @ngdoc method - * @methodOf ui.grid.cellNav.service:uiGridCellNavService - * @name scrollToIfNecessary - * @description Scrolls the grid to make a certain row and column combo visible, - * in the case that it is not completely visible on the screen already. - * @param {Grid} grid the grid you'd like to act upon, usually available - * from gridApi.grid - * @param {GridRow} gridRow row to make visible - * @param {GridCol} gridCol column to make visible - */ - scrollToIfNecessary: function (grid, gridRow, gridCol) { - var scrollEvent = new ScrollEvent(grid, 'uiGridCellNavService.scrollToIfNecessary'); - - // Alias the visible row and column caches - var visRowCache = grid.renderContainers.body.visibleRowCache; - var visColCache = grid.renderContainers.body.visibleColumnCache; - - /*-- Get the top, left, right, and bottom "scrolled" edges of the grid --*/ - - // The top boundary is the current Y scroll position PLUS the header height, because the header can obscure rows when the grid is scrolled downwards - var topBound = grid.renderContainers.body.prevScrollTop + grid.headerHeight; - - // Don't the let top boundary be less than 0 - topBound = (topBound < 0) ? 0 : topBound; - - // The left boundary is the current X scroll position - var leftBound = grid.renderContainers.body.prevScrollLeft; - - // The bottom boundary is the current Y scroll position, plus the height of the grid, but minus the header height and minus the footerHeight. - // Basically this is the viewport height added on to the scroll position - var bottomBound = grid.renderContainers.body.prevScrollTop + grid.gridHeight - grid.headerHeight - grid.footerHeight; - - // If there's a horizontal scrollbar, remove its height from the bottom boundary, otherwise we'll be letting it obscure rows - //if (grid.horizontalScrollbarHeight) { - // bottomBound = bottomBound - grid.horizontalScrollbarHeight; - //} - - // The right position is the current X scroll position minus the grid width - var rightBound = grid.renderContainers.body.prevScrollLeft + Math.ceil(grid.gridWidth); - - // If there's a vertical scrollbar, subtract it from the right boundary or we'll allow it to obscure cells - //if (grid.verticalScrollbarWidth) { - // rightBound = rightBound - grid.verticalScrollbarWidth; - //} - - // We were given a row to scroll to - if (gridRow !== null) { - // This is the index of the row we want to scroll to, within the list of rows that can be visible - var seekRowIndex = visRowCache.indexOf(gridRow); - - // Total vertical scroll length of the grid - var scrollLength = (grid.renderContainers.body.getCanvasHeight() - grid.renderContainers.body.getViewportHeight()); - - // Add the height of the native horizontal scrollbar to the scroll length, if it's there. Otherwise it will mask over the final row - //if (grid.horizontalScrollbarHeight && grid.horizontalScrollbarHeight > 0) { - // scrollLength = scrollLength + grid.horizontalScrollbarHeight; - //} - - // This is the minimum amount of pixels we need to scroll vertical in order to see this row. - var pixelsToSeeRow = ((seekRowIndex + 1) * grid.options.rowHeight); - - // Don't let the pixels required to see the row be less than zero - pixelsToSeeRow = (pixelsToSeeRow < 0) ? 0 : pixelsToSeeRow; - - var scrollPixels, percentage; - - // If the scroll position we need to see the row is LESS than the top boundary, i.e. obscured above the top of the grid... - if (pixelsToSeeRow < topBound) { - // Get the different between the top boundary and the required scroll position and subtract it from the current scroll position\ - // to get the full position we need - scrollPixels = grid.renderContainers.body.prevScrollTop - (topBound - pixelsToSeeRow); - - // Turn the scroll position into a percentage and make it an argument for a scroll event - percentage = scrollPixels / scrollLength; - scrollEvent.y = { percentage: percentage }; - } - // Otherwise if the scroll position we need to see the row is MORE than the bottom boundary, i.e. obscured below the bottom of the grid... - else if (pixelsToSeeRow > bottomBound) { - // Get the different between the bottom boundary and the required scroll position and add it to the current scroll position - // to get the full position we need - scrollPixels = pixelsToSeeRow - bottomBound + grid.renderContainers.body.prevScrollTop; - - // Turn the scroll position into a percentage and make it an argument for a scroll event - percentage = scrollPixels / scrollLength; - scrollEvent.y = { percentage: percentage }; - } - } - - // We were given a column to scroll to - if (gridCol !== null) { - // This is the index of the row we want to scroll to, within the list of rows that can be visible - var seekColumnIndex = visColCache.indexOf(gridCol); - - // Total vertical scroll length of the grid - var horizScrollLength = (grid.renderContainers.body.getCanvasWidth() - grid.renderContainers.body.getViewportWidth()); - - // Add the height of the native horizontal scrollbar to the scroll length, if it's there. Otherwise it will mask over the final row - // if (grid.verticalScrollbarWidth && grid.verticalScrollbarWidth > 0) { - // horizScrollLength = horizScrollLength + grid.verticalScrollbarWidth; - // } - - // This is the minimum amount of pixels we need to scroll vertical in order to see this column - var columnLeftEdge = 0; - for (var i = 0; i < seekColumnIndex; i++) { - var col = visColCache[i]; - columnLeftEdge += col.drawnWidth; - } - columnLeftEdge = (columnLeftEdge < 0) ? 0 : columnLeftEdge; - - var columnRightEdge = columnLeftEdge + gridCol.drawnWidth; - - // Don't let the pixels required to see the column be less than zero - columnRightEdge = (columnRightEdge < 0) ? 0 : columnRightEdge; - - var horizScrollPixels, horizPercentage; - - // If the scroll position we need to see the row is LESS than the top boundary, i.e. obscured above the top of the grid... - if (columnLeftEdge < leftBound) { - // Get the different between the top boundary and the required scroll position and subtract it from the current scroll position\ - // to get the full position we need - horizScrollPixels = grid.renderContainers.body.prevScrollLeft - (leftBound - columnLeftEdge); - - // Turn the scroll position into a percentage and make it an argument for a scroll event - horizPercentage = horizScrollPixels / horizScrollLength; - horizPercentage = (horizPercentage > 1) ? 1 : horizPercentage; - scrollEvent.x = { percentage: horizPercentage }; - } - // Otherwise if the scroll position we need to see the row is MORE than the bottom boundary, i.e. obscured below the bottom of the grid... - else if (columnRightEdge > rightBound) { - // Get the different between the bottom boundary and the required scroll position and add it to the current scroll position - // to get the full position we need - horizScrollPixels = columnRightEdge - rightBound + grid.renderContainers.body.prevScrollLeft; - - // Turn the scroll position into a percentage and make it an argument for a scroll event - horizPercentage = horizScrollPixels / horizScrollLength; - horizPercentage = (horizPercentage > 1) ? 1 : horizPercentage; - scrollEvent.x = { percentage: horizPercentage }; - } - } - - // If we need to scroll on either the x or y axes, fire a scroll event - if (scrollEvent.y || scrollEvent.x) { - scrollEvent.fireScrollingEvent(); - } - }, /** * @ngdoc method @@ -805,8 +631,8 @@
    */ - module.directive('uiGridCellnav', ['gridUtil', 'uiGridCellNavService', 'uiGridCellNavConstants', 'uiGridConstants', - function (gridUtil, uiGridCellNavService, uiGridCellNavConstants, uiGridConstants) { + module.directive('uiGridCellnav', ['gridUtil', 'uiGridCellNavService', 'uiGridCellNavConstants', 'uiGridConstants', '$timeout', + function (gridUtil, uiGridCellNavService, uiGridCellNavConstants, uiGridConstants, $timeout) { return { replace: true, priority: -150, @@ -823,11 +649,22 @@ uiGridCtrl.cellNav = {}; - uiGridCtrl.cellNav.focusCell = function (row, col) { - uiGridCtrl.cellNav.broadcastCellNav({ row: row, col: col }); + //uiGridCtrl.cellNav.focusActiveCell = function () { + // var elms = $elm[0].getElementsByClassName('ui-grid-cell-focus'); + // if (elms.length > 0){ + // elms[0].focus(); + // } + //}; + + uiGridCtrl.cellNav.getActiveCell = function () { + var elms = $elm[0].getElementsByClassName('ui-grid-cell-focus'); + if (elms.length > 0){ + return elms[0]; + } + + return undefined; }; - // gridUtil.logDebug('uiGridEdit preLink'); uiGridCtrl.cellNav.broadcastCellNav = grid.cellNav.broadcastCellNav = function (newRowCol, modifierDown) { modifierDown = !(modifierDown === undefined || !modifierDown); uiGridCtrl.cellNav.broadcastFocus(newRowCol, modifierDown); @@ -848,6 +685,7 @@ if (grid.cellNav.lastRowCol === null || rowColSelectIndex === -1) { var newRowCol = new RowCol(row, col); + grid.api.cellNav.raise.navigate(newRowCol, grid.cellNav.lastRowCol); grid.cellNav.lastRowCol = newRowCol; if (uiGridCtrl.grid.options.modifierKeysToMultiSelectCells && modifierDown) { @@ -865,7 +703,7 @@ uiGridCtrl.cellNav.handleKeyDown = function (evt) { var direction = uiGridCellNavService.getDirection(evt); if (direction === null) { - return true; + return null; } var containerId = 'body'; @@ -909,13 +747,15 @@ } - rowCol.eventType = uiGridCellNavConstants.EVENT_TYPE.KEYDOWN; + // rowCol.eventType = uiGridCellNavConstants.EVENT_TYPE.KEYDOWN; + + // Scroll to the new cell, if it's not completely visible within the render container's viewport - uiGridCellNavService.scrollToIfNecessary(grid, rowCol.row, rowCol.col); + grid.scrollToIfNecessary(rowCol.row, rowCol.col).then(function () { + uiGridCtrl.cellNav.broadcastCellNav(rowCol); + }); - // Broadcast the navigation - uiGridCtrl.cellNav.broadcastCellNav(rowCol); evt.stopPropagation(); evt.preventDefault(); @@ -931,8 +771,8 @@ }; }]); - module.directive('uiGridRenderContainer', ['$timeout', '$document', 'gridUtil', 'uiGridConstants', 'uiGridCellNavService', 'uiGridCellNavConstants', - function ($timeout, $document, gridUtil, uiGridConstants, uiGridCellNavService, uiGridCellNavConstants) { + module.directive('uiGridRenderContainer', ['$timeout', '$document', 'gridUtil', 'uiGridConstants', 'uiGridCellNavService', 'uiGridCellNavConstants','$log', + function ($timeout, $document, gridUtil, uiGridConstants, uiGridCellNavService, uiGridCellNavConstants, $log) { return { replace: true, priority: -99999, //this needs to run very last @@ -954,99 +794,96 @@ // Needs to run last after all renderContainers are built uiGridCellNavService.decorateRenderContainers(grid); + } + }; + } + }; + }]); + + module.directive('uiGridViewport', ['$timeout', '$document', 'gridUtil', 'uiGridConstants', 'uiGridCellNavService', 'uiGridCellNavConstants','$log', + function ($timeout, $document, gridUtil, uiGridConstants, uiGridCellNavService, uiGridCellNavConstants, $log) { + return { + replace: true, + priority: -99999, //this needs to run very last + require: ['^uiGrid', '^uiGridRenderContainer', '?^uiGridCellnav'], + scope: false, + compile: function () { + return { + post: function ($scope, $elm, $attrs, controllers) { + var uiGridCtrl = controllers[0], + renderContainerCtrl = controllers[1]; + + // Skip attaching cell-nav specific logic if the directive is not attached above us + if (!uiGridCtrl.grid.api.cellNav) { return; } + + var containerId = renderContainerCtrl.containerId; + + var grid = uiGridCtrl.grid; + + // Let the render container be focus-able $elm.attr("tabindex", -1); // Bind to keydown events in the render container $elm.on('keydown', function (evt) { evt.uiGridTargetRenderContainerId = containerId; - return uiGridCtrl.cellNav.handleKeyDown(evt); + var rowCol = uiGridCtrl.grid.api.cellNav.getFocusedCell(); + var result = uiGridCtrl.cellNav.handleKeyDown(evt); + if (result === null) { + uiGridCtrl.grid.api.cellNav.raise.viewPortKeyDown(evt, rowCol); + } }); - var needFocus = false; - - // When there's a scroll event we need to make sure to re-focus the right row, because the cell contents may have changed - grid.api.core.on.scrollEvent($scope, function (args) { - // Skip if not this grid that the event was broadcast for - if (args.grid && args.grid.id !== uiGridCtrl.grid.id) { + + grid.api.core.on.scrollBegin($scope, function (args) { + + // Skip if there's no currently-focused cell + var lastRowCol = uiGridCtrl.grid.api.cellNav.getFocusedCell(); + if (lastRowCol == null) { + return; + } + + //if not in my container, move on + //todo: worry about horiz scroll + if (!renderContainerCtrl.colContainer.containsColumn(lastRowCol.col)) { return; } + //clear dom of focused cell + + var elements = $elm[0].getElementsByClassName('ui-grid-cell-focus'); + Array.prototype.forEach.call(elements,function(e){angular.element(e).removeClass('ui-grid-cell-focus');}); + + }); + + grid.api.core.on.scrollEnd($scope, function (args) { + + // Skip if there's no currently-focused cell - if (uiGridCtrl.grid.api.cellNav.getFocusedCell() == null) { + var lastRowCol = uiGridCtrl.grid.api.cellNav.getFocusedCell(); + if (lastRowCol == null) { return; } - - /* - * If we have scrolled due to cellNav, we want to set the focus to the new cell after the - * virtualisation has run, and after scroll. If we scrolled through the browser scroll - * bar or other user action, we're going to discard the focus, because it will no longer - * be valid (and, noting #2423, trying to keep it causes problems) - * - * If cellNav triggers the scroll, we get a scrollToIfNecessary, then a viewport scroll. We - * want to wait for the viewport scroll to finish, then do a refocus. - * - * If someone manually scrolls we get just the viewport scroll, no scrollToIfNecessary. We - * want to just clear the focus - * - * Logic is: - * - if cellNav scroll, set a flag that will be resolved in the native scroll - * - if native scroll, look for the cellNav promise and resolve it - * - if not present, then use a timeout to clear focus - * - if it is present, then instead use a timeout to set focus - */ - - // We have to wrap in TWO timeouts so that we run AFTER the scroll event is resolved. - if ( args.source === 'uiGridCellNavService.scrollToIfNecessary'){ - needFocus = true; -/* - focusTimeout = $timeout(function () { - if ( clearFocusTimeout ){ - $timeout.cancel(clearFocusTimeout); - } - focusTimeout = $timeout(function () { - if ( clearFocusTimeout ){ - $timeout.cancel(clearFocusTimeout); - } - // Get the last row+col combo - var lastRowCol = uiGridCtrl.grid.api.cellNav.getFocusedCell(); - - // If the body element becomes active, re-focus on the render container so we can capture cellNav events again. - // NOTE: this happens when we navigate LET from the left-most cell (RIGHT from the right-most) and have to re-render a new - // set of cells. The cell element we are navigating to doesn't exist and focus gets lost. This will re-capture it, imperfectly... - if ($document.activeElement === $document.body) { - $elm[0].focus(); - } - - // broadcast a cellNav event so we clear the focus on all cells - uiGridCtrl.cellNav.broadcastCellNav(lastRowCol); - }); - }); - */ - } else { - if ( needFocus ){ - $timeout(function () { - $timeout(function () { - // Get the last row+col combo - var lastRowCol = uiGridCtrl.grid.api.cellNav.getFocusedCell(); - - // If the body element becomes active, re-focus on the render container so we can capture cellNav events again. - // NOTE: this happens when we navigate LET from the left-most cell (RIGHT from the right-most) and have to re-render a new - // set of cells. The cell element we are navigating to doesn't exist and focus gets lost. This will re-capture it, imperfectly... - if ($document.activeElement === $document.body) { - $elm[0].focus(); - } - - // broadcast a cellNav event so we clear the focus on all cells - uiGridCtrl.cellNav.broadcastCellNav(lastRowCol); - - needFocus = false; - }); - }); - } + + //if not in my container, move on + //todo: worry about horiz scroll + if (!renderContainerCtrl.colContainer.containsColumn(lastRowCol.col)) { + return; } - }); - + + //focus the viewport + //$elm[0].focus(); + + uiGridCtrl.cellNav.broadcastCellNav(lastRowCol); + + }); + + grid.api.cellNav.on.navigate($scope, function () { + //focus the viewport because this can sometimes be lost + $elm[0].focus(); + }); + + } }; } @@ -1075,7 +912,7 @@ return; } - setTabEnabled(); + //setTabEnabled(); // When a cell is clicked, broadcast a cellNav event saying that this row+col combo is now focused $elm.find('div').on('click', function (evt) { @@ -1104,10 +941,10 @@ setFocused(); } - // This cellNav event came from a keydown event so we can safely refocus - if (rowCol.hasOwnProperty('eventType') && rowCol.eventType === uiGridCellNavConstants.EVENT_TYPE.KEYDOWN) { - $elm.find('div')[0].focus(); - } + // // This cellNav event came from a keydown event so we can safely refocus + // if (rowCol.hasOwnProperty('eventType') && rowCol.eventType === uiGridCellNavConstants.EVENT_TYPE.KEYDOWN) { + //// $elm.find('div')[0].focus(); + // } } else if (!(uiGridCtrl.grid.options.modifierKeysToMultiSelectCells && modifierDown)) { clearFocus(); diff --git a/src/features/cellnav/test/uiGridCellNavDirective.spec.js b/src/features/cellnav/test/uiGridCellNavDirective.spec.js index 96ce38058c..1ab0d4be25 100644 --- a/src/features/cellnav/test/uiGridCellNavDirective.spec.js +++ b/src/features/cellnav/test/uiGridCellNavDirective.spec.js @@ -33,7 +33,7 @@ describe('ui.grid.cellNav directive', function () { it('should not throw exceptions when scrolling when a grid does NOT have the ui-grid-cellNav directive', function () { expect(function () { - $scope.gridApi.core.raise.scrollEvent({}); + $scope.gridApi.core.raise.scrollBegin({}); }).not.toThrow(); }); diff --git a/src/features/cellnav/test/uiGridCellNavService.spec.js b/src/features/cellnav/test/uiGridCellNavService.spec.js index d8cf28772f..95c278872b 100644 --- a/src/features/cellnav/test/uiGridCellNavService.spec.js +++ b/src/features/cellnav/test/uiGridCellNavService.spec.js @@ -5,19 +5,23 @@ describe('ui.grid.edit uiGridCellNavService', function () { var uiGridConstants; var uiGridCellNavConstants; var $rootScope; + var $timeout; beforeEach(module('ui.grid.cellNav')); - beforeEach(inject(function (_uiGridCellNavService_, _gridClassFactory_, $templateCache, _uiGridConstants_, _uiGridCellNavConstants_, _$rootScope_) { + beforeEach(inject(function (_uiGridCellNavService_, _gridClassFactory_, $templateCache, _uiGridConstants_, _uiGridCellNavConstants_, _$rootScope_, _$timeout_) { uiGridCellNavService = _uiGridCellNavService_; gridClassFactory = _gridClassFactory_; uiGridConstants = _uiGridConstants_; uiGridCellNavConstants = _uiGridCellNavConstants_; $rootScope = _$rootScope_; + $timeout = _$timeout_; $templateCache.put('ui-grid/uiGridCell', '
    '); grid = gridClassFactory.createGrid(); + //throttled scrolling isn't working in tests for some reason + grid.options.scrollDebounce = 0; grid.options.columnDefs = [ {name: 'col0', allowCellFocus: true}, {name: 'col1', allowCellFocus: false}, @@ -192,7 +196,9 @@ describe('ui.grid.edit uiGridCellNavService', function () { grid.setVisibleColumns(grid.columns); grid.setVisibleRows(grid.rows); - + + grid.renderContainers.body.headerHeight = 0; + for ( i = 0; i < 11; i++ ){ grid.columns[i].drawnWidth = i < 6 ? 100 : 200; } @@ -200,7 +206,7 @@ describe('ui.grid.edit uiGridCellNavService', function () { $scope = $rootScope.$new(); args = null; - grid.api.core.on.scrollEvent($scope, function( receivedArgs ){ + grid.api.core.on.scrollEnd($scope, function( receivedArgs ){ args = receivedArgs; }); @@ -211,51 +217,79 @@ describe('ui.grid.edit uiGridCellNavService', function () { // but it means these unit tests are now mostly checking that it is the same it used to // be, not that it is giving some specified result (i.e. I just updated them to what they were) it('should request scroll to row and column', function () { - uiGridCellNavService.scrollTo( grid, grid.options.data[4], grid.columns[4].colDef); - + $timeout(function () { + grid.scrollTo(grid.options.data[4], grid.columns[4].colDef); + }); + $timeout.flush(); + expect(args.grid).toEqual(grid); - expect(args.y).toEqual( { percentage : 5/11 }); + expect(args.y.percentage).toBe(0.41515151515151516); expect(isNaN(args.x.percentage)).toEqual( true ); }); it('should request scroll to row only - first row', function () { - uiGridCellNavService.scrollTo( grid, grid.options.data[0], null); + $timeout(function () { + grid.scrollTo( grid.options.data[0], null); + }); + $timeout.flush(); - expect(args.y).toEqual( { percentage : 2/11 }); + expect(args.y.percentage).toBe(0.14242424242424243); }); it('should request scroll to row only - last row', function () { - uiGridCellNavService.scrollTo( grid, grid.options.data[10], null); + $timeout(function () { + grid.scrollTo( grid.options.data[10], null); + }); + $timeout.flush(); - expect(args.y).toEqual( { percentage : 1 }); + expect(args.y.percentage).toBeGreaterThan(0.5); + expect(args.x).toBe(null); }); it('should request scroll to row only - row 4', function () { - uiGridCellNavService.scrollTo( grid, grid.options.data[5], null); + $timeout(function () { + grid.scrollTo( grid.options.data[5], null); + }); + $timeout.flush(); - expect(args.y).toEqual( { percentage : 6/11 }); + expect(args.y.percentage).toEqual( 0.5060606060606061); + expect(args.x).toBe(null); }); it('should request scroll to column only - first column', function () { - uiGridCellNavService.scrollTo( grid, null, grid.columns[0].colDef); - + $timeout(function () { + grid.scrollTo( null, grid.columns[0].colDef); + }); + $timeout.flush(); + + expect(isNaN(args.x.percentage)).toEqual( true ); }); it('should request scroll to column only - last column', function () { - uiGridCellNavService.scrollTo( grid, null, grid.columns[10].colDef); - + $timeout(function () { + grid.scrollTo( null, grid.columns[10].colDef); + }); + $timeout.flush(); + + expect(isNaN(args.x.percentage)).toEqual( true ); }); it('should request scroll to column only - column 7', function () { - uiGridCellNavService.scrollTo( grid, null, grid.columns[8].colDef); + $timeout(function () { + grid.scrollTo( null, grid.columns[8].colDef); + }); + $timeout.flush(); expect(isNaN(args.x.percentage)).toEqual( true ); }); it('should request no scroll as no row or column', function () { - uiGridCellNavService.scrollTo( grid, null, null ); + $timeout(function () { + grid.scrollTo( null, null ); + }); + $timeout.flush(); expect(args).toEqual( null ); }); diff --git a/src/features/edit/js/gridEdit.js b/src/features/edit/js/gridEdit.js index 78a73523ed..0323217259 100644 --- a/src/features/edit/js/gridEdit.js +++ b/src/features/edit/js/gridEdit.js @@ -52,6 +52,7 @@ service.defaultGridOptions(grid.options); grid.registerColumnBuilder(service.editColumnBuilder); + grid.edit = {}; /** * @ngdoc object @@ -330,6 +331,50 @@ }; }]); + /** + * @ngdoc directive + * @name ui.grid.edit.directive:uiGridRenderContainer + * @element div + * @restrict A + * + * @description Adds keydown listeners to renderContainer element so we can capture when to begin edits + * + */ + module.directive('uiGridViewport', ['$timeout', '$injector', 'gridUtil', 'uiGridEditConstants', + function ($timeout, $injector, gridUtil, uiGridEditConstants) { + return { + replace: true, + priority: -99998, //run before cellNav + require: ['^uiGrid', '^uiGridRenderContainer'], + scope: false, + compile: function () { + return { + post: function ($scope, $elm, $attrs, controllers) { + var uiGridCtrl = controllers[0], + renderContainerCtrl = controllers[1]; + + // Skip attaching if edit and cellNav is not enabled + if (!uiGridCtrl.grid.api.edit || !uiGridCtrl.grid.api.cellNav) { return; } + + //var uiGridCellNavService = $injector.get('uiGridCellNavService'); + + //var containerId = renderContainerCtrl.containerId; + + // var grid = uiGridCtrl.grid; + + $scope.$on(uiGridEditConstants.events.CANCEL_CELL_EDIT, function () { + $elm[0].focus(); + }); + $scope.$on(uiGridEditConstants.events.END_CELL_EDIT, function () { + $elm[0].focus(); + }); + + } + }; + } + }; + }]); + /** * @ngdoc directive * @name ui.grid.edit.directive:uiGridCell @@ -388,6 +433,9 @@ ['$compile', '$injector', '$timeout', 'uiGridConstants', 'uiGridEditConstants', 'gridUtil', '$parse', 'uiGridEditService', '$rootScope', function ($compile, $injector, $timeout, uiGridConstants, uiGridEditConstants, gridUtil, $parse, uiGridEditService, $rootScope) { var touchstartTimeout = 500; + if ($injector.has('uiGridCellNavService')) { + var uiGridCellNavService = $injector.get('uiGridCellNavService'); + } return { priority: -100, // run after default uiGridCell directive @@ -402,23 +450,52 @@ var html; var origCellValue; var inEdit = false; - var isFocusedBeforeEdit = false; var cellModel; var cancelTouchstartTimeout; var editCellScope; + var cellNavNavigateDereg = function() {}; + + // Bind to keydown events in the render container + if (uiGridCtrl && uiGridCtrl.grid.api.cellNav) { + + uiGridCtrl.grid.api.cellNav.on.viewPortKeyDown($scope, function (evt, rowCol) { + if (rowCol === null) { + return; + } + + //important to do this before scrollToIfNecessary + if (rowCol.row === $scope.row && + rowCol.col === $scope.col) { + beginEditKeyDown(evt); + } + + uiGridCtrl.grid.api.core.scrollToIfNecessary(rowCol.row, rowCol.col); + }); + } + registerBeginEditEvents(); function registerBeginEditEvents() { $elm.on('dblclick', beginEdit); - $elm.on('keydown', beginEditKeyDown); - if ($scope.col.colDef.enableCellEditOnFocus) { - $elm.find('div').on('focus', beginEditFocus); - } // Add touchstart handling. If the users starts a touch and it doesn't end after X milliseconds, then start the edit $elm.on('touchstart', touchStart); + + if ($scope.col.colDef.enableCellEditOnFocus) { + if (uiGridCtrl.grid.api.cellNav) { + cellNavNavigateDereg = uiGridCtrl.grid.api.cellNav.on.navigate($scope, function (newRowCol, oldRowCol) { + if (newRowCol.row === $scope.row && newRowCol.col === $scope.col) { + $timeout(function () { + beginEdit(); + }); + } + }); + } + } + + } function touchStart(event) { @@ -452,47 +529,10 @@ function cancelBeginEditEvents() { $elm.off('dblclick', beginEdit); $elm.off('keydown', beginEditKeyDown); - if ($scope.col.colDef.enableCellEditOnFocus) { - $elm.find('div').off('focus', beginEditFocus); - } $elm.off('touchstart', touchStart); + cellNavNavigateDereg(); } - function beginEditFocus(evt) { - // gridUtil.logDebug('begin edit'); - if (uiGridCtrl && uiGridCtrl.cellNav) { - // NOTE(c0bra): This is causing a loop where focusCell causes beginEditFocus to be called.... - uiGridCtrl.cellNav.focusCell($scope.row, $scope.col); - } - - evt.stopPropagation(); - beginEdit(); - } - - // If the cellNagv module is installed and we can get the uiGridCellNavConstants value injected, - // then if the column has enableCellEditOnFocus set to true, we need to listen for cellNav events - // to this cell and start editing when the "focus" reaches us - try { - var uiGridCellNavConstants = $injector.get('uiGridCellNavConstants'); - - if ($scope.col.colDef.enableCellEditOnFocus) { - $scope.$on(uiGridCellNavConstants.CELL_NAV_EVENT, function (evt, rowCol) { - if (rowCol.row === $scope.row && rowCol.col === $scope.col) { - // @PaulL: ugly nested timeout. Without this, this same scroll event ends the editing before it gets started - // Issue #2896 raised to fix this situation - $timeout(function() { - $timeout(function() { - beginEdit(); - }); - }); - } else { - endEdit(); - } - }); - } - } - catch (e) {} - function beginEditKeyDown(evt) { if (uiGridEditService.isStartEditKey(evt)) { beginEdit(); @@ -601,11 +641,7 @@ return; } - // if the cell isn't fully visible, and cellNav is present, scroll it to be fully visible before we start - if ( $scope.grid.api.cellNav ){ - $scope.grid.api.cellNav.scrollToIfNecessary( $scope.row, $scope.col ); - } - + cellModel = $parse($scope.row.getQualifiedColField($scope.col)); //get original value from the cell origCellValue = cellModel($scope); @@ -649,7 +685,6 @@ editCellScope = $scope.$new(); $compile(cellElement)(editCellScope); var gridCellContentsEl = angular.element($elm.children()[0]); - isFocusedBeforeEdit = gridCellContentsEl.hasClass('ui-grid-cell-focus'); gridCellContentsEl.addClass('ui-grid-cell-contents-hidden'); }; if (!$rootScope.$$phase) { @@ -659,7 +694,7 @@ } //stop editing when grid is scrolled - var deregOnGridScroll = $scope.col.grid.api.core.on.scrollEvent($scope, function () { + var deregOnGridScroll = $scope.col.grid.api.core.on.scrollBegin($scope, function () { endEdit(true); $scope.grid.api.edit.raise.afterCellEdit($scope.row.entity, $scope.col.colDef, cellModel($scope), origCellValue); deregOnGridScroll(); @@ -697,10 +732,6 @@ editCellScope.$destroy(); angular.element($elm.children()[1]).remove(); gridCellContentsEl.removeClass('ui-grid-cell-contents-hidden'); - if (retainFocus && isFocusedBeforeEdit) { - gridCellContentsEl[0].focus(); - } - isFocusedBeforeEdit = false; inEdit = false; registerBeginEditEvents(); $scope.grid.api.core.notifyDataChange( uiGridConstants.dataChange.EDIT ); @@ -756,8 +787,8 @@ * */ module.directive('uiGridEditor', - ['gridUtil', 'uiGridConstants', 'uiGridEditConstants', - function (gridUtil, uiGridConstants, uiGridEditConstants) { + ['gridUtil', 'uiGridConstants', 'uiGridEditConstants','$timeout', + function (gridUtil, uiGridConstants, uiGridEditConstants, $timeout) { return { scope: true, require: ['?^uiGrid', '?^uiGridRenderContainer'], @@ -775,6 +806,7 @@ $scope.$on(uiGridEditConstants.events.BEGIN_CELL_EDIT, function () { $elm[0].focus(); $elm[0].select(); + $elm.on('blur', function (evt) { $scope.stopEdit(evt); }); @@ -829,9 +861,11 @@ } } // Pass the keydown event off to the cellNav service, if it exists - else if (uiGridCtrl && uiGridCtrl.hasOwnProperty('cellNav') && renderContainerCtrl) { + else if (uiGridCtrl && uiGridCtrl.grid.api.cellNav) { evt.uiGridTargetRenderContainerId = renderContainerCtrl.containerId; - uiGridCtrl.cellNav.handleKeyDown(evt); + if (uiGridCtrl.cellNav.handleKeyDown(evt) !== null) { + $scope.stopEdit(evt); + } } return true; diff --git a/src/features/edit/test/uiGridCell.spec.js b/src/features/edit/test/uiGridCell.spec.js index 8262392ea5..c181750532 100644 --- a/src/features/edit/test/uiGridCell.spec.js +++ b/src/features/edit/test/uiGridCell.spec.js @@ -119,7 +119,7 @@ describe('ui.grid.edit GridCellDirective', function () { it('should stop when grid scrolls', function () { //stop edit - scope.grid.api.core.raise.scrollEvent(); + scope.grid.api.core.raise.scrollBegin(); scope.$digest(); //back to beginning expect(element.html()).toBe(displayHtml); diff --git a/src/features/edit/test/uiGridCellWithDropdown.spec.js b/src/features/edit/test/uiGridCellWithDropdown.spec.js index 264783060e..06a3da0ae8 100644 --- a/src/features/edit/test/uiGridCellWithDropdown.spec.js +++ b/src/features/edit/test/uiGridCellWithDropdown.spec.js @@ -141,7 +141,7 @@ describe('ui.grid.edit GridCellDirective - with dropdown', function () { it('should stop when grid scrolls', function () { //stop edit - scope.grid.api.core.raise.scrollEvent(scope); + scope.grid.api.core.raise.scrollBegin(scope); scope.$digest(); //back to beginning expect(element.html()).toBe(displayHtml); diff --git a/src/features/move-columns/js/column-movable.js b/src/features/move-columns/js/column-movable.js index 7b2b59f364..3b3dbe803d 100644 --- a/src/features/move-columns/js/column-movable.js +++ b/src/features/move-columns/js/column-movable.js @@ -320,7 +320,7 @@ changeValue *= 8; var scrollEvent = new ScrollEvent($scope.col.grid, null, null, 'uiGridHeaderCell.moveElement'); scrollEvent.x = {pixels: changeValue}; - scrollEvent.fireScrollingEvent(); + scrollEvent.grid.scrollContainers('',scrollEvent); } //Calculate total width of columns on the left of the moving column and the mouse movement diff --git a/src/features/saveState/js/saveState.js b/src/features/saveState/js/saveState.js index 0789171762..a4bf2d33ea 100644 --- a/src/features/saveState/js/saveState.js +++ b/src/features/saveState/js/saveState.js @@ -550,7 +550,7 @@ if (scrollFocusState.focus ){ grid.api.cellNav.scrollToFocus( entity, colDef ); } else { - grid.api.cellNav.scrollTo( entity, colDef ); + grid.scrollTo( entity, colDef ); } } }, diff --git a/src/features/saveState/test/saveState.spec.js b/src/features/saveState/test/saveState.spec.js index 0513cba311..470ed7f276 100644 --- a/src/features/saveState/test/saveState.spec.js +++ b/src/features/saveState/test/saveState.spec.js @@ -430,23 +430,23 @@ describe('ui.grid.saveState uiGridSaveStateService', function () { it('restores no row/col, without identity function', function() { uiGridCellNavService.initializeGrid(grid); - spyOn( grid.api.cellNav, 'scrollTo' ); + spyOn( grid.api.core, 'scrollTo' ); spyOn( grid.api.cellNav, 'scrollToFocus' ); uiGridSaveStateService.restoreScrollFocus( grid, $scope, {} ); - expect( grid.api.cellNav.scrollTo ).not.toHaveBeenCalled(); + expect( grid.api.core.scrollTo ).not.toHaveBeenCalled(); expect( grid.api.cellNav.scrollToFocus ).not.toHaveBeenCalled(); }); it('restores focus row only, without identity function', function() { uiGridCellNavService.initializeGrid(grid); - spyOn( grid.api.cellNav, 'scrollTo' ); + spyOn( grid.api.core, 'scrollTo' ); spyOn( grid.api.cellNav, 'scrollToFocus' ); uiGridSaveStateService.restoreScrollFocus( grid, $scope, { focus: true, rowVal: { identity: false, row: 2 } } ); - expect( grid.api.cellNav.scrollTo ).not.toHaveBeenCalled(); + expect( grid.api.core.scrollTo ).not.toHaveBeenCalled(); expect( grid.api.cellNav.scrollToFocus ).toHaveBeenCalledWith( grid.rows[3].entity, undefined ); }); @@ -457,23 +457,23 @@ describe('ui.grid.saveState uiGridSaveStateService', function () { return rowEntity.col1; }; - spyOn( grid.api.cellNav, 'scrollTo' ); + spyOn( grid.api.core, 'scrollTo' ); spyOn( grid.api.cellNav, 'scrollToFocus' ); uiGridSaveStateService.restoreScrollFocus( grid, $scope, { focus: true, rowVal: { identity: true, row: 'a_3' } } ); - expect( grid.api.cellNav.scrollTo ).not.toHaveBeenCalled(); + expect( grid.api.core.scrollTo ).not.toHaveBeenCalled(); expect( grid.api.cellNav.scrollToFocus ).toHaveBeenCalledWith( grid.rows[3].entity, undefined ); }); it('restores focus col only, without identity function', function() { uiGridCellNavService.initializeGrid(grid); - spyOn( grid.api.cellNav, 'scrollTo' ); + spyOn( grid.api.core, 'scrollTo' ); spyOn( grid.api.cellNav, 'scrollToFocus' ); uiGridSaveStateService.restoreScrollFocus( grid, $scope, { focus: true, colName: 'col2' } ); - expect( grid.api.cellNav.scrollTo ).not.toHaveBeenCalled(); + expect( grid.api.core.scrollTo ).not.toHaveBeenCalled(); expect( grid.api.cellNav.scrollToFocus ).toHaveBeenCalledWith( null, grid.options.columnDefs[1] ); }); @@ -484,23 +484,23 @@ describe('ui.grid.saveState uiGridSaveStateService', function () { return rowEntity.col1; }; - spyOn( grid.api.cellNav, 'scrollTo' ); + spyOn( grid.api.core, 'scrollTo' ); spyOn( grid.api.cellNav, 'scrollToFocus' ); uiGridSaveStateService.restoreScrollFocus( grid, $scope, { focus: true, colName: 'col2' } ); - expect( grid.api.cellNav.scrollTo ).not.toHaveBeenCalled(); + expect( grid.api.core.scrollTo ).not.toHaveBeenCalled(); expect( grid.api.cellNav.scrollToFocus ).toHaveBeenCalledWith( null, grid.options.columnDefs[1] ); }); it('restores focus col and row, without identity function', function() { uiGridCellNavService.initializeGrid(grid); - spyOn( grid.api.cellNav, 'scrollTo' ); + spyOn( grid.api.core, 'scrollTo' ); spyOn( grid.api.cellNav, 'scrollToFocus' ); uiGridSaveStateService.restoreScrollFocus( grid, $scope, { focus: true, colName: 'col2', rowVal: { identity: false, row: 2 } } ); - expect( grid.api.cellNav.scrollTo ).not.toHaveBeenCalled(); + expect( grid.api.core.scrollTo ).not.toHaveBeenCalled(); expect( grid.api.cellNav.scrollToFocus ).toHaveBeenCalledWith( grid.rows[3].entity, grid.options.columnDefs[1] ); }); @@ -511,12 +511,12 @@ describe('ui.grid.saveState uiGridSaveStateService', function () { return rowEntity.col1; }; - spyOn( grid.api.cellNav, 'scrollTo' ); + spyOn( grid.api.core, 'scrollTo' ); spyOn( grid.api.cellNav, 'scrollToFocus' ); uiGridSaveStateService.restoreScrollFocus( grid, $scope, { focus: true, colName: 'col2', rowVal: { identity: true, row: 'a_3' } } ); - expect( grid.api.cellNav.scrollTo ).not.toHaveBeenCalled(); + expect( grid.api.core.scrollTo ).not.toHaveBeenCalled(); expect( grid.api.cellNav.scrollToFocus ).toHaveBeenCalledWith( grid.rows[3].entity, grid.options.columnDefs[1] ); }); @@ -645,7 +645,7 @@ describe('ui.grid.saveState uiGridSaveStateService', function () { return { row: grid.rows[2], col: grid.columns[3] }; }); - spyOn( grid.api.cellNav, 'scrollTo' ); + spyOn( grid.api.core, 'scrollTo' ); spyOn( grid.api.cellNav, 'scrollToFocus' ); grid.options.saveRowIdentity = function( rowEntity ){ @@ -663,7 +663,7 @@ describe('ui.grid.saveState uiGridSaveStateService', function () { grid.api.saveState.restore( $scope, state ); expect( grid.api.selection.getSelectedGridRows() ).toEqual( [ grid.rows[0], grid.rows[3] ] ); - expect( grid.api.cellNav.scrollTo ).not.toHaveBeenCalled(); + expect( grid.api.core.scrollTo ).not.toHaveBeenCalled(); expect( grid.api.cellNav.scrollToFocus ).toHaveBeenCalledWith( grid.rows[2].entity, grid.options.columnDefs[3] ); }); }); diff --git a/src/features/selection/js/selection.js b/src/features/selection/js/selection.js index e1c8904f69..508b98af35 100644 --- a/src/features/selection/js/selection.js +++ b/src/features/selection/js/selection.js @@ -775,18 +775,38 @@ return { priority: -200, // run after default uiGridCell directive restrict: 'A', + require: '?^uiGrid', scope: false, - link: function ($scope, $elm, $attrs) { + link: function ($scope, $elm, $attrs, uiGridCtrl) { var touchStartTime = 0; var touchTimeout = 300; - $elm.bind('keydown', function (evt) { - if (evt.keyCode === 32 && $scope.col.colDef.name === "selectionRowHeaderCol") { - uiGridSelectionService.toggleRowSelection($scope.grid, $scope.row, evt, ($scope.grid.options.multiSelect && !$scope.grid.options.modifierKeysToMultiSelect), $scope.grid.options.noUnselect); - $scope.$apply(); - } - }); + // Bind to keydown events in the render container + if (uiGridCtrl.grid.api.cellNav) { + + uiGridCtrl.grid.api.cellNav.on.viewPortKeyDown($scope, function (evt, rowCol) { + if (rowCol === null || + rowCol.row !== $scope.row || + rowCol.col !== $scope.col) { + return; + } + + if (evt.keyCode === 32 && $scope.col.colDef.name === "selectionRowHeaderCol") { + uiGridSelectionService.toggleRowSelection($scope.grid, $scope.row, evt, ($scope.grid.options.multiSelect && !$scope.grid.options.modifierKeysToMultiSelect), $scope.grid.options.noUnselect); + $scope.$apply(); + } + + // uiGridCellNavService.scrollToIfNecessary(uiGridCtrl.grid, rowCol.row, rowCol.col); + }); + } + + //$elm.bind('keydown', function (evt) { + // if (evt.keyCode === 32 && $scope.col.colDef.name === "selectionRowHeaderCol") { + // uiGridSelectionService.toggleRowSelection($scope.grid, $scope.row, evt, ($scope.grid.options.multiSelect && !$scope.grid.options.modifierKeysToMultiSelect), $scope.grid.options.noUnselect); + // $scope.$apply(); + // } + //}); var selectCells = function(evt){ if (evt.shiftKey) { diff --git a/src/js/core/directives/ui-grid-menu.js b/src/js/core/directives/ui-grid-menu.js index e79c95f657..3f1658e045 100644 --- a/src/js/core/directives/ui-grid-menu.js +++ b/src/js/core/directives/ui-grid-menu.js @@ -148,7 +148,7 @@ function ($compile, $timeout, $window, $document, gridUtil, uiGridConstants) { }); if (uiGridCtrl) { - $scope.$on('$destroy', uiGridCtrl.grid.api.core.on.scrollEvent($scope, applyHideMenu )); + $scope.$on('$destroy', uiGridCtrl.grid.api.core.on.scrollBegin($scope, applyHideMenu )); } $scope.$on('$destroy', $scope.$on(uiGridConstants.events.ITEM_DRAGGING, applyHideMenu )); diff --git a/src/js/core/directives/ui-grid-render-container.js b/src/js/core/directives/ui-grid-render-container.js index 4b10cb4f2e..b8d9527192 100644 --- a/src/js/core/directives/ui-grid-render-container.js +++ b/src/js/core/directives/ui-grid-render-container.js @@ -68,56 +68,53 @@ $elm.addClass('ui-grid-render-container-' + $scope.containerId); // Bind to left/right-scroll events - if ($scope.bindScrollHorizontal || $scope.bindScrollVertical) { - grid.api.core.on.scrollEvent($scope,scrollHandler); - } - - function scrollHandler (args) { - // exit if not for this grid - if (args.grid && args.grid.id !== grid.id){ - return; - } - - - // Vertical scroll - if (args.y && $scope.bindScrollVertical) { - containerCtrl.prevScrollArgs = args; - - var newScrollTop = args.getNewScrollTop(rowContainer,containerCtrl.viewport); - - //only set scrollTop if we coming from something other than viewPort scrollBar or - //another column container - if (args.source !== ScrollEvent.Sources.ViewPortScroll || - args.sourceColContainer !== colContainer) { - containerCtrl.viewport[0].scrollTop = newScrollTop; - } - - } - - // Horizontal scroll - if (args.x && $scope.bindScrollHorizontal) { - containerCtrl.prevScrollArgs = args; - var newScrollLeft = args.getNewScrollLeft(colContainer, containerCtrl.viewport); - - // Make the current horizontal scroll position available in the $scope - $scope.newScrollLeft = newScrollLeft; - - if (containerCtrl.headerViewport) { - containerCtrl.headerViewport.scrollLeft = gridUtil.denormalizeScrollLeft(containerCtrl.headerViewport, newScrollLeft); - } - - if (containerCtrl.footerViewport) { - containerCtrl.footerViewport.scrollLeft = gridUtil.denormalizeScrollLeft(containerCtrl.footerViewport, newScrollLeft); - } - - // Scroll came from somewhere else, so the viewport must be positioned - if (args.source !== ScrollEvent.Sources.ViewPortScroll) { - containerCtrl.viewport[0].scrollLeft = gridUtil.denormalizeScrollLeft(containerCtrl.viewport, newScrollLeft); - } - - containerCtrl.prevScrollLeft = newScrollLeft; - } - } + //if ($scope.bindScrollHorizontal || $scope.bindScrollVertical) { + // grid.api.core.on.scrollEvent($scope,scrollHandler); + //} + + +// function scrollHandler (args) { +// +// // Vertical scroll +// if (args.y && $scope.bindScrollVertical) { +// containerCtrl.prevScrollArgs = args; +// +// var newScrollTop = args.getNewScrollTop(rowContainer,containerCtrl.viewport); +// +// //only set scrollTop if we coming from something other than viewPort scrollBar or +// //another column container +// if (args.source !== ScrollEvent.Sources.ViewPortScroll || +// args.sourceColContainer !== colContainer) { +// containerCtrl.viewport[0].scrollTop = newScrollTop; +// } +// +// } +// +// // Horizontal scroll +// if (args.x && $scope.bindScrollHorizontal) { +// containerCtrl.prevScrollArgs = args; +// +// var newScrollLeft = args.getNewScrollLeft(colContainer, containerCtrl.viewport); +// +// // Make the current horizontal scroll position available in the $scope +// $scope.newScrollLeft = newScrollLeft; +// +// if (containerCtrl.headerViewport) { +// containerCtrl.headerViewport.scrollLeft = gridUtil.denormalizeScrollLeft(containerCtrl.headerViewport, newScrollLeft); +// } +// +// if (containerCtrl.footerViewport) { +// containerCtrl.footerViewport.scrollLeft = gridUtil.denormalizeScrollLeft(containerCtrl.footerViewport, newScrollLeft); +// } +// +// // Scroll came from somewhere else, so the viewport must be positioned +// if (args.source !== ScrollEvent.Sources.ViewPortScroll) { +// containerCtrl.viewport[0].scrollLeft = gridUtil.denormalizeScrollLeft(containerCtrl.viewport, newScrollLeft); +// } +// +// containerCtrl.prevScrollLeft = newScrollLeft; +// } +// } // Scroll the render container viewport when the mousewheel is used gridUtil.on.mousewheel($elm, function (event) { @@ -149,12 +146,12 @@ } + scrollEvent.fireThrottledScrollingEvent('', scrollEvent); + // Let the parent container scroll if the grid is already at the top/bottom if ((scrollEvent.y && scrollEvent.y.percentage !== 0 && scrollEvent.y.percentage !== 1 && containerCtrl.viewport[0].scrollTop !== 0 ) || (scrollEvent.x && scrollEvent.x.percentage !== 0 && scrollEvent.x.percentage !== 1)) { - event.preventDefault(); - scrollEvent.fireThrottledScrollingEvent(); } }); diff --git a/src/js/core/directives/ui-grid-viewport.js b/src/js/core/directives/ui-grid-viewport.js index 5db29e0565..68454b2ebd 100644 --- a/src/js/core/directives/ui-grid-viewport.js +++ b/src/js/core/directives/ui-grid-viewport.js @@ -1,8 +1,8 @@ (function(){ 'use strict'; - angular.module('ui.grid').directive('uiGridViewport', ['gridUtil','ScrollEvent','uiGridConstants', - function(gridUtil, ScrollEvent, uiGridConstants) { + angular.module('ui.grid').directive('uiGridViewport', ['gridUtil','ScrollEvent','uiGridConstants', '$log', + function(gridUtil, ScrollEvent, uiGridConstants, $log) { return { replace: true, scope: {}, @@ -31,62 +31,77 @@ // Register this viewport with its container containerCtrl.viewport = $elm; - $elm.on('scroll', function (evt) { + $elm.on('scroll', scrollHandler); + + var ignoreScroll = false; + function scrollHandler(evt) { + if (ignoreScroll && (grid.isScrollingHorizontally || grid.isScrollingHorizontally)) { + //don't ask for scrollTop if we just set it + ignoreScroll = false; + return; + } + + + ignoreScroll = true; + var newScrollTop = $elm[0].scrollTop; - // var newScrollLeft = $elm[0].scrollLeft; var newScrollLeft = gridUtil.normalizeScrollLeft($elm); - var horizScrollPercentage = -1; - var vertScrollPercentage = -1; - // Handle RTL here + var vertScrollPercentage = rowContainer.scrollVertical(newScrollTop); + var horizScrollPercentage = colContainer.scrollHorizontal(newScrollLeft); - if (newScrollLeft !== colContainer.prevScrollLeft) { - grid.flagScrollingHorizontally(); - var xDiff = newScrollLeft - colContainer.prevScrollLeft; + var scrollEvent = new ScrollEvent(grid, rowContainer, colContainer, ScrollEvent.Sources.ViewPortScroll); + scrollEvent.newScrollLeft = newScrollLeft; + scrollEvent.newScrollTop = newScrollTop; + if ( horizScrollPercentage > -1 ){ + scrollEvent.x = { percentage: horizScrollPercentage }; + } - if (xDiff > 0) { grid.scrollDirection = uiGridConstants.scrollDirection.RIGHT; } - if (xDiff < 0) { grid.scrollDirection = uiGridConstants.scrollDirection.LEFT; } + if ( vertScrollPercentage > -1 ){ + scrollEvent.y = { percentage: vertScrollPercentage }; + } - var horizScrollLength = (colContainer.getCanvasWidth() - colContainer.getViewportWidth()); - if (horizScrollLength !== 0) { - horizScrollPercentage = newScrollLeft / horizScrollLength; - } - else { - horizScrollPercentage = 0; - } + grid.scrollContainers($scope.$parent.containerId, scrollEvent); + } - colContainer.adjustScrollHorizontal(newScrollLeft, horizScrollPercentage); - } + if ($scope.$parent.bindScrollVertical) { + grid.addVerticalScrollSync($scope.$parent.containerId, syncVerticalScroll); + } - if (newScrollTop !== rowContainer.prevScrollTop) { - grid.flagScrollingVertically(); - var yDiff = newScrollTop - rowContainer.prevScrollTop; + if ($scope.$parent.bindScrollHorizontal) { + grid.addHorizontalScrollSync($scope.$parent.containerId, syncHorizontalScroll); + grid.addHorizontalScrollSync($scope.$parent.containerId + 'header', syncHorizontalHeader); + grid.addHorizontalScrollSync($scope.$parent.containerId + 'footer', syncHorizontalFooter); + } - if (yDiff > 0 ) { grid.scrollDirection = uiGridConstants.scrollDirection.DOWN; } - if (yDiff < 0 ) { grid.scrollDirection = uiGridConstants.scrollDirection.UP; } + function syncVerticalScroll(scrollEvent){ + containerCtrl.prevScrollArgs = scrollEvent; + var newScrollTop = scrollEvent.getNewScrollTop(rowContainer,containerCtrl.viewport); + $elm[0].scrollTop = newScrollTop; - var vertScrollLength = rowContainer.getVerticalScrollLength(); + } - vertScrollPercentage = newScrollTop / vertScrollLength; + function syncHorizontalScroll(scrollEvent){ + containerCtrl.prevScrollArgs = scrollEvent; + var newScrollLeft = scrollEvent.getNewScrollLeft(colContainer, containerCtrl.viewport); + $elm[0].scrollLeft = newScrollLeft; + } - if (vertScrollPercentage > 1) { vertScrollPercentage = 1; } - if (vertScrollPercentage < 0) { vertScrollPercentage = 0; } - - rowContainer.adjustScrollVertical(newScrollTop, vertScrollPercentage); + function syncHorizontalHeader(scrollEvent){ + var newScrollLeft = scrollEvent.getNewScrollLeft(colContainer, containerCtrl.viewport); + if (containerCtrl.headerViewport) { + containerCtrl.headerViewport.scrollLeft = newScrollLeft; } + } + + function syncHorizontalFooter(scrollEvent){ + var newScrollLeft = scrollEvent.getNewScrollLeft(colContainer, containerCtrl.viewport); + if (containerCtrl.footerViewport) { + containerCtrl.footerViewport.scrollLeft = newScrollLeft; + } + } - var scrollEvent = new ScrollEvent(grid, rowContainer, colContainer, ScrollEvent.Sources.ViewPortScroll); - scrollEvent.newScrollLeft = newScrollLeft; - scrollEvent.newScrollTop = newScrollTop; - if ( horizScrollPercentage > -1 ){ - scrollEvent.x = { percentage: horizScrollPercentage }; - } - if ( vertScrollPercentage > -1 ){ - scrollEvent.y = { percentage: vertScrollPercentage }; - } - scrollEvent.fireScrollingEvent(); - }); }, controller: ['$scope', function ($scope) { this.rowStyle = function (index) { diff --git a/src/js/core/factories/Grid.js b/src/js/core/factories/Grid.js index 746a685546..996a470bdd 100644 --- a/src/js/core/factories/Grid.js +++ b/src/js/core/factories/Grid.js @@ -1,8 +1,8 @@ (function(){ angular.module('ui.grid') -.factory('Grid', ['$q', '$compile', '$parse', 'gridUtil', 'uiGridConstants', 'GridOptions', 'GridColumn', 'GridRow', 'GridApi', 'rowSorter', 'rowSearcher', 'GridRenderContainer', '$timeout', - function($q, $compile, $parse, gridUtil, uiGridConstants, GridOptions, GridColumn, GridRow, GridApi, rowSorter, rowSearcher, GridRenderContainer, $timeout) { +.factory('Grid', ['$q', '$compile', '$parse', 'gridUtil', 'uiGridConstants', 'GridOptions', 'GridColumn', 'GridRow', 'GridApi', 'rowSorter', 'rowSearcher', 'GridRenderContainer', '$timeout','ScrollEvent', + function($q, $compile, $parse, gridUtil, uiGridConstants, GridOptions, GridColumn, GridRow, GridApi, rowSorter, rowSearcher, GridRenderContainer, $timeout, ScrollEvent) { /** * @ngdoc object @@ -49,8 +49,23 @@ angular.module('ui.grid') self.headerHeight = self.options.headerRowHeight; + /** + * @ngdoc object + * @name footerHeight + * @propertyOf ui.grid.class:Grid + * @description returns the total footer height gridFooter + columnFooter + */ self.footerHeight = self.calcFooterHeight(); + + /** + * @ngdoc object + * @name columnFooterHeight + * @propertyOf ui.grid.class:Grid + * @description returns the total column footer height + */ + self.columnFooterHeight = self.calcColumnFooterHeight(); + self.rtl = false; self.gridHeight = 0; self.gridWidth = 0; @@ -62,7 +77,9 @@ angular.module('ui.grid') self.viewportAdjusters = []; self.rowHeaderColumns = []; self.dataChangeCallbacks = {}; - + self.verticalScrollSyncCallBackFns = {}; + self.horizontalScrollSyncCallBackFns = {}; + // self.visibleRowCache = []; // Set of 'render' containers for self grid, which can render sets of rows @@ -109,16 +126,24 @@ angular.module('ui.grid') */ self.scrollDirection = uiGridConstants.scrollDirection.NONE; - var debouncedVertical = gridUtil.debounce(function () { + function vertical (scrollEvent) { self.isScrollingVertically = false; + self.api.core.raise.scrollEnd(scrollEvent); self.scrollDirection = uiGridConstants.scrollDirection.NONE; - }, 1000); - - var debouncedHorizontal = gridUtil.debounce(function () { + } + + var debouncedVertical = gridUtil.debounce(vertical, self.options.scrollDebounce); + var debouncedVerticalMinDelay = gridUtil.debounce(vertical, 0); + + function horizontal (scrollEvent) { self.isScrollingHorizontally = false; + self.api.core.raise.scrollEnd(scrollEvent); self.scrollDirection = uiGridConstants.scrollDirection.NONE; - }, 1000); - + } + + var debouncedHorizontal = gridUtil.debounce(horizontal, self.options.scrollDebounce); + var debouncedHorizontalMinDelay = gridUtil.debounce(horizontal, 0); + /** * @ngdoc function @@ -126,9 +151,17 @@ angular.module('ui.grid') * @methodOf ui.grid.class:Grid * @description sets isScrollingVertically to true and sets it to false in a debounced function */ - self.flagScrollingVertically = function() { + self.flagScrollingVertically = function(scrollEvent) { + if (!self.isScrollingVertically && !self.isScrollingHorizontally) { + self.api.core.raise.scrollBegin(scrollEvent); + } self.isScrollingVertically = true; - debouncedVertical(); + if (self.options.scrollDebounce === 0 || !scrollEvent.withDelay) { + debouncedVerticalMinDelay(scrollEvent); + } + else { + debouncedVertical(scrollEvent); + } }; /** @@ -137,9 +170,17 @@ angular.module('ui.grid') * @methodOf ui.grid.class:Grid * @description sets isScrollingHorizontally to true and sets it to false in a debounced function */ - self.flagScrollingHorizontally = function() { + self.flagScrollingHorizontally = function(scrollEvent) { + if (!self.isScrollingVertically && !self.isScrollingHorizontally) { + self.api.core.raise.scrollBegin(scrollEvent); + } self.isScrollingHorizontally = true; - debouncedHorizontal(); + if (self.options.scrollDebounce === 0 || !scrollEvent.withDelay) { + debouncedHorizontalMinDelay(scrollEvent); + } + else { + debouncedHorizontal(scrollEvent); + } }; self.scrollbarHeight = 0; @@ -230,8 +271,34 @@ angular.module('ui.grid') * */ self.api.registerMethod( 'core', 'addRowHeaderColumn', this.addRowHeaderColumn ); - - + + /** + * @ngdoc function + * @name scrollToIfNecessary + * @methodOf ui.grid.core.api:PublicApi + * @description Scrolls the grid to make a certain row and column combo visible, + * in the case that it is not completely visible on the screen already. + * @param {GridRow} gridRow row to make visible + * @param {GridCol} gridCol column to make visible + * @returns {promise} a promise that is resolved when scrolling is complete + * + */ + self.api.registerMethod( 'core', 'scrollToIfNecessary', function(gridRow, gridCol) { return self.scrollToIfNecessary(gridRow, gridCol);} ); + + /** + * @ngdoc function + * @name scrollTo + * @methodOf ui.grid.core.api:PublicApi + * @description Scroll the grid such that the specified + * row and column is in view + * @param {object} rowEntity gridOptions.data[] array instance to make visible + * @param {object} colDef to make visible + * @returns {promise} a promise that is resolved after any scrolling is finished + */ + self.api.registerMethod( 'core', 'scrollTo', function (rowEntity, colDef) { return self.scrollTo(rowEntity, colDef);} ); + + + /** * @ngdoc function * @name sortHandleNulls @@ -330,6 +397,14 @@ angular.module('ui.grid') height += this.options.gridFooterHeight; } + height += this.calcColumnFooterHeight(); + + return height; + }; + + Grid.prototype.calcColumnFooterHeight = function () { + var height = 0; + if (this.options.showColumnFooter) { height += this.options.columnFooterHeight; } @@ -1519,6 +1594,67 @@ angular.module('ui.grid') return viewPortWidth; }; + Grid.prototype.addVerticalScrollSync = function (containerId, callBackFn) { + this.verticalScrollSyncCallBackFns[containerId] = callBackFn; + }; + + Grid.prototype.addHorizontalScrollSync = function (containerId, callBackFn) { + this.horizontalScrollSyncCallBackFns[containerId] = callBackFn; + }; + +/** + * Scroll needed containers by calling their ScrollSyncs + * @param sourceContainerId the containerId that has already set it's top/left. + * can be empty string which means all containers need to set top/left + * @param scrollEvent + */ + Grid.prototype.scrollContainers = function (sourceContainerId, scrollEvent) { + + if (scrollEvent.y) { + //default for no container Id (ex. mousewheel means that all containers must set scrollTop/Left) + var verts = ['body','left', 'right']; + + this.flagScrollingVertically(scrollEvent); + + if (sourceContainerId === 'body') { + verts = ['left', 'right']; + } + else if (sourceContainerId === 'left') { + verts = ['body', 'right']; + } + else if (sourceContainerId === 'right') { + verts = ['body', 'left']; + } + + for (var i = 0; i < verts.length; i++) { + var id = verts[i]; + if (this.verticalScrollSyncCallBackFns[id]) { + this.verticalScrollSyncCallBackFns[id](scrollEvent); + } + } + + } + + if (scrollEvent.x) { + //default for no container Id (ex. mousewheel means that all containers must set scrollTop/Left) + var horizs = ['body','bodyheader', 'bodyfooter']; + + this.flagScrollingHorizontally(scrollEvent); + if (sourceContainerId === 'body') { + horizs = ['bodyheader', 'bodyfooter']; + } + + for (var j = 0; j < horizs.length; j++) { + var idh = horizs[j]; + if (this.horizontalScrollSyncCallBackFns[idh]) { + this.horizontalScrollSyncCallBackFns[idh](scrollEvent); + } + } + + } + + }; + Grid.prototype.registerViewportAdjuster = function registerViewportAdjuster(func) { this.viewportAdjusters.push(func); }; @@ -1983,10 +2119,191 @@ angular.module('ui.grid') return this.hasRightContainer() && this.renderContainers.right.renderedColumns.length > 0; }; + /** + * @ngdoc method + * @methodOf ui.grid.class:Grid + * @name scrollToIfNecessary + * @description Scrolls the grid to make a certain row and column combo visible, + * in the case that it is not completely visible on the screen already. + * @param {GridRow} gridRow row to make visible + * @param {GridCol} gridCol column to make visible + * @returns {promise} a promise that is resolved when scrolling is complete + */ + Grid.prototype.scrollToIfNecessary = function (gridRow, gridCol) { + var self = this; + + var scrollEvent = new ScrollEvent(self, 'uiGrid.scrollToIfNecessary'); + + // Alias the visible row and column caches + var visRowCache = self.renderContainers.body.visibleRowCache; + var visColCache = self.renderContainers.body.visibleColumnCache; + + /*-- Get the top, left, right, and bottom "scrolled" edges of the grid --*/ + + // The top boundary is the current Y scroll position PLUS the header height, because the header can obscure rows when the grid is scrolled downwards + var topBound = self.renderContainers.body.prevScrollTop + self.headerHeight; + + // Don't the let top boundary be less than 0 + topBound = (topBound < 0) ? 0 : topBound; + + // The left boundary is the current X scroll position + var leftBound = self.renderContainers.body.prevScrollLeft; + + // The bottom boundary is the current Y scroll position, plus the height of the grid, but minus the header height. + // Basically this is the viewport height added on to the scroll position + var bottomBound = self.renderContainers.body.prevScrollTop + self.gridHeight - self.renderContainers.body.headerHeight - self.footerHeight - self.scrollbarWidth; + + // If there's a horizontal scrollbar, remove its height from the bottom boundary, otherwise we'll be letting it obscure rows + //if (self.horizontalScrollbarHeight) { + // bottomBound = bottomBound - self.horizontalScrollbarHeight; + //} + + // The right position is the current X scroll position minus the grid width + var rightBound = self.renderContainers.body.prevScrollLeft + Math.ceil(self.gridWidth); + + // If there's a vertical scrollbar, subtract it from the right boundary or we'll allow it to obscure cells + //if (self.verticalScrollbarWidth) { + // rightBound = rightBound - self.verticalScrollbarWidth; + //} + + // We were given a row to scroll to + if (gridRow !== null) { + // This is the index of the row we want to scroll to, within the list of rows that can be visible + var seekRowIndex = visRowCache.indexOf(gridRow); + + // Total vertical scroll length of the grid + var scrollLength = (self.renderContainers.body.getCanvasHeight() - self.renderContainers.body.getViewportHeight()); + + // Add the height of the native horizontal scrollbar to the scroll length, if it's there. Otherwise it will mask over the final row + //if (self.horizontalScrollbarHeight && self.horizontalScrollbarHeight > 0) { + // scrollLength = scrollLength + self.horizontalScrollbarHeight; + //} + + // This is the minimum amount of pixels we need to scroll vertical in order to see this row. + var pixelsToSeeRow = ((seekRowIndex + 1) * self.options.rowHeight); + + // Don't let the pixels required to see the row be less than zero + pixelsToSeeRow = (pixelsToSeeRow < 0) ? 0 : pixelsToSeeRow; + + var scrollPixels, percentage; + + // If the scroll position we need to see the row is LESS than the top boundary, i.e. obscured above the top of the self... + if (pixelsToSeeRow < topBound) { + // Get the different between the top boundary and the required scroll position and subtract it from the current scroll position\ + // to get the full position we need + scrollPixels = self.renderContainers.body.prevScrollTop - (topBound - pixelsToSeeRow); + + // Turn the scroll position into a percentage and make it an argument for a scroll event + percentage = scrollPixels / scrollLength; + scrollEvent.y = { percentage: percentage }; + } + // Otherwise if the scroll position we need to see the row is MORE than the bottom boundary, i.e. obscured below the bottom of the self... + else if (pixelsToSeeRow > bottomBound) { + // Get the different between the bottom boundary and the required scroll position and add it to the current scroll position + // to get the full position we need + scrollPixels = pixelsToSeeRow - bottomBound + self.renderContainers.body.prevScrollTop; + + // Turn the scroll position into a percentage and make it an argument for a scroll event + percentage = scrollPixels / scrollLength; + scrollEvent.y = { percentage: percentage }; + } + } + + // We were given a column to scroll to + if (gridCol !== null) { + // This is the index of the row we want to scroll to, within the list of rows that can be visible + var seekColumnIndex = visColCache.indexOf(gridCol); + + // Total vertical scroll length of the grid + var horizScrollLength = (self.renderContainers.body.getCanvasWidth() - self.renderContainers.body.getViewportWidth()); + + // Add the height of the native horizontal scrollbar to the scroll length, if it's there. Otherwise it will mask over the final row + // if (self.verticalScrollbarWidth && self.verticalScrollbarWidth > 0) { + // horizScrollLength = horizScrollLength + self.verticalScrollbarWidth; + // } + + // This is the minimum amount of pixels we need to scroll vertical in order to see this column + var columnLeftEdge = 0; + for (var i = 0; i < seekColumnIndex; i++) { + var col = visColCache[i]; + columnLeftEdge += col.drawnWidth; + } + columnLeftEdge = (columnLeftEdge < 0) ? 0 : columnLeftEdge; + + var columnRightEdge = columnLeftEdge + gridCol.drawnWidth; + + // Don't let the pixels required to see the column be less than zero + columnRightEdge = (columnRightEdge < 0) ? 0 : columnRightEdge; + + var horizScrollPixels, horizPercentage; + + // If the scroll position we need to see the row is LESS than the top boundary, i.e. obscured above the top of the self... + if (columnLeftEdge < leftBound) { + // Get the different between the top boundary and the required scroll position and subtract it from the current scroll position\ + // to get the full position we need + horizScrollPixels = self.renderContainers.body.prevScrollLeft - (leftBound - columnLeftEdge); + + // Turn the scroll position into a percentage and make it an argument for a scroll event + horizPercentage = horizScrollPixels / horizScrollLength; + horizPercentage = (horizPercentage > 1) ? 1 : horizPercentage; + scrollEvent.x = { percentage: horizPercentage }; + } + // Otherwise if the scroll position we need to see the row is MORE than the bottom boundary, i.e. obscured below the bottom of the self... + else if (columnRightEdge > rightBound) { + // Get the different between the bottom boundary and the required scroll position and add it to the current scroll position + // to get the full position we need + horizScrollPixels = columnRightEdge - rightBound + self.renderContainers.body.prevScrollLeft; + + // Turn the scroll position into a percentage and make it an argument for a scroll event + horizPercentage = horizScrollPixels / horizScrollLength; + horizPercentage = (horizPercentage > 1) ? 1 : horizPercentage; + scrollEvent.x = { percentage: horizPercentage }; + } + } + + var deferred = $q.defer(); + + // If we need to scroll on either the x or y axes, fire a scroll event + if (scrollEvent.y || scrollEvent.x) { + scrollEvent.withDelay = false; + self.scrollContainers('',scrollEvent); + var dereg = self.api.core.on.scrollEnd(null,function() { + deferred.resolve(scrollEvent); + dereg(); + }); + } + else { + deferred.resolve(); + } + + return deferred.promise; + }; + + /** + * @ngdoc method + * @methodOf ui.grid.class:Grid + * @name scrollTo + * @description Scroll the grid such that the specified + * row and column is in view + * @param {object} rowEntity gridOptions.data[] array instance to make visible + * @param {object} colDef to make visible + * @returns {promise} a promise that is resolved after any scrolling is finished + */ + Grid.prototype.scrollTo = function (rowEntity, colDef) { + var gridRow = null, gridCol = null; + if (rowEntity !== null && typeof(rowEntity) !== 'undefined' ) { + gridRow = this.getRow(rowEntity); + } + + if (colDef !== null && typeof(colDef) !== 'undefined' ) { + gridCol = this.getColumn(colDef.name ? colDef.name : colDef.field); + } + return this.scrollToIfNecessary(gridRow, gridCol); + }; - // Blatantly stolen from Angular as it isn't exposed (yet? 2.0?) + // Blatantly stolen from Angular as it isn't exposed (yet? 2.0?) function RowHashMap() {} RowHashMap.prototype = { diff --git a/src/js/core/factories/GridApi.js b/src/js/core/factories/GridApi.js index c0104f00fd..c27db0e775 100644 --- a/src/js/core/factories/GridApi.js +++ b/src/js/core/factories/GridApi.js @@ -119,11 +119,19 @@ /** * @ngdoc event - * @name scrollEvent + * @name scrollBegin * @eventOf ui.grid.core.api:PublicApi - * @description is raised on a scroll Event. Called frequently so be careful what you do with it + * @description is raised when scroll begins. Is throttled, so won't be raised too frequently */ - this.registerEvent( 'core', 'scrollEvent' ); + this.registerEvent( 'core', 'scrollBegin' ); + + /** + * @ngdoc event + * @name scrollEnd + * @eventOf ui.grid.core.api:PublicApi + * @description is raised when scroll has finished. Is throttled, so won't be raised too frequently + */ + this.registerEvent( 'core', 'scrollEnd' ); /** * @ngdoc event @@ -201,7 +209,8 @@ *
    * .on.eventName(scope, callBackFn, _this) *
    - * scope - a scope reference to add a deregister call to the scopes .$on('destroy') + * scope - a scope reference to add a deregister call to the scopes .$on('destroy'). Scope is optional and can be a null value, + * but in this case you must deregister it yourself via the returned deregister function *
    * callBackFn - The function to call *
    @@ -246,9 +255,12 @@ }; //destroy tracking when scope is destroyed - scope.$on('$destroy', function() { - removeListener(); - }); + if (scope) { + scope.$on('$destroy', function() { + removeListener(); + }); + } + return removeListener; }; diff --git a/src/js/core/factories/GridColumn.js b/src/js/core/factories/GridColumn.js index 95a4bee845..b3da7ecc08 100644 --- a/src/js/core/factories/GridColumn.js +++ b/src/js/core/factories/GridColumn.js @@ -106,6 +106,95 @@ angular.module('ui.grid') self.uid = uid; self.updateColumnDef(colDef, true); + + /** + * @ngdoc function + * @name hideColumn + * @methodOf ui.grid.class:GridColumn + * @description Hides the column by setting colDef.visible = false + */ + GridColumn.prototype.hideColumn = function() { + this.colDef.visible = false; + }; + + self.aggregationValue = undefined; + + var updateAggregationValue = function() { + + // gridUtil.logDebug('getAggregationValue for Column ' + self.colDef.name); + + if (!self.aggregationType) { + self.aggregationValue = undefined; + return; + } + + var result = 0; + var visibleRows = self.grid.getVisibleRows(); + + var cellValues = function(){ + var values = []; + visibleRows.forEach(function (row) { + var cellValue = self.grid.getCellValue(row, self); + var cellNumber = Number(cellValue); + if (!isNaN(cellNumber)) { + values.push(cellNumber); + } + }); + return values; + }; + + if (angular.isFunction(self.aggregationType)) { + self.aggregationValue = self.aggregationType(visibleRows, self); + } + else if (self.aggregationType === uiGridConstants.aggregationTypes.count) { + self.aggregationValue = self.grid.getVisibleRowCount(); + } + else if (self.aggregationType === uiGridConstants.aggregationTypes.sum) { + cellValues().forEach(function (value) { + result += value; + }); + self.aggregationValue = result; + } + else if (self.aggregationType === uiGridConstants.aggregationTypes.avg) { + cellValues().forEach(function (value) { + result += value; + }); + result = result / cellValues().length; + self.aggregationValue = result; + } + else if (self.aggregationType === uiGridConstants.aggregationTypes.min) { + self.aggregationValue = Math.min.apply(null, cellValues()); + } + else if (self.aggregationType === uiGridConstants.aggregationTypes.max) { + self.aggregationValue = Math.max.apply(null, cellValues()); + } + else { + self.aggregationValue = '\u00A0'; + } + }; + + var throttledUpdateAggregationValue = gridUtil.throttle(updateAggregationValue, self.grid.options.aggregationCalcThrottle); + + + + + /** + * @ngdoc function + * @name getAggregationValue + * @methodOf ui.grid.class:GridColumn + * @description gets the aggregation value based on the aggregation type for this column. + * Debounced using scrollDebounce option setting + */ + this.getAggregationValue = function() { + if (!self.grid.isScrollingVertically && !self.grid.isScrollingHorizontally) { + throttledUpdateAggregationValue(); + } + + return self.aggregationValue; + } ; + + + } @@ -640,7 +729,28 @@ angular.module('ui.grid') return prefixDot ? '.' + cls : cls; }; - /** + /** + * @ngdoc function + * @name isPinnedLeft + * @methodOf ui.grid.class:GridColumn + * @description Returns true if column is in the left render container + */ + GridColumn.prototype.isPinnedLeft = function () { + return this.renderContainer === 'left'; + }; + + /** + * @ngdoc function + * @name isPinnedRight + * @methodOf ui.grid.class:GridColumn + * @description Returns true if column is in the right render container + */ + GridColumn.prototype.isPinnedRight = function () { + return this.renderContainer === 'right'; + }; + + + /** * @ngdoc function * @name getColClassDefinition * @methodOf ui.grid.class:GridColumn @@ -680,69 +790,7 @@ angular.module('ui.grid') this.colDef.visible = true; }; - /** - * @ngdoc function - * @name hideColumn - * @methodOf ui.grid.class:GridColumn - * @description Hides the column by setting colDef.visible = false - */ - GridColumn.prototype.hideColumn = function() { - this.colDef.visible = false; - }; - /** - * @ngdoc function - * @name getAggregationValue - * @methodOf ui.grid.class:GridColumn - * @description gets the aggregation value based on the aggregation type for this column - */ - GridColumn.prototype.getAggregationValue = function () { - var self = this; - var result = 0; - var visibleRows = self.grid.getVisibleRows(); - - var cellValues = function(){ - var values = []; - angular.forEach(visibleRows, function (row) { - var cellValue = self.grid.getCellValue(row, self); - var cellNumber = Number(cellValue); - if (!isNaN(cellNumber)) { - values.push(cellNumber); - } - }); - return values; - }; - - if (angular.isFunction(self.aggregationType)) { - return self.aggregationType(visibleRows, self); - } - else if (self.aggregationType === uiGridConstants.aggregationTypes.count) { - return self.grid.getVisibleRowCount(); - } - else if (self.aggregationType === uiGridConstants.aggregationTypes.sum) { - angular.forEach(cellValues(), function (value) { - result += value; - }); - return result; - } - else if (self.aggregationType === uiGridConstants.aggregationTypes.avg) { - angular.forEach(cellValues(), function (value) { - result += value; - }); - result = result / cellValues().length; - return result; - } - else if (self.aggregationType === uiGridConstants.aggregationTypes.min) { - return Math.min.apply(null, cellValues()); - } - else if (self.aggregationType === uiGridConstants.aggregationTypes.max) { - return Math.max.apply(null, cellValues()); - } - else { - return '\u00A0'; - } - }; - /** * @ngdoc property * @name aggregationHideLabel diff --git a/src/js/core/factories/GridOptions.js b/src/js/core/factories/GridOptions.js index 137ad47328..c6b47e07a5 100644 --- a/src/js/core/factories/GridOptions.js +++ b/src/js/core/factories/GridOptions.js @@ -297,14 +297,32 @@ angular.module('ui.grid') * @description Defaults to 4 */ baseOptions.horizontalScrollThreshold = typeof(baseOptions.horizontalScrollThreshold) !== "undefined" ? baseOptions.horizontalScrollThreshold : 2; + + + /** + * @ngdoc property + * @name aggregationCalcThrottle + * @propertyOf ui.grid.class:GridOptions + * @description Default time in milliseconds to throttle aggregation calcuations, defaults to 1000ms + */ + baseOptions.aggregationCalcThrottle = typeof(baseOptions.aggregationCalcThrottle) !== "undefined" ? baseOptions.aggregationCalcThrottle : 1000; /** * @ngdoc property - * @name scrollThrottle + * @name wheelScrollThrottle + * @propertyOf ui.grid.class:GridOptions + * @description Default time in milliseconds to throttle scroll events to, defaults to 70ms + */ + baseOptions.wheelScrollThrottle = typeof(baseOptions.wheelScrollThrottle) !== "undefined" ? baseOptions.wheelScrollThrottle : 70; + + + /** + * @ngdoc property + * @name scrollDebounce * @propertyOf ui.grid.class:GridOptions - * @description Default time to throttle scroll events to, defaults to 70ms + * @description Default time in milliseconds to debounce scroll events, defaults to 300ms */ - baseOptions.scrollThrottle = typeof(baseOptions.scrollThrottle) !== "undefined" ? baseOptions.scrollThrottle : 70; + baseOptions.scrollDebounce = typeof(baseOptions.scrollDebounce) !== "undefined" ? baseOptions.scrollDebounce : 300; /** * @ngdoc boolean diff --git a/src/js/core/factories/GridRenderContainer.js b/src/js/core/factories/GridRenderContainer.js index 7c39c73545..263327faa2 100644 --- a/src/js/core/factories/GridRenderContainer.js +++ b/src/js/core/factories/GridRenderContainer.js @@ -90,6 +90,11 @@ angular.module('ui.grid') // TODO(c0bra): calculate size?? Should this be in a stackable directive? + + GridRenderContainer.prototype.containsColumn = function (col) { + return this.visibleColumnCache.indexOf(col) !== -1; + }; + GridRenderContainer.prototype.minRowsToRender = function minRowsToRender() { var self = this; var minRows = 0; @@ -305,6 +310,51 @@ angular.module('ui.grid') this.columnOffset = hiddenColumnsWidth; }; + GridRenderContainer.prototype.scrollVertical = function (newScrollTop) { + var vertScrollPercentage = -1; + + if (newScrollTop !== this.prevScrollTop) { + var yDiff = newScrollTop - this.prevScrollTop; + + if (yDiff > 0 ) { this.grid.scrollDirection = uiGridConstants.scrollDirection.DOWN; } + if (yDiff < 0 ) { this.grid.scrollDirection = uiGridConstants.scrollDirection.UP; } + + var vertScrollLength = this.getVerticalScrollLength(); + + vertScrollPercentage = newScrollTop / vertScrollLength; + + if (vertScrollPercentage > 1) { vertScrollPercentage = 1; } + if (vertScrollPercentage < 0) { vertScrollPercentage = 0; } + + this.adjustScrollVertical(newScrollTop, vertScrollPercentage); + return vertScrollPercentage; + } + }; + + GridRenderContainer.prototype.scrollHorizontal = function(newScrollLeft){ + var horizScrollPercentage = -1; + + // Handle RTL here + + if (newScrollLeft !== this.prevScrollLeft) { + var xDiff = newScrollLeft - this.prevScrollLeft; + + if (xDiff > 0) { this.grid.scrollDirection = uiGridConstants.scrollDirection.RIGHT; } + if (xDiff < 0) { this.grid.scrollDirection = uiGridConstants.scrollDirection.LEFT; } + + var horizScrollLength = (this.canvasWidth - this.getViewportWidth()); + if (horizScrollLength !== 0) { + horizScrollPercentage = newScrollLeft / horizScrollLength; + } + else { + horizScrollPercentage = 0; + } + + this.adjustScrollHorizontal(newScrollLeft, horizScrollPercentage); + return horizScrollPercentage; + } + }; + GridRenderContainer.prototype.adjustScrollVertical = function adjustScrollVertical(scrollTop, scrollPercentage, force) { if (this.prevScrollTop === scrollTop && !force) { return; diff --git a/src/js/core/factories/ScrollEvent.js b/src/js/core/factories/ScrollEvent.js index 6ffe43225c..ac0e0ad506 100644 --- a/src/js/core/factories/ScrollEvent.js +++ b/src/js/core/factories/ScrollEvent.js @@ -29,12 +29,22 @@ /** * @ngdoc object - * @name entity + * @name source * @propertyOf ui.grid.class:ScrollEvent * @description the source of the scroll event. limited to values from uiGridConstants.scrollEventSources */ self.source = source; + + /** + * @ngdoc object + * @name noDelay + * @propertyOf ui.grid.class:ScrollEvent + * @description most scroll events from the mouse or trackpad require delay to operate properly + * set to false to eliminate delay. Useful for scroll events that the grid causes, such as scrolling to make a row visible. + */ + self.withDelay = true; + self.sourceRowContainer = sourceRowContainer; self.sourceColContainer = sourceColContainer; @@ -50,22 +60,12 @@ * @methodOf ui.grid.class:ScrollEvent * @description fires a throttled event using grid.api.core.raise.scrollEvent */ - self.fireThrottledScrollingEvent = gridUtil.throttle(function() { - self.grid.api.core.raise.scrollEvent(self); - }, self.grid.options.scrollThrottle, {trailing: true}); + self.fireThrottledScrollingEvent = gridUtil.throttle(function(sourceContainerId) { + self.grid.scrollContainers(sourceContainerId, self); + }, self.grid.options.wheelScrollThrottle, {trailing: true}); } - /** - * @ngdoc function - * @name fireScrollingEvent - * @methodOf ui.grid.class:ScrollEvent - * @description fires an event using grid.api.core.raise.scrollEvent - */ - ScrollEvent.prototype.fireScrollingEvent = function() { - this.grid.api.core.raise.scrollEvent(this); - }; - /** * @ngdoc function diff --git a/src/js/core/services/ui-grid-util.js b/src/js/core/services/ui-grid-util.js index 73dfafcb4c..2cdfb02d5a 100644 --- a/src/js/core/services/ui-grid-util.js +++ b/src/js/core/services/ui-grid-util.js @@ -640,12 +640,12 @@ module.service('gridUtil', ['$log', '$window', '$document', '$http', '$templateC return found; }, - // Shim requestAnimationFrame - requestAnimationFrame: $window.requestAnimationFrame && $window.requestAnimationFrame.bind($window) || - $window.webkitRequestAnimationFrame && $window.webkitRequestAnimationFrame.bind($window) || - function(fn) { - return $timeout(fn, 10, false); - }, + //// Shim requestAnimationFrame + //requestAnimationFrame: $window.requestAnimationFrame && $window.requestAnimationFrame.bind($window) || + // $window.webkitRequestAnimationFrame && $window.webkitRequestAnimationFrame.bind($window) || + // function(fn) { + // return $timeout(fn, 10, false); + // }, numericAndNullSort: function (a, b) { if (a === null) { return 1; } diff --git a/src/less/body.less b/src/less/body.less index 9e381b33b7..264ed90420 100644 --- a/src/less/body.less +++ b/src/less/body.less @@ -18,9 +18,8 @@ overflow-y: scroll; -webkit-overflow-scrolling: touch; - // requested in #2312, IE9 compatibility with exporter. I have no ability to test this, so assuming it's needed PaulL - :focus { - outline: none; + &:focus { + outline: none !important; } } diff --git a/test/unit/core/factories/GridOptions.spec.js b/test/unit/core/factories/GridOptions.spec.js index f8363e3b27..5453d63f5c 100644 --- a/test/unit/core/factories/GridOptions.spec.js +++ b/test/unit/core/factories/GridOptions.spec.js @@ -36,7 +36,9 @@ describe('GridOptions factory', function () { scrollThreshold: 4, excessColumns: 4, horizontalScrollThreshold: 2, - scrollThrottle: 70, + aggregationCalcThrottle: 1000, + wheelScrollThrottle: 70, + scrollDebounce: 300, enableSorting: true, enableFiltering: false, enableColumnMenus: true, @@ -79,7 +81,8 @@ describe('GridOptions factory', function () { scrollThreshold: 6, excessColumns: 7, horizontalScrollThreshold: 3, - scrollThrottle: 75, + aggregationCalcThrottle: 1000, + wheelScrollThrottle: 75, enableSorting: true, enableFiltering: true, enableColumnMenus: true, @@ -119,7 +122,9 @@ describe('GridOptions factory', function () { scrollThreshold: 6, excessColumns: 7, horizontalScrollThreshold: 3, - scrollThrottle: 75, + aggregationCalcThrottle: 1000, + wheelScrollThrottle: 75, + scrollDebounce: 300, enableSorting: true, enableFiltering: true, enableColumnMenus: true, @@ -163,7 +168,8 @@ describe('GridOptions factory', function () { scrollThreshold: 6, excessColumns: 7, horizontalScrollThreshold: 3, - scrollThrottle: 75, + aggregationCalcThrottle: 1000, + wheelScrollThrottle: 75, enableFiltering: false, enableSorting: false, enableColumnMenus: false, @@ -202,7 +208,9 @@ describe('GridOptions factory', function () { scrollThreshold: 6, excessColumns: 7, horizontalScrollThreshold: 3, - scrollThrottle: 75, + aggregationCalcThrottle: 1000, + wheelScrollThrottle: 75, + scrollDebounce: 300, enableSorting: false, enableFiltering: false, enableColumnMenus: false, From 86a1ed834fb723edce6e82621a31e10be7491110 Mon Sep 17 00:00:00 2001 From: swalters Date: Thu, 26 Mar 2015 15:41:24 -0500 Subject: [PATCH 058/173] fix tests --- src/features/cellnav/test/uiGridCellNavService.spec.js | 6 +++--- src/features/infinite-scroll/js/infinite-scroll.js | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/features/cellnav/test/uiGridCellNavService.spec.js b/src/features/cellnav/test/uiGridCellNavService.spec.js index 95c278872b..95bdd4f507 100644 --- a/src/features/cellnav/test/uiGridCellNavService.spec.js +++ b/src/features/cellnav/test/uiGridCellNavService.spec.js @@ -223,7 +223,7 @@ describe('ui.grid.edit uiGridCellNavService', function () { $timeout.flush(); expect(args.grid).toEqual(grid); - expect(args.y.percentage).toBe(0.41515151515151516); + expect(Math.round(args.y.percentage * 10)/10).toBe(0.4); expect(isNaN(args.x.percentage)).toEqual( true ); }); @@ -233,7 +233,7 @@ describe('ui.grid.edit uiGridCellNavService', function () { }); $timeout.flush(); - expect(args.y.percentage).toBe(0.14242424242424243); + expect(Math.round(args.y.percentage * 10)/10).toBe(0.1); }); it('should request scroll to row only - last row', function () { @@ -252,7 +252,7 @@ describe('ui.grid.edit uiGridCellNavService', function () { }); $timeout.flush(); - expect(args.y.percentage).toEqual( 0.5060606060606061); + expect(Math.round(args.y.percentage * 10)/10).toEqual( 0.5); expect(args.x).toBe(null); }); diff --git a/src/features/infinite-scroll/js/infinite-scroll.js b/src/features/infinite-scroll/js/infinite-scroll.js index 06ba5b6e3a..f837cac610 100644 --- a/src/features/infinite-scroll/js/infinite-scroll.js +++ b/src/features/infinite-scroll/js/infinite-scroll.js @@ -41,7 +41,7 @@ } if (grid.options.enableInfiniteScroll) { - grid.api.core.on.scrollEvent($scope, service.handleScroll); + grid.api.core.on.scrollEnd($scope, service.handleScroll); } // tweak the scroll for infinite scroll up (if enabled) From c4954852d81cf871c6627a087e309cabc8a61e4a Mon Sep 17 00:00:00 2001 From: Peter Mihalik Date: Fri, 27 Mar 2015 12:56:08 +0100 Subject: [PATCH 059/173] added filter attribute 'disableCancelFilterButton' and select filter default empty option --- src/js/core/factories/GridColumn.js | 16 +++++++++++----- src/less/header.less | 6 +++++- src/templates/ui-grid/uiGridHeaderCell.html | 10 ++++++---- 3 files changed, 22 insertions(+), 10 deletions(-) diff --git a/src/js/core/factories/GridColumn.js b/src/js/core/factories/GridColumn.js index 95a4bee845..1a236b081e 100644 --- a/src/js/core/factories/GridColumn.js +++ b/src/js/core/factories/GridColumn.js @@ -538,14 +538,14 @@ angular.module('ui.grid') defaultFilters.push({}); } - /** +/** * @ngdoc property * @name filter * @propertyOf ui.grid.class:GridOptions.columnDef * @description Specify a single filter field on this column. - * + * * A filter consists of a condition, a term, and a placeholder: - * + * * - condition defines how rows are chosen as matching the filter term. This can be set to * one of the constants in uiGridConstants.filter, or you can supply a custom filter function * that gets passed the following arguments: [searchTerm, cellValue, row, column]. @@ -560,8 +560,10 @@ angular.module('ui.grid') * then a select box will be shown with options selectOptions * - selectOptions: options in the format `[ { value: 1, label: 'male' }]`. No i18n filter is provided, you need * to perform the i18n on the values before you provide them + * - disableCancelFilterButton: defaults to false. If set to true then the 'x' button that cancels/clears the filter + * will not be shown. * @example - *
    $scope.gridOptions.columnDefs = [ 
    +     * 
    $scope.gridOptions.columnDefs = [
          *   {
          *     field: 'field1',
          *     filter: {
    @@ -570,12 +572,16 @@ angular.module('ui.grid')
          *       placeholder: 'starts with...',
          *       flags: { caseSensitive: false },
          *       type: uiGridConstants.filter.SELECT,
    -     *       selectOptions: [ { value: 1, label: 'male' }, { value: 2, label: 'female' } ]
    +     *       selectOptions: [ { value: 1, label: 'male' }, { value: 2, label: 'female' } ],
    +     *       disableCancelFilterButton: true
          *     }
          *   }
          * ]; 
    * */ + + /* + /* diff --git a/src/less/header.less b/src/less/header.less index 8afba48bd8..ca4705b3af 100644 --- a/src/less/header.less +++ b/src/less/header.less @@ -214,4 +214,8 @@ select.ui-grid-filter-select { &:hover { border: @gridBorderWidth solid @borderColor; } -} \ No newline at end of file + + .ui-grid-filter-cancel-button-hidden & { + width: 100%; + } +} diff --git a/src/templates/ui-grid/uiGridHeaderCell.html b/src/templates/ui-grid/uiGridHeaderCell.html index d55070e328..2b39845052 100644 --- a/src/templates/ui-grid/uiGridHeaderCell.html +++ b/src/templates/ui-grid/uiGridHeaderCell.html @@ -12,19 +12,21 @@  
    -
    +
    -
    +
     
    - + -
    +
     
    From ba0923614c5533bc727266810e379ae4eab2d23d Mon Sep 17 00:00:00 2001 From: Paul Lambert Date: Sat, 28 Mar 2015 12:12:10 +1300 Subject: [PATCH 060/173] Enh(infiniteScroll): add a reset method, allow remove data, fix #3119 --- misc/tutorial/212_infinite_scroll.ngdoc | 106 ++++++++- .../infinite-scroll/js/infinite-scroll.js | 213 +++++++++++++++--- .../test/infiniteScroll.spec.js | 15 +- 3 files changed, 291 insertions(+), 43 deletions(-) diff --git a/misc/tutorial/212_infinite_scroll.ngdoc b/misc/tutorial/212_infinite_scroll.ngdoc index 44d26ed239..c81a47b528 100644 --- a/misc/tutorial/212_infinite_scroll.ngdoc +++ b/misc/tutorial/212_infinite_scroll.ngdoc @@ -30,6 +30,15 @@ Ideally the API would change to a number of rows. Finally, we suppress the normal grid behaviour of propagating the scroll to the parent container when you reach the end if infinite scroll is enabled and there is still data in that direction. +If you are using {@link 307_external_sorting external sorting} or {@link 308_external_filtering external filtering} you may +reload your data whenever scroll or filter events occur. In this situation you'll want to call `resetScroll` to tell the +grid not to try to preserve the previous scroll position. You may also use this call when you've otherwise reset the data +in the grid. You must also tell us whether you allow scrollUp or scrollDown from this position as part of the call. + +You may sometimes remove data, for example if you're keeping 10 pages of data in memory, and you start discarding data +from the top as you add data to the bottom. In this situation you can call optionally provide a scroll percentage to the +`resetScroll`, + @example In this example we have a data set that starts at page 2 of a 5 page data set. Each page is 100 records, so we start at @@ -37,11 +46,17 @@ record 200, and we can scroll up 2 pages, and scroll down 2 pages. You should s you hit record zero a touchpad scroll should propagate to the parent page. You should also see smooth scrolling as you move down, and when you hit record 499 a touchpad scroll should propagate to the parent page. +We also remove data from the data set in memory, we've decided that we only ever want to hold 4 pages in memory, so we +will discard pages and reset the scrollUp and scrollDown appropriately. Again, when this happens the grid should still +hold the scroll position. + +Finally, we can reset the data, which gets us back to the middle page and sets the scroll to the top. + var app = angular.module('app', ['ngTouch', 'ui.grid', 'ui.grid.infiniteScroll']); - app.controller('MainCtrl', ['$scope', '$http', function ($scope, $http) { + app.controller('MainCtrl', ['$scope', '$http', '$timeout', '$q', function ($scope, $http, $timeout, $q) { $scope.gridOptions = { infiniteScrollPercentage: 15, infiniteScrollUp: true, @@ -61,33 +76,54 @@ move down, and when you hit record 499 a touchpad scroll should propagate to the $scope.data = []; - var firstPage = 2; - var lastPage = 1; + $scope.firstPage = 2; + $scope.lastPage = 2; + + $scope.getFirstData = function() { + var promise = $q.defer(); + $http.get('/data/10000_complex.json') + .success(function(data) { + var newData = $scope.getPage(data, $scope.lastPage); + $scope.data = $scope.data.concat(newData); + promise.resolve(); + }); + return promise.promise; + }; $scope.getDataDown = function() { + var promise = $q.defer(); $http.get('/data/10000_complex.json') .success(function(data) { - lastPage++; - var newData = $scope.getPage(data, lastPage); + $scope.lastPage++; + var newData = $scope.getPage(data, $scope.lastPage); $scope.data = $scope.data.concat(newData); - $scope.gridApi.infiniteScroll.dataLoaded(null, lastPage === 4); + $scope.gridApi.infiniteScroll.dataLoaded($scope.firstPage > 0, $scope.lastPage < 4).then(function() {$scope.checkDataLength('up');}).then(function() { + promise.resolve(); + }); }) .error(function(error) { $scope.gridApi.infiniteScroll.dataLoaded(); + promise.reject(); }); + return promise.promise; }; $scope.getDataUp = function() { + var promise = $q.defer(); $http.get('/data/10000_complex.json') .success(function(data) { - firstPage--; - var newData = $scope.getPage(data, firstPage); + $scope.firstPage--; + var newData = $scope.getPage(data, $scope.firstPage); $scope.data = newData.concat($scope.data); - $scope.gridApi.infiniteScroll.dataLoaded(firstPage === 0, null); + $scope.gridApi.infiniteScroll.dataLoaded($scope.firstPage > 0, $scope.lastPage < 4).then(function() {$scope.checkDataLength('down');}).then(function() { + promise.resolve(); + }); }) .error(function(error) { $scope.gridApi.infiniteScroll.dataLoaded(); + promise.reject(); }); + return promise.promise; }; @@ -99,11 +135,61 @@ move down, and when you hit record 499 a touchpad scroll should propagate to the return res; }; - $scope.getDataDown(); + $scope.checkDataLength = function( discardDirection) { + // work out whether we need to discard a page, if so discard from the direction passed in + if( $scope.lastPage - $scope.firstPage > 3 ){ + // we want to remove a page + $scope.gridApi.infiniteScroll.saveScrollPercentage(); + + if( discardDirection === 'up' ){ + $scope.data = $scope.data.slice(100); + $scope.firstPage++; + $timeout(function() { + // wait for grid to ingest data changes + $scope.gridApi.infiniteScroll.dataRemovedTop($scope.firstPage > 0, $scope.lastPage < 4); + }); + } else { + $scope.data = $scope.data.slice(0, 400); + $scope.lastPage--; + $timeout(function() { + // wait for grid to ingest data changes + $scope.gridApi.infiniteScroll.dataRemovedBottom($scope.firstPage > 0, $scope.lastPage < 4); + }); + } + } + }; + + $scope.reset = function() { + $scope.firstPage = 2; + $scope.lastPage = 2; + + // turn off the infinite scroll handling up and down - hopefully this won't be needed after @swalters scrolling changes + $scope.gridApi.infiniteScroll.setScrollDirections( false, false ); + $scope.data = []; + + $scope.getFirstData().then(function(){ + $timeout(function() { + // timeout needed to allow digest cycle to complete,and grid to finish ingesting the data + $scope.gridApi.infiniteScroll.resetScroll( $scope.firstPage > 0, $scope.lastPage < 4 ); + }); + }); + }; + + $scope.getFirstData().then(function(){ + $timeout(function() { + // timeout needed to allow digest cycle to complete,and grid to finish ingesting the data + // you need to call resetData once you've loaded your data if you want to enable scroll up, + // it adjusts the scroll position down one pixel so that we can generate scroll up events + $scope.gridApi.infiniteScroll.resetScroll( $scope.firstPage > 0, $scope.lastPage < 4 ); + }); + }); + }]);
    + +     First page: {{ firstPage }}     Last page: {{ lastPage }}     data.length: {{ data.length }}
    diff --git a/src/features/infinite-scroll/js/infinite-scroll.js b/src/features/infinite-scroll/js/infinite-scroll.js index 06ba5b6e3a..98698c10c9 100644 --- a/src/features/infinite-scroll/js/infinite-scroll.js +++ b/src/features/infinite-scroll/js/infinite-scroll.js @@ -17,7 +17,7 @@ * * @description Service for infinite scroll features */ - module.service('uiGridInfiniteScrollService', ['gridUtil', '$compile', '$timeout', 'uiGridConstants', 'ScrollEvent', function (gridUtil, $compile, $timeout, uiGridConstants, ScrollEvent) { + module.service('uiGridInfiniteScrollService', ['gridUtil', '$compile', '$timeout', 'uiGridConstants', 'ScrollEvent', '$q', function (gridUtil, $compile, $timeout, uiGridConstants, ScrollEvent, $q) { var service = { @@ -30,23 +30,15 @@ initializeGrid: function(grid, $scope) { service.defaultGridOptions(grid.options); - grid.infiniteScroll = { dataLoading: false, scrollUp: grid.options.infiniteScrollUp, scrollDown: grid.options.infiniteScrollDown }; - if ( grid.options.infiniteScrollUp){ - grid.suppressParentScrollUp = true; - } - - if ( grid.options.infiniteScrollDown){ - grid.suppressParentScrollDown = true; + if (!grid.options.enableInfiniteScroll){ + return; } - if (grid.options.enableInfiniteScroll) { - grid.api.core.on.scrollEvent($scope, service.handleScroll); - } + grid.infiniteScroll = { dataLoading: false }; + service.setScrollDirections( grid, grid.options.infiniteScrollUp, grid.options.infiniteScrollDown ); + grid.api.core.on.scrollEvent($scope, service.handleScroll); - // tweak the scroll for infinite scroll up (if enabled) - service.adjustScroll(grid); - /** * @ngdoc object * @name ui.grid.infiniteScroll.api:PublicAPI @@ -92,27 +84,98 @@ * that we've reached the end of your data set, we won't fire any more events * for scroll in that direction. * See infinite_scroll tutorial for example of usage - * @param {boolean} noMoreDataTop flag that there are no more pages upwards, so don't fire + * @param {boolean} scrollUp if set to false flags that there are no more pages upwards, so don't fire * any more infinite scroll events upward - * @param {boolean} noMoreDataBottom flag that there are no more pages downwards, so don't + * @param {boolean} scrollDown if set to false flags that there are no more pages downwards, so don't * fire any more infinite scroll events downward + * @returns {promise} a promise that is resolved when the grid scrolling is fully adjusted. If you're + * planning to remove pages, you should wait on this promise first, or you'll break the scroll positioning */ - - dataLoaded: function( noMoreDataTop, noMoreDataBottom ) { + dataLoaded: function( scrollUp, scrollDown ) { grid.infiniteScroll.dataLoading = false; + service.setScrollDirections(grid, scrollUp, scrollDown); - if ( noMoreDataTop === true ){ - grid.infiniteScroll.scrollUp = false; - grid.suppressParentScrollUp = false; - } - - if ( noMoreDataBottom === true ){ - grid.infiniteScroll.scrollDown = false; - grid.suppressParentScrollDown = false; - } + return service.adjustScroll(grid); + }, + + /** + * @ngdoc function + * @name resetScroll + * @methodOf ui.grid.infiniteScroll.api:PublicAPI + * @description Call this function when you have taken some action that makes the current + * scroll position invalid. For example, if you're using external sorting and you've resorted + * then you might reset the scroll, or if you've otherwise substantially changed the data, perhaps + * you've reused an existing grid for a new data set + * + * You must tell us whether there is data upwards or downwards after the reset + * + * @param {boolean} scrollUp flag that there are pages upwards, fire + * infinite scroll events upward + * @param {boolean} scrollDown flag that there are pages downwards, so + * fire infinite scroll events downward + * @returns {promise} promise that is resolved when the scroll reset is complete + */ + resetScroll: function( scrollUp, scrollDown ) { + service.setScrollDirections( grid, scrollUp, scrollDown); - service.adjustScroll(grid); + return service.adjustInfiniteScrollPosition(grid, 0); + }, + + + /** + * @ngdoc function + * @name saveScrollPercentage + * @methodOf ui.grid.infiniteScroll.api:PublicAPI + * @description Saves the scroll percentage and number of visible rows before you adjust the data, + * used if you're subsequently going to call `dataRemovedTop` or `dataRemovedBottom` + */ + saveScrollPercentage: function() { + grid.infiniteScroll.prevScrolltopPercentage = grid.renderContainers.body.prevScrolltopPercentage; + grid.infiniteScroll.previousVisibleRows = grid.renderContainers.body.visibleRowCache.length; + }, + + + /** + * @ngdoc function + * @name dataRemovedTop + * @methodOf ui.grid.infiniteScroll.api:PublicAPI + * @description Adjusts the scroll position after you've removed data at the top + * @param {boolean} scrollUp flag that there are pages upwards, fire + * infinite scroll events upward + * @param {boolean} scrollDown flag that there are pages downwards, so + * fire infinite scroll events downward + */ + dataRemovedTop: function( scrollUp, scrollDown ) { + service.dataRemovedTop( grid, scrollUp, scrollDown ); + }, + + /** + * @ngdoc function + * @name dataRemovedBottom + * @methodOf ui.grid.infiniteScroll.api:PublicAPI + * @description Adjusts the scroll position after you've removed data at the bottom + * @param {boolean} scrollUp flag that there are pages upwards, fire + * infinite scroll events upward + * @param {boolean} scrollDown flag that there are pages downwards, so + * fire infinite scroll events downward + */ + dataRemovedBottom: function( scrollUp, scrollDown ) { + service.dataRemovedBottom( grid, scrollUp, scrollDown ); + }, + + /** + * @ngdoc function + * @name setScrollDirections + * @methodOf ui.grid.infiniteScroll.service:uiGridInfiniteScrollService + * @description Sets the scrollUp and scrollDown flags, handling nulls and undefined, + * and also sets the grid.suppressParentScroll + * @param {boolean} scrollUp whether there are pages available up - defaults to false + * @param {boolean} scrollDown whether there are pages available down - defaults to true + */ + setScrollDirections: function ( scrollUp, scrollDown ) { + service.setScrollDirections( grid, scrollUp, scrollDown ); } + } } }; @@ -178,6 +241,25 @@ }, + /** + * @ngdoc function + * @name setScrollDirections + * @methodOf ui.grid.infiniteScroll.service:uiGridInfiniteScrollService + * @description Sets the scrollUp and scrollDown flags, handling nulls and undefined, + * and also sets the grid.suppressParentScroll + * @param {grid} grid the grid we're operating on + * @param {boolean} scrollUp whether there are pages available up - defaults to false + * @param {boolean} scrollDown whether there are pages available down - defaults to true + */ + setScrollDirections: function ( grid, scrollUp, scrollDown ) { + grid.infiniteScroll.scrollUp = ( scrollUp === true ); + grid.suppressParentScrollUp = ( scrollUp === true ); + + grid.infiniteScroll.scrollDown = ( scrollDown !== false); + grid.suppressParentScrollDown = ( scrollDown !== false); + }, + + /** * @ngdoc function * @name handleScroll @@ -252,8 +334,10 @@ * somewhere else in the mean-time, in which case they'll get a jump back to the new data. Anyway, this will do for * now, until someone wants to do better. * @param {Grid} grid the grid we're working on + * @returns {promise} a promise that is resolved when scrolling has finished */ adjustScroll: function(grid){ + var promise = $q.defer(); $timeout(function () { var percentage; @@ -265,14 +349,20 @@ var newVisibleRows = grid.renderContainers.body.visibleRowCache.length; if ( grid.infiniteScroll.direction === uiGridConstants.scrollDirection.UP ){ percentage = ( newVisibleRows - grid.infiniteScroll.previousVisibleRows ) / newVisibleRows; - service.adjustInfiniteScrollPosition(grid, percentage); + service.adjustInfiniteScrollPosition(grid, percentage).then(function() { + promise.resolve(); + }); } if ( grid.infiniteScroll.direction === uiGridConstants.scrollDirection.DOWN ){ percentage = grid.infiniteScroll.previousVisibleRows / newVisibleRows; - service.adjustInfiniteScrollPosition(grid, percentage); + service.adjustInfiniteScrollPosition(grid, percentage).then(function() { + promise.resolve(); + }); } }, 0); + + return promise.promise; }, @@ -283,6 +373,7 @@ * @description This function fires 'needLoadMoreData' or 'needLoadMoreDataTop' event based on scrollDirection * @param {Grid} grid the grid we're working on * @param {number} percentage the percentage through the grid that we want to scroll to + * @returns {promise} a promise that is resolved when the scrolling finishes */ adjustInfiniteScrollPosition: function (grid, percentage) { var scrollEvent = new ScrollEvent(grid, null, null, 'ui.grid.adjustInfiniteScrollPosition'); @@ -295,11 +386,69 @@ scrollEvent.y = {percentage: percentage}; } scrollEvent.fireScrollingEvent(); - } - + // change this once @swalters has merged his scrolling changes, which will return a promise from the fireScrollingEvent + var promise = $q.defer(); + $timeout(function(){ + promise.resolve(); + }); + return promise.promise; + }, + + /** + * @ngdoc function + * @name dataRemovedTop + * @methodOf ui.grid.infiniteScroll.api:PublicAPI + * @description Adjusts the scroll position after you've removed data at the top. You should + * have called `saveScrollPercentage` before you remove the data, and if you're doing this in + * response to a `needMoreData` you should wait until the promise from `loadData` has resolved + * before you start removing data + * @param {Grid} grid the grid we're working on + * @param {boolean} scrollUp flag that there are pages upwards, fire + * infinite scroll events upward + * @param {boolean} scrollDown flag that there are pages downwards, so + * fire infinite scroll events downward + * @returns {promise} a promise that is resolved when the scrolling finishes + */ + dataRemovedTop: function( grid, scrollUp, scrollDown ) { + service.setScrollDirections( grid, scrollUp, scrollDown ); + var newVisibleRows = grid.renderContainers.body.visibleRowCache.length; + var oldScrollRow = grid.infiniteScroll.prevScrolltopPercentage * grid.infiniteScroll.prevVisibleRows; + + // since we removed from the top, our new scroll row will be the old scroll row less the number + // of rows removed + var newScrollRow = oldScrollRow - ( grid.infiniteScroll.prevVisibleRows - newVisibleRows ); + var newScrollPercent = newScrollRow / newVisibleRows; + + return service.adjustInfiniteScrollPosition( grid, newScrollPercent ); + }, + + /** + * @ngdoc function + * @name dataRemovedBottom + * @methodOf ui.grid.infiniteScroll.api:PublicAPI + * @description Adjusts the scroll position after you've removed data at the bottom. You should + * have called `saveScrollPercentage` before you remove the data, and if you're doing this in + * response to a `needMoreData` you should wait until the promise from `loadData` has resolved + * before you start removing data + * @param {Grid} grid the grid we're working on + * @param {boolean} scrollUp flag that there are pages upwards, fire + * infinite scroll events upward + * @param {boolean} scrollDown flag that there are pages downwards, so + * fire infinite scroll events downward + */ + dataRemovedBottom: function( grid, scrollUp, scrollDown ) { + service.setScrollDirections( grid, scrollUp, scrollDown ); + var newVisibleRows = grid.renderContainers.body.visibleRowCache.length; + var oldScrollRow = grid.infiniteScroll.prevScrolltopPercentage * grid.infiniteScroll.prevVisibleRows; + + // since we removed from the bottom, our new scroll row will be same as the old scroll row + var newScrollPercent = oldScrollRow / newVisibleRows; + + return service.adjustInfiniteScrollPosition( grid, newScrollPercent ); + } }; return service; }]); diff --git a/src/features/infinite-scroll/test/infiniteScroll.spec.js b/src/features/infinite-scroll/test/infiniteScroll.spec.js index b978ad5f99..685ac1d739 100644 --- a/src/features/infinite-scroll/test/infiniteScroll.spec.js +++ b/src/features/infinite-scroll/test/infiniteScroll.spec.js @@ -1,7 +1,7 @@ /* global _ */ (function () { 'use strict'; - describe('ui.grid.infiniteScroll uiGridInfiniteScrollService', function () { + ddescribe('ui.grid.infiniteScroll uiGridInfiniteScrollService', function () { var uiGridInfiniteScrollService; var grid; @@ -108,5 +108,18 @@ }); }); + + describe( 'dataRemovedTop', function() { + it( 'adjusts scroll as expected', function() { + + }); + }); + + + describe( 'dataRemovedBottom', function() { + it( 'adjusts scroll as expected', function() { + + }); + }); }); })(); \ No newline at end of file From 1ba19440c534514316c10863df023c81917238ba Mon Sep 17 00:00:00 2001 From: Paul Lambert Date: Sat, 28 Mar 2015 13:01:25 +1300 Subject: [PATCH 061/173] Enh(infiniteScroll): BREAKING: change percentage to rowsToEnd Infinite scroll was getting more data at a percentage from the end. This meant that as you got more pages you would trigger the request for more data further and further from the end, and eventually a request for new data would immediately trigger a request for still more data, as your scroll kept moving too close to the end. Change instead to infiniteScrollRowsFromEnd, which doesn't have this problem. --- misc/tutorial/212_infinite_scroll.ngdoc | 32 ++++++---- .../infinite-scroll/js/infinite-scroll.js | 62 +++++++++++++------ .../test/infiniteScroll.spec.js | 9 ++- 3 files changed, 68 insertions(+), 35 deletions(-) diff --git a/misc/tutorial/212_infinite_scroll.ngdoc b/misc/tutorial/212_infinite_scroll.ngdoc index c81a47b528..8b9969ddbf 100644 --- a/misc/tutorial/212_infinite_scroll.ngdoc +++ b/misc/tutorial/212_infinite_scroll.ngdoc @@ -10,8 +10,8 @@ optionally tell us up-front that there are no more pages up through `infiniteScr `infiniteScrollDown = true`, and we will never trigger pages in that direction. By default we assume you have pages down but not up. -You can specify the percentage of the grid at which the infinite scroll will trigger a request for -more data `infiniteScrollPercentage = 20`. By default we trigger when you are 20% away from the end of +You can specify the number of rows from the end of the dataset at which the infinite scroll will trigger a request for +more data `infiniteScrollRowsFromEnd = 20`. By default we trigger when you are 20 rows away from the end of the grid (in either direction). We will raise a `needMoreData` or `needMoreDataTop` event, which you must listen to and respond to if @@ -21,14 +21,18 @@ that you have loaded your data. Optionally, you can tell us that there is no mo further requests for more data in that direction. When you have loaded your data we will attempt to adjust the grid scroll to give the appearance of continuous -scrolling. This is a little jumpy at present, largely because we work of percentages instead of a number of -rows from the end. We basically assume that your user will have reached the end of the scroll (upwards or downwards) -by the time the data comes back, and scroll the user to the beginning of the newly added data to reflect that. If -your user has already scrolled a lot of pages, then they may not be at the end of the data (20% can be a long way). -Ideally the API would change to a number of rows. - -Finally, we suppress the normal grid behaviour of propagating the scroll to the parent container when you reach the end -if infinite scroll is enabled and there is still data in that direction. +scrolling. We basically assume that your user will have reached the end of the scroll (upwards or downwards) +by the time the data comes back, and scroll the user to the beginning of the newly added data to reflect that. +In some circumstances this can give "jumpy" scrolling, particularly if you have set your rowsFromEnd to quite a high +value so that you're prefetching the data - if the user is scrolling slowly they might be 50 rows from the end, and +when we process the dataLoaded we suddenly move them to what used to be the end. To avoid this, you can explicitly +save the scroll position before you add data to your data array, through calling `saveScrollPosition`, and the +`dataLoaded` call will then take that position into account, and attempt to adjust the scroll so that the same +rows are showing once the grid has ingested the data you have added. + +We suppress the normal grid behaviour of propagating the scroll to the parent container when you reach the end +if infinite scroll is enabled and if there is still data in that direction - so if there are pages upwards then +scrolling to the top will get those pages rather than hitting the top and then scrolling your whole page upwards. If you are using {@link 307_external_sorting external sorting} or {@link 308_external_filtering external filtering} you may reload your data whenever scroll or filter events occur. In this situation you'll want to call `resetScroll` to tell the @@ -36,8 +40,8 @@ grid not to try to preserve the previous scroll position. You may also use this in the grid. You must also tell us whether you allow scrollUp or scrollDown from this position as part of the call. You may sometimes remove data, for example if you're keeping 10 pages of data in memory, and you start discarding data -from the top as you add data to the bottom. In this situation you can call optionally provide a scroll percentage to the -`resetScroll`, +from the top as you add data to the bottom. You can use the `dataRemovedTop` and `dataRemovedBottom` to tell +us that you've discarded data, and we'll aim to set the scroll back to where it was before you removed that data. @example @@ -58,7 +62,7 @@ Finally, we can reset the data, which gets us back to the middle page and sets t app.controller('MainCtrl', ['$scope', '$http', '$timeout', '$q', function ($scope, $http, $timeout, $q) { $scope.gridOptions = { - infiniteScrollPercentage: 15, + infiniteScrollRowsFromEnd: 40, infiniteScrollUp: true, infiniteScrollDown: true, columnDefs: [ @@ -96,6 +100,7 @@ Finally, we can reset the data, which gets us back to the middle page and sets t .success(function(data) { $scope.lastPage++; var newData = $scope.getPage(data, $scope.lastPage); + $scope.gridApi.infiniteScroll.saveScrollPercentage(); $scope.data = $scope.data.concat(newData); $scope.gridApi.infiniteScroll.dataLoaded($scope.firstPage > 0, $scope.lastPage < 4).then(function() {$scope.checkDataLength('up');}).then(function() { promise.resolve(); @@ -114,6 +119,7 @@ Finally, we can reset the data, which gets us back to the middle page and sets t .success(function(data) { $scope.firstPage--; var newData = $scope.getPage(data, $scope.firstPage); + $scope.gridApi.infiniteScroll.saveScrollPercentage(); $scope.data = newData.concat($scope.data); $scope.gridApi.infiniteScroll.dataLoaded($scope.firstPage > 0, $scope.lastPage < 4).then(function() {$scope.checkDataLength('down');}).then(function() { promise.resolve(); diff --git a/src/features/infinite-scroll/js/infinite-scroll.js b/src/features/infinite-scroll/js/infinite-scroll.js index 98698c10c9..f042f80244 100644 --- a/src/features/infinite-scroll/js/infinite-scroll.js +++ b/src/features/infinite-scroll/js/infinite-scroll.js @@ -80,9 +80,15 @@ * @name dataLoaded * @methodOf ui.grid.infiniteScroll.api:PublicAPI * @description Call this function when you have loaded the additional data - * requested. You can set noMoreDataTop or noMoreDataBottom to indicate - * that we've reached the end of your data set, we won't fire any more events - * for scroll in that direction. + * requested. You should set scrollUp and scrollDown to indicate + * whether there are still more pages in each direction. + * + * If you call dataLoaded without first calling `saveScrollPercentage` then we will + * scroll the user to the start of the newly loaded data, which usually gives a smooth scroll + * experience, but can give a jumpy experience with large `infiniteScrollRowsFromEnd` values, and + * on variable speed internet connections. Using `saveScrollPercentage` as demonstrated in the tutorial + * should give a smoother scrolling experience for users. + * * See infinite_scroll tutorial for example of usage * @param {boolean} scrollUp if set to false flags that there are no more pages upwards, so don't fire * any more infinite scroll events upward @@ -92,10 +98,13 @@ * planning to remove pages, you should wait on this promise first, or you'll break the scroll positioning */ dataLoaded: function( scrollUp, scrollDown ) { - grid.infiniteScroll.dataLoading = false; service.setScrollDirections(grid, scrollUp, scrollDown); - return service.adjustScroll(grid); + var promise = service.adjustScroll(grid).then(function() { + grid.infiniteScroll.dataLoading = false; + }); + + return promise; }, /** @@ -205,17 +214,19 @@ /** * @ngdoc property - * @name infiniteScrollPercentage + * @name infiniteScrollRowsFromEnd * @propertyOf ui.grid.class:GridOptions - * @description This setting controls at what percentage remaining more data - * is requested by the infinite scroll, whether scrolling up or down. + * @description This setting controls how close to the end of the dataset a user gets before + * more data is requested by the infinite scroll, whether scrolling up or down. This allows you to + * 'prefetch' rows before the user actually runs out of scrolling. + * + * Note that if you set this value too high it may give jumpy scrolling behaviour, if you're getting + * this behaviour you could use the `saveScrollPercentageMethod` right before loading your data, and we'll + * preserve that scroll position * - * TODO: it would be nice if this were percentage of a page, not percentage of the - * total scroll - as you get more and more data, the needMoreData event is triggered - * further and further away from the end (in terms of number of rows) *
    Defaults to 20 */ - gridOptions.infiniteScrollPercentage = gridOptions.infiniteScrollPercentage || 20; + gridOptions.infiniteScrollRowsFromEnd = gridOptions.infiniteScrollRowsFromEnd || 20; /** * @ngdoc property @@ -276,14 +287,15 @@ if (args.y) { var percentage; + var targetPercentage = args.grid.options.infiniteScrollRowsFromEnd / args.grid.renderContainers.body.visibleRowCache.length; if (args.grid.scrollDirection === uiGridConstants.scrollDirection.UP ) { percentage = args.y.percentage; - if (percentage <= args.grid.options.infiniteScrollPercentage / 100){ + if (percentage <= targetPercentage){ service.loadData(args.grid); } } else if (args.grid.scrollDirection === uiGridConstants.scrollDirection.DOWN) { percentage = 1 - args.y.percentage; - if (percentage <= args.grid.options.infiniteScrollPercentage / 100){ + if (percentage <= targetPercentage){ service.loadData(args.grid); } } @@ -296,7 +308,8 @@ * @name loadData * @methodOf ui.grid.infiniteScroll.service:uiGridInfiniteScrollService * @description This function fires 'needLoadMoreData' or 'needLoadMoreDataTop' event based on scrollDirection - * and whether there are more pages upwards or downwards + * and whether there are more pages upwards or downwards. It also stores the number of rows that we had previously, + * and clears out any saved scroll position so that we know whether or not the user calls `saveScrollPercentage` * @param {Grid} grid the grid we're working on */ loadData: function (grid) { @@ -304,6 +317,7 @@ // to be at approximately the row we're currently at grid.infiniteScroll.previousVisibleRows = grid.renderContainers.body.visibleRowCache.length; grid.infiniteScroll.direction = grid.scrollDirection; + delete grid.infiniteScroll.prevScrolltopPercentage; if (grid.scrollDirection === uiGridConstants.scrollDirection.UP && grid.infiniteScroll.scrollUp ) { grid.infiniteScroll.dataLoading = true; @@ -339,7 +353,7 @@ adjustScroll: function(grid){ var promise = $q.defer(); $timeout(function () { - var percentage; + var newPercentage; if ( grid.infiniteScroll.direction === undefined ){ // called from initialize, tweak our scroll up a little @@ -347,16 +361,24 @@ } var newVisibleRows = grid.renderContainers.body.visibleRowCache.length; + var oldPercentage, oldTopRow; + if ( grid.infiniteScroll.direction === uiGridConstants.scrollDirection.UP ){ - percentage = ( newVisibleRows - grid.infiniteScroll.previousVisibleRows ) / newVisibleRows; - service.adjustInfiniteScrollPosition(grid, percentage).then(function() { + console.log( 'prevTop was: ' + grid.infiniteScroll.prevScrolltopPercentage); + console.log( 'grid was: ' + grid); + oldPercentage = grid.infiniteScroll.prevScrolltopPercentage || 0; + oldTopRow = oldPercentage * grid.infiniteScroll.previousVisibleRows; + newPercentage = ( newVisibleRows - grid.infiniteScroll.previousVisibleRows + oldTopRow ) / newVisibleRows; + service.adjustInfiniteScrollPosition(grid, newPercentage).then(function() { promise.resolve(); }); } if ( grid.infiniteScroll.direction === uiGridConstants.scrollDirection.DOWN ){ - percentage = grid.infiniteScroll.previousVisibleRows / newVisibleRows; - service.adjustInfiniteScrollPosition(grid, percentage).then(function() { + oldPercentage = grid.infiniteScroll.prevScrolltopPercentage || 1; + oldTopRow = oldPercentage * grid.infiniteScroll.previousVisibleRows; + newPercentage = oldTopRow / newVisibleRows; + service.adjustInfiniteScrollPosition(grid, newPercentage).then(function() { promise.resolve(); }); } diff --git a/src/features/infinite-scroll/test/infiniteScroll.spec.js b/src/features/infinite-scroll/test/infiniteScroll.spec.js index 685ac1d739..3bcaffb8c3 100644 --- a/src/features/infinite-scroll/test/infiniteScroll.spec.js +++ b/src/features/infinite-scroll/test/infiniteScroll.spec.js @@ -24,7 +24,7 @@ grid.options.columnDefs = [ {field: 'col1'} ]; - grid.options.infiniteScrollPercentage = 20; + grid.options.infiniteScrollRowsFromEnd = 20; uiGridInfiniteScrollService.initializeGrid(grid, $scope); spyOn(grid.api.infiniteScroll.raise, 'needLoadMoreData'); @@ -33,12 +33,17 @@ grid.options.data = [{col1:'a'},{col1:'b'}]; grid.buildColumns(); - + })); describe('event handling', function () { beforeEach(function() { spyOn(uiGridInfiniteScrollService, 'loadData').andCallFake(function() {}); + var arrayOf100 = []; + for ( var i = 0; i < 100; i++ ){ + arrayOf100.push(i); + } + grid.renderContainers = { body: { visibleRowCache: arrayOf100}}; }); it('should not request more data if scroll up to 21%', function() { From 0c2acebd613effff2b46aea2d324474536e60e24 Mon Sep 17 00:00:00 2001 From: Paul Lambert Date: Sat, 28 Mar 2015 13:21:22 +1300 Subject: [PATCH 062/173] Remove ddescribe --- src/features/infinite-scroll/test/infiniteScroll.spec.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/features/infinite-scroll/test/infiniteScroll.spec.js b/src/features/infinite-scroll/test/infiniteScroll.spec.js index 3bcaffb8c3..d996818eb8 100644 --- a/src/features/infinite-scroll/test/infiniteScroll.spec.js +++ b/src/features/infinite-scroll/test/infiniteScroll.spec.js @@ -1,7 +1,7 @@ /* global _ */ (function () { 'use strict'; - ddescribe('ui.grid.infiniteScroll uiGridInfiniteScrollService', function () { + describe('ui.grid.infiniteScroll uiGridInfiniteScrollService', function () { var uiGridInfiniteScrollService; var grid; From 4042db4ea329cbbb4bf5442995be27c39e9cd248 Mon Sep 17 00:00:00 2001 From: Paul Lambert Date: Sat, 28 Mar 2015 13:42:45 +1300 Subject: [PATCH 063/173] Doco(pagination): create external pagination tutorial, fix #3113 --- misc/tutorial/214_pagination.ngdoc | 4 ++ misc/tutorial/318_external_pagination.ngdoc | 59 +++++++++++++++++++++ 2 files changed, 63 insertions(+) create mode 100644 misc/tutorial/318_external_pagination.ngdoc diff --git a/misc/tutorial/214_pagination.ngdoc b/misc/tutorial/214_pagination.ngdoc index daa927fe29..7edc52881b 100644 --- a/misc/tutorial/214_pagination.ngdoc +++ b/misc/tutorial/214_pagination.ngdoc @@ -4,6 +4,10 @@ When pagination is enabled, the data is displayed in pages that can be browsed using using the built in pagination selector. +If you wanted server based pagination, you could look at {@link 318_external_pagination the external pagination tutorial} or consider +using {@link 212_infinite_scroll infinite scroll}, which also retrieves data in pages from the server. + + @example diff --git a/misc/tutorial/318_external_pagination.ngdoc b/misc/tutorial/318_external_pagination.ngdoc new file mode 100644 index 0000000000..58be6a1a20 --- /dev/null +++ b/misc/tutorial/318_external_pagination.ngdoc @@ -0,0 +1,59 @@ +@ngdoc overview +@name Tutorial: 318 External Pagination +@description + +Pagination can be server based, with clicking the pagination buttons getting a new page of data from the server +rather than internally filtering the data that is held in the grid. + +@example + + + var app = angular.module('app', ['ngTouch', 'ui.grid', 'ui.grid.pagination']); + + app.controller('MainCtrl', ['$scope', '$http', function ($scope, $http) { + $scope.gridOptions = { + paginationPageSizes: [25, 50, 75], + paginationPageSize: 25, + useExternalPagination: true, + totalItems: 100, + columnDefs: [ + { name: 'name' }, + { name: 'gender' }, + { name: 'company' } + ], + onRegisterApi: function(gridApi){ + $scope.gridApi = gridApi; + $scope.gridApi.pagination.on.paginationChanged( $scope, function( currentPage, pageSize){ + $scope.getPage(currentPage, pageSize); + }); + } + }; + + + $scope.getPage = function(pageNumber, pageSize){ + var startingRow = pageSize * ( pageNumber - 1); // page number starts at 1, not zero + $http.get('/data/100.json') + .success(function (data) { + var newData = []; + for( var i = startingRow; i < startingRow + $scope.gridOptions.paginationPageSize; i++ ) { + newData.push( data[i] ); + } + $scope.gridOptions.data = newData; + }); + }; + + $scope.getPage(1, 25); + }]); + + +
    +

    Grid with native pagination controls

    +
    +
    +
    + + .grid { + width: 600px; + } + +
    From 4fa7387cdcff82285ca6212e57e25055c008431b Mon Sep 17 00:00:00 2001 From: Paul Lambert Date: Sat, 28 Mar 2015 14:08:17 +1300 Subject: [PATCH 064/173] Fix(resizeable): fix #3129 move column then resize handles wrong --- src/features/move-columns/js/column-movable.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/features/move-columns/js/column-movable.js b/src/features/move-columns/js/column-movable.js index 7b2b59f364..684b0ee69b 100644 --- a/src/features/move-columns/js/column-movable.js +++ b/src/features/move-columns/js/column-movable.js @@ -150,9 +150,9 @@ } } columns[newPosition] = originalColumn; + grid.queueGridRefresh(); $timeout(function () { grid.api.core.notifyDataChange( uiGridConstants.dataChange.COLUMN ); - grid.queueGridRefresh(); grid.api.colMovable.raise.columnPositionChanged(originalColumn.colDef, originalPosition, newPosition); }); } From 77fefc5dd4eb482f6023ac7c31056daf28f9da7f Mon Sep 17 00:00:00 2001 From: Paul Lambert Date: Sun, 15 Mar 2015 20:20:48 +1300 Subject: [PATCH 065/173] Fix(touch events): touch events on resize, move, header click/sort/menu, selection Fix #3084, #2838, #2616, #2550, #2303. All relate to handling of events on touch devices. General philosophy is to register down events for both touch and mouse, but once we get a down event, we only listen for move and up events of the type we got a down on. This is a similar approach to hammer.js. Also includes a big refactor of how move columns listens for events, makes it more like other handlers. This all works for me on iPad, and on OSX/chrome (non-touch). I don't have the ability to test a machine that has both mouse and touch, so waiting on someone else to look at that. --- .../move-columns/js/column-movable.js | 387 +++++++++--------- .../js/ui-grid-column-resizer.js | 87 ++-- src/features/selection/js/selection.js | 22 +- src/js/core/directives/ui-grid-header-cell.js | 206 +++++++--- 4 files changed, 410 insertions(+), 292 deletions(-) diff --git a/src/features/move-columns/js/column-movable.js b/src/features/move-columns/js/column-movable.js index 684b0ee69b..a82e6407d3 100644 --- a/src/features/move-columns/js/column-movable.js +++ b/src/features/move-columns/js/column-movable.js @@ -247,220 +247,225 @@ if ($scope.col.colDef.enableColumnMoving) { - $scope.$on(uiGridConstants.events.COLUMN_HEADER_CLICK, function (event, args) { + /* + * Our general approach to column move is that we listen to a touchstart or mousedown + * event over the column header. When we hear one, then we wait for a move of the same type + * - if we are a touchstart then we listen for a touchmove, if we are a mousedown we listen for + * a mousemove (i.e. a drag) before we decide that there's a move underway. If there's never a move, + * and we instead get a mouseup or a touchend, then we just drop out again and do nothing. + * + */ + var $contentsElm = angular.element( $elm[0].querySelectorAll('.ui-grid-cell-contents') ); - if (args.columnName === $scope.col.colDef.name && !$scope.col.renderContainer) { + var gridLeft; + var previousMouseX; + var totalMouseMovement; + var rightMoveLimit; + var elmCloned = false; + var movingElm; + var reducedWidth; + var moveOccurred = false; - var evt = args.event; - if (evt.target.className !== 'ui-grid-icon-angle-down' && evt.target.tagName !== 'I' && - evt.target.className.indexOf('ui-grid-filter-input') < 0) { + var downFn = function( event ){ + //Setting some variables required for calculations. + gridLeft = $scope.grid.element[0].getBoundingClientRect().left; + previousMouseX = event.pageX; + totalMouseMovement = 0; + rightMoveLimit = gridLeft + $scope.grid.getViewportWidth(); - //Setting some variables required for calculations. - var gridLeft = $scope.grid.element[0].getBoundingClientRect().left; - var previousMouseX = evt.pageX; - var totalMouseMovement = 0; - var rightMoveLimit = gridLeft + $scope.grid.getViewportWidth();// - $scope.grid.verticalScrollbarWidth; - - //Clone element should move horizontally with mouse. - var elmCloned = false; - var movingElm; - var reducedWidth; - - var cloneElement = function () { - elmCloned = true; - - //Cloning header cell and appending to current header cell. - movingElm = $elm.clone(); - $elm.parent().append(movingElm); + if ( event.type === 'mousedown' ){ + $document.on('mousemove', moveFn); + $document.on('mouseup', upFn); + } else if ( event.type === 'touchstart' ){ + $document.on('touchmove', moveFn); + $document.on('touchend', upFn); + } + }; + + var moveFn = function( event ) { + //Disable text selection in Chrome during column move + document.onselectstart = function() { return false; }; + + moveOccurred = true; - //Left of cloned element should be aligned to original header cell. - movingElm.addClass('movingColumn'); - var movingElementStyles = {}; - var elmLeft = $elm[0].getBoundingClientRect().left; - movingElementStyles.left = (elmLeft - gridLeft) + 'px'; - var gridRight = $scope.grid.element[0].getBoundingClientRect().right; - var elmRight = $elm[0].getBoundingClientRect().right; - if (elmRight > gridRight) { - reducedWidth = $scope.col.drawnWidth + (gridRight - elmRight); - movingElementStyles.width = reducedWidth + 'px'; - } - movingElm.css(movingElementStyles); - }; + var changeValue = event.pageX - previousMouseX; + if (!elmCloned) { + cloneElement(); + } + else if (elmCloned) { + moveElement(changeValue); + previousMouseX = event.pageX; + } + }; + + var upFn = function( event ){ + //Re-enable text selection after column move + document.onselectstart = null; - var moveElement = function (changeValue) { - //Hide column menu - uiGridCtrl.fireEvent('hide-menu'); + //Remove the cloned element on mouse up. + if (movingElm) { + movingElm.remove(); + elmCloned = false; + } + + offAllEvents(); + onDownEvents(); - //Calculate total column width - var columns = $scope.grid.columns; - var totalColumnWidth = 0; - for (var i = 0; i < columns.length; i++) { - if (angular.isUndefined(columns[i].colDef.visible) || columns[i].colDef.visible === true) { - totalColumnWidth += columns[i].drawnWidth || columns[i].width || columns[i].colDef.width; - } - } + if (!moveOccurred){ + return; + } - //Calculate new position of left of column - var currentElmLeft = movingElm[0].getBoundingClientRect().left - 1; - var currentElmRight = movingElm[0].getBoundingClientRect().right; - var newElementLeft; - if (gridUtil.detectBrowser() === 'ie') { - newElementLeft = currentElmLeft + changeValue; - } - else { - newElementLeft = currentElmLeft - gridLeft + changeValue; - } - newElementLeft = newElementLeft < rightMoveLimit ? newElementLeft : rightMoveLimit; + var columns = $scope.grid.columns; + var columnIndex = 0; + for (var i = 0; i < columns.length; i++) { + if (columns[i].colDef.name !== $scope.col.colDef.name) { + columnIndex++; + } + else { + break; + } + } - //Update css of moving column to adjust to new left value or fire scroll in case column has reached edge of grid - if ((currentElmLeft >= gridLeft || changeValue > 0) && (currentElmRight <= rightMoveLimit || changeValue < 0)) { - movingElm.css({visibility: 'visible', 'left': newElementLeft + 'px'}); - } - else if (totalColumnWidth > Math.ceil(uiGridCtrl.grid.gridWidth)) { - changeValue *= 8; - var scrollEvent = new ScrollEvent($scope.col.grid, null, null, 'uiGridHeaderCell.moveElement'); - scrollEvent.x = {pixels: changeValue}; - scrollEvent.fireScrollingEvent(); + //Case where column should be moved to a position on its left + if (totalMouseMovement < 0) { + var totalColumnsLeftWidth = 0; + for (var il = columnIndex - 1; il >= 0; il--) { + if (angular.isUndefined(columns[il].colDef.visible) || columns[il].colDef.visible === true) { + totalColumnsLeftWidth += columns[il].drawnWidth || columns[il].width || columns[il].colDef.width; + if (totalColumnsLeftWidth > Math.abs(totalMouseMovement)) { + uiGridMoveColumnService.redrawColumnAtPosition + ($scope.grid, columnIndex, il + 1); + break; } + } + } + //Case where column should be moved to beginning of the grid. + if (totalColumnsLeftWidth < Math.abs(totalMouseMovement)) { + uiGridMoveColumnService.redrawColumnAtPosition + ($scope.grid, columnIndex, 0); + } + } - //Calculate total width of columns on the left of the moving column and the mouse movement - var totalColumnsLeftWidth = 0; - for (var il = 0; il < columns.length; il++) { - if (angular.isUndefined(columns[il].colDef.visible) || columns[il].colDef.visible === true) { - if (columns[il].colDef.name !== $scope.col.colDef.name) { - totalColumnsLeftWidth += columns[il].drawnWidth || columns[il].width || columns[il].colDef.width; - } - else { - break; - } - } - } - if ($scope.newScrollLeft === undefined) { - totalMouseMovement += changeValue; - } - else { - totalMouseMovement = $scope.newScrollLeft + newElementLeft - totalColumnsLeftWidth; + //Case where column should be moved to a position on its right + else if (totalMouseMovement > 0) { + var totalColumnsRightWidth = 0; + for (var ir = columnIndex + 1; ir < columns.length; ir++) { + if (angular.isUndefined(columns[ir].colDef.visible) || columns[ir].colDef.visible === true) { + totalColumnsRightWidth += columns[ir].drawnWidth || columns[ir].width || columns[ir].colDef.width; + if (totalColumnsRightWidth > totalMouseMovement) { + uiGridMoveColumnService.redrawColumnAtPosition + ($scope.grid, columnIndex, ir - 1); + break; } + } + } + //Case where column should be moved to end of the grid. + if (totalColumnsRightWidth < totalMouseMovement) { + uiGridMoveColumnService.redrawColumnAtPosition + ($scope.grid, columnIndex, columns.length - 1); + } + } + }; + + var onDownEvents = function(){ + $contentsElm.on('touchstart', downFn); + $contentsElm.on('mousedown', downFn); + }; + + var offAllEvents = function() { + $contentsElm.off('touchstart', downFn); + $contentsElm.off('mousedown', downFn); - //Increase width of moving column, in case the rightmost column was moved and its width was - //decreased because of overflow - if (reducedWidth < $scope.col.drawnWidth) { - reducedWidth += Math.abs(changeValue); - movingElm.css({'width': reducedWidth + 'px'}); - } - }; + $document.off('mousemove', moveFn); + $document.off('touchmove', moveFn); - var mouseMoveHandler = function (evt) { - //Disable text selection in Chrome during column move - document.onselectstart = function() { return false; }; + $document.off('mouseup', upFn); + $document.off('touchend', upFn); + }; + + onDownEvents(); - var changeValue = evt.pageX - previousMouseX; - if (!elmCloned && Math.abs(changeValue) > 50) { - cloneElement(); - } - else if (elmCloned) { - moveElement(changeValue); - previousMouseX = evt.pageX; - } - }; - /* - //Commenting these lines as they are creating trouble with column moving when grid has huge scroll - // On scope destroy, remove the mouse event handlers from the document body - $scope.$on('$destroy', function () { - $document.off('mousemove', mouseMoveHandler); - $document.off('mouseup', mouseUpHandler); - }); - */ - $document.on('mousemove', mouseMoveHandler); + var cloneElement = function () { + elmCloned = true; - var mouseUpHandler = function (evt) { - //Re-enable text selection after column move - document.onselectstart = null; + //Cloning header cell and appending to current header cell. + movingElm = $elm.clone(); + $elm.parent().append(movingElm); - //Remove the cloned element on mouse up. - if (movingElm) { - movingElm.remove(); - } + //Left of cloned element should be aligned to original header cell. + movingElm.addClass('movingColumn'); + var movingElementStyles = {}; + var elmLeft = $elm[0].getBoundingClientRect().left; + movingElementStyles.left = (elmLeft - gridLeft) + 'px'; + var gridRight = $scope.grid.element[0].getBoundingClientRect().right; + var elmRight = $elm[0].getBoundingClientRect().right; + if (elmRight > gridRight) { + reducedWidth = $scope.col.drawnWidth + (gridRight - elmRight); + movingElementStyles.width = reducedWidth + 'px'; + } + movingElm.css(movingElementStyles); + }; - var columns = $scope.grid.columns; - var columnIndex = 0; - for (var i = 0; i < columns.length; i++) { - if (columns[i].colDef.name !== $scope.col.colDef.name) { - columnIndex++; - } - else { - break; - } - } + var moveElement = function (changeValue) { + //Calculate total column width + var columns = $scope.grid.columns; + var totalColumnWidth = 0; + for (var i = 0; i < columns.length; i++) { + if (angular.isUndefined(columns[i].colDef.visible) || columns[i].colDef.visible === true) { + totalColumnWidth += columns[i].drawnWidth || columns[i].width || columns[i].colDef.width; + } + } - //Case where column should be moved to a position on its left - if (totalMouseMovement < 0) { - var totalColumnsLeftWidth = 0; - for (var il = columnIndex - 1; il >= 0; il--) { - if (angular.isUndefined(columns[il].colDef.visible) || columns[il].colDef.visible === true) { - totalColumnsLeftWidth += columns[il].drawnWidth || columns[il].width || columns[il].colDef.width; - if (totalColumnsLeftWidth > Math.abs(totalMouseMovement)) { - uiGridMoveColumnService.redrawColumnAtPosition - ($scope.grid, columnIndex, il + 1); - break; - } - } - } - //Case where column should be moved to beginning of the grid. - if (totalColumnsLeftWidth < Math.abs(totalMouseMovement)) { - uiGridMoveColumnService.redrawColumnAtPosition - ($scope.grid, columnIndex, 0); - } - } + //Calculate new position of left of column + var currentElmLeft = movingElm[0].getBoundingClientRect().left - 1; + var currentElmRight = movingElm[0].getBoundingClientRect().right; + var newElementLeft; + if (gridUtil.detectBrowser() === 'ie') { + newElementLeft = currentElmLeft + changeValue; + } + else { + newElementLeft = currentElmLeft - gridLeft + changeValue; + } + newElementLeft = newElementLeft < rightMoveLimit ? newElementLeft : rightMoveLimit; - //Case where column should be moved to a position on its right - else if (totalMouseMovement > 0) { - var totalColumnsRightWidth = 0; - for (var ir = columnIndex + 1; ir < columns.length; ir++) { - if (angular.isUndefined(columns[ir].colDef.visible) || columns[ir].colDef.visible === true) { - totalColumnsRightWidth += columns[ir].drawnWidth || columns[ir].width || columns[ir].colDef.width; - if (totalColumnsRightWidth > totalMouseMovement) { - uiGridMoveColumnService.redrawColumnAtPosition - ($scope.grid, columnIndex, ir - 1); - break; - } - } - } - //Case where column should be moved to end of the grid. - if (totalColumnsRightWidth < totalMouseMovement) { - uiGridMoveColumnService.redrawColumnAtPosition - ($scope.grid, columnIndex, columns.length - 1); - } - } -/* - else if (totalMouseMovement === 0) { - if (uiGridCtrl.grid.options.enableSorting && $scope.col.enableSorting) { - //sort the current column - var add = false; - if (evt.shiftKey) { - add = true; - } - // Sort this column then rebuild the grid's rows - uiGridCtrl.grid.sortColumn($scope.col, add) - .then(function () { - if (uiGridCtrl.columnMenuScope) { - uiGridCtrl.columnMenuScope.hideMenu(); - } - uiGridCtrl.grid.refresh(); - }); - } - } -*/ - $document.off('mousemove', mouseMoveHandler); - $document.off('mouseup', mouseUpHandler); - }; + //Update css of moving column to adjust to new left value or fire scroll in case column has reached edge of grid + if ((currentElmLeft >= gridLeft || changeValue > 0) && (currentElmRight <= rightMoveLimit || changeValue < 0)) { + movingElm.css({visibility: 'visible', 'left': newElementLeft + 'px'}); + } + else if (totalColumnWidth > Math.ceil(uiGridCtrl.grid.gridWidth)) { + changeValue *= 8; + var scrollEvent = new ScrollEvent($scope.col.grid, null, null, 'uiGridHeaderCell.moveElement'); + scrollEvent.x = {pixels: changeValue}; + scrollEvent.fireScrollingEvent(); + } - //Binding the mouseup event handler - $document.on('mouseup', mouseUpHandler); + //Calculate total width of columns on the left of the moving column and the mouse movement + var totalColumnsLeftWidth = 0; + for (var il = 0; il < columns.length; il++) { + if (angular.isUndefined(columns[il].colDef.visible) || columns[il].colDef.visible === true) { + if (columns[il].colDef.name !== $scope.col.colDef.name) { + totalColumnsLeftWidth += columns[il].drawnWidth || columns[il].width || columns[il].colDef.width; + } + else { + break; + } } } - }); + if ($scope.newScrollLeft === undefined) { + totalMouseMovement += changeValue; + } + else { + totalMouseMovement = $scope.newScrollLeft + newElementLeft - totalColumnsLeftWidth; + } + + //Increase width of moving column, in case the rightmost column was moved and its width was + //decreased because of overflow + if (reducedWidth < $scope.col.drawnWidth) { + reducedWidth += Math.abs(changeValue); + movingElm.css({'width': reducedWidth + 'px'}); + } + }; } } }; diff --git a/src/features/resize-columns/js/ui-grid-column-resizer.js b/src/features/resize-columns/js/ui-grid-column-resizer.js index 26776bc988..bf1950485a 100644 --- a/src/features/resize-columns/js/ui-grid-column-resizer.js +++ b/src/features/resize-columns/js/ui-grid-column-resizer.js @@ -288,19 +288,6 @@ module.directive('uiGridColumnResizer', ['$document', 'gridUtil', 'uiGridConstants', 'uiGridResizeColumnsService', function ($document, gridUtil, uiGridConstants, uiGridResizeColumnsService) { var resizeOverlay = angular.element('
    '); - var downEvent, upEvent, moveEvent; - - if (gridUtil.isTouchEnabled()) { - downEvent = 'touchstart'; - upEvent = 'touchend'; - moveEvent = 'touchmove'; - } - else { - downEvent = 'mousedown'; - upEvent = 'mouseup'; - moveEvent = 'mousemove'; - } - var resizer = { priority: 0, scope: { @@ -374,7 +361,15 @@ } - function mousemove(event, args) { + /* + * Our approach to event handling aims to deal with both touch devices and mouse devices + * We register down handlers on both touch and mouse. When a touchstart or mousedown event + * occurs, we register the corresponding touchmove/touchend, or mousemove/mouseend events. + * + * This way we can listen for both without worrying about the fact many touch devices also emulate + * mouse events - basically whichever one we hear first is what we'll go with. + */ + function moveFunction(event, args) { if (event.originalEvent) { event = event.originalEvent; } event.preventDefault(); @@ -409,7 +404,7 @@ } - function mouseup(event, args) { + function upFunction(event, args) { if (event.originalEvent) { event = event.originalEvent; } event.preventDefault(); @@ -422,8 +417,10 @@ var xDiff = x - startX; if (xDiff === 0) { - $document.off(upEvent, mouseup); - $document.off(moveEvent, mousemove); + // no movement, so just reset event handlers, including turning back on both + // down events - we turned one off when this event started + offAllEvents(); + onDownEvents(); return; } @@ -447,12 +444,14 @@ uiGridResizeColumnsService.fireColumnSizeChanged(uiGridCtrl.grid, col.colDef, xDiff); - $document.off(upEvent, mouseup); - $document.off(moveEvent, mousemove); + // stop listening of up and move events - wait for next down + // reset the down events - we will have turned one off when this event started + offAllEvents(); + onDownEvents(); } - $elm.on(downEvent, function(event, args) { + var downFunction = function(event, args) { if (event.originalEvent) { event = event.originalEvent; } event.stopPropagation(); @@ -469,14 +468,40 @@ // Place the resizer overlay at the start position resizeOverlay.css({ left: startX }); - // Add handlers for mouse move and up events - $document.on(upEvent, mouseup); - $document.on(moveEvent, mousemove); - }); - + // Add handlers for move and up events - if we were mousedown then we listen for mousemove and mouseup, if + // we were touchdown then we listen for touchmove and touchup. Also remove the handler for the equivalent + // down event - so if we're touchdown, then remove the mousedown handler until this event is over, if we're + // mousedown then remove the touchdown handler until this event is over, this avoids processing duplicate events + if ( event.type === 'touchstart' ){ + $document.on('touchend', upFunction); + $document.on('touchmove', moveFunction); + $elm.off('mousedown', downFunction); + } else { + $document.on('mouseup', upFunction); + $document.on('mousemove', moveFunction); + $elm.off('touchstart', downFunction); + } + }; + + var onDownEvents = function() { + $elm.on('mousedown', downFunction); + $elm.on('touchstart', downFunction); + }; + + var offAllEvents = function() { + $document.off('mouseup', upFunction); + $document.off('touchend', upFunction); + $document.off('mousemove', moveFunction); + $document.off('touchmove', moveFunction); + $elm.off('mousedown', downFunction); + $elm.off('touchstart', downFunction); + }; + + onDownEvents(); + // On doubleclick, resize to fit all rendered cells - $elm.on('dblclick', function(event, args) { + var dblClickFn = function(event, args){ event.stopPropagation(); var col = uiGridResizeColumnsService.findTargetCol($scope.col, $scope.position, rtlMultiplier); @@ -532,14 +557,12 @@ buildColumnsAndRefresh(xDiff); - uiGridResizeColumnsService.fireColumnSizeChanged(uiGridCtrl.grid, col.colDef, xDiff); - }); + uiGridResizeColumnsService.fireColumnSizeChanged(uiGridCtrl.grid, col.colDef, xDiff); }; + $elm.on('dblclick', dblClickFn); $elm.on('$destroy', function() { - $elm.off(downEvent); - $elm.off('dblclick'); - $document.off(moveEvent, mousemove); - $document.off(upEvent, mouseup); + $elm.off('dblclick', dblClickFn); + offAllEvents(); }); } }; diff --git a/src/features/selection/js/selection.js b/src/features/selection/js/selection.js index e1c8904f69..52067f663b 100644 --- a/src/features/selection/js/selection.js +++ b/src/features/selection/js/selection.js @@ -770,8 +770,8 @@ * @description Stacks on top of ui.grid.uiGridCell to provide selection feature */ module.directive('uiGridCell', - ['$compile', 'uiGridConstants', 'uiGridSelectionConstants', 'gridUtil', '$parse', 'uiGridSelectionService', - function ($compile, uiGridConstants, uiGridSelectionConstants, gridUtil, $parse, uiGridSelectionService) { + ['$compile', 'uiGridConstants', 'uiGridSelectionConstants', 'gridUtil', '$parse', 'uiGridSelectionService', '$timeout', + function ($compile, uiGridConstants, uiGridSelectionConstants, gridUtil, $parse, uiGridSelectionService, $timeout) { return { priority: -200, // run after default uiGridCell directive restrict: 'A', @@ -789,6 +789,9 @@ }); var selectCells = function(evt){ + // if we get a click, then stop listening for touchend + $elm.off('touchend', touchEnd); + if (evt.shiftKey) { uiGridSelectionService.shiftSelect($scope.grid, $scope.row, evt, $scope.grid.options.multiSelect); } @@ -799,10 +802,19 @@ uiGridSelectionService.toggleRowSelection($scope.grid, $scope.row, evt, ($scope.grid.options.multiSelect && !$scope.grid.options.modifierKeysToMultiSelect), $scope.grid.options.noUnselect); } $scope.$apply(); + + // don't re-enable the touchend handler for a little while - some devices generate both, and it will + // take a little while to move your hand from the mouse to the screen if you have both modes of input + $timeout(function() { + $elm.on('touchend', touchEnd); + }, touchTimeout); }; var touchStart = function(evt){ touchStartTime = (new Date()).getTime(); + + // if we get a touch event, then stop listening for click + $elm.off('click', selectCells); }; var touchEnd = function(evt) { @@ -813,6 +825,12 @@ // short touch selectCells(evt); } + + // don't re-enable the click handler for a little while - some devices generate both, and it will + // take a little while to move your hand from the screen to the mouse if you have both modes of input + $timeout(function() { + $elm.on('click', selectCells); + }, touchTimeout); }; function registerRowSelectionEvents() { diff --git a/src/js/core/directives/ui-grid-header-cell.js b/src/js/core/directives/ui-grid-header-cell.js index 225cfea709..e084dba6aa 100644 --- a/src/js/core/directives/ui-grid-header-cell.js +++ b/src/js/core/directives/ui-grid-header-cell.js @@ -5,6 +5,7 @@ function ($compile, $timeout, $window, $document, gridUtil, uiGridConstants, ScrollEvent) { // Do stuff after mouse has been down this many ms on the header cell var mousedownTimeout = 500; + var changeModeTimeout = 500; // length of time between a touch event and a mouse event being recognised again, and vice versa var uiGridHeaderCell = { priority: 0, @@ -48,6 +49,138 @@ // apply any headerCellClass var classAdded; + + /* + * Our basic approach here for event handlers is that we listen for a down event (mousedown or touchstart). + * Once we have a down event, we need to work out whether we have a click, a drag, or a + * hold. A click would sort the grid (if sortable). A drag would be used by moveable, so + * we ignore it. A hold would open the menu. + * + * So, on down event, we put in place handlers for move and up events, and a timer. If the + * timer expires before we see a move or up, then we have a long press and hence a column menu open. + * If the up happens before the timer, then we have a click, and we sort if the column is sortable. + * If a move happens before the timer, then we are doing column move, so we do nothing, the moveable feature + * will handle it. + * + * To deal with touch enabled devices that also have mice, we only create our handlers when + * we get the down event, and we create the corresponding handlers - if we're touchstart then + * we get touchmove and touchend, if we're mousedown then we get mousemove and mouseup. + * + * We also suppress the click action whilst this is happening - otherwise after the mouseup there + * will be a click event and that can cause the column menu to close + * + */ + + $scope.downFn = function( event ){ + event.stopPropagation(); + + if (typeof(event.originalEvent) !== 'undefined' && event.originalEvent !== undefined) { + event = event.originalEvent; + } + + // Don't show the menu if it's not the left button + if (event.button && event.button !== 0) { + return; + } + + $scope.mousedownStartTime = (new Date()).getTime(); + $scope.mousedownTimeout = $timeout(function() { }, mousedownTimeout); + + $scope.mousedownTimeout.then(function () { + if ( $scope.colMenu ) { + uiGridCtrl.columnMenuScope.showMenu($scope.col, $elm, event); + } + }); + + uiGridCtrl.fireEvent(uiGridConstants.events.COLUMN_HEADER_CLICK, {event: event, columnName: $scope.col.colDef.name}); + + $scope.offAllEvents(); + if ( event.type === 'touchstart'){ + $document.on('touchend', $scope.upFn); + $document.on('touchmove', $scope.moveFn); + } else if ( event.type === 'mousedown' ){ + $document.on('mouseup', $scope.upFn); + $document.on('mousemove', $scope.moveFn); + } + }; + + $scope.upFn = function( event ){ + event.stopPropagation(); + $timeout.cancel($scope.mousedownTimeout); + $scope.offAllEvents(); + $scope.onDownEvents(event.type); + + var mousedownEndTime = (new Date()).getTime(); + var mousedownTime = mousedownEndTime - $scope.mousedownStartTime; + + if (mousedownTime > mousedownTimeout) { + // long click, handled above with mousedown + } + else { + // short click + if ( $scope.sortable ){ + $scope.handleClick(event); + } + } + }; + + $scope.moveFn = function( event ){ + // we're a move, so do nothing and leave for column move (if enabled) to take over + $timeout.cancel($scope.mousedownTimeout); + $scope.offAllEvents(); + $scope.onDownEvents(event.type); + }; + + $scope.clickFn = function ( event ){ + event.stopPropagation(); + $contentsElm.off('click', $scope.clickFn); + }; + + + $scope.offAllEvents = function(){ + $contentsElm.off('touchstart', $scope.downFn); + $contentsElm.off('mousedown', $scope.downFn); + + $document.off('touchend', $scope.upFn); + $document.off('mouseup', $scope.upFn); + + $document.off('touchmove', $scope.moveFn); + $document.off('mousemove', $scope.moveFn); + + $contentsElm.off('click', $scope.clickFn); + }; + + $scope.onDownEvents = function( type ){ + // If there is a previous event, then wait a while before + // activating the other mode - i.e. if the last event was a touch event then + // don't enable mouse events for a wee while (500ms or so) + // Avoids problems with devices that emulate mouse events when you have touch events + + switch (type){ + case 'touchmove': + case 'touchend': + $contentsElm.on('click', $scope.clickFn); + $contentsElm.on('touchstart', $scope.downFn); + $timeout(function(){ + $contentsElm.on('mousedown', $scope.downFn); + }, changeModeTimeout); + break; + case 'mousemove': + case 'mouseup': + $contentsElm.on('click', $scope.clickFn); + $contentsElm.on('mousedown', $scope.downFn); + $timeout(function(){ + $contentsElm.on('touchstart', $scope.downFn); + }, changeModeTimeout); + break; + default: + $contentsElm.on('click', $scope.clickFn); + $contentsElm.on('touchstart', $scope.downFn); + $contentsElm.on('mousedown', $scope.downFn); + } + }; + + var updateHeaderOptions = function( grid ){ var contents = $elm; if ( classAdded ){ @@ -109,77 +242,16 @@ * */ - var downEvent = gridUtil.isTouchEnabled() ? 'touchstart' : 'mousedown'; + $scope.offAllEvents(); + if ($scope.sortable || $scope.colMenu) { - // Long-click (for mobile) - var cancelMousedownTimeout; - var mousedownStartTime = 0; - - $contentsElm.on(downEvent, function(event) { - event.stopPropagation(); - - if (typeof(event.originalEvent) !== 'undefined' && event.originalEvent !== undefined) { - event = event.originalEvent; - } - - // Don't show the menu if it's not the left button - if (event.button && event.button !== 0) { - return; - } - - mousedownStartTime = (new Date()).getTime(); - - cancelMousedownTimeout = $timeout(function() { }, mousedownTimeout); - - cancelMousedownTimeout.then(function () { - if ( $scope.colMenu ) { - uiGridCtrl.columnMenuScope.showMenu($scope.col, $elm, event); - } - }); - - uiGridCtrl.fireEvent(uiGridConstants.events.COLUMN_HEADER_CLICK, {event: event, columnName: $scope.col.colDef.name}); - }); + $scope.onDownEvents(); - var upEvent = gridUtil.isTouchEnabled() ? 'touchend' : 'mouseup'; - $contentsElm.on(upEvent, function () { - $timeout.cancel(cancelMousedownTimeout); - }); - $scope.$on('$destroy', function () { - $contentsElm.off('mousedown touchstart'); + $scope.offAllEvents(); }); - } else { - $contentsElm.off(downEvent); } - // If this column is sortable, add a click event handler - var clickEvent = gridUtil.isTouchEnabled() ? 'touchend' : 'click'; - if ($scope.sortable) { - $contentsElm.on(clickEvent, function(event) { - event.stopPropagation(); - - $timeout.cancel(cancelMousedownTimeout); - - var mousedownEndTime = (new Date()).getTime(); - var mousedownTime = mousedownEndTime - mousedownStartTime; - - if (mousedownTime > mousedownTimeout) { - // long click, handled above with mousedown - } - else { - // short click - handleClick(event); - } - }); - - $scope.$on('$destroy', function () { - // Cancel any pending long-click timeout - $timeout.cancel(cancelMousedownTimeout); - }); - } else { - $contentsElm.off(clickEvent); - } - // if column is filterable add a filter watcher var filterDeregisters = []; if ($scope.filterable) { @@ -222,7 +294,7 @@ $scope.$on( '$destroy', dataChangeDereg ); - function handleClick(event) { + $scope.handleClick = function(event) { // If the shift key is being held down, add this column to the sort var add = false; if (event.shiftKey) { @@ -235,7 +307,7 @@ if (uiGridCtrl.columnMenuScope) { uiGridCtrl.columnMenuScope.hideMenu(); } uiGridCtrl.grid.refresh(); }); - } + }; $scope.toggleMenu = function(event) { From 512f95031cdcf991f98969692a69af24bd1da83f Mon Sep 17 00:00:00 2001 From: Paul Lambert Date: Sat, 28 Mar 2015 17:24:52 +1300 Subject: [PATCH 066/173] Doco(tuts, faq): updates with some examples --- misc/tutorial/100_preReqs.ngdoc | 1 + misc/tutorial/499_FAQ.ngdoc | 3 ++- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/misc/tutorial/100_preReqs.ngdoc b/misc/tutorial/100_preReqs.ngdoc index 296a986842..05197e16b3 100644 --- a/misc/tutorial/100_preReqs.ngdoc +++ b/misc/tutorial/100_preReqs.ngdoc @@ -16,6 +16,7 @@ Current list of tested browsers: AngularJS
    • 1.2.10+
    • +
    • 1.3.0+
    diff --git a/misc/tutorial/499_FAQ.ngdoc b/misc/tutorial/499_FAQ.ngdoc index c52abd09e9..867e5be564 100644 --- a/misc/tutorial/499_FAQ.ngdoc +++ b/misc/tutorial/499_FAQ.ngdoc @@ -47,9 +47,10 @@ There are a number of common gotchas in using the grid, this FAQ aims to cover m ### What browsers are supported by ui.grid Our current testing verifies against IE9+, Chrome, Firefox, Safari 5+, Opera and Android. We expect that the functionality - is compatible with any HTML5 compliant and Javascript enabled browser. + is compatible with any HTML5 compliant and Javascript enabled browser. Refer {@link 100_preReqs preReqs} ## What angular versions are supported by ui.grid Our current testing uses 1.2.8, 1.2.14, 1.2.26, 1.3.0 and 1.3.6. We intend to remain compatible with all forward versions of 1.3. + Refer {@link 100_preReqs preReqs} \ No newline at end of file From 52e2deec407a30c1120051273332658daf667120 Mon Sep 17 00:00:00 2001 From: Paul Lambert Date: Sat, 28 Mar 2015 17:39:17 +1300 Subject: [PATCH 067/173] Doco(faq): updates --- misc/tutorial/499_FAQ.ngdoc | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/misc/tutorial/499_FAQ.ngdoc b/misc/tutorial/499_FAQ.ngdoc index 867e5be564..ac64c53d5c 100644 --- a/misc/tutorial/499_FAQ.ngdoc +++ b/misc/tutorial/499_FAQ.ngdoc @@ -53,4 +53,20 @@ There are a number of common gotchas in using the grid, this FAQ aims to cover m Our current testing uses 1.2.8, 1.2.14, 1.2.26, 1.3.0 and 1.3.6. We intend to remain compatible with all forward versions of 1.3. Refer {@link 100_preReqs preReqs} + +## How can I wrap text in a cell? + Refer also http://stackoverflow.com/questions/29298968/increase-width-of-column-in-ui-grid + + Firstly, to set the column width you need to use column definitions, then you can set a width in pixels or percentage on each. Refer http://ui-grid.info/docs/#/tutorial/201_editable as an example that has column widths. + + Secondly, there is the ability to add tooltips, which are one way to show longer cells that don't fit in the space available. Refer http://ui-grid.info/docs/#/tutorial/117_tooltips + + Thirdly, you can make the rows taller and therefore have space to wrap content within them. Be aware that all rows must be the same height, so you can't make only the rows that need it taller. + + `gridOptions.rowHeight = 50;` + + You'll also need to set the white-space attribute on the div so that it wraps, which you can do by setting a class in the cellTemplate, and then adding a style to the css. + + A plunker as an example: http://plnkr.co/edit/kyhRm08ZtIKYspDqgyRa?p=preview + \ No newline at end of file From b3a530d245dd4ca48e1c34b4937c1475aba54a7b Mon Sep 17 00:00:00 2001 From: Paul Lambert Date: Sat, 28 Mar 2015 20:33:24 +1300 Subject: [PATCH 068/173] Refactor(styleComputations): fixes rendercontainer rendering problems Change the style computations with the aim of making them deterministic and able to calculate widths in a single pass rather than being iterative. Fix #3050, and somewhat improves #1957, at least to my eye. Changes are: - change renderContainer updateColumnWidths to calculate across all visible columsn in all render containers, rather than trying to calculate one render container at a time (cannot otherwise work with * and %, these are of the whole grid, not just one render container) - change header updateColumnWidths to just rely on the renderContainer, not do it again itself in a slightly different way - change updateColumnWidths to be simpler, and to calculate percentages in a way that I think makes more sense - change resize feature to not attempt to 'resizeAround' the changed column, but rather allow updateColumnWidths to deal with it - change pinning to not call refresh twice when moving a column, this is now unnecessary - updates to tutorials --- misc/api/design-rendering-cycle.ngdoc | 4 +- misc/tutorial/204_column_resizing.ngdoc | 14 +- src/features/pinning/js/pinning.js | 28 +- .../js/ui-grid-column-resizer.js | 22 -- src/js/core/directives/ui-grid-header.js | 114 +------ .../directives/ui-grid-render-container.js | 2 +- src/js/core/directives/ui-pinned-container.js | 13 +- src/js/core/factories/GridRenderContainer.js | 278 +++++++----------- 8 files changed, 138 insertions(+), 337 deletions(-) diff --git a/misc/api/design-rendering-cycle.ngdoc b/misc/api/design-rendering-cycle.ngdoc index 25a5cc14d4..a0a6feb4df 100644 --- a/misc/api/design-rendering-cycle.ngdoc +++ b/misc/api/design-rendering-cycle.ngdoc @@ -44,8 +44,7 @@ appears to be to accomodate filters. The style builders include: -- `GridRenderContainer`, which appears to currently apply no styles -- `uiGrid.updateColumnWidths`, which calculates column widths based on the defined settings, including resolving * and ** etc. No rendering +- `GridRenderContainer.updateColumnWidths`, which calculates column widths based on the defined settings, including resolving * and ** etc. No rendering is involved - all based on the availableWidth. This may be the source of some of the iteration - because availableWidth must in some way be based on columnWidth - the canvas doesn't really have an available width. I also have question on why we calculate widths on the grid and not on the renderContainer, that may be another source of iteration. Having said that, things like % and * probably apply to the @@ -54,6 +53,7 @@ The style builders include: width of each column in the render container, and the width of the overall render container. - `Grid.prototype.getFooterStyles()`, sets the columnFooterHeight and the gridFooterHeight based on fixed values declared in the options - when there are multiple renderContainers (e.g. a left container), the non-body render containers appear to execute first +- `ui-pinned-container.updateContainerDimensions()`: sets the width of a pinned container. How does this interact with render container width? ### Vision The vision is to make the style calculations more deterministic, and remove any iteration or other dependencies. A single pass through diff --git a/misc/tutorial/204_column_resizing.ngdoc b/misc/tutorial/204_column_resizing.ngdoc index 95e19bf1c2..f5e19c20e4 100644 --- a/misc/tutorial/204_column_resizing.ngdoc +++ b/misc/tutorial/204_column_resizing.ngdoc @@ -4,7 +4,17 @@ The Resize Columns feature allows each column to be resized. -To enable, you must include the 'ui.grid.resizeColumns' module and you must include the ui-grid-resize-columns directive on your grid element. +To enable, you must include the 'ui.grid.resizeColumns' module and you must include the ui-grid-resize-columns directive +on your grid element. + +You can set individual columns to not be resizeable, if you do this it is recommended that those columns have a fixed +pixel width - otherwise they may get automatically resized to fill the remaining space if other columns are reduced in size, +and there will be no way to reduce their width again. + +When you resize a column any other columns with fixed widths, or that have already been resized, retain their width. All other +columns resize to take up the remaining space. As long as there is at least one variable column left your columns won't reduce +below the full grid width - but once you've resized all the columns then you can end up with the total column width less than the +grid width.
     angular.module('yourApp', ['ui.grid', 'ui.grid.resizeColumns']);
    @@ -45,7 +55,7 @@ $scope.gridOptions = {
           $scope.gridOptions = {
             enableSorting: true,
             columnDefs: [
    -          { field: 'name', minWidth: 200, width: '50%', enableColumnResizing: false },
    +          { field: 'name', minWidth: 200, width: 250, enableColumnResizing: false },
               { field: 'gender', width: '30%', maxWidth: 200, minWidth: 70 },
               { field: 'company', width: '20%' }
             ]
    diff --git a/src/features/pinning/js/pinning.js b/src/features/pinning/js/pinning.js
    index d357a73c14..19c92fac6a 100644
    --- a/src/features/pinning/js/pinning.js
    +++ b/src/features/pinning/js/pinning.js
    @@ -89,9 +89,6 @@
                 col.grid.refresh()
                     .then(function () {
                         col.renderContainer = 'left';
    -                    // Need to calculate the width. If col.drawnWidth is used instead then the width
    -                    // will be 100% if it's the first column, 50% if it's the second etc.
    -                    col.width = col.grid.canvasWidth / col.grid.columns.length;
                         col.grid.createLeftContainer();
                 });
               }
    @@ -106,9 +103,6 @@
                     col.grid.refresh()
                         .then(function () {
                             col.renderContainer = 'right';
    -                        // Need to calculate the width. If col.drawnWidth is used instead then the width
    -                        // will be 100% if it's the first column, 50% if it's the second etc.
    -                        col.width = col.grid.canvasWidth / col.grid.columns.length;
                             col.grid.createRightContainer();
                         });
                 }
    @@ -134,12 +128,7 @@
                 this.context.col.width = this.context.col.drawnWidth;
                 this.context.col.grid.createLeftContainer();
     
    -            // Need to call refresh twice; once to move our column over to the new render container and then
    -            //   a second time to update the grid viewport dimensions with our adjustments
    -            col.grid.refresh()
    -              .then(function () {
    -                col.grid.refresh();
    -              });
    +            col.grid.refresh();
               }
             };
     
    @@ -155,13 +144,7 @@
                 this.context.col.width = this.context.col.drawnWidth;
                 this.context.col.grid.createRightContainer();
     
    -
    -            // Need to call refresh twice; once to move our column over to the new render container and then
    -            //   a second time to update the grid viewport dimensions with our adjustments
    -            col.grid.refresh()
    -              .then(function () {
    -                col.grid.refresh();
    -              });
    +            col.grid.refresh();
               }
             };
     
    @@ -175,12 +158,7 @@
               action: function () {
                 this.context.col.renderContainer = null;
     
    -            // Need to call refresh twice; once to move our column over to the new render container and then
    -            //   a second time to update the grid viewport dimensions with our adjustments
    -            col.grid.refresh()
    -              .then(function () {
    -                col.grid.refresh();
    -              });
    +            col.grid.refresh();
               }
             };
     
    diff --git a/src/features/resize-columns/js/ui-grid-column-resizer.js b/src/features/resize-columns/js/ui-grid-column-resizer.js
    index 26776bc988..f1bab9fafe 100644
    --- a/src/features/resize-columns/js/ui-grid-column-resizer.js
    +++ b/src/features/resize-columns/js/ui-grid-column-resizer.js
    @@ -328,22 +328,6 @@
               $elm.addClass('right');
             }
     
    -        // Resize all the other columns around col
    -        function resizeAroundColumn(col) {
    -          // Get this column's render container
    -          var renderContainer = col.getRenderContainer();
    -
    -          renderContainer.visibleColumnCache.forEach(function (column) {
    -            // Skip the column we just resized
    -            if (column === col) { return; }
    -            
    -            var colDef = column.colDef;
    -            if (!colDef.width || (angular.isString(colDef.width) && (colDef.width.indexOf('*') !== -1 || colDef.width.indexOf('%') !== -1))) {
    -              column.width = column.drawnWidth;
    -            }
    -          });
    -        }
    -
             // Build the columns then refresh the grid canvas
             //   takes an argument representing the diff along the X-axis that the resize had
             function buildColumnsAndRefresh(xDiff) {
    @@ -440,9 +424,6 @@
               // check we're not outside the allowable bounds for this column
               col.width = constrainWidth(col, newWidth);
     
    -          // All other columns because fixed to their drawn width, if they aren't already
    -          resizeAroundColumn(col);
    -
               buildColumnsAndRefresh(xDiff);
     
               uiGridResizeColumnsService.fireColumnSizeChanged(uiGridCtrl.grid, col.colDef, xDiff);
    @@ -527,9 +508,6 @@
               // check we're not outside the allowable bounds for this column
               col.width = constrainWidth(col, maxWidth);
               
    -          // All other columns because fixed to their drawn width, if they aren't already
    -          resizeAroundColumn(col);
    -
               buildColumnsAndRefresh(xDiff);
               
               uiGridResizeColumnsService.fireColumnSizeChanged(uiGridCtrl.grid, col.colDef, xDiff);
    diff --git a/src/js/core/directives/ui-grid-header.js b/src/js/core/directives/ui-grid-header.js
    index 149c646f3d..6bcc74f655 100644
    --- a/src/js/core/directives/ui-grid-header.js
    +++ b/src/js/core/directives/ui-grid-header.js
    @@ -81,118 +81,22 @@
                 gridUtil.disableAnimations($elm);
     
                 function updateColumnWidths() {
    -              // Get the width of the viewport
    -              var availableWidth = containerCtrl.colContainer.getViewportWidth() - grid.scrollbarWidth;
    -
    -              //if (typeof(uiGridCtrl.grid.verticalScrollbarWidth) !== 'undefined' && uiGridCtrl.grid.verticalScrollbarWidth !== undefined && uiGridCtrl.grid.verticalScrollbarWidth > 0) {
    -              //  availableWidth = availableWidth + uiGridCtrl.grid.verticalScrollbarWidth;
    -              //}
    -
    -              // The total number of columns
    -              // var equalWidthColumnCount = columnCount = uiGridCtrl.grid.options.columnDefs.length;
    -              // var equalWidth = availableWidth / equalWidthColumnCount;
    -
    -              var columnCache = containerCtrl.colContainer.visibleColumnCache,
    -                  canvasWidth = 0,
    -                  asteriskNum = 0,
    -                  oneAsterisk = 0,
    -                  leftoverWidth = availableWidth,
    -                  hasVariableWidth = false;
    -              
    -              var getColWidth = function(column){
    -                if (column.widthType === "manual"){ 
    -                  return +column.width; 
    -                }
    -                else if (column.widthType === "percent"){ 
    -                  return parseInt(column.width.replace(/%/g, ''), 10) * availableWidth / 100;
    -                }
    -                else if (column.widthType === "auto"){
    -                  // leftOverWidth is subtracted from after each call to this
    -                  // function so we need to calculate oneAsterisk size only once
    -                  if (oneAsterisk === 0) {
    -                    oneAsterisk = parseInt(leftoverWidth / asteriskNum, 10);
    -                  }
    -                  return column.width.length * oneAsterisk; 
    -                }
    -              };
    -              
    -              // Populate / determine column width types:
    -              columnCache.forEach(function(column){
    -                column.widthType = null;
    -                if (isFinite(+column.width)){
    -                  column.widthType = "manual";
    -                }
    -                else if (gridUtil.endsWith(column.width, "%")){
    -                  column.widthType = "percent";
    -                  hasVariableWidth = true;
    -                }
    -                else if (angular.isString(column.width) && column.width.indexOf('*') !== -1){
    -                  column.widthType = "auto";
    -                  asteriskNum += column.width.length;
    -                  hasVariableWidth = true;
    -                }
    -              });
    -              
    -              // For sorting, calculate width from first to last:
    -              var colWidthPriority = ["manual", "percent", "auto"];
    -              columnCache.filter(function(column){
    -                // Only draw visible items with a widthType
    -                return (column.visible && column.widthType); 
    -              }).sort(function(a,b){
    -                // Calculate widths in order, so that manual comes first, etc.
    -                return colWidthPriority.indexOf(a.widthType) - colWidthPriority.indexOf(b.widthType);
    -              }).forEach(function(column){
    -                // Calculate widths:
    -                var colWidth = getColWidth(column);
    -                if (column.minWidth){
    -                  colWidth = Math.max(colWidth, column.minWidth);
    -                }
    -                if (column.maxWidth){
    -                  colWidth = Math.min(colWidth, column.maxWidth);
    -                }
    -                column.drawnWidth = Math.floor(colWidth);
    -                canvasWidth += column.drawnWidth;
    -                leftoverWidth -= column.drawnWidth;
    -              });
    -
    -              // If the grid width didn't divide evenly into the column widths and we have pixels left over, dole them out to the columns one by one to make everything fit
    -              if (hasVariableWidth && leftoverWidth > 0 && canvasWidth > 0 && canvasWidth < availableWidth) {
    -                var remFn = function (column) {
    -                  if (leftoverWidth > 0 && (column.widthType === "auto" || column.widthType === "percent")) {
    -                    column.drawnWidth = column.drawnWidth + 1;
    -                    canvasWidth = canvasWidth + 1;
    -                    leftoverWidth--;
    -                  }
    -                };
    -                var prevLeftover = 0;
    -                do {
    -                  prevLeftover = leftoverWidth;
    -                  columnCache.forEach(remFn);
    -                } while (leftoverWidth > 0 && leftoverWidth !== prevLeftover );
    -              }
    -              canvasWidth = Math.max(canvasWidth, availableWidth);
    +              // this styleBuilder always runs after the renderContainer, so we can rely on the column widths
    +              // already being populated correctly
     
    +              var columnCache = containerCtrl.colContainer.visibleColumnCache;
    +              
                   // Build the CSS
                   // uiGridCtrl.grid.columns.forEach(function (column) {
                   var ret = '';
    +              var canvasWidth = 0;
                   columnCache.forEach(function (column) {
                     ret = ret + column.getColClassDefinition();
    +                canvasWidth += column.drawnWidth;
                   });
     
    -              // Add the vertical scrollbar width back in to the canvas width, it's taken out in getViewportWidth
    -              //if (grid.verticalScrollbarWidth) {
    -              //  canvasWidth = canvasWidth + grid.verticalScrollbarWidth;
    -              //}
    -              // canvasWidth = canvasWidth + 1;
    -
    -              // if we have a grid menu, then we prune the width of the last column header
    -              // to allow room for the button whilst still getting to the column menu
    -              if (columnCache.length > 0) { // && grid.options.enableGridMenu) {
    -                columnCache[columnCache.length - 1].headerWidth = columnCache[columnCache.length - 1].drawnWidth - 30;
    -              }
    -
    -              containerCtrl.colContainer.canvasWidth = parseInt(canvasWidth, 10);
    -
    +              containerCtrl.colContainer.canvasWidth = canvasWidth;
    +              
                   // Return the styles back to buildStyles which pops them into the `customStyles` scope variable
                   return ret;
                 }
    @@ -207,7 +111,7 @@
                 //todo: remove this if by injecting gridCtrl into unit tests
                 if (uiGridCtrl) {
                   uiGridCtrl.grid.registerStyleComputation({
    -                priority: 5,
    +                priority: 15,
                     func: updateColumnWidths
                   });
                 }
    diff --git a/src/js/core/directives/ui-grid-render-container.js b/src/js/core/directives/ui-grid-render-container.js
    index 4b10cb4f2e..518a4b53a4 100644
    --- a/src/js/core/directives/ui-grid-render-container.js
    +++ b/src/js/core/directives/ui-grid-render-container.js
    @@ -170,7 +170,7 @@
                 function update() {
                   var ret = '';
     
    -              var canvasWidth = colContainer.getCanvasWidth();
    +              var canvasWidth = colContainer.canvasWidth;
                   var viewportWidth = colContainer.getViewportWidth();
     
                   var canvasHeight = rowContainer.getCanvasHeight();
    diff --git a/src/js/core/directives/ui-pinned-container.js b/src/js/core/directives/ui-pinned-container.js
    index 3ff5fd1b1f..7d3bd5859d 100644
    --- a/src/js/core/directives/ui-pinned-container.js
    +++ b/src/js/core/directives/ui-pinned-container.js
    @@ -32,18 +32,16 @@
                       width += col.drawnWidth || col.width || 0;
                     }
     
    -                myWidth = width;
    +                return width;
                   }              
                 }
                 
                 function updateContainerDimensions() {
    -              // gridUtil.logDebug('update ' + $scope.side + ' dimensions');
    -
                   var ret = '';
                   
                   // Column containers
                   if ($scope.side === 'left' || $scope.side === 'right') {
    -                updateContainerWidth();
    +                myWidth = updateContainerWidth();
     
                     // gridUtil.logDebug('myWidth', myWidth);
     
    @@ -51,7 +49,7 @@
                     $elm.attr('style', null);
     
                     var myHeight = grid.renderContainers.body.getViewportHeight(); // + grid.horizontalScrollbarHeight;
    -
    +                
                     ret += '.grid' + grid.id + ' .ui-grid-pinned-container-' + $scope.side + ', .grid' + grid.id + ' .ui-grid-pinned-container-' + $scope.side + ' .ui-grid-render-container-' + $scope.side + ' .ui-grid-viewport { width: ' + myWidth + 'px; height: ' + myHeight + 'px; } ';
                   }
     
    @@ -59,9 +57,8 @@
                 }
     
                 grid.renderContainers.body.registerViewportAdjuster(function (adjustment) {
    -              if ( myWidth === 0 || !myWidth ){
    -                updateContainerWidth();
    -              }
    +              myWidth = updateContainerWidth();
    +
                   // Subtract our own width
                   adjustment.width -= myWidth;
     
    diff --git a/src/js/core/factories/GridRenderContainer.js b/src/js/core/factories/GridRenderContainer.js
    index 7c39c73545..d186dabfe0 100644
    --- a/src/js/core/factories/GridRenderContainer.js
    +++ b/src/js/core/factories/GridRenderContainer.js
    @@ -68,14 +68,12 @@ angular.module('ui.grid')
         grid.registerStyleComputation({
           priority: 5,
           func: function () {
    +        self.updateColumnWidths();
             return self.columnStyles;
           }
         });
       }
     
    -  // GridRenderContainer.prototype.addRenderable = function addRenderable(renderable) {
    -  //   this.renderables.push(renderable);
    -  // };
     
       GridRenderContainer.prototype.reset = function reset() {
         // this.rowCache.length = 0;
    @@ -268,10 +266,6 @@ angular.module('ui.grid')
     
         var ret = self.canvasWidth;
     
    -    //if (typeof(self.verticalScrollbarWidth) !== 'undefined' && self.verticalScrollbarWidth !== undefined && self.verticalScrollbarWidth > 0) {
    -    //  ret = ret - self.verticalScrollbarWidth;
    -    //}
    -
         return ret;
       };
     
    @@ -481,210 +475,150 @@ angular.module('ui.grid')
         return null;
       };
     
    +    /**
    +     *  @ngdoc boolean
    +     *  @name updateColumnWidths
    +     *  @propertyOf  ui.grid.class:GridRenderContainer
    +     *  @description Determine the appropriate column width of each column across all render containers.
    +     *  
    +     *  Column width is easy when each column has a specified width.  When columns are variable width (i.e. 
    +     *  have an * or % of the viewport) then we try to calculate so that things fit in.  The problem is that
    +     *  we have multiple render containers, and we don't want one render container to just take the whole viewport
    +     *  when it doesn't need to - we want things to balance out across the render containers.
    +     * 
    +     *  To do this, we use this method to calculate all the renderContainers, recognising that in a given render
    +     *  cycle it'll get called once per render container, so it needs to return the same values each time.
    +     * 
    +     *  The constraints on this method are therefore:
    +     *  - must return the same value when called multiple times, to do this it needs to rely on properties of the
    +     *    columns, but not properties that change when this is called (so it shouldn't rely on drawnWidth)
    +     * 
    +     *  The general logic of this method is:
    +     *  - calculate our total available width
    +     *  - look at all the columns across all render containers, and work out which have widths and which have
    +     *    constraints such as % or * or something else
    +     *  - for those with *, count the total number of * we see and add it onto a running total, add this column to an * array
    +     *  - for those with a %, allocate the % as a percentage of the viewport, having consideration of min and max
    +     *  - for those with manual width (in pixels) we set the drawnWidth to the specified width
    +     *  - we end up with an asterisks array still to process
    +     *  - we look at our remaining width.  If it's greater than zero, we divide it up among the asterisk columns, then process
    +     *    them for min and max width constraints
    +     *  - if it's zero or less, we set the asterisk columns to their minimum widths
    +     *  - we use parseInt quite a bit, as we try to make all our column widths integers
    +     */
       GridRenderContainer.prototype.updateColumnWidths = function () {
         var self = this;
     
         var asterisksArray = [],
    -        percentArray = [],
    -        manualArray = [],
    -        asteriskNum = 0,
    -        totalWidth = 0;
    +        asteriskNum = 0, 
    +        usedWidthSum = 0,
    +        ret = '';
     
         // Get the width of the viewport
    -    var availableWidth = self.getViewportWidth() - self.grid.scrollbarWidth;
    -
    -    //if (typeof(self.grid.verticalScrollbarWidth) !== 'undefined' && self.grid.verticalScrollbarWidth !== undefined && self.grid.verticalScrollbarWidth > 0) {
    -    //  availableWidth = availableWidth + self.grid.verticalScrollbarWidth;
    -    //}
    -
    -    // The total number of columns
    -    // var equalWidthColumnCount = columnCount = uiGridCtrl.grid.options.columnDefs.length;
    -    // var equalWidth = availableWidth / equalWidthColumnCount;
    +    var availableWidth = self.grid.getViewportWidth() - self.grid.scrollbarWidth;
     
    -    // The last column we processed
    -    var lastColumn;
    -
    -    var manualWidthSum = 0;
    -
    -    var canvasWidth = 0;
    -
    -    var ret = '';
    -
    -
    -    // uiGridCtrl.grid.columns.forEach(function(column, i) {
    -
    -    var columnCache = self.visibleColumnCache;
    +    // get all the columns across all render containers, we have to calculate them all or one render container
    +    // could consume the whole viewport
    +    var columnCache = [];
    +    angular.forEach(self.grid.renderContainers, function( container, name){
    +      columnCache = columnCache.concat(container.visibleColumnCache);
    +    });
     
    +    // look at each column, process any manual values or %, put the * into an array to look at later
         columnCache.forEach(function(column, i) {
    -      // ret = ret + ' .grid' + uiGridCtrl.grid.id + ' .col' + i + ' { width: ' + equalWidth + 'px; left: ' + left + 'px; }';
    -      //var colWidth = (typeof(c.width) !== 'undefined' && c.width !== undefined) ? c.width : equalWidth;
    -
    +      var width = 0;
           // Skip hidden columns
           if (!column.visible) { return; }
     
    -      var colWidth,
    -          isPercent = false;
    -
    -      if (!angular.isNumber(column.width)) {
    -        isPercent = isNaN(column.width) && gridUtil.endsWith(column.width, "%");
    -      }
    -
    -      if (angular.isString(column.width) && column.width.indexOf('*') !== -1) { //  we need to save it until the end to do the calulations on the remaining width.
    -        asteriskNum = parseInt(asteriskNum + column.width.length, 10);
    +      if (angular.isNumber(column.width)) {
    +        // pixel width, set to this value
    +        width = parseInt(column.width, 10);
    +        usedWidthSum = usedWidthSum + width;
    +        column.drawnWidth = width;
             
    -        asterisksArray.push(column);
    -      }
    -      else if (isPercent) { // If the width is a percentage, save it until the very last.
    -        percentArray.push(column);
    -      }
    -      else if (angular.isNumber(column.width)) {
    -        manualWidthSum = parseInt(manualWidthSum + column.width, 10);
    +      } else if (gridUtil.endsWith(column.width, "%")) {
    +        // percentage width, set to percentage of the viewport
    +        width = parseInt(parseInt(column.width.replace(/%/g, ''), 10) / 100 * availableWidth);
             
    -        canvasWidth = parseInt(canvasWidth, 10) + parseInt(column.width, 10);
    -
    -        column.drawnWidth = column.width;
    -      }
    -    });
    -
    -    // Get the remaining width (available width subtracted by the manual widths sum)
    -    var remainingWidth = availableWidth - manualWidthSum;
    -
    -    var i, column, colWidth;
    -
    -    if (percentArray.length > 0) {
    -      // Pre-process to make sure they're all within any min/max values
    -      for (i = 0; i < percentArray.length; i++) {
    -        column = percentArray[i];
    -
    -        var percent = parseInt(column.width.replace(/%/g, ''), 10) / 100;
    -
    -        colWidth = parseInt(percent * remainingWidth, 10);
    -
    -        if (column.colDef.minWidth && colWidth < column.colDef.minWidth) {
    -          colWidth = column.colDef.minWidth;
    -
    -          remainingWidth = remainingWidth - colWidth;
    -
    -          canvasWidth += colWidth;
    -          column.drawnWidth = colWidth;
    -
    -          // Remove this element from the percent array so it's not processed below
    -          percentArray.splice(i, 1);
    +        if ( width > column.maxWidth ){
    +          width = column.maxWidth;
             }
    -        else if (column.colDef.maxWidth && colWidth > column.colDef.maxWidth) {
    -          colWidth = column.colDef.maxWidth;
    -
    -          remainingWidth = remainingWidth - colWidth;
    -
    -          canvasWidth += colWidth;
    -          column.drawnWidth = colWidth;
    -
    -          // Remove this element from the percent array so it's not processed below
    -          percentArray.splice(i, 1);
    +        
    +        if ( width < column.minWidth ){
    +          width = column.minWidth;
             }
    +        
    +        usedWidthSum = usedWidthSum + width;
    +        column.drawnWidth = width;
    +      } else if (angular.isString(column.width) && column.width.indexOf('*') !== -1) { 
    +        // is an asterisk column, the gridColumn already checked the string consists only of '****'
    +        asteriskNum = asteriskNum + column.width.length;
    +        asterisksArray.push(column);
           }
    +    });
     
    -      percentArray.forEach(function(column) {
    -        var percent = parseInt(column.width.replace(/%/g, ''), 10) / 100;
    -        var colWidth = parseInt(percent * remainingWidth, 10);
    -
    -        canvasWidth += colWidth;
    +    // Get the remaining width (available width subtracted by the used widths sum)
    +    var remainingWidth = availableWidth - usedWidthSum;
     
    -        column.drawnWidth = colWidth;
    -      });
    -    }
    +    var i, column, colWidth;
     
         if (asterisksArray.length > 0) {
    -      var asteriskVal = parseInt(remainingWidth / asteriskNum, 10);
    -
    -       // Pre-process to make sure they're all within any min/max values
    -      for (i = 0; i < asterisksArray.length; i++) {
    -        column = asterisksArray[i];
    -
    -        colWidth = parseInt(asteriskVal * column.width.length, 10);
    -
    -        if (column.colDef.minWidth && colWidth < column.colDef.minWidth) {
    -          colWidth = column.colDef.minWidth;
    +      // the width that each asterisk value would be assigned (this can be negative)
    +      var asteriskVal = remainingWidth / asteriskNum;
     
    -          remainingWidth = remainingWidth - colWidth;
    -          asteriskNum--;
    +      asterisksArray.forEach(function( column ){
    +        var width = parseInt(column.width.length * asteriskVal, 10);
     
    -          canvasWidth += colWidth;
    -          column.drawnWidth = colWidth;
    -
    -          lastColumn = column;
    -
    -          // Remove this element from the percent array so it's not processed below
    -          asterisksArray.splice(i, 1);
    +        if ( width > column.maxWidth ){
    +          width = column.maxWidth;
             }
    -        else if (column.colDef.maxWidth && colWidth > column.colDef.maxWidth) {
    -          colWidth = column.colDef.maxWidth;
    -
    -          remainingWidth = remainingWidth - colWidth;
    -          asteriskNum--;
    -
    -          canvasWidth += colWidth;
    -          column.drawnWidth = colWidth;
    -
    -          // Remove this element from the percent array so it's not processed below
    -          asterisksArray.splice(i, 1);
    +        
    +        if ( width < column.minWidth ){
    +          width = column.minWidth;
             }
    -      }
    -
    -      // Redo the asterisk value, as we may have removed columns due to width constraints
    -      asteriskVal = parseInt(remainingWidth / asteriskNum, 10);
    -
    -      asterisksArray.forEach(function(column) {
    -        var colWidth = parseInt(asteriskVal * column.width.length, 10);
    -
    -        canvasWidth += colWidth;
    -
    -        column.drawnWidth = colWidth;
    +        
    +        usedWidthSum = usedWidthSum + width;
    +        column.drawnWidth = width;
           });
         }
     
    -    // If the grid width didn't divide evenly into the column widths and we have pixels left over, dole them out to the columns one by one to make everything fit
    -    var leftoverWidth = availableWidth - parseInt(canvasWidth, 10);
     
    -    if (leftoverWidth > 0 && canvasWidth > 0 && canvasWidth < availableWidth) {
    -      var variableColumn = false;
    -      // uiGridCtrl.grid.columns.forEach(function(col) {
    -      columnCache.forEach(function(col) {
    -        if (col.width && !angular.isNumber(col.width)) {
    -          variableColumn = true;
    -        }
    -      });
    +    // If the grid width didn't divide evenly into the column widths and we have pixels left over, or our  
    +    // calculated widths would have the grid narrower than the available space, 
    +    // dole the remainder out one by one to make everything fit
    +    var leftoverWidth = availableWidth - usedWidthSum;
     
    -      if (variableColumn) {
    -        var remFn = function (column) {
    -          if (leftoverWidth > 0) {
    -            column.drawnWidth = column.drawnWidth + 1;
    -            canvasWidth = canvasWidth + 1;
    -            leftoverWidth--;
    -          }
    -        };
    -        while (leftoverWidth > 0) {
    -          columnCache.forEach(remFn);
    -        }
    +    var columnsToChange = true; 
    +    
    +    var processColumn = function(column){
    +      if (isNaN(column.width) && column.drawnWidth < column.maxWidth && leftoverWidth > 0) {
    +        column.drawnWidth++;
    +        usedWidthSum++;
    +        leftoverWidth--;
    +        columnsToChange = true;
           }
    +    };
    +    
    +    while (leftoverWidth > 0 && columnsToChange) {
    +      columnsToChange = false;
    +      columnCache.forEach(processColumn);
         }
     
    -    if (canvasWidth < availableWidth) {
    -      canvasWidth = availableWidth;
    -    }
    +    // all that was across all the renderContainers, now we need to work out what that calculation decided for
    +    // our renderContainer
    +    var canvasWidth = 0;
    +    self.visibleColumnCache.forEach(function(column){
    +      if ( column.visible ){
    +        canvasWidth = canvasWidth + column.drawnWidth;
    +      }
    +    });
     
         // Build the CSS
         columnCache.forEach(function (column) {
           ret = ret + column.getColClassDefinition();
         });
     
    -    // Add the vertical scrollbar width back in to the canvas width, it's taken out in getCanvasWidth
    -    //if (self.grid.verticalScrollbarWidth) {
    -    //  canvasWidth = canvasWidth + self.grid.verticalScrollbarWidth;
    -    //}
    -    // canvasWidth = canvasWidth + 1;
    -
    -    self.canvasWidth = parseInt(canvasWidth, 10);
    +    self.canvasWidth = canvasWidth;
     
         // Return the styles back to buildStyles which pops them into the `customStyles` scope variable
         // return ret;
    
    From 6ce1604c33f0542a4b4a2a2432c1ae0fe588e112 Mon Sep 17 00:00:00 2001
    From: Paul Lambert 
    Date: Sun, 29 Mar 2015 09:26:11 +1300
    Subject: [PATCH 069/173] Fix(expandable): fix #2964 expandable breaking grid
     rendering
    
    ---
     src/features/expandable/js/expandable.js      | 6 +++++-
     src/js/core/directives/ui-grid-column-menu.js | 3 ++-
     2 files changed, 7 insertions(+), 2 deletions(-)
    
    diff --git a/src/features/expandable/js/expandable.js b/src/features/expandable/js/expandable.js
    index 32e89d8a92..c0caa9ed9d 100644
    --- a/src/features/expandable/js/expandable.js
    +++ b/src/features/expandable/js/expandable.js
    @@ -392,6 +392,10 @@
                       return ret;
                     };
     
    + /*
    +  * Commented out @PaulL1.  This has no purpose that I can see, and causes #2964.  If this code needs to be reinstated for some
    +  * reason it needs to use drawnWidth, not width, and needs to check column visibility.  It should really use render container
    +  * visible column cache also instead of checking column.renderContainer. 
                       function updateRowContainerWidth() {
                           var grid = $scope.grid;
                           var colWidth = 0;
    @@ -411,7 +415,7 @@
                               priority: 15,
                               func: updateRowContainerWidth
                           });
    -                  }
    +                  }*/
     
                   },
                   post: function ($scope, $elm, $attrs, controllers) {
    diff --git a/src/js/core/directives/ui-grid-column-menu.js b/src/js/core/directives/ui-grid-column-menu.js
    index edd01df6d5..344e345746 100644
    --- a/src/js/core/directives/ui-grid-column-menu.js
    +++ b/src/js/core/directives/ui-grid-column-menu.js
    @@ -437,8 +437,9 @@ function ($timeout, gridUtil, uiGridConstants, uiGridColumnMenuService) {
     
           $scope.hideColumn = function () {
             $scope.col.colDef.visible = false;
    +        $scope.col.visible = false;
     
    -        $scope.grid.refresh();
    +        $scope.grid.queueGridRefresh();
             $scope.hideMenu();
             $scope.grid.api.core.notifyDataChange( uiGridConstants.dataChange.COLUMN );
             $scope.grid.api.core.raise.columnVisibilityChanged( $scope.col );        
    
    From 8a55014d265fe98a3cb582572875a0056eff8884 Mon Sep 17 00:00:00 2001
    From: Paul Lambert 
    Date: Sun, 29 Mar 2015 10:22:01 +1300
    Subject: [PATCH 070/173] Doco(footer): fix #2640 reference to showFooter
    
    ---
     src/features/selection/js/selection.js | 2 +-
     1 file changed, 1 insertion(+), 1 deletion(-)
    
    diff --git a/src/features/selection/js/selection.js b/src/features/selection/js/selection.js
    index e1c8904f69..b4f7585242 100644
    --- a/src/features/selection/js/selection.js
    +++ b/src/features/selection/js/selection.js
    @@ -422,7 +422,7 @@
                *  @propertyOf  ui.grid.selection.api:GridOptions
                *  @description Shows the total number of selected items in footer if true.
                *  
    Defaults to true. - *
    GridOptions.showFooter must also be set to true. + *
    GridOptions.showGridFooter must also be set to true. */ gridOptions.enableFooterTotalSelected = gridOptions.enableFooterTotalSelected !== false; From 227b9000c4a2a7976d514c36cc3eeb36298c524f Mon Sep 17 00:00:00 2001 From: Paul Lambert Date: Mon, 30 Mar 2015 16:32:54 +1300 Subject: [PATCH 071/173] Fix(saveState): if no cell is focused, save scroll position instead --- src/features/saveState/js/saveState.js | 4 +++- src/features/saveState/test/saveState.spec.js | 4 ++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/src/features/saveState/js/saveState.js b/src/features/saveState/js/saveState.js index 0789171762..109c8353b0 100644 --- a/src/features/saveState/js/saveState.js +++ b/src/features/saveState/js/saveState.js @@ -379,7 +379,9 @@ scrollFocus.rowVal = service.getRowVal( grid, rowCol.row ); } } - } else if ( grid.options.saveScroll ) { + } + + if ( grid.options.saveScroll || grid.options.saveFocus && !scrollFocus.colName && !scrollFocus.rowVal ) { scrollFocus.focus = false; if ( grid.renderContainers.body.prevRowScrollIndex ){ scrollFocus.rowVal = service.getRowVal( grid, grid.renderContainers.body.visibleRowCache[ grid.renderContainers.body.prevRowScrollIndex ]); diff --git a/src/features/saveState/test/saveState.spec.js b/src/features/saveState/test/saveState.spec.js index 0513cba311..94e126bf5f 100644 --- a/src/features/saveState/test/saveState.spec.js +++ b/src/features/saveState/test/saveState.spec.js @@ -141,10 +141,10 @@ describe('ui.grid.saveState uiGridSaveStateService', function () { expect( uiGridSaveStateService.saveScrollFocus( grid ) ).toEqual( {} ); }); - it('save focus, no focus present', function() { + it('save focus, no focus present, tries to save scroll instead', function() { uiGridCellNavService.initializeGrid(grid); - expect( uiGridSaveStateService.saveScrollFocus( grid ) ).toEqual( { focus: true } ); + expect( uiGridSaveStateService.saveScrollFocus( grid ) ).toEqual( { focus: false } ); }); it('save focus, focus present, no row identity', function() { From cb25a8319f9b9550d0ee65703308d3db227d8d26 Mon Sep 17 00:00:00 2001 From: swalters Date: Mon, 30 Mar 2015 11:22:52 -0500 Subject: [PATCH 072/173] cleanup code; WIP Allow tab into grid; --- src/features/cellnav/js/cellnav.js | 93 +++++-------------- src/features/edit/js/gridEdit.js | 31 +++---- .../test/infiniteScroll.spec.js | 2 +- src/js/core/directives/ui-grid-header.js | 24 ++++- .../directives/ui-grid-render-container.js | 51 ---------- src/js/core/directives/ui-grid-viewport.js | 20 ++-- 6 files changed, 73 insertions(+), 148 deletions(-) diff --git a/src/features/cellnav/js/cellnav.js b/src/features/cellnav/js/cellnav.js index 931faaa396..082b51bffc 100644 --- a/src/features/cellnav/js/cellnav.js +++ b/src/features/cellnav/js/cellnav.js @@ -94,6 +94,18 @@ }; + UiGridCellNav.prototype.initializeSelection = function () { + var focusableCols = this.getFocusableCols(); + var focusableRows = this.getFocusableRows(); + if (focusableCols.length === 0 || focusableRows.length === 0) { + return null; + } + + var curRowIndex = 0; + var curColIndex = 0; + return new RowCol(focusableRows[0], focusableCols[0]); //return same row + }; + UiGridCellNav.prototype.getRowColLeft = function (curRow, curCol) { var focusableCols = this.getFocusableCols(); var focusableRows = this.getFocusableRows(); @@ -126,6 +138,8 @@ } }; + + UiGridCellNav.prototype.getRowColRight = function (curRow, curCol) { var focusableCols = this.getFocusableCols(); var focusableRows = this.getFocusableRows(); @@ -508,55 +522,6 @@ }, - /** - * @ngdoc method - * @methodOf ui.grid.cellNav.service:uiGridCellNavService - * @name scrollToInternal - * @description Like scrollTo, but takes gridRow and gridCol. - * In calculating the scroll height we have to deal with wanting - * 0% for the first row, and 100% for the last row. Normal maths - * for a 10 row list would return 1/10 = 10% for the first row, so - * we need to tweak the numbers to add an extra 10% somewhere. The - * formula if we're trying to get to row 0 in a 10 row list (assuming our - * index is zero based, so the last row is row 9) is: - *
    -         *   0 + 0 / 10 = 0%
    -         * 
    - * - * To get to row 9 (i.e. the last row) in the same list, we want to - * go to: - *
    -         *  ( 9 + 1 ) / 10 = 100%
    -         * 
    - * So we need to apportion one whole row within the overall grid scroll, - * the formula is: - *
    -         *   ( index + ( index / (total rows - 1) ) / total rows
    -         * 
    - * @param {Grid} grid the grid you'd like to act upon, usually available - * from gridApi.grid - * @param {GridRow} gridRow row to make visible - * @param {GridCol} gridCol column to make visible - */ - scrollToInternal: function (grid, gridRow, gridCol) { - var scrollEvent = new ScrollEvent(grid,null,null,'uiGridCellNavService.scrollToInternal'); - - if (gridRow !== null) { - var seekRowIndex = grid.renderContainers.body.visibleRowCache.indexOf(gridRow); - var totalRows = grid.renderContainers.body.visibleRowCache.length; - var percentage = ( seekRowIndex + ( seekRowIndex / ( totalRows - 1 ) ) ) / totalRows; - scrollEvent.y = { percentage: percentage }; - } - - if (gridCol !== null) { - scrollEvent.x = { percentage: this.getLeftWidth(grid, gridCol) / this.getLeftWidth(grid, grid.renderContainers.body.visibleColumnCache[grid.renderContainers.body.visibleColumnCache.length - 1] ) }; - } - - if (scrollEvent.y || scrollEvent.x) { - grid.scrollContainers('', scrollEvent); - } - }, - /** * @ngdoc method @@ -649,13 +614,6 @@ uiGridCtrl.cellNav = {}; - //uiGridCtrl.cellNav.focusActiveCell = function () { - // var elms = $elm[0].getElementsByClassName('ui-grid-cell-focus'); - // if (elms.length > 0){ - // elms[0].focus(); - // } - //}; - uiGridCtrl.cellNav.getActiveCell = function () { var elms = $elm[0].getElementsByClassName('ui-grid-cell-focus'); if (elms.length > 0){ @@ -746,11 +704,6 @@ return true; } - - // rowCol.eventType = uiGridCellNavConstants.EVENT_TYPE.KEYDOWN; - - - // Scroll to the new cell, if it's not completely visible within the render container's viewport grid.scrollToIfNecessary(rowCol.row, rowCol.col).then(function () { uiGridCtrl.cellNav.broadcastCellNav(rowCol); @@ -794,6 +747,17 @@ // Needs to run last after all renderContainers are built uiGridCellNavService.decorateRenderContainers(grid); + //enable tabbing to renderContainer + $elm.attr("tabindex", 0); + + $elm.on('focus', function (evt) { + var rowCol = uiGridCtrl.grid.api.cellNav.getFocusedCell(); + if (!rowCol) { + rowCol = grid.renderContainers.body.cellNav.initializeSelection(); + uiGridCtrl.cellNav.broadcastCellNav(rowCol); + } + }); + } }; } @@ -834,7 +798,6 @@ } }); - grid.api.core.on.scrollBegin($scope, function (args) { // Skip if there's no currently-focused cell @@ -857,8 +820,6 @@ }); grid.api.core.on.scrollEnd($scope, function (args) { - - // Skip if there's no currently-focused cell var lastRowCol = uiGridCtrl.grid.api.cellNav.getFocusedCell(); if (lastRowCol == null) { @@ -871,9 +832,6 @@ return; } - //focus the viewport - //$elm[0].focus(); - uiGridCtrl.cellNav.broadcastCellNav(lastRowCol); }); @@ -883,7 +841,6 @@ $elm[0].focus(); }); - } }; } diff --git a/src/features/edit/js/gridEdit.js b/src/features/edit/js/gridEdit.js index 0323217259..51009d4ea6 100644 --- a/src/features/edit/js/gridEdit.js +++ b/src/features/edit/js/gridEdit.js @@ -340,8 +340,8 @@ * @description Adds keydown listeners to renderContainer element so we can capture when to begin edits * */ - module.directive('uiGridViewport', ['$timeout', '$injector', 'gridUtil', 'uiGridEditConstants', - function ($timeout, $injector, gridUtil, uiGridEditConstants) { + module.directive('uiGridViewport', [ 'uiGridEditConstants', + function ( uiGridEditConstants) { return { replace: true, priority: -99998, //run before cellNav @@ -350,18 +350,11 @@ compile: function () { return { post: function ($scope, $elm, $attrs, controllers) { - var uiGridCtrl = controllers[0], - renderContainerCtrl = controllers[1]; + var uiGridCtrl = controllers[0]; // Skip attaching if edit and cellNav is not enabled if (!uiGridCtrl.grid.api.edit || !uiGridCtrl.grid.api.cellNav) { return; } - //var uiGridCellNavService = $injector.get('uiGridCellNavService'); - - //var containerId = renderContainerCtrl.containerId; - - // var grid = uiGridCtrl.grid; - $scope.$on(uiGridEditConstants.events.CANCEL_CELL_EDIT, function () { $elm[0].focus(); }); @@ -464,13 +457,12 @@ return; } - //important to do this before scrollToIfNecessary - if (rowCol.row === $scope.row && - rowCol.col === $scope.col) { + if (rowCol.row === $scope.row && rowCol.col === $scope.col && !$scope.col.colDef.enableCellEditOnFocus) { + //important to do this before scrollToIfNecessary beginEditKeyDown(evt); + uiGridCtrl.grid.api.core.scrollToIfNecessary(rowCol.row, rowCol.col); } - uiGridCtrl.grid.api.core.scrollToIfNecessary(rowCol.row, rowCol.col); }); } @@ -483,19 +475,20 @@ // Add touchstart handling. If the users starts a touch and it doesn't end after X milliseconds, then start the edit $elm.on('touchstart', touchStart); - if ($scope.col.colDef.enableCellEditOnFocus) { - if (uiGridCtrl.grid.api.cellNav) { - cellNavNavigateDereg = uiGridCtrl.grid.api.cellNav.on.navigate($scope, function (newRowCol, oldRowCol) { + if (uiGridCtrl.grid.api.cellNav) { + cellNavNavigateDereg = uiGridCtrl.grid.api.cellNav.on.navigate($scope, function (newRowCol, oldRowCol) { + if ($scope.col.colDef.enableCellEditOnFocus) { if (newRowCol.row === $scope.row && newRowCol.col === $scope.col) { $timeout(function () { beginEdit(); }); } - }); - } + } + }); } + } function touchStart(event) { diff --git a/src/features/infinite-scroll/test/infiniteScroll.spec.js b/src/features/infinite-scroll/test/infiniteScroll.spec.js index b978ad5f99..8b980950ef 100644 --- a/src/features/infinite-scroll/test/infiniteScroll.spec.js +++ b/src/features/infinite-scroll/test/infiniteScroll.spec.js @@ -66,7 +66,7 @@ }); }); - describe('loadData', function() { + ddescribe('loadData', function() { it('scroll up and there is data up', function() { grid.scrollDirection = uiGridConstants.scrollDirection.UP; grid.infiniteScroll.scrollUp = true; diff --git a/src/js/core/directives/ui-grid-header.js b/src/js/core/directives/ui-grid-header.js index 149c646f3d..c06bf9bd34 100644 --- a/src/js/core/directives/ui-grid-header.js +++ b/src/js/core/directives/ui-grid-header.js @@ -1,7 +1,8 @@ (function(){ 'use strict'; - angular.module('ui.grid').directive('uiGridHeader', ['$templateCache', '$compile', 'uiGridConstants', 'gridUtil', '$timeout', function($templateCache, $compile, uiGridConstants, gridUtil, $timeout) { + angular.module('ui.grid').directive('uiGridHeader', ['$templateCache', '$compile', 'uiGridConstants', 'gridUtil', '$timeout', 'ScrollEvent', + function($templateCache, $compile, uiGridConstants, gridUtil, $timeout, ScrollEvent) { var defaultTemplate = 'ui-grid/ui-grid-header'; var emptyTemplate = 'ui-grid/ui-grid-no-header'; @@ -47,8 +48,13 @@ // Inject a reference to the header viewport (if it exists) into the grid controller for use in the horizontal scroll handler below var headerViewport = $elm[0].getElementsByClassName('ui-grid-header-viewport')[0]; + if (headerViewport) { containerCtrl.headerViewport = headerViewport; + angular.element(headerViewport).on('scroll', scrollHandler); + $scope.$on('$destroy', function () { + angular.element(headerViewport).off('scroll', scrollHandler); + }); } } @@ -67,6 +73,22 @@ containerCtrl.headerCanvas = null; } } + + function scrollHandler(evt) { + if (uiGridCtrl.grid.isScrollingHorizontally) { + return; + } + var newScrollLeft = gridUtil.normalizeScrollLeft(containerCtrl.headerViewport); + var horizScrollPercentage = containerCtrl.colContainer.scrollHorizontal(newScrollLeft); + + var scrollEvent = new ScrollEvent(uiGridCtrl.grid, null, containerCtrl.colContainer, ScrollEvent.Sources.ViewPortScroll); + scrollEvent.newScrollLeft = newScrollLeft; + if ( horizScrollPercentage > -1 ){ + scrollEvent.x = { percentage: horizScrollPercentage }; + } + + uiGridCtrl.grid.scrollContainers(null, scrollEvent); + } }, post: function ($scope, $elm, $attrs, controllers) { diff --git a/src/js/core/directives/ui-grid-render-container.js b/src/js/core/directives/ui-grid-render-container.js index b8d9527192..8c95a3bcdb 100644 --- a/src/js/core/directives/ui-grid-render-container.js +++ b/src/js/core/directives/ui-grid-render-container.js @@ -23,7 +23,6 @@ compile: function () { return { pre: function prelink($scope, $elm, $attrs, controllers) { - // gridUtil.logDebug('render container ' + $scope.containerId + ' pre-link'); var uiGridCtrl = controllers[0]; var containerCtrl = controllers[1]; @@ -53,7 +52,6 @@ containerCtrl.colContainer = colContainer; }, post: function postlink($scope, $elm, $attrs, controllers) { - // gridUtil.logDebug('render container ' + $scope.containerId + ' post-link'); var uiGridCtrl = controllers[0]; var containerCtrl = controllers[1]; @@ -67,55 +65,6 @@ // Put the container name on this element as a class $elm.addClass('ui-grid-render-container-' + $scope.containerId); - // Bind to left/right-scroll events - //if ($scope.bindScrollHorizontal || $scope.bindScrollVertical) { - // grid.api.core.on.scrollEvent($scope,scrollHandler); - //} - - -// function scrollHandler (args) { -// -// // Vertical scroll -// if (args.y && $scope.bindScrollVertical) { -// containerCtrl.prevScrollArgs = args; -// -// var newScrollTop = args.getNewScrollTop(rowContainer,containerCtrl.viewport); -// -// //only set scrollTop if we coming from something other than viewPort scrollBar or -// //another column container -// if (args.source !== ScrollEvent.Sources.ViewPortScroll || -// args.sourceColContainer !== colContainer) { -// containerCtrl.viewport[0].scrollTop = newScrollTop; -// } -// -// } -// -// // Horizontal scroll -// if (args.x && $scope.bindScrollHorizontal) { -// containerCtrl.prevScrollArgs = args; -// -// var newScrollLeft = args.getNewScrollLeft(colContainer, containerCtrl.viewport); -// -// // Make the current horizontal scroll position available in the $scope -// $scope.newScrollLeft = newScrollLeft; -// -// if (containerCtrl.headerViewport) { -// containerCtrl.headerViewport.scrollLeft = gridUtil.denormalizeScrollLeft(containerCtrl.headerViewport, newScrollLeft); -// } -// -// if (containerCtrl.footerViewport) { -// containerCtrl.footerViewport.scrollLeft = gridUtil.denormalizeScrollLeft(containerCtrl.footerViewport, newScrollLeft); -// } -// -// // Scroll came from somewhere else, so the viewport must be positioned -// if (args.source !== ScrollEvent.Sources.ViewPortScroll) { -// containerCtrl.viewport[0].scrollLeft = gridUtil.denormalizeScrollLeft(containerCtrl.viewport, newScrollLeft); -// } -// -// containerCtrl.prevScrollLeft = newScrollLeft; -// } -// } - // Scroll the render container viewport when the mousewheel is used gridUtil.on.mousewheel($elm, function (event) { var scrollEvent = new ScrollEvent(grid, rowContainer, colContainer, ScrollEvent.Sources.RenderContainerMouseWheel); diff --git a/src/js/core/directives/ui-grid-viewport.js b/src/js/core/directives/ui-grid-viewport.js index 68454b2ebd..c4b3c05d6d 100644 --- a/src/js/core/directives/ui-grid-viewport.js +++ b/src/js/core/directives/ui-grid-viewport.js @@ -31,18 +31,22 @@ // Register this viewport with its container containerCtrl.viewport = $elm; + $elm.on('scroll', scrollHandler); var ignoreScroll = false; - function scrollHandler(evt) { - if (ignoreScroll && (grid.isScrollingHorizontally || grid.isScrollingHorizontally)) { - //don't ask for scrollTop if we just set it - ignoreScroll = false; - return; - } - - ignoreScroll = true; + function scrollHandler(evt) { + //Leaving in this commented code in case it can someday be used + //It does improve performance, but because the horizontal scroll is normalized, + // using this code will lead to the column header getting slightly out of line with columns + // + //if (ignoreScroll && (grid.isScrollingHorizontally || grid.isScrollingHorizontally)) { + // //don't ask for scrollTop if we just set it + // ignoreScroll = false; + // return; + //} + //ignoreScroll = true; var newScrollTop = $elm[0].scrollTop; var newScrollLeft = gridUtil.normalizeScrollLeft($elm); From a2ee126e2d78ff6897ba2425599402b68ea6a53c Mon Sep 17 00:00:00 2001 From: swalters Date: Mon, 30 Mar 2015 12:24:20 -0500 Subject: [PATCH 073/173] grid container is now scrolled if at boundary of grid --- .../directives/ui-grid-render-container.js | 18 +++++++++++------- src/js/core/factories/ScrollEvent.js | 16 ++++++++++++++++ 2 files changed, 27 insertions(+), 7 deletions(-) diff --git a/src/js/core/directives/ui-grid-render-container.js b/src/js/core/directives/ui-grid-render-container.js index 8c95a3bcdb..d1866e4c42 100644 --- a/src/js/core/directives/ui-grid-render-container.js +++ b/src/js/core/directives/ui-grid-render-container.js @@ -93,15 +93,19 @@ scrollEvent.x = { percentage: scrollXPercentage, pixels: scrollXAmount }; } - - - scrollEvent.fireThrottledScrollingEvent('', scrollEvent); - + // Let the parent container scroll if the grid is already at the top/bottom - if ((scrollEvent.y && scrollEvent.y.percentage !== 0 && scrollEvent.y.percentage !== 1 && containerCtrl.viewport[0].scrollTop !== 0 ) || - (scrollEvent.x && scrollEvent.x.percentage !== 0 && scrollEvent.x.percentage !== 1)) { - event.preventDefault(); + if (scrollEvent.atTop(scrollTop) || + scrollEvent.atBottom(scrollTop) || + scrollEvent.atLeft(scrollLeft) || + scrollEvent.atRight(scrollLeft)) { + //parent controller scrolls + } + else { + event.preventDefault(); + scrollEvent.fireThrottledScrollingEvent('', scrollEvent); } + }); $elm.bind('$destroy', function() { diff --git a/src/js/core/factories/ScrollEvent.js b/src/js/core/factories/ScrollEvent.js index ac0e0ad506..68d31e1f16 100644 --- a/src/js/core/factories/ScrollEvent.js +++ b/src/js/core/factories/ScrollEvent.js @@ -131,6 +131,22 @@ return self.newScrollTop; }; + ScrollEvent.prototype.atTop = function(scrollTop) { + return (this.y && this.y.percentage < 1 && scrollTop === 0); + }; + + ScrollEvent.prototype.atBottom = function(scrollTop) { + return (this.y && this.y.percentage === 1 && scrollTop > 0); + }; + + ScrollEvent.prototype.atLeft = function(scrollLeft) { + return (this.x && this.x.percentage < 1 && scrollLeft === 0); + }; + + ScrollEvent.prototype.atRight = function(scrollLeft) { + return (this.x && this.x.percentage === 1 && scrollLeft > 0); + }; + ScrollEvent.Sources = { ViewPortScroll: 'ViewPortScroll', From 1bf35c15d66c674103c20884964f481ac0996b0c Mon Sep 17 00:00:00 2001 From: swalters Date: Mon, 30 Mar 2015 12:28:15 -0500 Subject: [PATCH 074/173] fix last commit --- src/js/core/directives/ui-grid-render-container.js | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/src/js/core/directives/ui-grid-render-container.js b/src/js/core/directives/ui-grid-render-container.js index d1866e4c42..7c9156ff1f 100644 --- a/src/js/core/directives/ui-grid-render-container.js +++ b/src/js/core/directives/ui-grid-render-container.js @@ -26,7 +26,6 @@ var uiGridCtrl = controllers[0]; var containerCtrl = controllers[1]; - var grid = $scope.grid = uiGridCtrl.grid; // Verify that the render container for this element exists @@ -59,6 +58,9 @@ var grid = uiGridCtrl.grid; var rowContainer = containerCtrl.rowContainer; var colContainer = containerCtrl.colContainer; + var scrollTop = null; + var scrollLeft = null; + var renderContainer = grid.renderContainers[$scope.containerId]; @@ -71,8 +73,9 @@ if (event.deltaY !== 0) { var scrollYAmount = event.deltaY * -1 * event.deltaFactor; + scrollTop = containerCtrl.viewport[0].scrollTop; // Get the scroll percentage - var scrollYPercentage = (containerCtrl.viewport[0].scrollTop + scrollYAmount) / rowContainer.getVerticalScrollLength(); + var scrollYPercentage = (scrollTop + scrollYAmount) / rowContainer.getVerticalScrollLength(); // Keep scrollPercentage within the range 0-1. if (scrollYPercentage < 0) { scrollYPercentage = 0; } @@ -84,7 +87,7 @@ var scrollXAmount = event.deltaX * event.deltaFactor; // Get the scroll percentage - var scrollLeft = gridUtil.normalizeScrollLeft(containerCtrl.viewport); + scrollLeft = gridUtil.normalizeScrollLeft(containerCtrl.viewport); var scrollXPercentage = (scrollLeft + scrollXAmount) / (colContainer.getCanvasWidth() - colContainer.getViewportWidth()); // Keep scrollPercentage within the range 0-1. @@ -93,7 +96,7 @@ scrollEvent.x = { percentage: scrollXPercentage, pixels: scrollXAmount }; } - + // Let the parent container scroll if the grid is already at the top/bottom if (scrollEvent.atTop(scrollTop) || scrollEvent.atBottom(scrollTop) || From 7375c4967f175b5242f10961324b9945b64ebbb1 Mon Sep 17 00:00:00 2001 From: Brian Hann Date: Mon, 30 Mar 2015 12:51:32 -0500 Subject: [PATCH 075/173] fix(Column Resizing): Prevent selection in IE9 IE9 requires a `unselectable="on"` property for preventing selection. Adding this to the resizer elements seems to have fixed the issue. Fixes #2594 --- src/features/resize-columns/templates/columnResizer.html | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/features/resize-columns/templates/columnResizer.html b/src/features/resize-columns/templates/columnResizer.html index 78e8e64436..597648d597 100644 --- a/src/features/resize-columns/templates/columnResizer.html +++ b/src/features/resize-columns/templates/columnResizer.html @@ -4,5 +4,6 @@ class="ui-grid-column-resizer" col="col" position="right" - render-index="renderIndex"> + render-index="renderIndex" + unselectable="on">
    \ No newline at end of file From 7c5cdca1f471a0a3c1ef340fe65af268df68cae3 Mon Sep 17 00:00:00 2001 From: Brian Hann Date: Mon, 30 Mar 2015 14:17:34 -0500 Subject: [PATCH 076/173] fix(uiGridHeader): Allow header to shrink in size The combo of ui-grid-selection and filtering was causes the headers to be unable to shrink in size if filtering were toggled off, because the selection grid header had an explicit height and when we saw it we would use that as the height to match up with. This change removes all explicit heights from the "max height" check, so we only rely on rendered dynamic heights to calculate the max height. A unit test has also been added to the selection specs to cover this case. Fixes #3138 --- .../test/uiGridSelectionDirective.spec.js | 72 ++++++++++++++----- src/js/core/factories/Grid.js | 31 ++++++-- 2 files changed, 78 insertions(+), 25 deletions(-) diff --git a/src/features/selection/test/uiGridSelectionDirective.spec.js b/src/features/selection/test/uiGridSelectionDirective.spec.js index 84b87d1b40..6f16a2e134 100644 --- a/src/features/selection/test/uiGridSelectionDirective.spec.js +++ b/src/features/selection/test/uiGridSelectionDirective.spec.js @@ -2,21 +2,19 @@ describe('ui.grid.selection uiGridSelectionDirective', function() { var parentScope, elm, scope, - gridCtrl; + gridCtrl, + $compile, + $rootScope, + uiGridConstants; beforeEach(module('ui.grid.selection')); - beforeEach(function() { - var rootScope; + beforeEach(inject(function(_$rootScope_, _$compile_, _uiGridConstants_) { + $compile = _$compile_; + $rootScope = _$rootScope_; + uiGridConstants = _uiGridConstants_; - inject([ - '$rootScope', - function (rootScopeInj) { - rootScope = rootScopeInj; - } - ]); - - parentScope = rootScope.$new(); + parentScope = $rootScope.$new(); parentScope.options = { columnDefs : [{field: 'id'}] @@ -32,19 +30,14 @@ describe('ui.grid.selection uiGridSelectionDirective', function() { } var tpl = '
    '; - - inject([ - '$compile', - function ($compile) { - elm = $compile(tpl)(parentScope); - }]); + elm = $compile(tpl)(parentScope); parentScope.$digest(); scope = elm.scope(); gridCtrl = elm.controller('uiGrid'); - }); + })); it('should set the "enableSelection" field of the row using the function specified in "isRowSelectable"', function() { for (var i = 0; i < gridCtrl.grid.rows.length; i++) { @@ -61,4 +54,47 @@ describe('ui.grid.selection uiGridSelectionDirective', function() { } } }); + + describe('with filtering turned on', function () { + var elm, $timeout; + + /* + NOTES + - We have to flush $timeout because the header calculations are done post-$timeout, as that's when the header has been fully rendered. + - We have to actually attach the grid element to the document body, otherwise it will not have a rendered height. + */ + + beforeEach(inject(function (_$timeout_) { + $timeout = _$timeout_; + + parentScope.options.enableFiltering = true; + + elm = angular.element('
    '); + document.body.appendChild(elm[0]); + $compile(elm)(parentScope); + $timeout.flush(); + parentScope.$digest(); + })); + + afterEach(function () { + $(elm).remove(); + }); + + it("doesn't prevent headers from shrinking when filtering gets turned off", function () { + // Header height with filtering on + var filteringHeight = $(elm).find('.ui-grid-header').height(); + + dump(elm.controller('uiGrid').grid.api.core.notifyDataChange); + + parentScope.options.enableFiltering = false; + elm.controller('uiGrid').grid.api.core.notifyDataChange( uiGridConstants.dataChange.COLUMN ); + $timeout.flush(); + parentScope.$digest(); + + var noFilteringHeight = $(elm).find('.ui-grid-header').height(); + + expect(noFilteringHeight).not.toEqual(filteringHeight); + expect(noFilteringHeight < filteringHeight).toBe(true); + }); + }); }); diff --git a/src/js/core/factories/Grid.js b/src/js/core/factories/Grid.js index 746a685546..bd79f254b1 100644 --- a/src/js/core/factories/Grid.js +++ b/src/js/core/factories/Grid.js @@ -1877,8 +1877,9 @@ angular.module('ui.grid') container.innerHeaderHeight = innerHeaderHeight; - // Save the largest header height for use later - if (innerHeaderHeight > maxHeaderHeight) { + // If the header doesn't have an explicit height set, save the largest header height for use later + // Explicit header heights are based off of the max we are calculating here. We never want to base the max on something we're setting explicitly + if (!container.explicitHeaderHeight && innerHeaderHeight > maxHeaderHeight) { maxHeaderHeight = innerHeaderHeight; } } @@ -1893,8 +1894,9 @@ angular.module('ui.grid') rebuildStyles = true; } - // Save the largest header canvas height for use later - if (headerCanvasHeight > maxHeaderCanvasHeight) { + // If the header doesn't have an explicit canvas height, save the largest header canvas height for use later + // Explicit header heights are based off of the max we are calculating here. We never want to base the max on something we're setting explicitly + if (!container.explicitHeaderCanvasHeight && headerCanvasHeight > maxHeaderCanvasHeight) { maxHeaderCanvasHeight = headerCanvasHeight; } } @@ -1904,12 +1906,27 @@ angular.module('ui.grid') for (i = 0; i < containerHeadersToRecalc.length; i++) { container = containerHeadersToRecalc[i]; - // If this header's height is less than another header's height, then explicitly set it so they're the same and one isn't all offset and weird looking - if (maxHeaderHeight > 0 && typeof(container.headerHeight) !== 'undefined' && container.headerHeight !== null && container.headerHeight < maxHeaderHeight) { + /* If: + 1. We have a max header height + 2. This container has a header height defined + 3. And either this container has an explicit header height set, OR its header height is less than the max + + then: + + Give this container's header an explicit height so it will line up with the tallest header + */ + if ( + maxHeaderHeight > 0 && typeof(container.headerHeight) !== 'undefined' && container.headerHeight !== null && + (container.explicitHeaderHeight || container.headerHeight < maxHeaderHeight) + ) { container.explicitHeaderHeight = maxHeaderHeight; } - if (typeof(container.headerCanvasHeight) !== 'undefined' && container.headerCanvasHeight !== null && maxHeaderCanvasHeight > 0 && container.headerCanvasHeight < maxHeaderCanvasHeight) { + // Do the same as above except for the header canvas + if ( + maxHeaderCanvasHeight > 0 && typeof(container.headerCanvasHeight) !== 'undefined' && container.headerCanvasHeight !== null && + (container.explicitHeaderCanvasHeight || container.headerCanvasHeight < maxHeaderCanvasHeight) + ) { container.explicitHeaderCanvasHeight = maxHeaderCanvasHeight; } } From 949013c332c5af1b3e37b1d3fa515dfd96c8acb2 Mon Sep 17 00:00:00 2001 From: Brian Hann Date: Mon, 30 Mar 2015 15:12:21 -0500 Subject: [PATCH 077/173] fix(Expandable): Run with lower priority than ngIf ngIf runs at 600 priority and this was running at 1000. Changing it to 599 should allow ngIf to live with ui-grid and expandable. Fixes #2804 --- src/features/expandable/js/expandable.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/features/expandable/js/expandable.js b/src/features/expandable/js/expandable.js index c0caa9ed9d..3ce2cf66d3 100644 --- a/src/features/expandable/js/expandable.js +++ b/src/features/expandable/js/expandable.js @@ -281,7 +281,7 @@ function (uiGridExpandableService, $templateCache) { return { replace: true, - priority: 1000, + priority: 599, require: '^uiGrid', scope: false, compile: function () { From b0d943a82a1d5c64808b759c8b96833e66380b02 Mon Sep 17 00:00:00 2001 From: Jonathan Stevens Date: Mon, 30 Mar 2015 16:30:18 -0400 Subject: [PATCH 078/173] feat(saveState): add pinning to save state The idea was to update saveState to include pinning. This turned out to be more involved than initial anticipated. I ended up having to update the pinning feature to include a public event and method so saveState could include pinning as an option. I updated documentation as I worked through it and even updated the tutorial to include pinning. This is in reference to an feature request I made recently #3123. --- misc/demo/grid-save.html | 83 +++++++++++++++ misc/tutorial/208_save_state.ngdoc | 8 +- src/features/pinning/js/pinning.js | 100 +++++++++++++++--- .../pinning/test/uiGridPinningService.spec.js | 73 ++++++++++++- src/features/saveState/js/saveState.js | 35 ++++-- src/features/saveState/test/saveState.spec.js | 51 +++++++-- 6 files changed, 318 insertions(+), 32 deletions(-) create mode 100644 misc/demo/grid-save.html diff --git a/misc/demo/grid-save.html b/misc/demo/grid-save.html new file mode 100644 index 0000000000..8a19c65df6 --- /dev/null +++ b/misc/demo/grid-save.html @@ -0,0 +1,83 @@ + + + + + + + + + + + + + + + + + + + +

    Grid

    +
    + + + +
    +
    + + + + + \ No newline at end of file diff --git a/misc/tutorial/208_save_state.ngdoc b/misc/tutorial/208_save_state.ngdoc index d9c6809240..e2880435bc 100644 --- a/misc/tutorial/208_save_state.ngdoc +++ b/misc/tutorial/208_save_state.ngdoc @@ -30,6 +30,7 @@ default: - saveVisible - saveSort - saveFilter +- savePinning @example In this example we provide a button to save the grid state. You can then modify the grid layout @@ -38,17 +39,17 @@ to something different, and use the restore button to set the grid back the way - var app = angular.module('app', ['ngAnimate', 'ngTouch', 'ui.grid', 'ui.grid.saveState', 'ui.grid.selection', 'ui.grid.cellNav', 'ui.grid.resizeColumns', 'ui.grid.moveColumns' ]); + var app = angular.module('app', ['ngAnimate', 'ngTouch', 'ui.grid', 'ui.grid.saveState', 'ui.grid.selection', 'ui.grid.cellNav', 'ui.grid.resizeColumns', 'ui.grid.moveColumns', 'ui.grid.pinning' ]); app.controller('MainCtrl', ['$scope', '$http', '$interval', function ($scope, $http, $interval) { $scope.gridOptions = { saveFocus: false, saveScroll: true, - enableFiltering: true, onRegisterApi: function(gridApi){ $scope.gridApi = gridApi; } }; + $scope.gridOptions.enableFiltering = true; $scope.state = {}; $scope.saveState = function() { @@ -68,7 +69,7 @@ to something different, and use the restore button to set the grid back the way
    -
    +
    @@ -80,6 +81,7 @@ to something different, and use the restore button to set the grid back the way height: 400px; }
    + var gridTestUtils = require('../../test/e2e/gridTestUtils.spec.js'); describe( '208 save state', function() { diff --git a/src/features/pinning/js/pinning.js b/src/features/pinning/js/pinning.js index 19c92fac6a..7030f3b1e8 100644 --- a/src/features/pinning/js/pinning.js +++ b/src/features/pinning/js/pinning.js @@ -16,7 +16,15 @@ var module = angular.module('ui.grid.pinning', ['ui.grid']); - module.service('uiGridPinningService', ['gridUtil', 'GridRenderContainer', 'i18nService', function (gridUtil, GridRenderContainer, i18nService) { + module.constant('uiGridPinningConstants', { + container: { + LEFT: 'left', + RIGHT: 'right', + NONE: '' + } + }); + + module.service('uiGridPinningService', ['gridUtil', 'GridRenderContainer', 'i18nService', 'uiGridPinningConstants', function (gridUtil, GridRenderContainer, i18nService, uiGridPinningConstants) { var service = { initializeGrid: function (grid) { @@ -24,6 +32,54 @@ // Register a column builder to add new menu items for pinning left and right grid.registerColumnBuilder(service.pinningColumnBuilder); + + /** + * @ngdoc object + * @name ui.grid.pinning.api:PublicApi + * + * @description Public Api for pinning feature + */ + var publicApi = { + events: { + pinning: { + /** + * @ngdoc event + * @name columnPin + * @eventOf ui.grid.pinning.api:PublicApi + * @description raised when column pin state has changed + *
    +               *   gridApi.pinning.on.columnPinned(scope, function(colDef){})
    +               * 
    + * @param {object} colDef the column that was changed + * @param {string} container the render container the column is in ('left', 'right', '') + */ + columnPinned: function(colDef, container) { + } + } + }, + methods: { + pinning: { + /** + * @ngdoc function + * @name pinColumn + * @methodOf ui.grid.pinning.api:PublicApi + * @description pin column left, right, or none + *
    +               *   gridApi.pinning.pinColumn(col, uiGridPinningConstants.container.LEFT)
    +               * 
    + * @param {gridColumn} col the column being pinned + * @param {string} container one of the recognised types + * from uiGridPinningConstants + */ + pinColumn: function(col, container) { + service.pinColumn(grid, col, container); + } + } + } + }; + + grid.api.registerEventsFromObject(publicApi.events); + grid.api.registerMethodsFromObject(publicApi.methods); }, defaultGridOptions: function (gridOptions) { @@ -124,11 +180,7 @@ return typeof(this.context.col.renderContainer) === 'undefined' || !this.context.col.renderContainer || this.context.col.renderContainer !== 'left'; }, action: function () { - this.context.col.renderContainer = 'left'; - this.context.col.width = this.context.col.drawnWidth; - this.context.col.grid.createLeftContainer(); - - col.grid.refresh(); + service.pinColumn(this.context.col.grid, this.context.col, uiGridPinningConstants.container.LEFT); } }; @@ -140,11 +192,7 @@ return typeof(this.context.col.renderContainer) === 'undefined' || !this.context.col.renderContainer || this.context.col.renderContainer !== 'right'; }, action: function () { - this.context.col.renderContainer = 'right'; - this.context.col.width = this.context.col.drawnWidth; - this.context.col.grid.createRightContainer(); - - col.grid.refresh(); + service.pinColumn(this.context.col.grid, this.context.col, uiGridPinningConstants.container.RIGHT); } }; @@ -156,9 +204,7 @@ return typeof(this.context.col.renderContainer) !== 'undefined' && this.context.col.renderContainer !== null && this.context.col.renderContainer !== 'body'; }, action: function () { - this.context.col.renderContainer = null; - - col.grid.refresh(); + service.pinColumn(this.context.col.grid, this.context.col, uiGridPinningConstants.container.UNPIN); } }; @@ -171,6 +217,32 @@ if (!gridUtil.arrayContainsObjectWithProperty(col.menuItems, 'name', 'ui.grid.pinning.unpin')) { col.menuItems.push(removePinAction); } + }, + + pinColumn: function(grid, col, container) { + if (container === uiGridPinningConstants.container.NONE) { + col.renderContainer = null; + } + else { + col.renderContainer = container; + col.width = col.drawnWidth; + if (container === uiGridPinningConstants.container.LEFT) { + grid.createLeftContainer(); + } + else if (container === uiGridPinningConstants.container.RIGHT) { + grid.createRightContainer(); + } + } + + // Need to call refresh twice; once to move our column over to the new render container and then + // a second time to update the grid viewport dimensions with our adjustments + grid.refresh() + .then(function () { + grid.refresh() + .then(function() { + grid.api.pinning.raise.columnPinned( col.colDef, container ); + }); + }); } }; diff --git a/src/features/pinning/test/uiGridPinningService.spec.js b/src/features/pinning/test/uiGridPinningService.spec.js index ac66b19f94..3c518e20b7 100644 --- a/src/features/pinning/test/uiGridPinningService.spec.js +++ b/src/features/pinning/test/uiGridPinningService.spec.js @@ -1,14 +1,16 @@ /* global _ */ describe('ui.grid.pinning uiGridPinningService', function () { var uiGridPinningService; + var uiGridPinningConstants; var gridClassFactory; var grid; var GridRenderContainer; beforeEach(module('ui.grid.pinning')); - beforeEach(inject(function (_uiGridPinningService_,_gridClassFactory_, $templateCache, _GridRenderContainer_) { + beforeEach(inject(function (_uiGridPinningService_,_gridClassFactory_, $templateCache, _GridRenderContainer_, _uiGridPinningConstants_) { uiGridPinningService = _uiGridPinningService_; + uiGridPinningConstants = _uiGridPinningConstants_; gridClassFactory = _gridClassFactory_; GridRenderContainer = _GridRenderContainer_; @@ -131,4 +133,73 @@ describe('ui.grid.pinning uiGridPinningService', function () { }); + describe('pinColumn', function() { + + var previousWidth; + + beforeEach(function() { + spyOn(grid, 'createLeftContainer').andCallThrough(); + spyOn(grid, 'createRightContainer').andCallThrough(); + previousWidth = grid.columns[0].drawnWidth; + }); + + describe('left', function() { + + beforeEach(function() { + grid.api.pinning.pinColumn(grid.columns[0], uiGridPinningConstants.container.LEFT); + }); + + it('should set renderContainer to be left', function(){ + expect(grid.columns[0].renderContainer).toEqual('left'); + }); + + it('should call createLeftContainer', function() { + expect(grid.createLeftContainer).toHaveBeenCalled(); + }); + + it('should set width based on previous setting', function() { + expect(grid.width).toEqual(previousWidth); + }); + + }); + + describe('right', function() { + + beforeEach(function() { + grid.api.pinning.pinColumn(grid.columns[0], uiGridPinningConstants.container.RIGHT); + }); + + it('should set renderContainer to be right', function(){ + expect(grid.columns[0].renderContainer).toEqual('right'); + }); + + it('should call createLeftContainer', function() { + expect(grid.createRightContainer).toHaveBeenCalled(); + }); + + it('should set width based on previous setting', function() { + expect(grid.width).toEqual(previousWidth); + }); + + }); + + describe('none', function() { + + beforeEach(function() { + grid.api.pinning.pinColumn(grid.columns[0], uiGridPinningConstants.container.NONE); + }); + + it('should set renderContainer to be null', function(){ + expect(grid.columns[0].renderContainer).toBeNull(); + }); + + it('should NOT call either container creation methods', function() { + expect(grid.createLeftContainer).not.toHaveBeenCalled(); + expect(grid.createRightContainer).not.toHaveBeenCalled(); + }); + + }); + + }); + }); \ No newline at end of file diff --git a/src/features/saveState/js/saveState.js b/src/features/saveState/js/saveState.js index 109c8353b0..2b25a19943 100644 --- a/src/features/saveState/js/saveState.js +++ b/src/features/saveState/js/saveState.js @@ -20,7 +20,7 @@ *
    */ - var module = angular.module('ui.grid.saveState', ['ui.grid', 'ui.grid.selection', 'ui.grid.cellNav', 'ui.grid.grouping']); + var module = angular.module('ui.grid.saveState', ['ui.grid', 'ui.grid.selection', 'ui.grid.cellNav', 'ui.grid.grouping', 'ui.grid.pinning']); /** * @ngdoc object @@ -243,7 +243,16 @@ * *
    Defaults to false */ - gridOptions.saveGroupingExpandedStates = gridOptions.saveGroupingExpandedStates === true; + gridOptions.saveGroupingExpandedStates = gridOptions.saveGroupingExpandedStates === true; + /** + * @ngdoc object + * @name savePinning + * @propertyOf ui.grid.saveState.api:GridOptions + * @description Save pinning state for columns. + * + *
    Defaults to true + */ + gridOptions.savePinning = gridOptions.savePinning !== false; }, @@ -295,8 +304,10 @@ if ( state.grouping ){ service.restoreGrouping( grid, state.grouping ); } - - grid.queueGridRefresh(); + + // refresh twice due to rendering issue. + // specific case: save with column pinned left, hide that column, then restore + grid.refresh().then(function(){ grid.refresh(); }); }, @@ -304,8 +315,8 @@ * @ngdoc function * @name saveColumns * @methodOf ui.grid.saveState.service:uiGridSaveStateService - * @description Saves the column setup, including sort, filters, ordering - * and column widths. + * @description Saves the column setup, including sort, filters, ordering, + * pinning and column widths. * * Works through the current columns, storing them in order. Stores the * column name, then the visible flag, width, sort and filters for each column. @@ -335,6 +346,10 @@ if ( grid.options.saveFilter ){ savedColumn.filters = angular.copy ( column.filters ); } + + if ( !!grid.api.pinning && grid.options.savePinning ){ + savedColumn.pinned = column.renderContainer ? column.renderContainer : ''; + } columns.push( savedColumn ); }); @@ -466,8 +481,8 @@ * @ngdoc function * @name restoreColumns * @methodOf ui.grid.saveState.service:uiGridSaveStateService - * @description Restores the columns, including order, visible, width - * sort and filters. + * @description Restores the columns, including order, visible, width, + * pinning, sort and filters. * * @param {Grid} grid the grid whose state we'd like to restore * @param {object} columnsState the list of columns we had before, with their state @@ -503,6 +518,10 @@ grid.columns[currentIndex].filters = angular.copy( columnState.filters ); grid.api.core.raise.filterChanged(); } + + if ( !!grid.api.pinning && grid.options.savePinning && grid.columns[currentIndex].renderContainer !== columnState.pinned ){ + grid.api.pinning.pinColumn(grid.columns[currentIndex], columnState.pinned); + } if ( grid.options.saveOrder && currentIndex !== index ){ var column = grid.columns.splice( currentIndex, 1 )[0]; diff --git a/src/features/saveState/test/saveState.spec.js b/src/features/saveState/test/saveState.spec.js index 94e126bf5f..180b99daf8 100644 --- a/src/features/saveState/test/saveState.spec.js +++ b/src/features/saveState/test/saveState.spec.js @@ -4,6 +4,7 @@ describe('ui.grid.saveState uiGridSaveStateService', function () { var uiGridSelectionService; var uiGridCellNavService; var uiGridGroupingService; + var uiGridPinningService; var gridClassFactory; var grid; var $compile; @@ -15,12 +16,13 @@ describe('ui.grid.saveState uiGridSaveStateService', function () { beforeEach(inject(function (_uiGridSaveStateService_, _gridClassFactory_, _uiGridSaveStateConstants_, _$compile_, _$rootScope_, _$document_, _uiGridSelectionService_, - _uiGridCellNavService_, _uiGridGroupingService_ ) { + _uiGridCellNavService_, _uiGridGroupingService_, _uiGridPinningService_ ) { uiGridSaveStateService = _uiGridSaveStateService_; uiGridSaveStateConstants = _uiGridSaveStateConstants_; uiGridSelectionService = _uiGridSelectionService_; uiGridCellNavService = _uiGridCellNavService_; uiGridGroupingService = _uiGridGroupingService_; + uiGridPinningService = _uiGridPinningService_; gridClassFactory = _gridClassFactory_; $compile = _$compile_; $scope = _$rootScope_.$new(); @@ -28,10 +30,10 @@ describe('ui.grid.saveState uiGridSaveStateService', function () { grid = gridClassFactory.createGrid({}); grid.options.columnDefs = [ - {field: 'col1', name: 'col1', displayName: 'Col1', width: 50}, + {field: 'col1', name: 'col1', displayName: 'Col1', width: 50, pinnedLeft:true }, {field: 'col2', name: 'col2', displayName: 'Col2', width: '*', type: 'number'}, {field: 'col3', name: 'col3', displayName: 'Col3', width: 100}, - {field: 'col4', name: 'col4', displayName: 'Col4', width: 200} + {field: 'col4', name: 'col4', displayName: 'Col4', width: 200, pinnedRight:true } ]; _uiGridSaveStateService_.initializeGrid(grid); @@ -75,7 +77,8 @@ describe('ui.grid.saveState uiGridSaveStateService', function () { saveFilter: true, saveSelection: true, saveGrouping: true, - saveGroupingExpandedStates: false + saveGroupingExpandedStates: false, + savePinning: true }); }); @@ -91,7 +94,8 @@ describe('ui.grid.saveState uiGridSaveStateService', function () { saveFilter: false, saveSelection: false, saveGrouping: false, - saveGroupingExpandedStates: true + saveGroupingExpandedStates: true, + savePinning: false }; uiGridSaveStateService.defaultGridOptions(options); expect( options ).toEqual({ @@ -104,7 +108,8 @@ describe('ui.grid.saveState uiGridSaveStateService', function () { saveFilter: false, saveSelection: false, saveGrouping: false, - saveGroupingExpandedStates: true + saveGroupingExpandedStates: true, + savePinning: false }); }); }); @@ -133,6 +138,40 @@ describe('ui.grid.saveState uiGridSaveStateService', function () { { name: 'col4' } ]); }); + + describe('pinning enabled', function() { + + beforeEach(function(){ + uiGridPinningService.initializeGrid(grid); + grid.buildColumns(); + grid.columns[2].visible = false; + grid.setVisibleColumns(grid.columns); + }); + + it('save columns', function() { + expect( uiGridSaveStateService.saveColumns( grid ) ).toEqual([ + { name: 'col1', visible: true, width: 50, sort: [], filters: [ {} ], pinned: 'left' }, + { name: 'col2', visible: true, width: '*', sort: [], filters: [ {} ], pinned: '' }, + { name: 'col3', visible: false, width: 100, sort: [], filters: [ {} ], pinned: '' }, + { name: 'col4', visible: true, width: 200, sort: [], filters: [ {} ], pinned: 'right' } + ]); + }); + + it('save columns with most options turned off', function() { + grid.options.saveWidths = false; + grid.options.saveVisible = false; + grid.options.saveSort = false; + grid.options.saveFilter = false; + grid.options.savePinning = false; + + expect( uiGridSaveStateService.saveColumns( grid ) ).toEqual([ + { name: 'col1' }, + { name: 'col2' }, + { name: 'col3' }, + { name: 'col4' } + ]); + }); + }); }); From e2a9660bab5fbf72215184b7570074cfa8d53028 Mon Sep 17 00:00:00 2001 From: Brian Hann Date: Tue, 31 Mar 2015 13:15:21 -0500 Subject: [PATCH 079/173] chore(Pagination): Give spec file proper extension It was pagination.spec,js Tests have been disabled as they are failing; the plugin author needs to check them over. --- .../pagination/test/{pagination.spec,js => pagination.spec.js} | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) rename src/features/pagination/test/{pagination.spec,js => pagination.spec.js} (98%) diff --git a/src/features/pagination/test/pagination.spec,js b/src/features/pagination/test/pagination.spec.js similarity index 98% rename from src/features/pagination/test/pagination.spec,js rename to src/features/pagination/test/pagination.spec.js index 5f4cc3f9fe..6fd2ce0b3d 100644 --- a/src/features/pagination/test/pagination.spec,js +++ b/src/features/pagination/test/pagination.spec.js @@ -1,4 +1,4 @@ -describe('ui.grid.pagination uiGridPaginationService', function () { +xdescribe('ui.grid.pagination uiGridPaginationService', function () { 'use strict'; var gridApi; From 98ed01049015b22caddb651b1884f6e383fc58aa Mon Sep 17 00:00:00 2001 From: Brian Hann Date: Tue, 31 Mar 2015 15:55:55 -0500 Subject: [PATCH 080/173] fix(uiGridHeader): Use parseInt on header heights We were using parseInt but not in the right place, so browsers that use fractional pixel values were not allowing the header to shrink appropriately. --- .../selection/test/uiGridSelectionDirective.spec.js | 2 -- src/js/core/factories/Grid.js | 8 ++------ 2 files changed, 2 insertions(+), 8 deletions(-) diff --git a/src/features/selection/test/uiGridSelectionDirective.spec.js b/src/features/selection/test/uiGridSelectionDirective.spec.js index 6f16a2e134..89e3619be1 100644 --- a/src/features/selection/test/uiGridSelectionDirective.spec.js +++ b/src/features/selection/test/uiGridSelectionDirective.spec.js @@ -83,8 +83,6 @@ describe('ui.grid.selection uiGridSelectionDirective', function() { it("doesn't prevent headers from shrinking when filtering gets turned off", function () { // Header height with filtering on var filteringHeight = $(elm).find('.ui-grid-header').height(); - - dump(elm.controller('uiGrid').grid.api.core.notifyDataChange); parentScope.options.enableFiltering = false; elm.controller('uiGrid').grid.api.core.notifyDataChange( uiGridConstants.dataChange.COLUMN ); diff --git a/src/js/core/factories/Grid.js b/src/js/core/factories/Grid.js index bd79f254b1..266af830ed 100644 --- a/src/js/core/factories/Grid.js +++ b/src/js/core/factories/Grid.js @@ -1860,9 +1860,7 @@ angular.module('ui.grid') if (container.header) { var oldHeaderHeight = container.headerHeight; - var headerHeight = gridUtil.outerElementHeight(container.header); - - container.headerHeight = parseInt(headerHeight, 10); + var headerHeight = container.headerHeight = parseInt(gridUtil.outerElementHeight(container.header), 10); if (oldHeaderHeight !== headerHeight) { rebuildStyles = true; @@ -1886,9 +1884,7 @@ angular.module('ui.grid') if (container.headerCanvas) { var oldHeaderCanvasHeight = container.headerCanvasHeight; - var headerCanvasHeight = gridUtil.outerElementHeight(container.headerCanvas); - - container.headerCanvasHeight = parseInt(headerCanvasHeight, 10); + var headerCanvasHeight = container.headerCanvasHeight = parseInt(gridUtil.outerElementHeight(container.headerCanvas), 10); if (oldHeaderCanvasHeight !== headerCanvasHeight) { rebuildStyles = true; From b9d38d1ca2c67cc17e8e713b431ae618c05dfd63 Mon Sep 17 00:00:00 2001 From: Paul Lambert Date: Wed, 1 Apr 2015 16:07:28 +1300 Subject: [PATCH 081/173] Fix (renderContainer): Fix #3111 column widths that add up to < 100% --- src/js/core/factories/GridRenderContainer.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/js/core/factories/GridRenderContainer.js b/src/js/core/factories/GridRenderContainer.js index d186dabfe0..91e12d2a3e 100644 --- a/src/js/core/factories/GridRenderContainer.js +++ b/src/js/core/factories/GridRenderContainer.js @@ -591,7 +591,7 @@ angular.module('ui.grid') var columnsToChange = true; var processColumn = function(column){ - if (isNaN(column.width) && column.drawnWidth < column.maxWidth && leftoverWidth > 0) { + if ( column.drawnWidth < column.maxWidth && leftoverWidth > 0) { column.drawnWidth++; usedWidthSum++; leftoverWidth--; @@ -601,7 +601,7 @@ angular.module('ui.grid') while (leftoverWidth > 0 && columnsToChange) { columnsToChange = false; - columnCache.forEach(processColumn); + asterisksArray.forEach(processColumn); } // all that was across all the renderContainers, now we need to work out what that calculation decided for From f694729e007c650205fa19cfc6ee477c6575e0ee Mon Sep 17 00:00:00 2001 From: Paul Lambert Date: Wed, 1 Apr 2015 16:51:48 +1300 Subject: [PATCH 082/173] Fix(renderContainer): widths can be crept down due to max width --- src/js/core/factories/GridRenderContainer.js | 32 ++++- .../factories/GridRenderContainer.spec.js | 131 +++++++++++++++++- 2 files changed, 153 insertions(+), 10 deletions(-) diff --git a/src/js/core/factories/GridRenderContainer.js b/src/js/core/factories/GridRenderContainer.js index 91e12d2a3e..369b554fe7 100644 --- a/src/js/core/factories/GridRenderContainer.js +++ b/src/js/core/factories/GridRenderContainer.js @@ -582,15 +582,10 @@ angular.module('ui.grid') }); } - // If the grid width didn't divide evenly into the column widths and we have pixels left over, or our // calculated widths would have the grid narrower than the available space, // dole the remainder out one by one to make everything fit - var leftoverWidth = availableWidth - usedWidthSum; - - var columnsToChange = true; - - var processColumn = function(column){ + var processColumnUpwards = function(column){ if ( column.drawnWidth < column.maxWidth && leftoverWidth > 0) { column.drawnWidth++; usedWidthSum++; @@ -599,11 +594,34 @@ angular.module('ui.grid') } }; + var leftoverWidth = availableWidth - usedWidthSum; + var columnsToChange = true; + while (leftoverWidth > 0 && columnsToChange) { columnsToChange = false; - asterisksArray.forEach(processColumn); + asterisksArray.forEach(processColumnUpwards); } + // We can end up with too much width even though some columns aren't at their max width, in this situation + // we can trim the columns a little + var processColumnDownwards = function(column){ + if ( column.drawnWidth > column.minWidth && excessWidth > 0) { + column.drawnWidth--; + usedWidthSum--; + excessWidth--; + columnsToChange = true; + } + }; + + var excessWidth = usedWidthSum - availableWidth; + columnsToChange = true; + + while (excessWidth > 0 && columnsToChange) { + columnsToChange = false; + asterisksArray.forEach(processColumnDownwards); + } + + // all that was across all the renderContainers, now we need to work out what that calculation decided for // our renderContainer var canvasWidth = 0; diff --git a/test/unit/core/factories/GridRenderContainer.spec.js b/test/unit/core/factories/GridRenderContainer.spec.js index 60d506453c..4c909e07dd 100644 --- a/test/unit/core/factories/GridRenderContainer.spec.js +++ b/test/unit/core/factories/GridRenderContainer.spec.js @@ -16,11 +16,15 @@ describe('GridRenderContainer factory', function () { GridRenderContainer = _GridRenderContainer_; uiGridConstants = _uiGridConstants_; - cols = [ - { field: 'firstName' } + grid = new Grid({ id: 1 }); + + grid.options.columnDefs = [ + { field: 'firstName' }, + { field: 'lastName' }, + { field: 'company' }, + { field: 'gender' } ]; - grid = new Grid({ id: 1 }); })); describe('constructor', function () { @@ -94,5 +98,126 @@ describe('GridRenderContainer factory', function () { }); + describe('updateWidths', function() { + beforeEach(function() { + grid.buildColumns(); + grid.setVisibleColumns(grid.columns); + spyOn(grid, 'getViewportWidth').andCallFake(function() { return 415;}); // actual width 400 after scrollbar + }); + + it('all percentages', function() { + grid.columns[0].width = '25%'; + grid.columns[1].width = '25%'; + grid.columns[2].width = '25%'; + grid.columns[3].width = '25%'; + + grid.renderContainers.body.updateColumnWidths(); + + expect( grid.columns[0].drawnWidth ).toEqual(100); + expect( grid.columns[1].drawnWidth ).toEqual(100); + expect( grid.columns[2].drawnWidth ).toEqual(100); + expect( grid.columns[3].drawnWidth ).toEqual(100); + }); + + it('all percentages, less than 100%', function() { + grid.columns[0].width = '20%'; + grid.columns[1].width = '15%'; + grid.columns[2].width = '20%'; + grid.columns[3].width = '15%'; + + grid.renderContainers.body.updateColumnWidths(); + + expect( grid.columns[0].drawnWidth ).toEqual(80); + expect( grid.columns[1].drawnWidth ).toEqual(60); + expect( grid.columns[2].drawnWidth ).toEqual(80); + expect( grid.columns[3].drawnWidth ).toEqual(60); + }); + + it('all percentages, more than 100%', function() { + grid.columns[0].width = '40%'; + grid.columns[1].width = '30%'; + grid.columns[2].width = '40%'; + grid.columns[3].width = '30%'; + + grid.renderContainers.body.updateColumnWidths(); + + expect( grid.columns[0].drawnWidth ).toEqual(160); + expect( grid.columns[1].drawnWidth ).toEqual(120); + expect( grid.columns[2].drawnWidth ).toEqual(160); + expect( grid.columns[3].drawnWidth ).toEqual(120); + }); + + it('fixed widths', function() { + grid.columns[0].width = 50; + grid.columns[1].width = 150; + grid.columns[2].width = 50; + grid.columns[3].width = 150; + + grid.renderContainers.body.updateColumnWidths(); + + expect( grid.columns[0].drawnWidth ).toEqual(50); + expect( grid.columns[1].drawnWidth ).toEqual(150); + expect( grid.columns[2].drawnWidth ).toEqual(50); + expect( grid.columns[3].drawnWidth ).toEqual(150); + }); + + it('asterixes', function() { + grid.columns[0].width = '*'; + grid.columns[1].width = '*'; + grid.columns[2].width = '*'; + grid.columns[3].width = '*'; + + grid.renderContainers.body.updateColumnWidths(); + + expect( grid.columns[0].drawnWidth ).toEqual(100); + expect( grid.columns[1].drawnWidth ).toEqual(100); + expect( grid.columns[2].drawnWidth ).toEqual(100); + expect( grid.columns[3].drawnWidth ).toEqual(100); + }); + + it('double asterixes', function() { + grid.columns[0].width = '***'; + grid.columns[1].width = '*'; + grid.columns[2].width = '***'; + grid.columns[3].width = '*'; + + grid.renderContainers.body.updateColumnWidths(); + + expect( grid.columns[0].drawnWidth ).toEqual(150); + expect( grid.columns[1].drawnWidth ).toEqual(50); + expect( grid.columns[2].drawnWidth ).toEqual(150); + expect( grid.columns[3].drawnWidth ).toEqual(50); + }); + + it('asterixes, min width', function() { + grid.columns[0].width = '*'; + grid.columns[1].width = '*'; + grid.columns[2].width = '*'; + grid.columns[3].width = '*'; + grid.columns[0].minWidth = 130; + + grid.renderContainers.body.updateColumnWidths(); + + expect( grid.columns[0].drawnWidth ).toEqual(130); + expect( grid.columns[1].drawnWidth ).toEqual(90); + expect( grid.columns[2].drawnWidth ).toEqual(90); + expect( grid.columns[3].drawnWidth ).toEqual(90); + }); + + it('asterixes, max widths', function() { + grid.columns[0].width = '*'; + grid.columns[1].width = '*'; + grid.columns[2].width = '*'; + grid.columns[3].width = '*'; + grid.columns[0].maxWidth = 70; + + grid.renderContainers.body.updateColumnWidths(); + + expect( grid.columns[0].drawnWidth ).toEqual(70); + expect( grid.columns[1].drawnWidth ).toEqual(110); + expect( grid.columns[2].drawnWidth ).toEqual(110); + expect( grid.columns[3].drawnWidth ).toEqual(110); + }); + }); }); \ No newline at end of file From cccc32d559dd2ad328e5ea47a7eebcac11b2d0d0 Mon Sep 17 00:00:00 2001 From: Paul Lambert Date: Wed, 1 Apr 2015 16:57:16 +1300 Subject: [PATCH 083/173] Fix linux unit tests - scrollbar width different on linuxgit add . --- test/unit/core/factories/GridRenderContainer.spec.js | 1 + 1 file changed, 1 insertion(+) diff --git a/test/unit/core/factories/GridRenderContainer.spec.js b/test/unit/core/factories/GridRenderContainer.spec.js index 4c909e07dd..881b82e44a 100644 --- a/test/unit/core/factories/GridRenderContainer.spec.js +++ b/test/unit/core/factories/GridRenderContainer.spec.js @@ -103,6 +103,7 @@ describe('GridRenderContainer factory', function () { grid.buildColumns(); grid.setVisibleColumns(grid.columns); spyOn(grid, 'getViewportWidth').andCallFake(function() { return 415;}); // actual width 400 after scrollbar + grid.scrollbarWidth = 15; }); it('all percentages', function() { From d7a6b8aead71898c2d48c7df81669e3f2581673c Mon Sep 17 00:00:00 2001 From: Gareth Oakley Date: Wed, 1 Apr 2015 10:14:44 +0100 Subject: [PATCH 084/173] Fix ngdoc typo for setSelected --- src/features/selection/js/selection.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/features/selection/js/selection.js b/src/features/selection/js/selection.js index 3f743eec88..694edc4cdf 100644 --- a/src/features/selection/js/selection.js +++ b/src/features/selection/js/selection.js @@ -63,7 +63,7 @@ * @methodOf ui.grid.selection.api:GridRow * @description Sets the isSelected property and updates the selectedCount * Changes to isSelected state should only be made via this function - * @param {bool} selelected value to set + * @param {bool} selected value to set */ $delegate.prototype.setSelected = function(selected) { this.isSelected = selected; From dd2b29d3906e29ae4f1dfd70b06cd9065f53cdd1 Mon Sep 17 00:00:00 2001 From: swalters Date: Wed, 1 Apr 2015 10:57:36 -0500 Subject: [PATCH 085/173] enable focus back into render container to try and fix tab in/out --- src/features/cellnav/js/cellnav.js | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/src/features/cellnav/js/cellnav.js b/src/features/cellnav/js/cellnav.js index 082b51bffc..3a6b6fe186 100644 --- a/src/features/cellnav/js/cellnav.js +++ b/src/features/cellnav/js/cellnav.js @@ -785,8 +785,8 @@ var grid = uiGridCtrl.grid; - // Let the render container be focus-able - $elm.attr("tabindex", -1); + //// Let the render container be focus-able + //$elm.attr("tabindex", -1); // Bind to keydown events in the render container $elm.on('keydown', function (evt) { @@ -869,8 +869,6 @@ return; } - //setTabEnabled(); - // When a cell is clicked, broadcast a cellNav event saying that this row+col combo is now focused $elm.find('div').on('click', function (evt) { uiGridCtrl.cellNav.broadcastCellNav(new RowCol($scope.row, $scope.col), evt.ctrlKey || evt.metaKey); @@ -908,10 +906,6 @@ } }); - function setTabEnabled() { - $elm.find('div').attr("tabindex", 0); - } - function setFocused() { var div = $elm.find('div'); div.addClass('ui-grid-cell-focus'); From 04b76791cb59ee1aa86c733a6502ae76bea2d533 Mon Sep 17 00:00:00 2001 From: swalters Date: Wed, 1 Apr 2015 11:03:51 -0500 Subject: [PATCH 086/173] remove ddescribe and fix tests --- src/features/edit/js/gridEdit.js | 2 +- src/features/infinite-scroll/test/infiniteScroll.spec.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/features/edit/js/gridEdit.js b/src/features/edit/js/gridEdit.js index 51009d4ea6..722cab1d65 100644 --- a/src/features/edit/js/gridEdit.js +++ b/src/features/edit/js/gridEdit.js @@ -475,7 +475,7 @@ // Add touchstart handling. If the users starts a touch and it doesn't end after X milliseconds, then start the edit $elm.on('touchstart', touchStart); - if (uiGridCtrl.grid.api.cellNav) { + if (uiGridCtrl && uiGridCtrl.grid.api.cellNav) { cellNavNavigateDereg = uiGridCtrl.grid.api.cellNav.on.navigate($scope, function (newRowCol, oldRowCol) { if ($scope.col.colDef.enableCellEditOnFocus) { if (newRowCol.row === $scope.row && newRowCol.col === $scope.col) { diff --git a/src/features/infinite-scroll/test/infiniteScroll.spec.js b/src/features/infinite-scroll/test/infiniteScroll.spec.js index 96619d7900..d996818eb8 100644 --- a/src/features/infinite-scroll/test/infiniteScroll.spec.js +++ b/src/features/infinite-scroll/test/infiniteScroll.spec.js @@ -71,7 +71,7 @@ }); }); - ddescribe('loadData', function() { + describe('loadData', function() { it('scroll up and there is data up', function() { grid.scrollDirection = uiGridConstants.scrollDirection.UP; grid.infiniteScroll.scrollUp = true; From 7818bbcb9b5ffd4cac7f06698298a17744f6045f Mon Sep 17 00:00:00 2001 From: swalters Date: Wed, 1 Apr 2015 11:19:38 -0500 Subject: [PATCH 087/173] tabbing into container code disabled cellNav --- src/features/cellnav/js/cellnav.js | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/src/features/cellnav/js/cellnav.js b/src/features/cellnav/js/cellnav.js index 3a6b6fe186..196f73608c 100644 --- a/src/features/cellnav/js/cellnav.js +++ b/src/features/cellnav/js/cellnav.js @@ -747,16 +747,16 @@ // Needs to run last after all renderContainers are built uiGridCellNavService.decorateRenderContainers(grid); - //enable tabbing to renderContainer - $elm.attr("tabindex", 0); - - $elm.on('focus', function (evt) { - var rowCol = uiGridCtrl.grid.api.cellNav.getFocusedCell(); - if (!rowCol) { - rowCol = grid.renderContainers.body.cellNav.initializeSelection(); - uiGridCtrl.cellNav.broadcastCellNav(rowCol); - } - }); + ////enable tabbing to renderContainer + //$elm.attr("tabindex", -1); + // + //$elm.on('focus', function (evt) { + // var rowCol = uiGridCtrl.grid.api.cellNav.getFocusedCell(); + // if (!rowCol) { + // rowCol = grid.renderContainers.body.cellNav.initializeSelection(); + // uiGridCtrl.cellNav.broadcastCellNav(rowCol); + // } + //}); } }; @@ -785,8 +785,8 @@ var grid = uiGridCtrl.grid; - //// Let the render container be focus-able - //$elm.attr("tabindex", -1); + // Let the render container be focus-able + $elm.attr("tabindex", -1); // Bind to keydown events in the render container $elm.on('keydown', function (evt) { From deaa690dfd9209958d28854558d462c922c041df Mon Sep 17 00:00:00 2001 From: Abhishek Date: Wed, 1 Apr 2015 11:08:38 -0700 Subject: [PATCH 088/173] remove unnecessary service injection from 'edit' feature --- src/features/edit/js/gridEdit.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/features/edit/js/gridEdit.js b/src/features/edit/js/gridEdit.js index 722cab1d65..e71b3a7925 100644 --- a/src/features/edit/js/gridEdit.js +++ b/src/features/edit/js/gridEdit.js @@ -42,8 +42,8 @@ * * @description Services for editing features */ - module.service('uiGridEditService', ['$q', '$templateCache', 'uiGridConstants', 'gridUtil', - function ($q, $templateCache, uiGridConstants, gridUtil) { + module.service('uiGridEditService', ['$q', 'uiGridConstants', 'gridUtil', + function ($q, uiGridConstants, gridUtil) { var service = { From 2bc8899d6ee2555b3fc59231c6ce90a18a0025d8 Mon Sep 17 00:00:00 2001 From: Paul Lambert Date: Thu, 2 Apr 2015 09:24:14 +1300 Subject: [PATCH 089/173] Fix(infiniteScroll): update to align to new scrolling --- src/features/infinite-scroll/js/infinite-scroll.js | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/src/features/infinite-scroll/js/infinite-scroll.js b/src/features/infinite-scroll/js/infinite-scroll.js index 115c64d43f..258da91b63 100644 --- a/src/features/infinite-scroll/js/infinite-scroll.js +++ b/src/features/infinite-scroll/js/infinite-scroll.js @@ -407,13 +407,7 @@ else { scrollEvent.y = {percentage: percentage}; } - scrollEvent.fireScrollingEvent(); - // change this once @swalters has merged his scrolling changes, which will return a promise from the fireScrollingEvent - var promise = $q.defer(); - $timeout(function(){ - promise.resolve(); - }); - return promise.promise; + return grid.scrollContainers('body', scrollEvent); }, From 7f20068e9df04113456d62a02649c7ca2a53dbba Mon Sep 17 00:00:00 2001 From: Paul Lambert Date: Thu, 2 Apr 2015 11:28:10 +1300 Subject: [PATCH 090/173] Enh(grid): add priority to rows processors --- src/features/grouping/js/grouping.js | 2 +- .../infinite-scroll/js/infinite-scroll.js | 12 ++++----- src/features/pagination/js/pagination.js | 7 ++++-- src/features/selection/js/selection.js | 15 ++++++----- src/js/core/factories/Grid.js | 25 +++++++++++++++---- test/unit/core/directives/ui-grid-row.spec.js | 2 +- test/unit/core/factories/Grid.spec.js | 8 +++--- 7 files changed, 45 insertions(+), 26 deletions(-) diff --git a/src/features/grouping/js/grouping.js b/src/features/grouping/js/grouping.js index aa0cccce9b..f21f221d90 100644 --- a/src/features/grouping/js/grouping.js +++ b/src/features/grouping/js/grouping.js @@ -173,7 +173,7 @@ service.defaultGridOptions(grid.options); - grid.registerRowsProcessor(service.groupRows); + grid.registerRowsProcessor(service.groupRows, 400); grid.registerColumnsProcessor(service.groupingColumnProcessor); diff --git a/src/features/infinite-scroll/js/infinite-scroll.js b/src/features/infinite-scroll/js/infinite-scroll.js index 258da91b63..05624cd8d4 100644 --- a/src/features/infinite-scroll/js/infinite-scroll.js +++ b/src/features/infinite-scroll/js/infinite-scroll.js @@ -369,18 +369,16 @@ oldPercentage = grid.infiniteScroll.prevScrolltopPercentage || 0; oldTopRow = oldPercentage * grid.infiniteScroll.previousVisibleRows; newPercentage = ( newVisibleRows - grid.infiniteScroll.previousVisibleRows + oldTopRow ) / newVisibleRows; - service.adjustInfiniteScrollPosition(grid, newPercentage).then(function() { - promise.resolve(); - }); + service.adjustInfiniteScrollPosition(grid, newPercentage); + promise.resolve(); } if ( grid.infiniteScroll.direction === uiGridConstants.scrollDirection.DOWN ){ oldPercentage = grid.infiniteScroll.prevScrolltopPercentage || 1; oldTopRow = oldPercentage * grid.infiniteScroll.previousVisibleRows; newPercentage = oldTopRow / newVisibleRows; - service.adjustInfiniteScrollPosition(grid, newPercentage).then(function() { - promise.resolve(); - }); + service.adjustInfiniteScrollPosition(grid, newPercentage); + promise.resolve(); } }, 0); @@ -407,7 +405,7 @@ else { scrollEvent.y = {percentage: percentage}; } - return grid.scrollContainers('body', scrollEvent); + grid.scrollContainers('body', scrollEvent); }, diff --git a/src/features/pagination/js/pagination.js b/src/features/pagination/js/pagination.js index 8b2032a297..fda52b8926 100644 --- a/src/features/pagination/js/pagination.js +++ b/src/features/pagination/js/pagination.js @@ -131,7 +131,8 @@ grid.api.registerEventsFromObject(publicApi.events); grid.api.registerMethodsFromObject(publicApi.methods); - grid.registerRowsProcessor(function (renderableRows) { + + var processPagination = function( renderableRows ){ if (grid.options.useExternalPagination || !grid.options.enablePagination) { return renderableRows; } @@ -148,7 +149,9 @@ firstRow = (currentPage - 1) * pageSize; } return visibleRows.slice(firstRow, firstRow + pageSize); - }); + }; + + grid.registerRowsProcessor(processPagination, 900 ); }, defaultGridOptions: function (gridOptions) { diff --git a/src/features/selection/js/selection.js b/src/features/selection/js/selection.js index e94e37f272..620b53f4d1 100644 --- a/src/features/selection/js/selection.js +++ b/src/features/selection/js/selection.js @@ -644,14 +644,17 @@ } var processorSet = false; + + var processSelectableRows = function( rows ){ + rows.forEach(function(row){ + row.enableSelection = uiGridCtrl.grid.options.isRowSelectable(row); + }); + return rows; + }; + var updateOptions = function(){ if (uiGridCtrl.grid.options.isRowSelectable !== angular.noop && processorSet !== true) { - uiGridCtrl.grid.registerRowsProcessor(function(rows) { - rows.forEach(function(row){ - row.enableSelection = uiGridCtrl.grid.options.isRowSelectable(row); - }); - return rows; - }); + uiGridCtrl.grid.registerRowsProcessor(processSelectableRows, 500); processorSet = true; } }; diff --git a/src/js/core/factories/Grid.js b/src/js/core/factories/Grid.js index e353a50a1a..f030f3957a 100644 --- a/src/js/core/factories/Grid.js +++ b/src/js/core/factories/Grid.js @@ -1149,13 +1149,23 @@ angular.module('ui.grid') the grid calls each registered "rows processor", which has a chance to alter the set of rows (sorting, etc) as long as the count is not modified. + + @param {function} processor a function that takes in rows, and returns updated rows + @param {number} priority the priority of this processor. In general we try to do them in 100s to leave room + for other people to inject rows processors at intermediate priorities. Lower priority rowsProcessors run earlier. + + At present all rows visible is running at 50, filter is running at 100, sort is at 200, grouping at 400, selectable rows at 500, pagination at 900 (pagination will generally want to be last) + */ - Grid.prototype.registerRowsProcessor = function registerRowsProcessor(processor) { + Grid.prototype.registerRowsProcessor = function registerRowsProcessor(processor, priority) { if (!angular.isFunction(processor)) { throw 'Attempt to register non-function rows processor: ' + processor; } - this.rowsProcessors.push(processor); + this.rowsProcessors.push({processor: processor, priority: priority}); + this.rowsProcessors.sort(function sortByPriority( a, b ){ + return a.priority - b.priority; + }); }; /** @@ -1166,9 +1176,14 @@ angular.module('ui.grid') * @description Remove a registered rows processor */ Grid.prototype.removeRowsProcessor = function removeRowsProcessor(processor) { - var idx = this.rowsProcessors.indexOf(processor); + var idx = -1; + this.rowsProcessors.forEach(function(rowsProcessor, index){ + if ( rowsProcessor.processor === processor ){ + idx = index; + } + }); - if (typeof(idx) !== 'undefined' && idx !== undefined) { + if ( idx !== -1 ) { this.rowsProcessors.splice(idx, 1); } }; @@ -1206,7 +1221,7 @@ angular.module('ui.grid') // the result. function startProcessor(i, renderedRowsToProcess) { // Get the processor at 'i' - var processor = self.rowsProcessors[i]; + var processor = self.rowsProcessors[i].processor; // Call the processor, passing in the rows to process and the current columns // (note: it's wrapped in $q.when() in case the processor does not return a promise) diff --git a/test/unit/core/directives/ui-grid-row.spec.js b/test/unit/core/directives/ui-grid-row.spec.js index 80c9b03946..b92511b58a 100644 --- a/test/unit/core/directives/ui-grid-row.spec.js +++ b/test/unit/core/directives/ui-grid-row.spec.js @@ -57,7 +57,7 @@ describe('uiGridRow', function () { row.getRowTemplateFn = $q.when($compile(template)); }); } - }); + }, 10); return rows; }); diff --git a/test/unit/core/factories/Grid.spec.js b/test/unit/core/factories/Grid.spec.js index 75f47bb663..3fe02f3665 100644 --- a/test/unit/core/factories/Grid.spec.js +++ b/test/unit/core/factories/Grid.spec.js @@ -97,8 +97,8 @@ describe('Grid factory', function () { testObj.proc2 = jasmine.createSpy('proc2').andCallFake(proc2); // Register the two spies as rows processors - grid.registerRowsProcessor(testObj.proc1); - grid.registerRowsProcessor(testObj.proc2); + grid.registerRowsProcessor(testObj.proc1, 70); + grid.registerRowsProcessor(testObj.proc2, 80); }); it('should call both processors', function() { @@ -142,7 +142,7 @@ describe('Grid factory', function () { grid.registerRowsProcessor(function (blargh) { return "goobers!"; - }); + }, 70); }); it('should throw an exception', function () { @@ -170,7 +170,7 @@ describe('Grid factory', function () { describe('registering a non-function as a rows processor', function () { it('should error', function () { expect(function () { - grid.registerRowsProcessor('blah'); + grid.registerRowsProcessor('blah', 70); }).toThrow(); }); }); From 0a53b88cfd76e10a333267eec9c9227a52397b6d Mon Sep 17 00:00:00 2001 From: Paul Lambert Date: Thu, 2 Apr 2015 11:50:52 +1300 Subject: [PATCH 091/173] Enh(grid): add registerRowsProcessor to API --- src/js/core/factories/Grid.js | 45 +++++++++++++++++------- src/js/core/services/gridClassFactory.js | 8 ++--- 2 files changed, 36 insertions(+), 17 deletions(-) diff --git a/src/js/core/factories/Grid.js b/src/js/core/factories/Grid.js index f030f3957a..2ef431cd2d 100644 --- a/src/js/core/factories/Grid.js +++ b/src/js/core/factories/Grid.js @@ -297,6 +297,26 @@ angular.module('ui.grid') */ self.api.registerMethod( 'core', 'scrollTo', function (rowEntity, colDef) { return self.scrollTo(rowEntity, colDef);} ); + /** + * @ngdoc function + * @name registerRowsProcessor + * @methodOf ui.grid.core.api:PublicApi + * @description + * Register a "rows processor" function. When the rows are updated, + * the grid calls each registered "rows processor", which has a chance + * to alter the set of rows (sorting, etc) as long as the count is not + * modified. + * + * @param {function(renderedRowsToProcess, columns )} processorFunction rows processor function, which + * is run in the context of the grid (i.e. this for the function will be the grid), and must + * return the updated rows list, which is passed to the next processor in the chain + * @param {number} priority the priority of this processor. In general we try to do them in 100s to leave room + * for other people to inject rows processors at intermediate priorities. Lower priority rowsProcessors run earlier. + * + * At present allRowsVisible is running at 50, filter is running at 100, sort is at 200, grouping at 400, selectable rows at 500, pagination at 900 (pagination will generally want to be last) + */ + self.api.registerMethod( 'core', 'registerRowsProcessor', this.registerRowsProcessor ); + /** @@ -1140,22 +1160,21 @@ angular.module('ui.grid') * @ngdoc function * @name registerRowsProcessor * @methodOf ui.grid.class:Grid + * @description + * + * Register a "rows processor" function. When the rows are updated, + * the grid calls each registered "rows processor", which has a chance + * to alter the set of rows (sorting, etc) as long as the count is not + * modified. + * * @param {function(renderedRowsToProcess, columns )} processorFunction rows processor function, which * is run in the context of the grid (i.e. this for the function will be the grid), and must * return the updated rows list, which is passed to the next processor in the chain - * @description - - Register a "rows processor" function. When the rows are updated, - the grid calls each registered "rows processor", which has a chance - to alter the set of rows (sorting, etc) as long as the count is not - modified. - - @param {function} processor a function that takes in rows, and returns updated rows - @param {number} priority the priority of this processor. In general we try to do them in 100s to leave room - for other people to inject rows processors at intermediate priorities. Lower priority rowsProcessors run earlier. - - At present all rows visible is running at 50, filter is running at 100, sort is at 200, grouping at 400, selectable rows at 500, pagination at 900 (pagination will generally want to be last) - + * @param {number} priority the priority of this processor. In general we try to do them in 100s to leave room + * for other people to inject rows processors at intermediate priorities. Lower priority rowsProcessors run earlier. + * + * At present all rows visible is running at 50, filter is running at 100, sort is at 200, grouping at 400, selectable rows at 500, pagination at 900 (pagination will generally want to be last) + * */ Grid.prototype.registerRowsProcessor = function registerRowsProcessor(processor, priority) { if (!angular.isFunction(processor)) { diff --git a/src/js/core/services/gridClassFactory.js b/src/js/core/services/gridClassFactory.js index 86f090a2ff..61e7b6f11f 100644 --- a/src/js/core/services/gridClassFactory.js +++ b/src/js/core/services/gridClassFactory.js @@ -50,7 +50,7 @@ grid.registerRowsProcessor(function allRowsVisible(rows) { rows.forEach(function (row) { row.visible = true; - }); + }, 50); return rows; }); @@ -74,14 +74,14 @@ }); - grid.registerRowsProcessor(grid.searchRows); + grid.registerRowsProcessor(grid.searchRows, 100); // Register the default row processor, it sorts rows by selected columns if (grid.options.externalSort && angular.isFunction(grid.options.externalSort)) { - grid.registerRowsProcessor(grid.options.externalSort); + grid.registerRowsProcessor(grid.options.externalSort, 200); } else { - grid.registerRowsProcessor(grid.sortByColumn); + grid.registerRowsProcessor(grid.sortByColumn, 200); } return grid; From 4ab61871eb50ea6abd6ff746f7c6dbb40c1c112c Mon Sep 17 00:00:00 2001 From: Paul Lambert Date: Fri, 3 Apr 2015 08:08:57 +1300 Subject: [PATCH 092/173] Fix(infiniteScroll): migrate to using new scroll architecture --- .../infinite-scroll/js/infinite-scroll.js | 23 +++++++++++-------- 1 file changed, 13 insertions(+), 10 deletions(-) diff --git a/src/features/infinite-scroll/js/infinite-scroll.js b/src/features/infinite-scroll/js/infinite-scroll.js index 05624cd8d4..d421025078 100644 --- a/src/features/infinite-scroll/js/infinite-scroll.js +++ b/src/features/infinite-scroll/js/infinite-scroll.js @@ -362,23 +362,26 @@ var newVisibleRows = grid.renderContainers.body.visibleRowCache.length; var oldPercentage, oldTopRow; + var halfViewport = grid.getViewportHeight() / grid.options.rowHeight / 2; if ( grid.infiniteScroll.direction === uiGridConstants.scrollDirection.UP ){ - console.log( 'prevTop was: ' + grid.infiniteScroll.prevScrolltopPercentage); - console.log( 'grid was: ' + grid); oldPercentage = grid.infiniteScroll.prevScrolltopPercentage || 0; oldTopRow = oldPercentage * grid.infiniteScroll.previousVisibleRows; - newPercentage = ( newVisibleRows - grid.infiniteScroll.previousVisibleRows + oldTopRow ) / newVisibleRows; + newPercentage = ( newVisibleRows - grid.infiniteScroll.previousVisibleRows + oldTopRow + halfViewport ) / newVisibleRows; service.adjustInfiniteScrollPosition(grid, newPercentage); - promise.resolve(); + $timeout( function() { + promise.resolve(); + }); } if ( grid.infiniteScroll.direction === uiGridConstants.scrollDirection.DOWN ){ oldPercentage = grid.infiniteScroll.prevScrolltopPercentage || 1; oldTopRow = oldPercentage * grid.infiniteScroll.previousVisibleRows; - newPercentage = oldTopRow / newVisibleRows; + newPercentage = ( oldTopRow - halfViewport ) / newVisibleRows; service.adjustInfiniteScrollPosition(grid, newPercentage); - promise.resolve(); + $timeout( function() { + promise.resolve(); + }); } }, 0); @@ -405,7 +408,7 @@ else { scrollEvent.y = {percentage: percentage}; } - grid.scrollContainers('body', scrollEvent); + grid.scrollContainers('', scrollEvent); }, @@ -428,11 +431,11 @@ service.setScrollDirections( grid, scrollUp, scrollDown ); var newVisibleRows = grid.renderContainers.body.visibleRowCache.length; - var oldScrollRow = grid.infiniteScroll.prevScrolltopPercentage * grid.infiniteScroll.prevVisibleRows; + var oldScrollRow = grid.infiniteScroll.prevScrolltopPercentage * grid.infiniteScroll.previousVisibleRows; // since we removed from the top, our new scroll row will be the old scroll row less the number // of rows removed - var newScrollRow = oldScrollRow - ( grid.infiniteScroll.prevVisibleRows - newVisibleRows ); + var newScrollRow = oldScrollRow - ( grid.infiniteScroll.previousVisibleRows - newVisibleRows ); var newScrollPercent = newScrollRow / newVisibleRows; return service.adjustInfiniteScrollPosition( grid, newScrollPercent ); @@ -456,7 +459,7 @@ service.setScrollDirections( grid, scrollUp, scrollDown ); var newVisibleRows = grid.renderContainers.body.visibleRowCache.length; - var oldScrollRow = grid.infiniteScroll.prevScrolltopPercentage * grid.infiniteScroll.prevVisibleRows; + var oldScrollRow = grid.infiniteScroll.prevScrolltopPercentage * grid.infiniteScroll.previousVisibleRows; // since we removed from the bottom, our new scroll row will be same as the old scroll row var newScrollPercent = oldScrollRow / newVisibleRows; From ca969034121a3a77261c92e81f527e97e2f6d96c Mon Sep 17 00:00:00 2001 From: Shane Walters Date: Thu, 2 Apr 2015 17:05:18 -0500 Subject: [PATCH 093/173] fix issue caused by scrollRefactor - rtl demo when using mousewheel --- src/js/core/directives/ui-grid-viewport.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/js/core/directives/ui-grid-viewport.js b/src/js/core/directives/ui-grid-viewport.js index c4b3c05d6d..0a8a83d6f9 100644 --- a/src/js/core/directives/ui-grid-viewport.js +++ b/src/js/core/directives/ui-grid-viewport.js @@ -88,20 +88,20 @@ function syncHorizontalScroll(scrollEvent){ containerCtrl.prevScrollArgs = scrollEvent; var newScrollLeft = scrollEvent.getNewScrollLeft(colContainer, containerCtrl.viewport); - $elm[0].scrollLeft = newScrollLeft; + $elm[0].scrollLeft = gridUtil.denormalizeScrollLeft(containerCtrl.viewport,newScrollLeft); } function syncHorizontalHeader(scrollEvent){ var newScrollLeft = scrollEvent.getNewScrollLeft(colContainer, containerCtrl.viewport); if (containerCtrl.headerViewport) { - containerCtrl.headerViewport.scrollLeft = newScrollLeft; + containerCtrl.headerViewport.scrollLeft = gridUtil.denormalizeScrollLeft(containerCtrl.viewport,newScrollLeft); } } function syncHorizontalFooter(scrollEvent){ var newScrollLeft = scrollEvent.getNewScrollLeft(colContainer, containerCtrl.viewport); if (containerCtrl.footerViewport) { - containerCtrl.footerViewport.scrollLeft = newScrollLeft; + containerCtrl.footerViewport.scrollLeft = gridUtil.denormalizeScrollLeft(containerCtrl.viewport,newScrollLeft); } } From db1a0e31bda39bef7c51c0a4f811e5575058851d Mon Sep 17 00:00:00 2001 From: Peter Mihalik Date: Thu, 2 Apr 2015 16:39:53 +0200 Subject: [PATCH 094/173] added support for filter terms equal 0 and false --- src/js/core/services/rowSearcher.js | 13 ++++--- src/templates/ui-grid/uiGridHeaderCell.html | 4 +- test/unit/core/row-filtering.spec.js | 42 ++++++++++++++++----- 3 files changed, 42 insertions(+), 17 deletions(-) diff --git a/src/js/core/services/rowSearcher.js b/src/js/core/services/rowSearcher.js index 0e6bc89131..d187010261 100644 --- a/src/js/core/services/rowSearcher.js +++ b/src/js/core/services/rowSearcher.js @@ -114,7 +114,8 @@ module.service('rowSearcher', ['gridUtil', 'uiGridConstants', function (gridUtil var filtersLength = filters.length; for ( var i = 0; i < filtersLength; i++ ){ var filter = filters[i]; - if ( filter.noTerm || filter.term ){ + + if ( filter.noTerm || !gridUtil.isNullOrUndefined(filter.term) ){ var newFilter = {}; var regexpFlags = ''; @@ -122,7 +123,7 @@ module.service('rowSearcher', ['gridUtil', 'uiGridConstants', function (gridUtil regexpFlags += 'i'; } - if ( filter.term ){ + if ( !gridUtil.isNullOrUndefined(filter.term) ){ // it is possible to have noTerm. We don't need to copy that across, it was just a flag to avoid // getting the filter ignored if the filter was a function that didn't use a term newFilter.term = rowSearcher.stripTerm(filter); @@ -323,11 +324,11 @@ module.service('rowSearcher', ['gridUtil', 'uiGridConstants', function (gridUtil var colsLength = columns.length; for (var i = 0; i < colsLength; i++) { var col = columns[i]; - - if (typeof(col.filters) !== 'undefined' && ( col.filters.length > 1 || col.filters.length === 1 && ( typeof(col.filters[0].term) !== 'undefined' && col.filters[0].term || col.filters[0].noTerm ) ) ) { + + if (typeof(col.filters) !== 'undefined' && ( col.filters.length > 1 || col.filters.length === 1 && ( !gridUtil.isNullOrUndefined(col.filters[0].term) || col.filters[0].noTerm ) ) ) { filterData.push( { col: col, filters: rowSearcher.setupFilters(col.filters) } ); } - else if (typeof(col.filter) !== 'undefined' && col.filter && ( typeof(col.filter.term) !== 'undefined' && col.filter.term || col.filter.noTerm ) ) { + else if (typeof(col.filter) !== 'undefined' && col.filter && ( !gridUtil.isNullOrUndefined(col.filters[0].term) || col.filter.noTerm ) ) { filterData.push( { col: col, filters: rowSearcher.setupFilters([col.filter]) } ); } } @@ -364,4 +365,4 @@ module.service('rowSearcher', ['gridUtil', 'uiGridConstants', function (gridUtil return rowSearcher; }]); -})(); \ No newline at end of file +})(); diff --git a/src/templates/ui-grid/uiGridHeaderCell.html b/src/templates/ui-grid/uiGridHeaderCell.html index d55070e328..73ad61f14d 100644 --- a/src/templates/ui-grid/uiGridHeaderCell.html +++ b/src/templates/ui-grid/uiGridHeaderCell.html @@ -17,7 +17,7 @@
    -   +  
    @@ -25,7 +25,7 @@
    -   +  
    diff --git a/test/unit/core/row-filtering.spec.js b/test/unit/core/row-filtering.spec.js index 21790cfcff..bac55a6d3b 100644 --- a/test/unit/core/row-filtering.spec.js +++ b/test/unit/core/row-filtering.spec.js @@ -3,10 +3,10 @@ describe('rowSearcher', function() { rows, columns, rowSearcher, uiGridConstants, filter; var data = [ - { "name": "Ethel Price", "gender": "female", "company": "Enersol" }, - { "name": "Claudine Neal", "gender": "female", "company": "Sealoud" }, - { "name": "Beryl Rice", "gender": "female", "company": "Velity" }, - { "name": "Wilder Gonzales", "gender": "male", "company": "Geekko" } + { "name": "Ethel Price", "gender": "female", "company": "Enersol", "isActive" : true }, + { "name": "Claudine Neal", "gender": "female", "company": "Sealoud", "isActive" : false }, + { "name": "Beryl Rice", "gender": "female", "company": "Velity", "isActive" : true }, + { "name": "Wilder Gonzales", "gender": "male", "company": "Geekko", "isActive" : false } ]; beforeEach(module('ui.grid')); @@ -37,14 +37,16 @@ describe('rowSearcher', function() { }); rows = grid.rows = [ - new GridRow({ name: 'Bill', company: 'Gruber, Inc.', age: 25 }, 0, grid), - new GridRow({ name: 'Frank', company: 'Foo Co', age: 45 }, 1, grid) + new GridRow({ name: 'Bill', company: 'Gruber, Inc.', age: 25, isActive: true }, 0, grid), + new GridRow({ name: 'Frank', company: 'Foo Co', age: 45, isActive: false }, 1, grid), + new GridRow({ name: 'Joe', company: 'Movers, Inc.', age: 0, isActive: false }, 2, grid) ]; columns = grid.columns = [ new GridColumn({ name: 'name' }, 0, grid), new GridColumn({ name: 'company' }, 1, grid), - new GridColumn({ name: 'age' }, 2, grid) + new GridColumn({ name: 'age' }, 2, grid), + new GridColumn({ name: 'isActive' }, 3, grid) ]; filter = null; @@ -227,6 +229,28 @@ describe('rowSearcher', function() { }); }); + describe('with logically falsy terms (0 and false)', function() { + it('should filter by false', function() { + setFilter(columns[3], false); + + var ret = rowSearcher.search(grid, rows, columns); + + expect(ret[0].visible).toBe(false); + expect(ret[1].visible).toBe(true); + expect(ret[2].visible).toBe(true); + }); + + it('should filter by 0', function() { + setFilter(columns[2], 0); + + var ret = rowSearcher.search(grid, rows, columns); + + expect(ret[0].visible).toBe(false); + expect(ret[1].visible).toBe(false); + expect(ret[2].visible).toBe(true); + }); + }); + describe('with external filtering', function () { it('should not filter at all', function () { grid.options.useExternalFiltering = true; @@ -271,7 +295,7 @@ describe('rowSearcher', function() { ret = rowSearcher.search(grid, rows, columns); }); it('should run the function for each row', function() { - expect(custom.filterFn.calls.length).toEqual(2); + expect(custom.filterFn.calls.length).toEqual(3); expect(custom.filterFn.calls[0].args).toEqual(['>27', 25, rows[0], columns[2]]); expect(custom.filterFn.calls[1].args).toEqual(['>27', 45, rows[1], columns[2]]); }); @@ -281,4 +305,4 @@ describe('rowSearcher', function() { }); }); -}); \ No newline at end of file +}); From fba5b19fbaca8d7e1a07881429a9d30d88a71270 Mon Sep 17 00:00:00 2001 From: Paul Lambert Date: Fri, 3 Apr 2015 08:08:57 +1300 Subject: [PATCH 095/173] Fix(infiniteScroll): migrate to using new scroll architecture --- .../infinite-scroll/js/infinite-scroll.js | 23 +++++++++++-------- 1 file changed, 13 insertions(+), 10 deletions(-) diff --git a/src/features/infinite-scroll/js/infinite-scroll.js b/src/features/infinite-scroll/js/infinite-scroll.js index 05624cd8d4..d421025078 100644 --- a/src/features/infinite-scroll/js/infinite-scroll.js +++ b/src/features/infinite-scroll/js/infinite-scroll.js @@ -362,23 +362,26 @@ var newVisibleRows = grid.renderContainers.body.visibleRowCache.length; var oldPercentage, oldTopRow; + var halfViewport = grid.getViewportHeight() / grid.options.rowHeight / 2; if ( grid.infiniteScroll.direction === uiGridConstants.scrollDirection.UP ){ - console.log( 'prevTop was: ' + grid.infiniteScroll.prevScrolltopPercentage); - console.log( 'grid was: ' + grid); oldPercentage = grid.infiniteScroll.prevScrolltopPercentage || 0; oldTopRow = oldPercentage * grid.infiniteScroll.previousVisibleRows; - newPercentage = ( newVisibleRows - grid.infiniteScroll.previousVisibleRows + oldTopRow ) / newVisibleRows; + newPercentage = ( newVisibleRows - grid.infiniteScroll.previousVisibleRows + oldTopRow + halfViewport ) / newVisibleRows; service.adjustInfiniteScrollPosition(grid, newPercentage); - promise.resolve(); + $timeout( function() { + promise.resolve(); + }); } if ( grid.infiniteScroll.direction === uiGridConstants.scrollDirection.DOWN ){ oldPercentage = grid.infiniteScroll.prevScrolltopPercentage || 1; oldTopRow = oldPercentage * grid.infiniteScroll.previousVisibleRows; - newPercentage = oldTopRow / newVisibleRows; + newPercentage = ( oldTopRow - halfViewport ) / newVisibleRows; service.adjustInfiniteScrollPosition(grid, newPercentage); - promise.resolve(); + $timeout( function() { + promise.resolve(); + }); } }, 0); @@ -405,7 +408,7 @@ else { scrollEvent.y = {percentage: percentage}; } - grid.scrollContainers('body', scrollEvent); + grid.scrollContainers('', scrollEvent); }, @@ -428,11 +431,11 @@ service.setScrollDirections( grid, scrollUp, scrollDown ); var newVisibleRows = grid.renderContainers.body.visibleRowCache.length; - var oldScrollRow = grid.infiniteScroll.prevScrolltopPercentage * grid.infiniteScroll.prevVisibleRows; + var oldScrollRow = grid.infiniteScroll.prevScrolltopPercentage * grid.infiniteScroll.previousVisibleRows; // since we removed from the top, our new scroll row will be the old scroll row less the number // of rows removed - var newScrollRow = oldScrollRow - ( grid.infiniteScroll.prevVisibleRows - newVisibleRows ); + var newScrollRow = oldScrollRow - ( grid.infiniteScroll.previousVisibleRows - newVisibleRows ); var newScrollPercent = newScrollRow / newVisibleRows; return service.adjustInfiniteScrollPosition( grid, newScrollPercent ); @@ -456,7 +459,7 @@ service.setScrollDirections( grid, scrollUp, scrollDown ); var newVisibleRows = grid.renderContainers.body.visibleRowCache.length; - var oldScrollRow = grid.infiniteScroll.prevScrolltopPercentage * grid.infiniteScroll.prevVisibleRows; + var oldScrollRow = grid.infiniteScroll.prevScrolltopPercentage * grid.infiniteScroll.previousVisibleRows; // since we removed from the bottom, our new scroll row will be same as the old scroll row var newScrollPercent = oldScrollRow / newVisibleRows; From 079c799d81f87561f02100d078b73a6872ed9608 Mon Sep 17 00:00:00 2001 From: Brian Hann Date: Wed, 1 Apr 2015 12:25:50 -0500 Subject: [PATCH 096/173] Add IE karma launcher --- package.json | 1 + 1 file changed, 1 insertion(+) diff --git a/package.json b/package.json index a4fd95d77b..8b2742f0c0 100644 --- a/package.json +++ b/package.json @@ -44,6 +44,7 @@ "karma-requirejs": "~0.2", "karma-chrome-launcher": "~0.1", "karma-firefox-launcher": "~0.1", + "karma-ie-launcher": "^0.1.5", "karma-phantomjs-launcher": "~0.1", "karma-sauce-launcher": "~0.2.10", "karma-script-launcher": "~0.1", From 76029e79574e27d82de5a1026a68549ce5282f63 Mon Sep 17 00:00:00 2001 From: Brian Hann Date: Fri, 3 Apr 2015 13:16:24 -0500 Subject: [PATCH 097/173] chore(Docs): Fix method name saveScrollPercentage not saveScrollPosition --- misc/tutorial/212_infinite_scroll.ngdoc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/misc/tutorial/212_infinite_scroll.ngdoc b/misc/tutorial/212_infinite_scroll.ngdoc index 8b9969ddbf..bb8a72390a 100644 --- a/misc/tutorial/212_infinite_scroll.ngdoc +++ b/misc/tutorial/212_infinite_scroll.ngdoc @@ -26,7 +26,7 @@ by the time the data comes back, and scroll the user to the beginning of the new In some circumstances this can give "jumpy" scrolling, particularly if you have set your rowsFromEnd to quite a high value so that you're prefetching the data - if the user is scrolling slowly they might be 50 rows from the end, and when we process the dataLoaded we suddenly move them to what used to be the end. To avoid this, you can explicitly -save the scroll position before you add data to your data array, through calling `saveScrollPosition`, and the +save the scroll position before you add data to your data array, through calling `saveScrollPercentage`, and the `dataLoaded` call will then take that position into account, and attempt to adjust the scroll so that the same rows are showing once the grid has ingested the data you have added. From c2315ebd30ab0ed037d6b8c066c8bea55cad24ff Mon Sep 17 00:00:00 2001 From: Ben Tesser Date: Mon, 6 Apr 2015 16:35:53 -0400 Subject: [PATCH 098/173] fix(GridRenderContainer) Capitalization of viewport The capitalization of viewport in the getViewportStyle method (previously getViewPortStyle) was not consistent with other references to viewport. This has been changed to be consistent. --- src/js/core/factories/GridRenderContainer.js | 2 +- src/templates/ui-grid/uiGridViewport.html | 2 +- .../factories/GridRenderContainer.spec.js | 20 +++++++++---------- 3 files changed, 12 insertions(+), 12 deletions(-) diff --git a/src/js/core/factories/GridRenderContainer.js b/src/js/core/factories/GridRenderContainer.js index c80801783b..3df93b2e12 100644 --- a/src/js/core/factories/GridRenderContainer.js +++ b/src/js/core/factories/GridRenderContainer.js @@ -695,7 +695,7 @@ angular.module('ui.grid') this.columnStyles = ret; }; - GridRenderContainer.prototype.getViewPortStyle = function () { + GridRenderContainer.prototype.getViewportStyle = function () { var self = this; var styles = {}; diff --git a/src/templates/ui-grid/uiGridViewport.html b/src/templates/ui-grid/uiGridViewport.html index 97c4ba206a..16f8946366 100644 --- a/src/templates/ui-grid/uiGridViewport.html +++ b/src/templates/ui-grid/uiGridViewport.html @@ -1,4 +1,4 @@ -
    +
    diff --git a/test/unit/core/factories/GridRenderContainer.spec.js b/test/unit/core/factories/GridRenderContainer.spec.js index 881b82e44a..ca1d9d4e90 100644 --- a/test/unit/core/factories/GridRenderContainer.spec.js +++ b/test/unit/core/factories/GridRenderContainer.spec.js @@ -36,7 +36,7 @@ describe('GridRenderContainer factory', function () { }); - describe('getViewPortStyle', function () { + describe('getViewportStyle', function () { var r; beforeEach(function () { @@ -45,54 +45,54 @@ describe('GridRenderContainer factory', function () { it('should have a vert and horiz scrollbar on body', function () { r.name = 'body'; - expect(r.getViewPortStyle()).toEqual({'overflow-x':'scroll', 'overflow-y':'scroll'}); + expect(r.getViewportStyle()).toEqual({'overflow-x':'scroll', 'overflow-y':'scroll'}); }); it('should have a vert only', function () { r.name = 'body'; grid.options.enableVerticalScrollbar = uiGridConstants.scrollbars.NEVER; - expect(r.getViewPortStyle()).toEqual({'overflow-x':'scroll', 'overflow-y':'hidden'}); + expect(r.getViewportStyle()).toEqual({'overflow-x':'scroll', 'overflow-y':'hidden'}); }); it('should have a horiz only', function () { r.name = 'body'; grid.options.enableHorizontalScrollbar = uiGridConstants.scrollbars.NEVER; - expect(r.getViewPortStyle()).toEqual({'overflow-x':'hidden', 'overflow-y':'scroll'}); + expect(r.getViewportStyle()).toEqual({'overflow-x':'hidden', 'overflow-y':'scroll'}); }); it('left should have a no scrollbar when not rtl', function () { r.name = 'left'; - expect(r.getViewPortStyle()).toEqual({'overflow-x':'hidden', 'overflow-y':'hidden'}); + expect(r.getViewportStyle()).toEqual({'overflow-x':'hidden', 'overflow-y':'hidden'}); }); it('right should have a vert scrollbar when not rtl', function () { r.name = 'right'; - expect(r.getViewPortStyle()).toEqual({'overflow-x':'hidden', 'overflow-y':'scroll'}); + expect(r.getViewportStyle()).toEqual({'overflow-x':'hidden', 'overflow-y':'scroll'}); }); it('right should have no scrollbar when configured', function () { r.name = 'right'; grid.options.enableVerticalScrollbar = uiGridConstants.scrollbars.NEVER; - expect(r.getViewPortStyle()).toEqual({'overflow-x':'hidden', 'overflow-y':'hidden'}); + expect(r.getViewportStyle()).toEqual({'overflow-x':'hidden', 'overflow-y':'hidden'}); }); it('left should have a vert scrollbar when rtl', function () { r.name = 'left'; grid.rtl = true; - expect(r.getViewPortStyle()).toEqual({'overflow-x':'hidden', 'overflow-y':'scroll'}); + expect(r.getViewportStyle()).toEqual({'overflow-x':'hidden', 'overflow-y':'scroll'}); }); it('left should have no vert scrollbar when rtl and configured Never', function () { r.name = 'left'; grid.rtl = true; grid.options.enableVerticalScrollbar = uiGridConstants.scrollbars.NEVER; - expect(r.getViewPortStyle()).toEqual({'overflow-x':'hidden', 'overflow-y':'hidden'}); + expect(r.getViewportStyle()).toEqual({'overflow-x':'hidden', 'overflow-y':'hidden'}); }); it('right should have no scrollbars when rtl', function () { r.name = 'right'; grid.rtl = true; - expect(r.getViewPortStyle()).toEqual({'overflow-x':'hidden', 'overflow-y':'hidden'}); + expect(r.getViewportStyle()).toEqual({'overflow-x':'hidden', 'overflow-y':'hidden'}); }); }); From 1787d925a17bdfbc8de124111dce6bd22801faae Mon Sep 17 00:00:00 2001 From: jpuri Date: Mon, 30 Mar 2015 23:46:13 +0530 Subject: [PATCH 099/173] fixes for #2863 and #2793 --- misc/tutorial/306_expandable_grid.ngdoc | 5 ++++- src/features/expandable/less/expandable.less | 5 +++++ .../expandable/templates/expandableScrollFiller.html | 4 ++-- 3 files changed, 11 insertions(+), 3 deletions(-) diff --git a/misc/tutorial/306_expandable_grid.ngdoc b/misc/tutorial/306_expandable_grid.ngdoc index f336484247..8fbd3ba42a 100644 --- a/misc/tutorial/306_expandable_grid.ngdoc +++ b/misc/tutorial/306_expandable_grid.ngdoc @@ -30,6 +30,9 @@ provided following events and methods fos subGrids: SubGrid nesting can be done upto multiple levels. +In addition to above configuration 'scrollFillerClass' is also available and can be used to style the scroll filler, scroll filler +appears when you quickly scroll through the grid. + @example @@ -162,6 +165,6 @@ SubGrid nesting can be done upto multiple levels. } -
    +
    \ No newline at end of file diff --git a/src/features/expandable/less/expandable.less b/src/features/expandable/less/expandable.less index 90caee4d1c..389e85b66f 100644 --- a/src/features/expandable/less/expandable.less +++ b/src/features/expandable/less/expandable.less @@ -16,4 +16,9 @@ } +.scrollFiller { + float:left; + border:1px solid @borderColor; +} + .ui-grid-expandable-buttons-cell { } diff --git a/src/features/expandable/templates/expandableScrollFiller.html b/src/features/expandable/templates/expandableScrollFiller.html index 95538cee12..e9cf544c7b 100644 --- a/src/features/expandable/templates/expandableScrollFiller.html +++ b/src/features/expandable/templates/expandableScrollFiller.html @@ -1,7 +1,7 @@
    Date: Wed, 8 Apr 2015 15:17:09 +0200 Subject: [PATCH 101/173] Update i18n nl.js for grouping feature avoid crash on title: i18nService.get().grouping.group, when language is set to dutch --- src/js/i18n/nl.js | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/src/js/i18n/nl.js b/src/js/i18n/nl.js index e62534a8d0..727049c3ea 100644 --- a/src/js/i18n/nl.js +++ b/src/js/i18n/nl.js @@ -58,9 +58,19 @@ invalidCsv: 'Het bestand kan niet verwerkt worden. Is het een valide csv bestand?', invalidJson: 'Het bestand kan niet verwerkt worden. Is het valide json?', jsonNotArray: 'Het json bestand moet een array bevatten. De actie wordt geannuleerd.' + }, + grouping: { + group: 'Groepeer', + ungroup: 'Groepering opheffen', + aggregate_count: 'Agg: Aantal', + aggregate_sum: 'Agg: Som', + aggregate_max: 'Agg: Max', + aggregate_min: 'Agg: Min', + aggregate_avg: 'Agg: Gem', + aggregate_remove: 'Agg: Verwijder' } }); return $delegate; }]); }]); -})(); \ No newline at end of file +})(); From e7dfb8c2dfac69bb3a38f7253062367671fec56d Mon Sep 17 00:00:00 2001 From: Brian Hann Date: Wed, 1 Apr 2015 12:22:46 -0500 Subject: [PATCH 102/173] fix(uiGrid): Wait for grid to get dimensions If the grid has no width initially, wait for up to 2 seconds for the grid to be shown and have dimensions. Once that happens it will re-initialize its height and width and redraw the canvas. This should fix the problems with the grid taking up too much space in a modal. BREAKING CHANGE: gridUtil will no longer calculate dimensions of hidden elements --- misc/demo/modal.html | 70 ++++++ src/js/core/directives/ui-grid.js | 226 ++++++++++-------- src/js/core/directives/ui-pinned-container.js | 2 +- src/js/core/services/ui-grid-util.js | 8 +- src/less/header.less | 2 +- src/less/menu.less | 2 +- test/unit/core/services/ui-grid-util.spec.js | 3 +- 7 files changed, 205 insertions(+), 108 deletions(-) create mode 100644 misc/demo/modal.html diff --git a/misc/demo/modal.html b/misc/demo/modal.html new file mode 100644 index 0000000000..60ebcc34ad --- /dev/null +++ b/misc/demo/modal.html @@ -0,0 +1,70 @@ + + + + + + + + + + + + + + + + + + + +
    + +
    + + + + + + diff --git a/src/js/core/directives/ui-grid.js b/src/js/core/directives/ui-grid.js index a6cf821597..1cbddbe89f 100644 --- a/src/js/core/directives/ui-grid.js +++ b/src/js/core/directives/ui-grid.js @@ -160,124 +160,148 @@ */ -angular.module('ui.grid').directive('uiGrid', - [ - '$compile', - '$templateCache', - 'gridUtil', - '$window', - 'uiGridConstants', - function( - $compile, - $templateCache, - gridUtil, - $window, - uiGridConstants - ) { +angular.module('ui.grid').directive('uiGrid', uiGridDirective); + +uiGridDirective.$inject = ['$compile', '$templateCache', '$timeout', '$window', 'gridUtil', 'uiGridConstants']; +function uiGridDirective($compile, $templateCache, $timeout, $window, gridUtil, uiGridConstants) { + return { + templateUrl: 'ui-grid/ui-grid', + scope: { + uiGrid: '=' + }, + replace: true, + transclude: true, + controller: 'uiGridController', + compile: function () { return { - templateUrl: 'ui-grid/ui-grid', - scope: { - uiGrid: '=' - }, - replace: true, - transclude: true, - controller: 'uiGridController', - compile: function () { - return { - post: function ($scope, $elm, $attrs, uiGridCtrl) { - // gridUtil.logDebug('ui-grid postlink'); - - var grid = uiGridCtrl.grid; - - // Initialize scrollbars (TODO: move to controller??) - uiGridCtrl.scrollbars = []; - - //todo: assume it is ok to communicate that rendering is complete?? - grid.renderingComplete(); - - grid.element = $elm; - - grid.gridWidth = $scope.gridWidth = gridUtil.elementWidth($elm); - - // Default canvasWidth to the grid width, in case we don't get any column definitions to calculate it from - grid.canvasWidth = uiGridCtrl.grid.gridWidth; - - grid.gridHeight = $scope.gridHeight = gridUtil.elementHeight($elm); - - // If the grid isn't tall enough to fit a single row, it's kind of useless. Resize it to fit a minimum number of rows - if (grid.gridHeight < grid.options.rowHeight && grid.options.enableMinHeightCheck) { - // Figure out the new height - var contentHeight = grid.options.minRowsToShow * grid.options.rowHeight; - var headerHeight = grid.options.showHeader ? grid.options.headerRowHeight : 0; - var footerHeight = grid.calcFooterHeight(); - - var scrollbarHeight = 0; - if (grid.options.enableHorizontalScrollbar === uiGridConstants.scrollbars.ALWAYS) { - scrollbarHeight = gridUtil.getScrollbarWidth(); - } + post: function ($scope, $elm, $attrs, uiGridCtrl) { + var grid = uiGridCtrl.grid; + // Initialize scrollbars (TODO: move to controller??) + uiGridCtrl.scrollbars = []; + grid.element = $elm; - var maxNumberOfFilters = 0; - // Calculates the maximum number of filters in the columns - angular.forEach(grid.options.columnDefs, function(col) { - if (col.hasOwnProperty('filter')) { - if (maxNumberOfFilters < 1) { - maxNumberOfFilters = 1; - } - } - else if (col.hasOwnProperty('filters')) { - if (maxNumberOfFilters < col.filters.length) { - maxNumberOfFilters = col.filters.length; - } - } - }); - var filterHeight = maxNumberOfFilters * headerHeight; - var newHeight = headerHeight + contentHeight + footerHeight + scrollbarHeight + filterHeight; + // See if the grid has a rendered width, if not, wait a bit and try again + var sizeCheckInterval = 100; // ms + var maxSizeChecks = 20; // 2 seconds total + var sizeChecks = 0; - $elm.css('height', newHeight + 'px'); + // Setup (event listeners) the grid + setup(); - grid.gridHeight = $scope.gridHeight = gridUtil.elementHeight($elm); - } + // And initialize it + init(); - // Run initial canvas refresh - grid.refreshCanvas(); + // Mark rendering complete so API events can happen + grid.renderingComplete(); - //if we add a left container after render, we need to watch and react - $scope.$watch(function () { return grid.hasLeftContainer();}, function (newValue, oldValue) { - if (newValue === oldValue) { - return; - } - grid.refreshCanvas(true); - }); + // If the grid doesn't have size currently, wait for a bit to see if it gets size + checkSize(); - //if we add a right container after render, we need to watch and react - $scope.$watch(function () { return grid.hasRightContainer();}, function (newValue, oldValue) { - if (newValue === oldValue) { - return; - } - grid.refreshCanvas(true); - }); + /*-- Methods --*/ + function checkSize() { + // If the grid has no width and we haven't checked more than times, check again in milliseconds + if ($elm[0].offsetWidth <= 0 && sizeChecks < maxSizeChecks) { + setTimeout(checkSize, sizeCheckInterval); + sizeChecks++; + } + else { + $timeout(init); + } + } - // Resize the grid on window resize events - function gridResize($event) { - grid.gridWidth = $scope.gridWidth = gridUtil.elementWidth($elm); - grid.gridHeight = $scope.gridHeight = gridUtil.elementHeight($elm); + // Setup event listeners and watchers + function setup() { + // Bind to window resize events + angular.element($window).on('resize', gridResize); - grid.refreshCanvas(true); + // Unbind from window resize events when the grid is destroyed + $elm.on('$destroy', function () { + angular.element($window).off('resize', gridResize); + }); + + // If we add a left container after render, we need to watch and react + $scope.$watch(function () { return grid.hasLeftContainer();}, function (newValue, oldValue) { + if (newValue === oldValue) { + return; } + grid.refreshCanvas(true); + }); - angular.element($window).on('resize', gridResize); + // If we add a right container after render, we need to watch and react + $scope.$watch(function () { return grid.hasRightContainer();}, function (newValue, oldValue) { + if (newValue === oldValue) { + return; + } + grid.refreshCanvas(true); + }); + } - // Unbind from window resize events when the grid is destroyed - $elm.on('$destroy', function () { - angular.element($window).off('resize', gridResize); - }); + // Initialize the directive + function init() { + grid.gridWidth = $scope.gridWidth = gridUtil.elementWidth($elm); + + // Default canvasWidth to the grid width, in case we don't get any column definitions to calculate it from + grid.canvasWidth = uiGridCtrl.grid.gridWidth; + + grid.gridHeight = $scope.gridHeight = gridUtil.elementHeight($elm); + + // If the grid isn't tall enough to fit a single row, it's kind of useless. Resize it to fit a minimum number of rows + if (grid.gridHeight < grid.options.rowHeight && grid.options.enableMinHeightCheck) { + autoAdjustHeight(); + } + + // Run initial canvas refresh + grid.refreshCanvas(true); + } + + // Set the grid's height ourselves in the case that its height would be unusably small + function autoAdjustHeight() { + // Figure out the new height + var contentHeight = grid.options.minRowsToShow * grid.options.rowHeight; + var headerHeight = grid.options.showHeader ? grid.options.headerRowHeight : 0; + var footerHeight = grid.calcFooterHeight(); + + var scrollbarHeight = 0; + if (grid.options.enableHorizontalScrollbar === uiGridConstants.scrollbars.ALWAYS) { + scrollbarHeight = gridUtil.getScrollbarWidth(); } - }; + + var maxNumberOfFilters = 0; + // Calculates the maximum number of filters in the columns + angular.forEach(grid.options.columnDefs, function(col) { + if (col.hasOwnProperty('filter')) { + if (maxNumberOfFilters < 1) { + maxNumberOfFilters = 1; + } + } + else if (col.hasOwnProperty('filters')) { + if (maxNumberOfFilters < col.filters.length) { + maxNumberOfFilters = col.filters.length; + } + } + }); + var filterHeight = maxNumberOfFilters * headerHeight; + + var newHeight = headerHeight + contentHeight + footerHeight + scrollbarHeight + filterHeight; + + $elm.css('height', newHeight + 'px'); + + grid.gridHeight = $scope.gridHeight = gridUtil.elementHeight($elm); + } + + // Resize the grid on window resize events + function gridResize($event) { + grid.gridWidth = $scope.gridWidth = gridUtil.elementWidth($elm); + grid.gridHeight = $scope.gridHeight = gridUtil.elementHeight($elm); + + grid.refreshCanvas(true); + } } }; } - ]); + }; +} })(); diff --git a/src/js/core/directives/ui-pinned-container.js b/src/js/core/directives/ui-pinned-container.js index 7d3bd5859d..343ea7ab3b 100644 --- a/src/js/core/directives/ui-pinned-container.js +++ b/src/js/core/directives/ui-pinned-container.js @@ -33,7 +33,7 @@ } return width; - } + } } function updateContainerDimensions() { diff --git a/src/js/core/services/ui-grid-util.js b/src/js/core/services/ui-grid-util.js index 2cdfb02d5a..051a2aa2e2 100644 --- a/src/js/core/services/ui-grid-util.js +++ b/src/js/core/services/ui-grid-util.js @@ -106,7 +106,7 @@ function augmentWidthOrHeight( elem, name, extra, isBorderBox, styles ) { function getWidthOrHeight( elem, name, extra ) { // Start with offset property, which is equivalent to the border-box value var valueIsBorderBox = true, - val, + val, // = name === 'width' ? elem.offsetWidth : elem.offsetHeight, styles = getStyles(elem), isBorderBox = styles['boxSizing'] === 'border-box'; @@ -173,6 +173,8 @@ module.service('gridUtil', ['$log', '$window', '$document', '$http', '$templateC function ($log, $window, $document, $http, $templateCache, $timeout, $injector, $q, $interpolate, uiGridConstants) { var s = { + augmentWidthOrHeight: augmentWidthOrHeight, + getStyles: getStyles, /** @@ -786,8 +788,8 @@ module.service('gridUtil', ['$log', '$window', '$document', '$http', '$templateC if (e) { var styles = getStyles(e); return e.offsetWidth === 0 && rdisplayswap.test(styles.display) ? - s.fakeElement(e, cssShow, function(newElm) { - return getWidthOrHeight( newElm, name, extra ); + s.swap(e, cssShow, function() { + return getWidthOrHeight(e, name, extra ); }) : getWidthOrHeight( e, name, extra ); } diff --git a/src/less/header.less b/src/less/header.less index 8afba48bd8..d9c4c4cfde 100644 --- a/src/less/header.less +++ b/src/less/header.less @@ -7,7 +7,7 @@ .ui-grid-header { border-bottom: 1px solid @borderColor; - box-sizing: content-box; + box-sizing: border-box; } .ui-grid-top-panel { diff --git a/src/less/menu.less b/src/less/menu.less index ce68d2df38..3d67978b35 100644 --- a/src/less/menu.less +++ b/src/less/menu.less @@ -28,7 +28,7 @@ overflow: hidden; padding: 0 10px 20px 10px; cursor: pointer; - box-sizing: content-box; + box-sizing: border-box; } .ui-grid-menu .ui-grid-menu-inner { diff --git a/test/unit/core/services/ui-grid-util.spec.js b/test/unit/core/services/ui-grid-util.spec.js index 2f3cdf5086..d2b595cb00 100644 --- a/test/unit/core/services/ui-grid-util.spec.js +++ b/test/unit/core/services/ui-grid-util.spec.js @@ -199,7 +199,8 @@ describe('ui.grid.utilService', function() { expect(w).toEqual(300); }); - it('should work with hidden element', function() { + // Width is no longer calculated for hidden elements + xit('should work with hidden element', function() { angular.element(elm).remove(); elm = document.createElement('div'); From 9898667910bc4d354fcbbe5986e27b4f98cba9d5 Mon Sep 17 00:00:00 2001 From: dllabs Date: Thu, 9 Apr 2015 11:00:55 +1000 Subject: [PATCH 103/173] Added 'numberStr' option to colDef.type make use of sortNumberStr function --- src/js/core/services/rowSorter.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/js/core/services/rowSorter.js b/src/js/core/services/rowSorter.js index 210d19d9eb..a46277e169 100644 --- a/src/js/core/services/rowSorter.js +++ b/src/js/core/services/rowSorter.js @@ -43,6 +43,8 @@ module.service('rowSorter', ['$parse', 'uiGridConstants', function ($parse, uiGr switch (itemType) { case "number": return rowSorter.sortNumber; + case "numberStr": + return rowSorter.sortNumberStr; case "boolean": return rowSorter.sortBool; case "string": From 6967afda57203e4bb6e7a84fa66a9c0c881a6891 Mon Sep 17 00:00:00 2001 From: Paul Lambert Date: Mon, 6 Apr 2015 08:43:30 +1200 Subject: [PATCH 104/173] Fix(rowsProcessors): handle invisible rows more cleanly This change has each rowsProcessor return only the visible rows, which reduces the number of rows that the sort processor might need to process, and the number of rows in the visibleRowsCache (which might have some performance benefits). This also handles setRowInvisible properly - previously this was being ignored. x --- src/features/grouping/js/grouping.js | 10 ++------ src/features/grouping/test/grouping.spec.js | 11 ++++++--- src/js/core/factories/GridRow.js | 8 ++++--- src/js/core/services/gridClassFactory.js | 2 +- src/js/core/services/rowSearcher.js | 8 +++++-- src/js/core/services/rowSorter.js | 8 +++---- test/unit/core/row-filtering.spec.js | 26 +++++++-------------- 7 files changed, 35 insertions(+), 38 deletions(-) diff --git a/src/features/grouping/js/grouping.js b/src/features/grouping/js/grouping.js index f21f221d90..2dc1c65ad5 100644 --- a/src/features/grouping/js/grouping.js +++ b/src/features/grouping/js/grouping.js @@ -1161,7 +1161,7 @@ service.writeOutAggregations( grid, groupingProcessingState, 0); - return renderableRows; + return renderableRows.filter(function (row) { return row.visible; }); }, @@ -1403,18 +1403,12 @@ return; } - var visible = true; var groupLevel = typeof(row.groupLevel) !== 'undefined' ? row.groupLevel : groupingProcessingState.length; for (var i = 0; i < groupLevel; i++){ if ( groupingProcessingState[i].currentGroupHeader.expandedState.state === uiGridGroupingConstants.COLLAPSED ){ - visible = false; + row.visible = false; } } - - // we're running in a rowProcessor, so default is always visible, we don't need to set it unless we want invisible - if ( !visible ){ - row.setThisRowInvisible( 'grouping', true ); - } }, diff --git a/src/features/grouping/test/grouping.spec.js b/src/features/grouping/test/grouping.spec.js index 8611e27738..4fc0dadc0d 100644 --- a/src/features/grouping/test/grouping.spec.js +++ b/src/features/grouping/test/grouping.spec.js @@ -79,8 +79,14 @@ describe('ui.grid.grouping uiGridGroupingService', function () { grid.columns[0].grouping = { groupPriority: 1 }; grid.columns[1].grouping = { groupPriority: 2 }; - var groupedRows = uiGridGroupingService.groupRows.call( grid, grid.rows ); - + var groupedRows = uiGridGroupingService.groupRows.call( grid, grid.rows.slice(0) ); + expect( groupedRows.length ).toEqual( 3, 'only the level 1 rows are visible' ); + + grid.api.grouping.expandAllRows(); + grid.rows.forEach(function( row ){ + row.visible = true; + }); + groupedRows = uiGridGroupingService.groupRows.call( grid, grid.rows.slice(0) ); expect( groupedRows.length ).toEqual( 18, 'we\'ve added 3 col0 headers, and 5 col2 headers' ); }); }); @@ -822,7 +828,6 @@ describe('ui.grid.grouping uiGridGroupingService', function () { uiGridGroupingService.setVisibility( grid, grid.rows[1], processingStates ); expect( grid.rows[1].visible ).toEqual(false); - expect( grid.rows[1].invisibleReason.grouping).toEqual(true); }); it( 'visible', function() { diff --git a/src/js/core/factories/GridRow.js b/src/js/core/factories/GridRow.js index 57f93f1ddc..3e14e0c80e 100644 --- a/src/js/core/factories/GridRow.js +++ b/src/js/core/factories/GridRow.js @@ -175,7 +175,9 @@ angular.module('ui.grid') * @param {boolean} fromRowsProcessor whether we were called from a rowsProcessor, passed through to evaluateRowVisibility */ GridRow.prototype.clearThisRowInvisible = function ( reason, fromRowsProcessor ) { - delete this.invisibleReason.user; + if (typeof(this.invisibleReason) !== 'undefined' ) { + delete this.invisibleReason.user; + } this.evaluateRowVisibility( fromRowsProcessor ); }; @@ -193,7 +195,7 @@ angular.module('ui.grid') */ GridRow.prototype.evaluateRowVisibility = function ( fromRowProcessor ) { var newVisibility = true; - if ( this.invisibleReason ){ + if ( typeof(this.invisibleReason) !== 'undefined' ){ angular.forEach(this.invisibleReason, function( value, key ){ if ( value ){ newVisibility = false; @@ -201,7 +203,7 @@ angular.module('ui.grid') }); } - if ( this.visible !== newVisibility ){ + if ( typeof(this.visible) === 'undefined' || this.visible !== newVisibility ){ this.visible = newVisibility; if ( !fromRowProcessor ){ this.grid.queueGridRefresh(); diff --git a/src/js/core/services/gridClassFactory.js b/src/js/core/services/gridClassFactory.js index 61e7b6f11f..9b74898721 100644 --- a/src/js/core/services/gridClassFactory.js +++ b/src/js/core/services/gridClassFactory.js @@ -49,7 +49,7 @@ // Reset all rows to visible initially grid.registerRowsProcessor(function allRowsVisible(rows) { rows.forEach(function (row) { - row.visible = true; + row.evaluateRowVisibility( true ); }, 50); return rows; diff --git a/src/js/core/services/rowSearcher.js b/src/js/core/services/rowSearcher.js index 0e6bc89131..caa9571bbf 100644 --- a/src/js/core/services/rowSearcher.js +++ b/src/js/core/services/rowSearcher.js @@ -335,8 +335,8 @@ module.service('rowSearcher', ['gridUtil', 'uiGridConstants', function (gridUtil if (filterData.length > 0) { // define functions outside the loop, performance optimisation var foreachRow = function(grid, row, col, filters){ - if ( !rowSearcher.searchColumn(grid, row, col, filters) ) { - row.setThisRowInvisible('filtered', true); + if ( row.visible && !rowSearcher.searchColumn(grid, row, col, filters) ) { + row.visible = false; } }; @@ -356,6 +356,10 @@ module.service('rowSearcher', ['gridUtil', 'uiGridConstants', function (gridUtil if (grid.api.core.raise.rowsVisibleChanged) { grid.api.core.raise.rowsVisibleChanged(); } + + // drop any invisible rows + rows = rows.filter(function(row){ return row.visible; }); + } return rows; diff --git a/src/js/core/services/rowSorter.js b/src/js/core/services/rowSorter.js index 210d19d9eb..acd6fd4923 100644 --- a/src/js/core/services/rowSorter.js +++ b/src/js/core/services/rowSorter.js @@ -407,16 +407,16 @@ module.service('rowSorter', ['$parse', 'uiGridConstants', function ($parse, uiGr // Re-usable variables var col, direction; - // IE9-11 HACK.... the 'rows' variable would be empty where we call rowSorter.getSortFn(...) below. We have to use a separate reference - // var d = data.slice(0); - var r = rows.slice(0); - // put a custom index field on each row, used to make a stable sort out of unstable sorts (e.g. Chrome) var setIndex = function( row, idx ){ row.entity.$$uiGridIndex = idx; }; rows.forEach(setIndex); + // IE9-11 HACK.... the 'rows' variable would be empty where we call rowSorter.getSortFn(...) below. We have to use a separate reference + // var d = data.slice(0); + var r = rows.slice(0); + // Now actually sort the data var rowSortFn = function (rowA, rowB) { var tem = 0, diff --git a/test/unit/core/row-filtering.spec.js b/test/unit/core/row-filtering.spec.js index 21790cfcff..3a6f82ab3c 100644 --- a/test/unit/core/row-filtering.spec.js +++ b/test/unit/core/row-filtering.spec.js @@ -153,8 +153,7 @@ describe('rowSearcher', function() { var ret = rowSearcher.search(grid, rows, columns); - expect(ret[0].visible).toBe(true); - expect(ret[1].visible).toBe(false); + expect(ret.length).toEqual(1); }); }); @@ -165,8 +164,7 @@ describe('rowSearcher', function() { var ret = rowSearcher.search(grid, rows, columns); - expect(ret[0].visible).toBe(true); - expect(ret[1].visible).toBe(false); + expect(ret.length).toEqual(1); }); }); @@ -179,7 +177,7 @@ describe('rowSearcher', function() { var ret = rowSearcher.search(grid, rows, columns); - expect(ret[0].visible).toBe(false); + expect(ret.length).toEqual(0); }); }); @@ -189,8 +187,7 @@ describe('rowSearcher', function() { var ret = rowSearcher.search(grid, rows, columns); - expect(ret[0].visible).toBe(true); - expect(ret[1].visible).toBe(false); + expect(ret.length).toEqual(1); }); }); @@ -200,8 +197,7 @@ describe('rowSearcher', function() { var ret = rowSearcher.search(grid, rows, columns); - expect(ret[0].visible).toBe(true); - expect(ret[1].visible).toBe(false); + expect(ret.length).toEqual(1); }); }); @@ -211,8 +207,7 @@ describe('rowSearcher', function() { var ret = rowSearcher.search(grid, rows, columns); - expect(ret[0].visible).toBe(true); - expect(ret[1].visible).toBe(false); + expect(ret.length).toEqual(1); }); }); @@ -222,8 +217,7 @@ describe('rowSearcher', function() { var ret = rowSearcher.search(grid, rows, columns); - expect(ret[0].visible).toBe(true); - expect(ret[1].visible).toBe(false); + expect(ret.length).toEqual(1); }); }); @@ -234,8 +228,7 @@ describe('rowSearcher', function() { var ret = rowSearcher.search(grid, rows, columns); - expect(ret[0].visible).toBe(true); - expect(ret[1].visible).toBe(true); + expect(ret.length).toEqual(2); }); }); @@ -276,8 +269,7 @@ describe('rowSearcher', function() { expect(custom.filterFn.calls[1].args).toEqual(['>27', 45, rows[1], columns[2]]); }); it('should honor the result of the function call when filtering', function() { - expect(ret[0].visible).toBe(false); - expect(ret[1].visible).toBe(true); + expect(ret.length).toEqual(1); }); }); From e4eb65c2527d984438e2aa1ee751f0459f13844a Mon Sep 17 00:00:00 2001 From: Brian Hann Date: Thu, 9 Apr 2015 12:11:35 -0500 Subject: [PATCH 105/173] Disable irc build notification --- .travis.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 04be86e827..97c86ac117 100644 --- a/.travis.yml +++ b/.travis.yml @@ -4,7 +4,6 @@ node_js: - '0.10' notifications: - irc: "chat.freenode.net#ui-grid" webhooks: urls: - https://webhooks.gitter.im/e/c9dc628573cc153706fa From c315178af8a4e00798951dde1cd659812f2466cd Mon Sep 17 00:00:00 2001 From: Paul Lambert Date: Sat, 11 Apr 2015 07:17:38 +1200 Subject: [PATCH 106/173] Fix(filtering): failing unit tests from merge --- test/unit/core/row-filtering.spec.js | 25 +++---------------------- 1 file changed, 3 insertions(+), 22 deletions(-) diff --git a/test/unit/core/row-filtering.spec.js b/test/unit/core/row-filtering.spec.js index 731085f386..eb00c78f25 100644 --- a/test/unit/core/row-filtering.spec.js +++ b/test/unit/core/row-filtering.spec.js @@ -16,21 +16,6 @@ describe('rowSearcher', function() { rowSearcher = _rowSearcher_; uiGridConstants = _uiGridConstants_; - // $compile = _$compile_; - - // $scope.gridOpts = { - // data: data - // }; - - // recompile = function () { - // grid = angular.element('
    '); - // // document.body.appendChild(grid[0]); - // $compile(grid)($scope); - // $scope.$digest(); - // }; - - // recompile(); - grid = new Grid({ id: 1, enableFiltering: true @@ -229,9 +214,7 @@ describe('rowSearcher', function() { var ret = rowSearcher.search(grid, rows, columns); - expect(ret[0].visible).toBe(false); - expect(ret[1].visible).toBe(true); - expect(ret[2].visible).toBe(true); + expect(ret.length).toEqual(2); }); it('should filter by 0', function() { @@ -239,9 +222,7 @@ describe('rowSearcher', function() { var ret = rowSearcher.search(grid, rows, columns); - expect(ret[0].visible).toBe(false); - expect(ret[1].visible).toBe(false); - expect(ret[2].visible).toBe(true); + expect(ret.length).toEqual(1); }); }); @@ -252,7 +233,7 @@ describe('rowSearcher', function() { var ret = rowSearcher.search(grid, rows, columns); - expect(ret.length).toEqual(2); + expect(ret.length).toEqual(3); }); }); From 798a2a80cc5a99f8b85ba7dad27a8bf15f4be475 Mon Sep 17 00:00:00 2001 From: Eugen Istoc Date: Thu, 9 Apr 2015 19:08:54 -0400 Subject: [PATCH 107/173] Add registerColumnsProcessor to API I also introduced the priority for the processor. Followed the same style as `registerRowsProcessor(...)` method Get the processor from the right location Add priority for when calling registerColumnProcessor Update tests to reflect updated search() return Add registerColumnsProcessor to API commit 3f30d94369ec677c2fe1d5c721045247f02b96c2 Merge: 5034a86 69f421b Author: Paul Date: Sat Apr 11 07:08:39 2015 +1200 Merge pull request #3132 from pedroclayman/select-filter added filter attribute 'disableCancelFilterButton' commit 69f421bea57aa01b398139ead7260f56bf67ac55 Merge: c495485 5034a86 Author: Peter Mihalik Date: Fri Apr 10 14:51:08 2015 +0200 Merge branch 'upstream_master' into select-filter Conflicts: src/templates/ui-grid/uiGridHeaderCell.html commit c4954852d81cf871c6627a087e309cabc8a61e4a Author: Peter Mihalik Date: Fri Mar 27 12:56:08 2015 +0100 added filter attribute 'disableCancelFilterButton' and select filter default empty option Get the processor from the right location Add priority for when calling registerColumnProcessor Update tests to reflect updated search() return Add registerColumnsProcessor to API commit 3f30d94369ec677c2fe1d5c721045247f02b96c2 Merge: 5034a86 69f421b Author: Paul Date: Sat Apr 11 07:08:39 2015 +1200 Merge pull request #3132 from pedroclayman/select-filter added filter attribute 'disableCancelFilterButton' commit 69f421bea57aa01b398139ead7260f56bf67ac55 Merge: c495485 5034a86 Author: Peter Mihalik Date: Fri Apr 10 14:51:08 2015 +0200 Merge branch 'upstream_master' into select-filter Conflicts: src/templates/ui-grid/uiGridHeaderCell.html commit c4954852d81cf871c6627a087e309cabc8a61e4a Author: Peter Mihalik Date: Fri Mar 27 12:56:08 2015 +0100 added filter attribute 'disableCancelFilterButton' and select filter default empty option Get the processor from the right location Add priority for when calling registerColumnProcessor Update tests to reflect updated search() return Add registerColumnsProcessor to API commit 3f30d94369ec677c2fe1d5c721045247f02b96c2 Merge: 5034a86 69f421b Author: Paul Date: Sat Apr 11 07:08:39 2015 +1200 Merge pull request #3132 from pedroclayman/select-filter added filter attribute 'disableCancelFilterButton' commit 69f421bea57aa01b398139ead7260f56bf67ac55 Merge: c495485 5034a86 Author: Peter Mihalik Date: Fri Apr 10 14:51:08 2015 +0200 Merge branch 'upstream_master' into select-filter Conflicts: src/templates/ui-grid/uiGridHeaderCell.html commit c4954852d81cf871c6627a087e309cabc8a61e4a Author: Peter Mihalik Date: Fri Mar 27 12:56:08 2015 +0100 added filter attribute 'disableCancelFilterButton' and select filter default empty option --- src/features/grouping/js/grouping.js | 2 +- src/js/core/factories/Grid.js | 33 +++++++++++++++++++++--- src/js/core/services/gridClassFactory.js | 4 +-- test/unit/core/row-filtering.spec.js | 10 +++---- 4 files changed, 36 insertions(+), 13 deletions(-) diff --git a/src/features/grouping/js/grouping.js b/src/features/grouping/js/grouping.js index 2dc1c65ad5..e2db43b2e0 100644 --- a/src/features/grouping/js/grouping.js +++ b/src/features/grouping/js/grouping.js @@ -175,7 +175,7 @@ grid.registerRowsProcessor(service.groupRows, 400); - grid.registerColumnsProcessor(service.groupingColumnProcessor); + grid.registerColumnsProcessor(service.groupingColumnProcessor, 400); /** * @ngdoc object diff --git a/src/js/core/factories/Grid.js b/src/js/core/factories/Grid.js index 2ef431cd2d..44c4526252 100644 --- a/src/js/core/factories/Grid.js +++ b/src/js/core/factories/Grid.js @@ -317,6 +317,26 @@ angular.module('ui.grid') */ self.api.registerMethod( 'core', 'registerRowsProcessor', this.registerRowsProcessor ); + /** + * @ngdoc function + * @name registerColumnsProcessor + * @methodOf ui.grid.core.api:PublicApi + * @description + * Register a "columns processor" function. When the columns are updated, + * the grid calls each registered "columns processor", which has a chance + * to alter the set of columns as long as the count is not + * modified. + * + * @param {function(renderedColumnsToProcess, rows )} processorFunction columns processor function, which + * is run in the context of the grid (i.e. this for the function will be the grid), and must + * return the updated columns list, which is passed to the next processor in the chain + * @param {number} priority the priority of this processor. In general we try to do them in 100s to leave room + * for other people to inject columns processors at intermediate priorities. Lower priority columnsProcessors run earlier. + * + * At present allRowsVisible is running at 50, filter is running at 100, sort is at 200, grouping at 400, selectable rows at 500, pagination at 900 (pagination will generally want to be last) + */ + self.api.registerMethod( 'core', 'registerColumnsProcessor', this.registerColumnsProcessor ); + /** @@ -1316,18 +1336,25 @@ angular.module('ui.grid') * is run in the context of the grid (i.e. this for the function will be the grid), and * which must return an updated renderedColumnsToProcess which can be passed to the next processor * in the chain + * @param {number} priority the priority of this processor. In general we try to do them in 100s to leave room + * for other people to inject columns processors at intermediate priorities. Lower priority columnsProcessors run earlier. + * + * At present all rows visible is running at 50, filter is running at 100, sort is at 200, grouping at 400, selectable rows at 500, pagination at 900 (pagination will generally want to be last) * @description Register a "columns processor" function. When the columns are updated, the grid calls each registered "columns processor", which has a chance to alter the set of columns, as long as the count is not modified. */ - Grid.prototype.registerColumnsProcessor = function registerColumnsProcessor(processor) { + Grid.prototype.registerColumnsProcessor = function registerColumnsProcessor(processor, priority) { if (!angular.isFunction(processor)) { throw 'Attempt to register non-function rows processor: ' + processor; } - this.columnsProcessors.push(processor); + this.columnsProcessors.push({processor: processor, priority: priority}); + this.columnsProcessors.sort(function sortByPriority( a, b ){ + return a.priority - b.priority; + }); }; Grid.prototype.removeColumnsProcessor = function removeColumnsProcessor(processor) { @@ -1363,7 +1390,7 @@ angular.module('ui.grid') // the result. function startProcessor(i, renderedColumnsToProcess) { // Get the processor at 'i' - var processor = self.columnsProcessors[i]; + var processor = self.columnsProcessors[i].processor; // Call the processor, passing in the rows to process and the current columns // (note: it's wrapped in $q.when() in case the processor does not return a promise) diff --git a/src/js/core/services/gridClassFactory.js b/src/js/core/services/gridClassFactory.js index 9b74898721..e0d1c2a734 100644 --- a/src/js/core/services/gridClassFactory.js +++ b/src/js/core/services/gridClassFactory.js @@ -61,7 +61,7 @@ }); return columns; - }); + }, 50); grid.registerColumnsProcessor(function(renderableColumns) { renderableColumns.forEach(function (column) { @@ -71,7 +71,7 @@ }); return renderableColumns; - }); + }, 50); grid.registerRowsProcessor(grid.searchRows, 100); diff --git a/test/unit/core/row-filtering.spec.js b/test/unit/core/row-filtering.spec.js index 731085f386..3bb36bdd65 100644 --- a/test/unit/core/row-filtering.spec.js +++ b/test/unit/core/row-filtering.spec.js @@ -229,9 +229,7 @@ describe('rowSearcher', function() { var ret = rowSearcher.search(grid, rows, columns); - expect(ret[0].visible).toBe(false); - expect(ret[1].visible).toBe(true); - expect(ret[2].visible).toBe(true); + expect(ret.length).toBe(2); }); it('should filter by 0', function() { @@ -239,9 +237,7 @@ describe('rowSearcher', function() { var ret = rowSearcher.search(grid, rows, columns); - expect(ret[0].visible).toBe(false); - expect(ret[1].visible).toBe(false); - expect(ret[2].visible).toBe(true); + expect(ret.length).toBe(1); }); }); @@ -252,7 +248,7 @@ describe('rowSearcher', function() { var ret = rowSearcher.search(grid, rows, columns); - expect(ret.length).toEqual(2); + expect(ret.length).toEqual(3); }); }); From d9f9bf7cb5531f8b072d1041b4ed530c1ec8b679 Mon Sep 17 00:00:00 2001 From: Paul Lambert Date: Fri, 10 Apr 2015 21:09:40 +1200 Subject: [PATCH 108/173] Feat(treeView): allows the grid to be used as a tree Similar to grouping and expandable in some respects, this feature allows you to have expandable rows within the same grid, with the same template, and use some of your data rows as parent nodes, and others as children. --- misc/tutorial/209_grouping.ngdoc | 15 +- misc/tutorial/215_treeView.ngdoc | 116 +++ src/features/grouping/js/grouping.js | 26 +- src/features/tree-view/js/tree-view.js | 824 ++++++++++++++++++ src/features/tree-view/less/tree-view.less | 10 + .../templates/treeViewExpandAllButtons.html | 2 + .../templates/treeViewHeaderCell.html | 5 + .../templates/treeViewRowHeader.html | 3 + .../templates/treeViewRowHeaderButtons.html | 4 + src/features/tree-view/test/tree-view.spec.js | 194 +++++ src/js/core/factories/GridApi.js | 4 + 11 files changed, 1186 insertions(+), 17 deletions(-) create mode 100644 misc/tutorial/215_treeView.ngdoc create mode 100644 src/features/tree-view/js/tree-view.js create mode 100644 src/features/tree-view/less/tree-view.less create mode 100644 src/features/tree-view/templates/treeViewExpandAllButtons.html create mode 100644 src/features/tree-view/templates/treeViewHeaderCell.html create mode 100644 src/features/tree-view/templates/treeViewRowHeader.html create mode 100644 src/features/tree-view/templates/treeViewRowHeaderButtons.html create mode 100644 src/features/tree-view/test/tree-view.spec.js diff --git a/misc/tutorial/209_grouping.ngdoc b/misc/tutorial/209_grouping.ngdoc index 8bfe7a1201..75e1b70658 100644 --- a/misc/tutorial/209_grouping.ngdoc +++ b/misc/tutorial/209_grouping.ngdoc @@ -55,13 +55,12 @@ to allow people to start using it. Notable outstandings are: Options to watch out for include: - `groupingIndent`: the expand buttons are indented by a number of pixels (default 10) as the grouping - level gets deeper. Larger values look nicer, but mean that you probably need to make your groupHeader - wider if you're allowing deep grouping -- `groupingRowHeaderWidth`: the width of the grouping row header, important as above + level gets deeper. Larger values look nicer, but take up more space +- `groupingRowHeaderWidth`: the base width of the grouping row header - `groupingSuppressAggregationText`: if your column has a cellFilter, the insertion of text (e.g. 'min: xxxx') - usually breaks the cellFilter. So you can suppress the aggregation text, but then you don't get a clear - visual indication of what sort of aggregation is going on. Refer the example below, the balance column with - an average + usually breaks the cellFilter. So you can suppress the aggregation text, but then you don't get a clear + visual indication of what sort of aggregation is going on. Refer the example below, the balance column with + an average - `groupingShowCounts`: set to false if you don't like the counts against the groupHeaders @@ -82,7 +81,7 @@ we can't easily see that it's an average. - var app = angular.module('app', ['ngAnimate', 'ngTouch', 'ui.grid', 'ui.grid.grouping', 'ui.grid.pinning' ]); + var app = angular.module('app', ['ngAnimate', 'ngTouch', 'ui.grid', 'ui.grid.grouping' ]); app.controller('MainCtrl', ['$scope', '$http', '$interval', 'uiGridGroupingConstants', function ($scope, $http, $interval, uiGridGroupingConstants ) { $scope.gridOptions = { @@ -132,7 +131,7 @@ we can't easily see that it's an average. -
    +
    diff --git a/misc/tutorial/215_treeView.ngdoc b/misc/tutorial/215_treeView.ngdoc new file mode 100644 index 0000000000..0b1039e901 --- /dev/null +++ b/misc/tutorial/215_treeView.ngdoc @@ -0,0 +1,116 @@ +@ngdoc overview +@name Tutorial: 215 Tree View +@description The tree view feature allows you to create a tree from your grid, specifying which +of your data rows are nodes and which are leaves. + +In your data you tell us the nodes by setting the property $$treeLevel on a given row. Levels +start at 0 and increase as you move down the tree. + +If you wish to load your tree incrementally, you can listen to the rowExpanded event, which will +tell you whenever a row is expanded. You can then retrieve additional data from the server and +splice it into the data array at the right point, the grid will automatically render the data +when it arrives. + +In general it doesn't make sense to allow sorting when you're using the grid as a tree - the +structure of the data is very positional, and if the user were to sort the data it would break +the tree. + +TreeView is still alpha, and under development, however it is included in the distribution files +to allow people to start using it. Notable outstandings are: +- doesn't calculate or display counts of child nodes anywhere, it would be nice if it did +- it would be nice to display an hourglass or icon whilst additional data was loading, the current + arrangement means the grid doesn't know whether or not you're adding additional data +- perhaps we could permit sorting of the nodes, and the children within each node, rather than sorting the + whole data set. This would be a whole new sort algorithm though +- it might be nice if nodes with no children could have their + removed, we'd need some way to be sure + that these weren't nodes for which children are going to be dynamically loaded + +Options to watch out for include: + +- `treeViewIndent`: the expand buttons are indented by a number of pixels (default 10) as the tree + level gets deeper. Larger values look nicer +- `treeViewRowHeaderWidth`: the width of the tree row header + +@example +In this example most of the data is loaded on initial page load. The nodes under Guerrero Lopez, however, +are loaded only when that row is expanded. They have a 2 second delay to simulate loading from a server. + + + + var app = angular.module('app', ['ngAnimate', 'ngTouch', 'ui.grid', 'ui.grid.treeView' ]); + + app.controller('MainCtrl', ['$scope', '$http', '$interval', 'uiGridTreeViewConstants', function ($scope, $http, $interval, uiGridTreeViewConstants ) { + $scope.gridOptions = { + enableSorting: false, + enableFiltering: false, + columnDefs: [ + { name: 'name', width: '30%' }, + { name: 'gender', width: '20%' }, + { name: 'age', width: '20%' }, + { name: 'company', width: '25%' }, + { name: 'state', width: '35%' }, + { name: 'balance', width: '25%', cellFilter: 'currency' } + ], + onRegisterApi: function( gridApi ) { + $scope.gridApi = gridApi; + $scope.gridApi.treeView.on.rowExpanded($scope, function(row) { + if( row.entity.$$hashKey === $scope.gridOptions.data[50].$$hashKey && !$scope.nodeLoaded ) { + $interval(function() { + $scope.gridOptions.data.splice(51,0, + {name: 'Dynamic 1', gender: 'female', age: 53, company: 'Griddable grids', balance: 38000, $$treeLevel: 1}, + {name: 'Dynamic 2', gender: 'male', age: 18, company: 'Griddable grids', balance: 29000, $$treeLevel: 1} + ); + $scope.nodeLoaded = true; + }, 2000, 1); + } + }); + } + }; + + $http.get('/data/500_complex.json') + .success(function(data) { + for ( var i = 0; i < data.length; i++ ){ + data[i].state = data[i].address.state; + data[i].balance = Number( data[i].balance.slice(1).replace(/,/,'') ); + } + data[0].$$treeLevel = 0; + data[1].$$treeLevel = 1; + data[10].$$treeLevel = 1; + data[20].$$treeLevel = 0; + data[25].$$treeLevel = 1; + data[50].$$treeLevel = 0; + data[51].$$treeLevel = 0; + $scope.gridOptions.data = data; + }); + + $scope.expandAll = function(){ + $scope.gridApi.treeView.expandAllRows(); + }; + + $scope.toggleRow = function( rowNum ){ + $scope.gridApi.treeView.toggleRowTreeViewState($scope.gridApi.grid.renderContainers.body.visibleRowCache[rowNum]); + }; + }]); + + + +
    + + + +
    +
    +
    + + + .grid { + width: 500px; + height: 400px; + } + + + var gridTestUtils = require('../../test/e2e/gridTestUtils.spec.js'); + describe( '215 tree view', function() { + }); + +
    diff --git a/src/features/grouping/js/grouping.js b/src/features/grouping/js/grouping.js index 2dc1c65ad5..650096839a 100644 --- a/src/features/grouping/js/grouping.js +++ b/src/features/grouping/js/grouping.js @@ -52,10 +52,6 @@ * row cache without calling the processors, and once we've built the logic into the rowProcessors we may as * well use it all the time. * - * Note that we don't really manipulate row visibility directly - we set the reasonInvisible.grouping - * flag, and then ask the row to calculate it's own visibility. This means we should work fine with - * filtering - filtered rows wouldn't get included in our grouping logic. - * *
    *
    * @@ -248,13 +244,25 @@ * @name collapseRow * @methodOf ui.grid.grouping.api:PublicApi * @description collapse all children of the specified row. When - * you expand the row again, all grandchildren will be collapsed - * @param {gridRow} row the row you wish to expand + * you expand the row again, all grandchildren will retain their state + * @param {gridRow} row the row you wish to collapse */ collapseRow: function ( row ) { service.collapseRow(grid, row); }, + /** + * @ngdoc function + * @name collapseRowChildren + * @methodOf ui.grid.grouping.api:PublicApi + * @description collapse all children of the specified row. When + * you expand the row again, all grandchildren will be collapsed + * @param {gridRow} row the row you wish to collapse + */ + collapseRowChildren: function ( row ) { + service.collapseRowChildren(grid, row); + }, + /** * @ngdoc function * @name getGrouping @@ -405,7 +413,7 @@ /** * @ngdoc object - * @name groupingRowHeaderWidth + * @name groupingRowHeaderBaseWidth * @propertyOf ui.grid.grouping.api:GridOptions * @description Base width of the grouping header, provides for a single level of grouping. This * is incremented by `groupingIndent` for each extra level @@ -1030,7 +1038,7 @@ return; } - service.setAllNodes(row.expandedState, uiGridGroupingConstants.COLLAPSED); + service.setAllNodes(row.expandedState, uiGridGroupingConstants.EXPANDED); grid.queueGridRefresh(); }, @@ -1513,7 +1521,7 @@ var groupingRowHeaderDef = { name: uiGridGroupingConstants.groupingRowHeaderColName, displayName: '', - width: uiGridCtrl.grid.options.groupingRowHeaderWidth, + width: uiGridCtrl.grid.options.groupingRowHeaderBaseWidth, minWidth: 10, cellTemplate: 'ui-grid/groupingRowHeader', headerCellTemplate: 'ui-grid/groupingHeaderCell', diff --git a/src/features/tree-view/js/tree-view.js b/src/features/tree-view/js/tree-view.js new file mode 100644 index 0000000000..34a5d68f65 --- /dev/null +++ b/src/features/tree-view/js/tree-view.js @@ -0,0 +1,824 @@ +(function () { + 'use strict'; + + /** + * @ngdoc overview + * @name ui.grid.treeView + * @description + * + * # ui.grid.treeView + * This module provides a tree view of the data that it is provided, with nodes in that + * tree and leaves. Unlike grouping, the tree is an inherent property of the data and must + * be provided with your data array. If you are using treeView you probably should disable sorting. + * + * Filtering is plausible, but requires some reworking to work with treeView - ideally the + * parent nodes would be shown whenever a child node or leaf node under them matched the filter + * + * Design information: + * ------------------- + * + * The raw data that is provided must come with a $$treeLevel on any non-leaf node. TreeView + * will run a rowsProcessor to set expand buttons alongside these nodes, and will maintain the + * expand/collapse state of each node. + * + * In future a count of the direct children of each node could optionally be calculated and displayed + * alongside the node - the current issue is deciding where to display that. For now we calculate it + * but don't display it. + * + * In future the count could be used to remove the + from a row that doesn't actually have any children. + * + * Optionally the treeView can be populated only when nodes are clicked on. This will provide callbacks when + * nodes are expanded, requesting the additional data. The node will be set to expanded, and when the data + * is added to the grid then it will automatically be displayed by the rowsProcessor. + * + * Treeview adds information to the rows + * - treeLevel - if present and > -1 tells us the level (level 0 is the top level) + * - expandedState = object: pointer to the node in the grid.treeView.rowExpandedStates that refers + * to this row, allowing us to manipulate the state + * + * Since the logic is baked into the rowsProcessors, it should get triggered whenever + * row order or filtering or anything like that is changed. We recall the expanded state + * across invocations of the rowsProcessors by putting it into the grid.treeView.rowExpandedStates hash. + * + * By default rows are collapsed, which means all data rows have their visible property + * set to false, and only level 0 group rows are set to visible. + * + * We rely on the rowsProcessors to do the actual expanding and collapsing, so we set the flags we want into + * grid.treeView.rowExpandedStates, then call refresh. This is because we can't easily change the visible + * row cache without calling the processors, and once we've built the logic into the rowProcessors we may as + * well use it all the time. + * + *
    + *
    + * + *
    + */ + + var module = angular.module('ui.grid.treeView', ['ui.grid']); + + /** + * @ngdoc object + * @name ui.grid.treeView.constant:uiGridTreeViewConstants + * + * @description constants available in treeView module + * + */ + module.constant('uiGridTreeViewConstants', { + featureName: "treeView", + treeViewRowHeaderColName: 'treeViewRowHeaderCol', + EXPANDED: 'expanded', + COLLAPSED: 'collapsed' + }); + + /** + * @ngdoc service + * @name ui.grid.treeView.service:uiGridTreeViewService + * + * @description Services for treeView features + */ + module.service('uiGridTreeViewService', ['$q', 'uiGridTreeViewConstants', 'gridUtil', 'GridRow', 'gridClassFactory', 'i18nService', 'uiGridConstants', + function ($q, uiGridTreeViewConstants, gridUtil, GridRow, gridClassFactory, i18nService, uiGridConstants) { + + var service = { + + initializeGrid: function (grid, $scope) { + + //add feature namespace and any properties to grid for needed + /** + * @ngdoc object + * @name ui.grid.treeView.grid:treeView + * + * @description Grid properties and functions added for treeView + */ + grid.treeView = {}; + + /** + * @ngdoc property + * @propertyOf ui.grid.treeView.grid:treeView + * @name numberLevels + * + * @description Total number of tree levels currently used, calculated by the rowsProcessor by + * retaining the highest tree level it sees + */ + grid.treeView.numberLevels = 0; + + /** + * @ngdoc property + * @propertyOf ui.grid.treeView.grid:treeView + * @name expandAll + * + * @description Whether or not the expandAll box is selected + */ + grid.treeView.expandAll = false; + + /** + * @ngdoc property + * @propertyOf ui.grid.treeView.grid:treeView + * @name rowExpandedStates + * + * @description Nested hash that holds all the expanded states based on the nodes. + * We use the row.uid as the key into the hash, only because we need a key. + * + * ``` + * { + * uiGrid-DXNP: { + * state: 'expanded', + * uiGrid-DAP: { state: 'expanded' }, + * uiGrid-BBB: { state: 'collapsed' }, + * uiGrid-AAA: { state: 'expanded' }, + * uiGrid-CCC: { state: 'collapsed' } + * }, + * uiGrid-DXNG: { + * state: 'collapsed', + * uiGrid-DDD: { state: 'expanded' }, + * uiGrid-XXX: { state: 'collapsed' }, + * uiGrid-YYY: { state: 'expanded' } + * } + * } + * ``` + * Missing values are false - meaning they aren't expanded. + * + * This is used because the rowProcessors run every time the grid is refreshed, so + * we'd lose the expanded state every time the grid was refreshed. This instead gives + * us a reliable lookup that persists across rowProcessors. + * + */ + grid.treeView.rowExpandedStates = {}; + + service.defaultGridOptions(grid.options); + + grid.registerRowsProcessor(service.treeRows, 410); + + /** + * @ngdoc object + * @name ui.grid.treeView.api:PublicApi + * + * @description Public Api for treeView feature + */ + var publicApi = { + events: { + treeView: { + /** + * @ngdoc event + * @eventOf ui.grid.treeView.api:PublicApi + * @name rowExpanded + * @description raised whenever a row is expanded. If you are dynamically + * rendering your tree you can listen to this event, and then retrieve + * the children of this row and load them into the grid data. + * + * When the data is loaded the grid will automatically refresh to show these new rows + * + *
    +                 *      gridApi.treeView.on.rowExpanded(scope,function(row){})
    +                 * 
    + * @param {gridRow} row the row that was expanded. You can also + * retrieve the grid from this row with row.grid + */ + rowExpanded: {}, + + /** + * @ngdoc event + * @eventOf ui.grid.treeView.api:PublicApi + * @name rowCollapsed + * @description raised whenever a row is collapsed. Doesn't really have + * a purpose at the moment, included for symmetry + * + *
    +                 *      gridApi.treeView.on.rowCollapsed(scope,function(row){})
    +                 * 
    + * @param {gridRow} row the row that was collapsed. You can also + * retrieve the grid from this row with row.grid + */ + rowCollapsed: {} + } + }, + methods: { + treeView: { + /** + * @ngdoc function + * @name expandAllRows + * @methodOf ui.grid.treeView.api:PublicApi + * @description Expands all tree rows + */ + expandAllRows: function () { + service.expandAllRows(grid); + }, + + /** + * @ngdoc function + * @name collapseAllRows + * @methodOf ui.grid.treeView.api:PublicApi + * @description collapse all tree rows + */ + collapseAllRows: function () { + service.collapseAllRows(grid); + }, + + /** + * @ngdoc function + * @name toggleRowTreeViewState + * @methodOf ui.grid.treeView.api:PublicApi + * @description call expand if the row is collapsed, collapse if it is expanded + * @param {gridRow} row the row you wish to toggle + */ + toggleRowTreeViewState: function (row) { + service.toggleRowTreeViewState(grid, row); + }, + + /** + * @ngdoc function + * @name expandRow + * @methodOf ui.grid.treeView.api:PublicApi + * @description expand the immediate children of the specified row + * @param {gridRow} row the row you wish to expand + */ + expandRow: function (row) { + service.expandRow(grid, row); + }, + + /** + * @ngdoc function + * @name expandRowChildren + * @methodOf ui.grid.treeView.api:PublicApi + * @description expand all children of the specified row + * @param {gridRow} row the row you wish to expand + */ + expandRowChildren: function (row) { + service.expandRowChildren(grid, row); + }, + + /** + * @ngdoc function + * @name collapseRow + * @methodOf ui.grid.treeView.api:PublicApi + * @description collapse the specified row. When + * you expand the row again, all grandchildren will retain their state + * @param {gridRow} row the row you wish to collapse + */ + collapseRow: function ( row ) { + service.collapseRow(grid, row); + }, + + /** + * @ngdoc function + * @name collapseRowChildren + * @methodOf ui.grid.treeView.api:PublicApi + * @description collapse all children of the specified row. When + * you expand the row again, all grandchildren will be collapsed + * @param {gridRow} row the row you wish to collapse children for + */ + collapseRowChildren: function ( row ) { + service.collapseRowChildren(grid, row); + }, + + /** + * @ngdoc function + * @name getGrouping + * @methodOf ui.grid.treeView.api:PublicApi + * @description Get the tree state for this grid, + * used by the saveState feature + * Returned treeView is an object + * `{ expandedState: hash }` + * where expandedState is a hash of the currently expanded nodes + * + * @returns {object} treeView state + */ + getTreeView: function () { + return { expandedState: grid.treeView.rowExpandedStates }; + }, + + /** + * @ngdoc function + * @name setTreeView + * @methodOf ui.grid.treeView.api:PublicApi + * @description Set the expanded states of the tree + * @param {object} config the config you want to apply, in the format + * provided out by getTreeView + */ + setTreeView: function ( config ) { + if ( typeof(config.expandedState) !== 'undefined' ){ + grid.treeView.rowExpandedStates = config.expandedState; + } + } + } + } + }; + + grid.api.registerEventsFromObject(publicApi.events); + + grid.api.registerMethodsFromObject(publicApi.methods); + + }, + + defaultGridOptions: function (gridOptions) { + //default option to true unless it was explicitly set to false + /** + * @ngdoc object + * @name ui.grid.treeView.api:GridOptions + * + * @description GridOptions for treeView feature, these are available to be + * set using the ui-grid {@link ui.grid.class:GridOptions gridOptions} + */ + + /** + * @ngdoc object + * @name enableTreeView + * @propertyOf ui.grid.treeView.api:GridOptions + * @description Enable row tree view for entire grid. + *
    Defaults to true + */ + gridOptions.enableTreeView = gridOptions.enableTreeView !== false; + + /** + * @ngdoc object + * @name treeViewRowHeaderBaseWidth + * @propertyOf ui.grid.treeView.api:GridOptions + * @description Base width of the treeView header, provides for a single level of tree. This + * is incremented by `treeViewIndent` for each extra level + *
    Defaults to 30 + */ + gridOptions.treeViewRowHeaderBaseWidth = gridOptions.treeViewRowHeaderBaseWidth || 30; + + /** + * @ngdoc object + * @name treeViewIndent + * @propertyOf ui.grid.treeView.api:GridOptions + * @description Number of pixels of indent for the icon at each treeView level, wider indents are visually more pleasing, + * but will make the tree view row header wider + *
    Defaults to 10 + */ + gridOptions.treeViewIndent = gridOptions.treeViewIndent || 10; + }, + + + /** + * @ngdoc function + * @name expandAllRows + * @methodOf ui.grid.treeView.service:uiGridTreeViewService + * @description Expands all nodes in the tree + * + * @param {Grid} grid grid object + */ + expandAllRows: function (grid) { + service.setAllNodes( grid, grid.treeView.rowExpandedStates, uiGridTreeViewConstants.EXPANDED ); + grid.queueGridRefresh(); + }, + + + /** + * @ngdoc function + * @name collapseAllRows + * @methodOf ui.grid.treeView.service:uiGridTreeViewService + * @description Collapses all nodes in the tree + * + * @param {Grid} grid grid object + */ + collapseAllRows: function (grid) { + service.setAllNodes( grid, grid.treeView.rowExpandedStates, uiGridTreeViewConstants.COLLAPSED ); + grid.queueGridRefresh(); + }, + + + /** + * @ngdoc function + * @name setAllNodes + * @methodOf ui.grid.treeView.service:uiGridTreeViewService + * @description Works through a subset of grid.treeView.rowExpandedStates, setting + * all child nodes (and their descendents) of the provided node to the given state. + * + * Calls itself recursively on all nodes so as to achieve this. + * + * @param {Grid} grid the grid we're operating on (so we can raise events) + * @param {object} expandedStatesSubset the portion of the tree that we want to update + * @param {string} targetState the state we want to set it to + */ + setAllNodes: function (grid, expandedStatesSubset, targetState) { + // set this node - if this is a node (first invocation in the recursion doesn't have a root node) + if ( typeof(expandedStatesSubset.state) !== 'undefined' && expandedStatesSubset.state !== targetState ){ + expandedStatesSubset.state = targetState; + if ( targetState === uiGridTreeViewConstants.EXPANDED ){ + grid.api.treeView.raise.rowExpanded(expandedStatesSubset.row); + } else { + grid.api.treeView.raise.rowCollapsed(expandedStatesSubset.row); + } + } + + // set all child nodes + angular.forEach(expandedStatesSubset, function( childNode, key){ + if (key !== 'state' && key !== 'row'){ + service.setAllNodes(grid, childNode, targetState); + } + }); + }, + + + /** + * @ngdoc function + * @name toggleRowTreeViewState + * @methodOf ui.grid.treeView.service:uiGridTreeViewService + * @description Toggles the expand or collapse state of this grouped row. + * If the row isn't a groupHeader, does nothing. + * + * @param {Grid} grid grid object + * @param {GridRow} row the row we want to toggle + */ + toggleRowTreeViewState: function ( grid, row ){ + if ( typeof(row.treeLevel) === 'undefined' || row.treeLevel === null || row.treeLevel < 0 ){ + return; + } + + if (row.treeExpandedState.state === uiGridTreeViewConstants.EXPANDED){ + service.collapseRow(grid, row); + } else { + service.expandRow(grid, row); + } + + grid.queueGridRefresh(); + }, + + + /** + * @ngdoc function + * @name expandRow + * @methodOf ui.grid.treeView.service:uiGridTreeViewService + * @description Expands this specific row, showing only immediate children. + * + * @param {Grid} grid grid object + * @param {GridRow} row the row we want to expand + */ + expandRow: function ( grid, row ){ + if ( typeof(row.treeLevel) === 'undefined' || row.treeLevel === null || row.treeLevel < 0 ){ + return; + } + + if ( row.treeExpandedState.state !== uiGridTreeViewConstants.EXPANDED ){ + row.treeExpandedState.state = uiGridTreeViewConstants.EXPANDED; + grid.api.treeView.raise.rowExpanded(row); + grid.queueGridRefresh(); + } + }, + + + /** + * @ngdoc function + * @name expandRowChildren + * @methodOf ui.grid.treeView.service:uiGridTreeViewService + * @description Expands this specific row, showing all children. + * + * @param {Grid} grid grid object + * @param {GridRow} row the row we want to expand + */ + expandRowChildren: function ( grid, row ){ + if ( typeof(row.treeLevel) === 'undefined' || row.treeLevel === null || row.treeLevel < 0 ){ + return; + } + + service.setAllNodes(grid, row.treeExpandedState, uiGridTreeViewConstants.EXPANDED); + grid.queueGridRefresh(); + }, + + + /** + * @ngdoc function + * @name collapseRow + * @methodOf ui.grid.treeView.service:uiGridTreeViewService + * @description Collapses this specific row + * + * @param {Grid} grid grid object + * @param {GridRow} row the row we want to collapse + */ + collapseRow: function( grid, row ){ + if ( typeof(row.treeLevel) === 'undefined' || row.treeLevel === null || row.treeLevel < 0 ){ + return; + } + + if ( row.treeExpandedState.state !== uiGridTreeViewConstants.COLLAPSED ){ + row.treeExpandedState.state = uiGridTreeViewConstants.COLLAPSED; + grid.api.treeView.raise.rowCollapsed(row); + grid.queueGridRefresh(); + } + }, + + + /** + * @ngdoc function + * @name collapseRowChildren + * @methodOf ui.grid.treeView.service:uiGridTreeViewService + * @description Collapses this specific row and all children + * + * @param {Grid} grid grid object + * @param {GridRow} row the row we want to collapse + */ + collapseRowChildren: function( grid, row ){ + if ( typeof(row.treeLevel) === 'undefined' || row.treeLevel === null || row.treeLevel < 0 ){ + return; + } + + service.setAllNodes(grid, row.treeExpandedState, uiGridTreeViewConstants.COLLAPSED); + grid.queueGridRefresh(); + }, + + + /** + * @ngdoc function + * @name treeRows + * @methodOf ui.grid.treeView.service:uiGridTreeViewService + * @description The rowProcessor that adds the nodes to the tree, and sets the visible + * state of each row based on it's parent state + * + * Assumes it is always called after the sorting processor + * + * Processes all the rows in order, setting the group level based on the $$treeLevel in the associated + * entity, and setting the visible state based on the parent's state. + * + * Calculates the deepest level of tree whilst it goes, and updates that so that the header column can be correctly + * sized. + * + * @param {array} renderableRows the rows we want to process, usually the output from the previous rowProcessor + * @returns {array} the updated rows, including our new group rows + */ + treeRows: function( renderableRows ) { + if (renderableRows.length === 0){ + return renderableRows; + } + + var grid = this; + var currentLevel = 0; + var currentState = uiGridTreeViewConstants.EXPANDED; + var parents = []; + + var updateState = function( row ) { + row.treeLevel = row.entity.$$treeLevel; + + if ( !row.visible ){ + return; + } + + if ( row.treeLevel <= currentLevel ){ + // pop any levels that aren't parents of this level + while ( row.treeLevel <= currentLevel ){ + parents.pop(); + currentLevel--; + } + + // reset our current state based on the new parent, set to expanded if this is a root node + if ( parents.length > 0 ){ + currentState = service.setCurrentState(parents); + } else { + currentState = uiGridTreeViewConstants.EXPANDED; + } + } + + // set visibility based on the parent's state + if ( currentState === uiGridTreeViewConstants.COLLAPSED ){ + row.visible = false; + } else { + row.visible = true; + } + + // if this row is a node, then add it to the parents array + if ( typeof(row.treeLevel) !== 'undefined' && row.treeLevel > -1 ){ + service.addOrUseState(grid, row, parents); + currentLevel++; + currentState = service.setCurrentState(parents); + } + + + // update the tree number of levels, so we can set header width if we need to + if ( grid.treeView.numberLevels < row.treeLevel ){ + grid.treeView.numberLevels = row.treeLevel; + } + }; + + renderableRows.forEach(updateState); + + var newWidth = grid.options.treeViewRowHeaderBaseWidth + grid.options.treeViewIndent * grid.treeView.numberLevels; + var rowHeader = grid.getColumn(uiGridTreeViewConstants.treeViewRowHeaderColName); + if ( rowHeader && newWidth !== rowHeader.width ){ + rowHeader.width = newWidth; + grid.queueRefresh(); + } + return renderableRows.filter(function (row) { return row.visible; }); + }, + + /** + * @ngdoc function + * @name addOrUseState + * @methodOf ui.grid.treeView.service:uiGridTreeViewService + * @description If a state already exists for this row with the right parents, use that state, + * otherwise create a new state for this row and set it's expand/collapse to the same as it's parent. + * + * @param {grid} grid the grid we're operating on + * @param {gridRow} row the row we want to set + * @param {array} parents an array of the parents this row should have + * @returns {undefined} updates the parents array, updates the row to have a treeExpandedState, and updates the + * grid.treeView.expandedStates + */ + addOrUseState: function( grid, row, parents ){ + if ( row.entity.$$treeLevel === 0 ){ + if ( typeof(grid.treeView.rowExpandedStates[row.uid]) === 'undefined' ) { + grid.treeView.rowExpandedStates[row.uid] = { state: uiGridTreeViewConstants.COLLAPSED, row: row }; + } + row.treeExpandedState = grid.treeView.rowExpandedStates[row.uid]; + } else { + var parentState = parents[parents.length - 1].treeExpandedState; + if ( typeof(parentState[row.uid]) === 'undefined') { + parentState[row.uid] = { state: parentState.state, row: row }; + } + row.treeExpandedState = parentState[row.uid]; + } + parents.push(row); + }, + + + /** + * @ngdoc function + * @name setCurrentState + * @methodOf ui.grid.treeView.service:uiGridTreeViewService + * @description Looks at the parents array to determine our current state. + * If any node in the hierarchy is collapsed, then return collapsed, otherwise return + * expanded. + * + * @param {array} parents an array of the parents this row should have + * @returns {string} the state we should be setting to any nodes we see + */ + setCurrentState: function( parents ){ + var currentState = uiGridTreeViewConstants.EXPANDED; + parents.forEach( function(parent){ + if ( parent.treeExpandedState.state === uiGridTreeViewConstants.COLLAPSED ){ + currentState = uiGridTreeViewConstants.COLLAPSED; + } + }); + + return currentState; + } + + }; + + return service; + + }]); + + /** + * @ngdoc directive + * @name ui.grid.treeView.directive:uiGridTreeView + * @element div + * @restrict A + * + * @description Adds treeView features to grid + * + * @example + + + var app = angular.module('app', ['ui.grid', 'ui.grid.treeView']); + + app.controller('MainCtrl', ['$scope', function ($scope) { + $scope.data = [ + { name: 'Bob', title: 'CEO' }, + { name: 'Frank', title: 'Lowly Developer' } + ]; + + $scope.columnDefs = [ + {name: 'name', enableCellEdit: true}, + {name: 'title', enableCellEdit: true} + ]; + + $scope.gridOptions = { columnDefs: $scope.columnDefs, data: $scope.data }; + }]); + + +
    +
    +
    +
    +
    + */ + module.directive('uiGridTreeView', ['uiGridTreeViewConstants', 'uiGridTreeViewService', '$templateCache', + function (uiGridTreeViewConstants, uiGridTreeViewService, $templateCache) { + return { + replace: true, + priority: 0, + require: '^uiGrid', + scope: false, + compile: function () { + return { + pre: function ($scope, $elm, $attrs, uiGridCtrl) { + if (uiGridCtrl.grid.options.enableTreeView !== false){ + uiGridTreeViewService.initializeGrid(uiGridCtrl.grid, $scope); + var treeViewRowHeaderDef = { + name: uiGridTreeViewConstants.treeViewRowHeaderColName, + displayName: '', + width: uiGridCtrl.grid.options.treeViewRowHeaderBaseWidth, + minWidth: 10, + cellTemplate: 'ui-grid/treeViewRowHeader', + headerCellTemplate: 'ui-grid/treeViewHeaderCell', + enableColumnResizing: false, + enableColumnMenu: false, + exporterSuppressExport: true, + allowCellFocus: true + }; + + uiGridCtrl.grid.addRowHeaderColumn(treeViewRowHeaderDef); + } + }, + post: function ($scope, $elm, $attrs, uiGridCtrl) { + + } + }; + } + }; + }]); + + + /** + * @ngdoc directive + * @name ui.grid.treeView.directive:uiGridTreeViewRowHeaderButtons + * @element div + * + * @description Provides the expand/collapse button on groupHeader rows + */ + module.directive('uiGridTreeViewRowHeaderButtons', ['$templateCache', 'uiGridTreeViewService', + function ($templateCache, uiGridTreeViewService) { + return { + replace: true, + restrict: 'E', + template: $templateCache.get('ui-grid/treeViewRowHeaderButtons'), + scope: true, + require: '^uiGrid', + link: function($scope, $elm, $attrs, uiGridCtrl) { + var self = uiGridCtrl.grid; + $scope.treeViewButtonClick = function(row, evt) { + uiGridTreeViewService.toggleRowTreeViewState(self, row, evt); + }; + } + }; + }]); + + + /** + * @ngdoc directive + * @name ui.grid.treeView.directive:uiGridTreeViewExpandAllButtons + * @element div + * + * @description Provides the expand/collapse all button + */ + module.directive('uiGridTreeViewExpandAllButtons', ['$templateCache', 'uiGridTreeViewService', + function ($templateCache, uiGridTreeViewService) { + return { + replace: true, + restrict: 'E', + template: $templateCache.get('ui-grid/treeViewExpandAllButtons'), + scope: false, + link: function($scope, $elm, $attrs, uiGridCtrl) { + var self = $scope.col.grid; + + $scope.headerButtonClick = function(row, evt) { + if ( self.treeView.expandAll ){ + uiGridTreeViewService.collapseAllRows(self, evt); + self.treeView.expandAll = false; + } else { + uiGridTreeViewService.expandAllRows(self, evt); + self.treeView.expandAll = true; + } + }; + } + }; + }]); + + /** + * @ngdoc directive + * @name ui.grid.treeView.directive:uiGridViewport + * @element div + * + * @description Stacks on top of ui.grid.uiGridViewport to set formatting on a tree view header row + */ + module.directive('uiGridViewport', + ['$compile', 'uiGridConstants', 'uiGridTreeViewConstants', 'gridUtil', '$parse', 'uiGridTreeViewService', + function ($compile, uiGridConstants, uiGridTreeViewConstants, gridUtil, $parse, uiGridTreeViewService) { + return { + priority: -200, // run after default directive + scope: false, + compile: function ($elm, $attrs) { + var rowRepeatDiv = angular.element($elm.children().children()[0]); + + var existingNgClass = rowRepeatDiv.attr("ng-class"); + var newNgClass = ''; + if ( existingNgClass ) { + newNgClass = existingNgClass.slice(0, -1) + ",'ui-grid-tree-view-header-row': row.treeLevel > -1}"; + } else { + newNgClass = "{'ui-grid-tree-view-header-row': row.treeLevel > -1}"; + } + rowRepeatDiv.attr("ng-class", newNgClass); + + return { + pre: function ($scope, $elm, $attrs, controllers) { + + }, + post: function ($scope, $elm, $attrs, controllers) { + } + }; + } + }; + }]); + +})(); diff --git a/src/features/tree-view/less/tree-view.less b/src/features/tree-view/less/tree-view.less new file mode 100644 index 0000000000..ab94c729f8 --- /dev/null +++ b/src/features/tree-view/less/tree-view.less @@ -0,0 +1,10 @@ +@import '../../../less/variables'; + +.ui-grid-group-header-row { + font-weight: bold !important; +} + +.ui-grid-grouping-row-header-buttons.ui-grid-group-header { + cursor: pointer; + opacity: 1; +} diff --git a/src/features/tree-view/templates/treeViewExpandAllButtons.html b/src/features/tree-view/templates/treeViewExpandAllButtons.html new file mode 100644 index 0000000000..f6f126a871 --- /dev/null +++ b/src/features/tree-view/templates/treeViewExpandAllButtons.html @@ -0,0 +1,2 @@ +
    +
    \ No newline at end of file diff --git a/src/features/tree-view/templates/treeViewHeaderCell.html b/src/features/tree-view/templates/treeViewHeaderCell.html new file mode 100644 index 0000000000..4c4b02e8ff --- /dev/null +++ b/src/features/tree-view/templates/treeViewHeaderCell.html @@ -0,0 +1,5 @@ +
    +
    + +
    +
    diff --git a/src/features/tree-view/templates/treeViewRowHeader.html b/src/features/tree-view/templates/treeViewRowHeader.html new file mode 100644 index 0000000000..5a0475c579 --- /dev/null +++ b/src/features/tree-view/templates/treeViewRowHeader.html @@ -0,0 +1,3 @@ +
    + +
    diff --git a/src/features/tree-view/templates/treeViewRowHeaderButtons.html b/src/features/tree-view/templates/treeViewRowHeaderButtons.html new file mode 100644 index 0000000000..30cda00931 --- /dev/null +++ b/src/features/tree-view/templates/treeViewRowHeaderButtons.html @@ -0,0 +1,4 @@ +
    + +   +
    \ No newline at end of file diff --git a/src/features/tree-view/test/tree-view.spec.js b/src/features/tree-view/test/tree-view.spec.js new file mode 100644 index 0000000000..85ad7fd960 --- /dev/null +++ b/src/features/tree-view/test/tree-view.spec.js @@ -0,0 +1,194 @@ +ddescribe('ui.grid.treeView uiGridTreeViewService', function () { + var uiGridTreeViewService; + var uiGridTreeViewConstants; + var gridClassFactory; + var grid; + var $rootScope; + var $scope; + var GridRow; + + beforeEach(module('ui.grid.treeView')); + + beforeEach(inject(function (_uiGridTreeViewService_,_gridClassFactory_, $templateCache, _uiGridTreeViewConstants_, + _$rootScope_, _GridRow_) { + uiGridTreeViewService = _uiGridTreeViewService_; + uiGridTreeViewConstants = _uiGridTreeViewConstants_; + gridClassFactory = _gridClassFactory_; + $rootScope = _$rootScope_; + $scope = $rootScope.$new(); + GridRow = _GridRow_; + + $templateCache.put('ui-grid/uiGridCell', '
    '); + $templateCache.put('ui-grid/editableCell', '
    '); + + grid = gridClassFactory.createGrid({}); + grid.options.columnDefs = [ + {field: 'col0'}, + {field: 'col1'}, + {field: 'col2'}, + {field: 'col3'} + ]; + + _uiGridTreeViewService_.initializeGrid(grid, $scope); + var data = []; + for (var i = 0; i < 10; i++) { + data.push({col0: 'a_' + Math.floor(i/4), col1: 'b_' + Math.floor(i/2), col2: 'c_' + i, col3: 'd_' + i}); + } + data[0].$$treeLevel = 0; + data[1].$$treeLevel = 1; + data[3].$$treeLevel = 1; + data[4].$$treeLevel = 2; + data[7].$$treeLevel = 0; + data[9].$$treeLevel = 1; + + grid.options.data = data; + + grid.buildColumns(); + grid.modifyRows(grid.options.data); + })); + + describe( 'treeRows', function() { + it( 'tree the rows', function() { + spyOn(gridClassFactory, 'rowTemplateAssigner').andCallFake( function() {}); + + var treeRows = uiGridTreeViewService.treeRows.call( grid, grid.rows.slice(0) ); + expect( treeRows.length ).toEqual( 2, 'only the level 0 rows are visible' ); + + expect( grid.treeView.numberLevels).toEqual(2); + }); + + it( 'expandAll', function() { + spyOn(gridClassFactory, 'rowTemplateAssigner').andCallFake( function() {}); + + var expandCount = 0; + grid.api.treeView.on.rowExpanded( $scope, function(row){ + expandCount++; + }); + + var treeRows = uiGridTreeViewService.treeRows.call( grid, grid.rows.slice(0) ); + expect( treeRows.length ).toEqual( 2, 'only the level 0 rows are visible' ); + + grid.api.treeView.expandAllRows(); + grid.rows.forEach(function( row ){ + row.visible = true; + }); + treeRows = uiGridTreeViewService.treeRows.call( grid, grid.rows.slice(0) ); + expect( treeRows.length ).toEqual( 10, 'all rows are visible' ); + expect( expandCount ).toEqual(6); + }); + + it( 'expandRow', function() { + spyOn(gridClassFactory, 'rowTemplateAssigner').andCallFake( function() {}); + + var expandCount = 0; + grid.api.treeView.on.rowExpanded( $scope, function(row){ + expandCount++; + }); + + var treeRows = uiGridTreeViewService.treeRows.call( grid, grid.rows.slice(0) ); + expect( treeRows.length ).toEqual( 2, 'only the level 0 rows are visible' ); + + grid.api.treeView.expandRow(grid.rows[0]); + grid.rows.forEach(function( row ){ + row.visible = true; + }); + treeRows = uiGridTreeViewService.treeRows.call( grid, grid.rows.slice(0) ); + expect( treeRows.length ).toEqual( 4, 'children of row 0 are also visible' ); + + expect( expandCount ).toEqual(1); + }); + + it( 'expandRowChildren', function() { + spyOn(gridClassFactory, 'rowTemplateAssigner').andCallFake( function() {}); + + var expandCount = 0; + grid.api.treeView.on.rowExpanded( $scope, function(row){ + expandCount++; + }); + + var treeRows = uiGridTreeViewService.treeRows.call( grid, grid.rows.slice(0) ); + expect( treeRows.length ).toEqual( 2, 'only the level 0 rows are visible' ); + + grid.api.treeView.expandRowChildren(grid.rows[0]); + grid.rows.forEach(function( row ){ + row.visible = true; + }); + treeRows = uiGridTreeViewService.treeRows.call( grid, grid.rows.slice(0) ); + expect( treeRows.length ).toEqual( 8, 'all children of row 0 are also visible' ); + + expect( expandCount ).toEqual(4, 'called for row 0, 1, 3 and 4'); + }); + + it( 'collapseRow', function() { + spyOn(gridClassFactory, 'rowTemplateAssigner').andCallFake( function() {}); + + var collapseCount = 0; + grid.api.treeView.on.rowCollapsed( $scope, function(row){ + collapseCount++; + }); + + var treeRows = uiGridTreeViewService.treeRows.call( grid, grid.rows.slice(0) ); + expect( treeRows.length ).toEqual( 2, 'only the level 0 rows are visible' ); + + grid.api.treeView.expandAllRows(); + grid.api.treeView.collapseRow(grid.rows[7]); + grid.rows.forEach(function( row ){ + row.visible = true; + }); + treeRows = uiGridTreeViewService.treeRows.call( grid, grid.rows.slice(0) ); + expect( treeRows.length ).toEqual( 8, 'children of row 7 are hidden' ); + expect( collapseCount ).toEqual( 1 ); + }); + + it( 'collapseRowChildren', function() { + spyOn(gridClassFactory, 'rowTemplateAssigner').andCallFake( function() {}); + + var collapseCount = 0; + grid.api.treeView.on.rowCollapsed( $scope, function(row){ + collapseCount++; + }); + + var treeRows = uiGridTreeViewService.treeRows.call( grid, grid.rows.slice(0) ); + expect( treeRows.length ).toEqual( 2, 'only the level 0 rows are visible' ); + + grid.api.treeView.expandAllRows(); + grid.api.treeView.collapseRowChildren(grid.rows[0]); + grid.rows.forEach(function( row ){ + row.visible = true; + }); + treeRows = uiGridTreeViewService.treeRows.call( grid, grid.rows.slice(0) ); + expect( treeRows.length ).toEqual( 4, 'children of row 0 are hidden' ); + expect( collapseCount ).toEqual( 4 ); + }); + + it( 'collapseAllRows', function() { + spyOn(gridClassFactory, 'rowTemplateAssigner').andCallFake( function() {}); + + var collapseCount = 0; + grid.api.treeView.on.rowCollapsed( $scope, function(row){ + collapseCount++; + }); + + var treeRows = uiGridTreeViewService.treeRows.call( grid, grid.rows.slice(0) ); + expect( treeRows.length ).toEqual( 2, 'only the level 0 rows are visible' ); + + grid.api.treeView.expandAllRows(); + grid.rows.forEach(function( row ){ + row.visible = true; + }); + treeRows = uiGridTreeViewService.treeRows.call( grid, grid.rows.slice(0) ); + expect( treeRows.length ).toEqual( 10, 'all rows visible' ); + + grid.api.treeView.collapseAllRows(); + grid.rows.forEach(function( row ){ + row.visible = true; + }); + treeRows = uiGridTreeViewService.treeRows.call( grid, grid.rows.slice(0) ); + expect( treeRows.length ).toEqual( 2, 'only level 0 is visible' ); + expect( collapseCount ).toEqual( 6 ); + }); + }); + + + +}); \ No newline at end of file diff --git a/src/js/core/factories/GridApi.js b/src/js/core/factories/GridApi.js index c27db0e775..bd1b741332 100644 --- a/src/js/core/factories/GridApi.js +++ b/src/js/core/factories/GridApi.js @@ -242,6 +242,10 @@ // gridUtil.logDebug('Creating on event method ' + featureName + '.on.' + eventName); feature.on[eventName] = function (scope, handler, _this) { + if ( typeof(scope.$on) === 'undefined' ){ + gridUtil.logError('asked to listen on ' + featureName + '.on.' + eventName + ' but scope wasn\'t passed in the input parameters, you probably forgot to provide it, not registering'); + return; + } var deregAngularOn = registerEventWithAngular(eventId, handler, self.grid, _this); //track our listener so we can turn off and on From 32489aa4eae27734d12245b59875e356855403b0 Mon Sep 17 00:00:00 2001 From: Paul Lambert Date: Sat, 11 Apr 2015 15:21:19 +1200 Subject: [PATCH 109/173] Doco(tutorials): renumber so that all features are 2xx, not 3xx --- misc/tutorial/{304_grid_menu.ngdoc => 121_grid_menu.ngdoc} | 2 +- .../{306_expandable_grid.ngdoc => 216_expandable_grid.ngdoc} | 2 +- .../{310_column_moving.ngdoc => 217_column_moving.ngdoc} | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) rename misc/tutorial/{304_grid_menu.ngdoc => 121_grid_menu.ngdoc} (99%) rename misc/tutorial/{306_expandable_grid.ngdoc => 216_expandable_grid.ngdoc} (99%) rename misc/tutorial/{310_column_moving.ngdoc => 217_column_moving.ngdoc} (98%) diff --git a/misc/tutorial/304_grid_menu.ngdoc b/misc/tutorial/121_grid_menu.ngdoc similarity index 99% rename from misc/tutorial/304_grid_menu.ngdoc rename to misc/tutorial/121_grid_menu.ngdoc index 4aa34eef10..e88e180be6 100644 --- a/misc/tutorial/304_grid_menu.ngdoc +++ b/misc/tutorial/121_grid_menu.ngdoc @@ -1,5 +1,5 @@ @ngdoc overview -@name Tutorial: 304 Grid Menu +@name Tutorial: 121 Grid Menu @description The grid menu can be enabled through setting the gridOption `enableGridMenu`. This adds a settings icon in the top right of the grid, which floats above the column header. The menu by default gives access to show/hide columns, but can be customised to show additional diff --git a/misc/tutorial/306_expandable_grid.ngdoc b/misc/tutorial/216_expandable_grid.ngdoc similarity index 99% rename from misc/tutorial/306_expandable_grid.ngdoc rename to misc/tutorial/216_expandable_grid.ngdoc index 8fbd3ba42a..2fa8d97b65 100644 --- a/misc/tutorial/306_expandable_grid.ngdoc +++ b/misc/tutorial/216_expandable_grid.ngdoc @@ -1,5 +1,5 @@ @ngdoc overview -@name Tutorial: 306 Expandable grid +@name Tutorial: 216 Expandable grid @description Module 'ui.grid.expandable' adds the subgrid feature to grid. To show the subgrid you need to provide following grid option: diff --git a/misc/tutorial/310_column_moving.ngdoc b/misc/tutorial/217_column_moving.ngdoc similarity index 98% rename from misc/tutorial/310_column_moving.ngdoc rename to misc/tutorial/217_column_moving.ngdoc index d55d334fd8..34352a7e87 100644 --- a/misc/tutorial/310_column_moving.ngdoc +++ b/misc/tutorial/217_column_moving.ngdoc @@ -1,5 +1,5 @@ @ngdoc overview -@name Tutorial: 310 Column Moving +@name Tutorial: 217 Column Moving @description Feature ui.grid.moveColumns allows moving column to a different position. To enable, you must include the `ui.grid.moveColumns` module From 95638040771ab097bb1e381f76a2b83695a7b224 Mon Sep 17 00:00:00 2001 From: Paul Lambert Date: Sun, 12 Apr 2015 16:02:12 +1200 Subject: [PATCH 110/173] Enh(filtering): support custom filter template Similar in concept to #2918, but works with templates rather than directives. Allows use of widgets other than the standard widgets, and provides a tutorial on using the capability to produce custom dropdowns and the like. --- misc/tutorial/103_filtering.ngdoc | 4 + misc/tutorial/306_custom_filters.ngdoc | 184 ++++++++++++++++++ src/features/tree-view/test/tree-view.spec.js | 2 +- src/js/core/directives/ui-grid-filter.js | 27 +++ src/js/core/directives/ui-grid-header-cell.js | 6 + src/js/core/factories/GridApi.js | 2 +- src/js/core/services/gridClassFactory.js | 99 +++++----- src/templates/ui-grid/ui-grid-filter.html | 19 ++ src/templates/ui-grid/uiGridHeaderCell.html | 21 +- 9 files changed, 289 insertions(+), 75 deletions(-) create mode 100644 misc/tutorial/306_custom_filters.ngdoc create mode 100644 src/js/core/directives/ui-grid-filter.js create mode 100644 src/templates/ui-grid/ui-grid-filter.html diff --git a/misc/tutorial/103_filtering.ngdoc b/misc/tutorial/103_filtering.ngdoc index 93912a0a05..60d06af952 100644 --- a/misc/tutorial/103_filtering.ngdoc +++ b/misc/tutorial/103_filtering.ngdoc @@ -58,6 +58,10 @@ and If you need to internationalize the labels you'll need to complete that before providing the selectOptions array. +### Cancel icon +By default the filter shows a cancel X beside the dropdown. You can set `disableCancelFilterButton: true` to suppress +this button. + @example diff --git a/misc/tutorial/306_custom_filters.ngdoc b/misc/tutorial/306_custom_filters.ngdoc new file mode 100644 index 0000000000..ffe8f2a466 --- /dev/null +++ b/misc/tutorial/306_custom_filters.ngdoc @@ -0,0 +1,184 @@ +@ngdoc overview +@name Tutorial: 306 Custom Filters +@description + +You can provide custom templates for your filter objects, allowing you to use custom widgets +or to implement a filter that calls custom functions in your controller. + +For example, you might implement a filter widget that sets query parameters and passes them to your +http query, and that triggers a refresh whenever the filter changes. + +Alternatively you might implement a bootstrap modal that supports multiple selection, and then +insert those multiple selections into a regex that is used by the filter logic. + +You can bind to any of the information within the filters object within your template/directive. + +In this example we do both of those things: we create a directive that pops up a modal window and +allows selection of one or more from a list of values (using an embedded ng-grid), and we implement +a bootstrap dropdown. + +@example + + + var app = angular.module('app', ['ngAnimate', 'ngTouch', 'ui.grid', 'ui.grid.selection']); + + app.controller('MainCtrl', ['$scope', '$http', 'uiGridConstants', function ($scope, $http, uiGridConstants) { + var today = new Date(); + var nextWeek = new Date(); + nextWeek.setDate(nextWeek.getDate() + 7); + + $scope.gridOptions = { + enableFiltering: true, + onRegisterApi: function(gridApi){ + $scope.gridApi = gridApi; + }, + columnDefs: [ + { field: 'name' }, + { field: 'gender', + filterHeaderTemplate: '
    ', + filter: { + term: 1, + options: [ {id: 1, value: 'male'}, {id: 2, value: 'female'}] // custom attribute that goes with custom directive above + }, + cellFilter: 'mapGender' }, + { field: 'company', enableFiltering: false }, + { field: 'email', enableFiltering: false }, + { field: 'phone', enableFiltering: false }, + { field: 'age', + filterHeaderTemplate: '
    ' + }, + { field: 'mixedDate', cellFilter: 'date', width: '15%', enableFiltering: false } + ] + }; + + $http.get('/data/500_complex.json') + .success(function(data) { + $scope.gridOptions.data = data; + $scope.gridOptions.data[0].age = -5; + + data.forEach( function addDates( row, index ){ + row.mixedDate = new Date(); + row.mixedDate.setDate(today.getDate() + ( index % 14 ) ); + row.gender = row.gender==='male' ? '1' : '2'; + }); + }); + }]) + + .filter('mapGender', function() { + var genderHash = { + 1: 'male', + 2: 'female' + }; + + return function(input) { + if (!input){ + return ''; + } else { + return genderHash[input]; + } + }; + }) + + .directive('myCustomDropdown', function() { + return { + template: '' + }; + }) + + .controller('myCustomModalCtrl', function( $scope, $compile, $timeout ) { + var $elm; + + $scope.showAgeModal = function() { + $scope.listOfAges = []; + + $scope.col.grid.appScope.gridOptions.data.forEach( function ( row ) { + if ( $scope.listOfAges.indexOf( row.age ) === -1 ) { + $scope.listOfAges.push( row.age ); + } + }); + $scope.listOfAges.sort(); + + $scope.gridOptions = { + data: [], + enableColumnMenus: false, + onRegisterApi: function( gridApi) { + $scope.gridApi = gridApi; + + if ( $scope.colFilter && $scope.colFilter.listTerm ){ + $timeout(function() { + $scope.colFilter.listTerm.forEach( function( age ) { + var entities = $scope.gridOptions.data.filter( function( row ) { + return row.age === age; + }); + + if( entities.length > 0 ) { + $scope.gridApi.selection.selectRow(entities[0]); + } + }); + }); + } + } + }; + + $scope.listOfAges.forEach(function( age ) { + $scope.gridOptions.data.push({age: age}); + }); + + var html = ''; + $elm = angular.element(html); + angular.element(document.body).prepend($elm); + + $compile($elm)($scope); + + }; + + $scope.close = function() { + var ages = $scope.gridApi.selection.getSelectedRows(); + $scope.colFilter.listTerm = []; + + ages.forEach( function( age ) { + $scope.colFilter.listTerm.push( age.age ); + }); + + $scope.colFilter.term = $scope.colFilter.listTerm.join(', '); + $scope.colFilter.condition = new RegExp($scope.colFilter.listTerm.join('|')); + + if ($elm) { + $elm.remove(); + } + }; + }) + + + .directive('myCustomModal', function() { + return { + template: '', + controller: 'myCustomModalCtrl' + }; + }) + ; + +
    + +
    +
    +
    +
    + + .grid { + width: 650px; + height: 400px; + } + .modalGrid { + width: 100px; + height: 200px; + } + .modal-dialog { + width: 150px; + } + + + var gridTestUtils = require('../../test/e2e/gridTestUtils.spec.js'); + }); + +
    \ No newline at end of file diff --git a/src/features/tree-view/test/tree-view.spec.js b/src/features/tree-view/test/tree-view.spec.js index 85ad7fd960..4e4b299e3e 100644 --- a/src/features/tree-view/test/tree-view.spec.js +++ b/src/features/tree-view/test/tree-view.spec.js @@ -1,4 +1,4 @@ -ddescribe('ui.grid.treeView uiGridTreeViewService', function () { +describe('ui.grid.treeView uiGridTreeViewService', function () { var uiGridTreeViewService; var uiGridTreeViewConstants; var gridClassFactory; diff --git a/src/js/core/directives/ui-grid-filter.js b/src/js/core/directives/ui-grid-filter.js new file mode 100644 index 0000000000..e9a642f70c --- /dev/null +++ b/src/js/core/directives/ui-grid-filter.js @@ -0,0 +1,27 @@ +(function(){ + 'use strict'; + + angular.module('ui.grid').directive('uiGridFilter', function ($compile, $templateCache) { + + return { + compile: function() { + return { + pre: function ($scope, $elm, $attrs, controllers) { + $scope.col.updateFilters = function( filterable ){ + $elm.children().remove(); + if ( filterable ){ + var template = $scope.col.filterHeaderTemplate; + + $elm.append($compile(template)($scope)); + } + }; + + $scope.$on( '$destroy', function() { + delete $scope.col.updateFilters; + }); + } + }; + } + }; + }); +})(); diff --git a/src/js/core/directives/ui-grid-header-cell.js b/src/js/core/directives/ui-grid-header-cell.js index e084dba6aa..2cb1325037 100644 --- a/src/js/core/directives/ui-grid-header-cell.js +++ b/src/js/core/directives/ui-grid-header-cell.js @@ -208,12 +208,18 @@ } // Figure out whether this column is filterable or not + var oldFilterable = $scope.filterable; if (uiGridCtrl.grid.options.enableFiltering && $scope.col.enableFiltering) { $scope.filterable = true; } else { $scope.filterable = false; } + + if ( oldFilterable !== $scope.filterable && typeof($scope.col.updateFilters) !== 'undefined'){ + + $scope.col.updateFilters($scope.filterable); + } // figure out whether we support column menus if ($scope.col.grid.options && $scope.col.grid.options.enableColumnMenus !== false && diff --git a/src/js/core/factories/GridApi.js b/src/js/core/factories/GridApi.js index bd1b741332..b4015e5272 100644 --- a/src/js/core/factories/GridApi.js +++ b/src/js/core/factories/GridApi.js @@ -242,7 +242,7 @@ // gridUtil.logDebug('Creating on event method ' + featureName + '.on.' + eventName); feature.on[eventName] = function (scope, handler, _this) { - if ( typeof(scope.$on) === 'undefined' ){ + if ( !scope || typeof(scope.$on) === 'undefined' ){ gridUtil.logError('asked to listen on ' + featureName + '.on.' + eventName + ' but scope wasn\'t passed in the input parameters, you probably forgot to provide it, not registering'); return; } diff --git a/src/js/core/services/gridClassFactory.js b/src/js/core/services/gridClassFactory.js index e0d1c2a734..d07b9ad48f 100644 --- a/src/js/core/services/gridClassFactory.js +++ b/src/js/core/services/gridClassFactory.js @@ -101,20 +101,31 @@ var templateGetPromises = []; - /** - * @ngdoc property - * @name headerCellTemplate - * @propertyOf ui.grid.class:GridOptions.columnDef - * @description a custom template for the header for this column. The default - * is ui-grid/uiGridHeaderCell - * - */ - if (!colDef.headerCellTemplate) { - col.providedHeaderCellTemplate = 'ui-grid/uiGridHeaderCell'; - } else { - col.providedHeaderCellTemplate = colDef.headerCellTemplate; - } + // Abstracts the standard template processing we do for every template type + var processTemplate = function( templateType, providedType, defaultTemplate, filterType ) { + if ( !colDef[templateType] ){ + col[providedType] = defaultTemplate; + } else { + col[providedType] = colDef[templateType]; + } + + templateGetPromises.push(gridUtil.getTemplate(col[providedType]) + .then( + function (template) { + if ( filterType ){ + col[templateType] = template.replace(uiGridConstants.CUSTOM_FILTERS, col[filterType] ? "|" + col[filterType] : ""); + } else { + col[templateType] = template; + } + }, + function (res) { + throw new Error("Couldn't fetch/use colDef." + templateType + " '" + colDef[templateType] + "'"); + }) + ); + }; + + /** * @ngdoc property * @name cellTemplate @@ -124,11 +135,18 @@ * must contain a div that can receive focus. * */ - if (!colDef.cellTemplate) { - col.providedCellTemplate = 'ui-grid/uiGridCell'; - } else { - col.providedCellTemplate = colDef.cellTemplate; - } + processTemplate( 'cellTemplate', 'providedCellTemplate', 'ui-grid/uiGridCell', 'cellFilter' ); + col.cellTemplatePromise = templateGetPromises[0]; + + /** + * @ngdoc property + * @name headerCellTemplate + * @propertyOf ui.grid.class:GridOptions.columnDef + * @description a custom template for the header for this column. The default + * is ui-grid/uiGridHeaderCell + * + */ + processTemplate( 'headerCellTemplate', 'providedHeaderCellTemplate', 'ui-grid/uiGridHeaderCell', 'headerCellFilter' ); /** * @ngdoc property @@ -138,48 +156,23 @@ * is ui-grid/uiGridFooterCell * */ - if (!colDef.footerCellTemplate) { - col.providedFooterCellTemplate = 'ui-grid/uiGridFooterCell'; - } else { - col.providedFooterCellTemplate = colDef.footerCellTemplate; - } - - col.cellTemplatePromise = gridUtil.getTemplate(col.providedCellTemplate); - templateGetPromises.push(col.cellTemplatePromise - .then( - function (template) { - col.cellTemplate = template.replace(uiGridConstants.CUSTOM_FILTERS, col.cellFilter ? "|" + col.cellFilter : ""); - }, - function (res) { - throw new Error("Couldn't fetch/use colDef.cellTemplate '" + colDef.cellTemplate + "'"); - }) - ); + processTemplate( 'footerCellTemplate', 'providedFooterCellTemplate', 'ui-grid/uiGridFooterCell', 'footerCellFilter' ); - templateGetPromises.push(gridUtil.getTemplate(col.providedHeaderCellTemplate) - .then( - function (template) { - col.headerCellTemplate = template.replace(uiGridConstants.CUSTOM_FILTERS, col.headerCellFilter ? "|" + col.headerCellFilter : ""); - }, - function (res) { - throw new Error("Couldn't fetch/use colDef.headerCellTemplate '" + colDef.headerCellTemplate + "'"); - }) - ); - - templateGetPromises.push(gridUtil.getTemplate(col.providedFooterCellTemplate) - .then( - function (template) { - col.footerCellTemplate = template.replace(uiGridConstants.CUSTOM_FILTERS, col.footerCellFilter ? "|" + col.footerCellFilter : ""); - }, - function (res) { - throw new Error("Couldn't fetch/use colDef.footerCellTemplate '" + colDef.footerCellTemplate + "'"); - }) - ); + /** + * @ngdoc property + * @name filterHeaderTemplate + * @propertyOf ui.grid.class:GridOptions.columnDef + * @description a custom template for the filter input. The default is ui-grid/ui-grid-filter + * + */ + processTemplate( 'filterHeaderTemplate', 'providedFilterHeaderTemplate', 'ui-grid/ui-grid-filter' ); // Create a promise for the compiled element function col.compiledElementFnDefer = $q.defer(); return $q.all(templateGetPromises); }, + rowTemplateAssigner: function rowTemplateAssigner(row) { var grid = this; diff --git a/src/templates/ui-grid/ui-grid-filter.html b/src/templates/ui-grid/ui-grid-filter.html new file mode 100644 index 0000000000..467577134f --- /dev/null +++ b/src/templates/ui-grid/ui-grid-filter.html @@ -0,0 +1,19 @@ +
    +
    + + +
    +   +
    +
    + +
    + + +
    +   +
    +
    +
    diff --git a/src/templates/ui-grid/uiGridHeaderCell.html b/src/templates/ui-grid/uiGridHeaderCell.html index a91c1bdf5b..a37a409bb3 100644 --- a/src/templates/ui-grid/uiGridHeaderCell.html +++ b/src/templates/ui-grid/uiGridHeaderCell.html @@ -12,24 +12,5 @@  
    -
    -
    - - -
    -   -
    -
    - -
    - - -
    -   -
    -
    - -
    +
    From 8e3f0da1ad0a50bf19e7a8add071daca3fa84ded Mon Sep 17 00:00:00 2001 From: Paul Lambert Date: Sun, 12 Apr 2015 19:11:18 +1200 Subject: [PATCH 111/173] Fix(doco): fix #3225 doco issues with flatEntityAccess --- misc/tutorial/404_large_data_sets_and_performance.ngdoc | 2 +- src/js/core/factories/GridOptions.js | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/misc/tutorial/404_large_data_sets_and_performance.ngdoc b/misc/tutorial/404_large_data_sets_and_performance.ngdoc index 3cce520a62..2cc21af434 100644 --- a/misc/tutorial/404_large_data_sets_and_performance.ngdoc +++ b/misc/tutorial/404_large_data_sets_and_performance.ngdoc @@ -51,7 +51,7 @@ with a data set of 640,000 rows. {field:'age'} ]; - $http.get('https://cdn.rawgit.com/angular-ui/ui-grid.info/gh-pages/data/10000_complex.json') + $http.get('/data/10000_complex.json') .success(function(data) { for( var i=0; i<6; i++){ data = data.concat(data); diff --git a/src/js/core/factories/GridOptions.js b/src/js/core/factories/GridOptions.js index c6b47e07a5..d96c83e7c4 100644 --- a/src/js/core/factories/GridOptions.js +++ b/src/js/core/factories/GridOptions.js @@ -156,9 +156,9 @@ angular.module('ui.grid') }; /** - * @ngdoc function + * @ngdoc property * @name flatEntityAccess - * @methodOf ui.grid.class:GridOptions + * @propertyOf ui.grid.class:GridOptions * @description Set to true if your columns are all related directly to fields in a flat object structure - i.e. * each of your columns associate directly with a propery one each of the entities in your data array. * From df73bf23d3815f2d83d63e2630b4cafe16fa0e97 Mon Sep 17 00:00:00 2001 From: Paul Lambert Date: Sun, 12 Apr 2015 19:17:53 +1200 Subject: [PATCH 112/173] Fix(pinning): fix #3252 remove double refresh - no longer needed --- src/features/pinning/js/pinning.js | 39 ++++++------------------------ 1 file changed, 7 insertions(+), 32 deletions(-) diff --git a/src/features/pinning/js/pinning.js b/src/features/pinning/js/pinning.js index 7030f3b1e8..80fde3dd7a 100644 --- a/src/features/pinning/js/pinning.js +++ b/src/features/pinning/js/pinning.js @@ -140,32 +140,12 @@ *
    Defaults to false */ if (colDef.pinnedLeft) { - if (col.width === '*') { - // Need to refresh so the width can be calculated. - col.grid.refresh() - .then(function () { - col.renderContainer = 'left'; - col.grid.createLeftContainer(); - }); - } - else { - col.renderContainer = 'left'; - col.grid.createLeftContainer(); - } + col.renderContainer = 'left'; + col.grid.createLeftContainer(); } else if (colDef.pinnedRight) { - if (col.width === '*') { - // Need to refresh so the width can be calculated. - col.grid.refresh() - .then(function () { - col.renderContainer = 'right'; - col.grid.createRightContainer(); - }); - } - else { - col.renderContainer = 'right'; - col.grid.createRightContainer(); - } + col.renderContainer = 'right'; + col.grid.createRightContainer(); } if (!colDef.enablePinning) { @@ -234,15 +214,10 @@ } } - // Need to call refresh twice; once to move our column over to the new render container and then - // a second time to update the grid viewport dimensions with our adjustments grid.refresh() - .then(function () { - grid.refresh() - .then(function() { - grid.api.pinning.raise.columnPinned( col.colDef, container ); - }); - }); + .then(function() { + grid.api.pinning.raise.columnPinned( col.colDef, container ); + }); } }; From cd068a9b510e3830a0be2b6f5c65c88c64bed089 Mon Sep 17 00:00:00 2001 From: Paul Lambert Date: Sun, 12 Apr 2015 19:32:49 +1200 Subject: [PATCH 113/173] Fix(exporter): fix #3230 remove dependency on selection - was unused anyway --- src/features/exporter/js/exporter.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/features/exporter/js/exporter.js b/src/features/exporter/js/exporter.js index f8b580bed9..91d65af8d2 100644 --- a/src/features/exporter/js/exporter.js +++ b/src/features/exporter/js/exporter.js @@ -71,8 +71,8 @@ * * @description Services for exporter feature */ - module.service('uiGridExporterService', ['$q', 'uiGridExporterConstants', 'uiGridSelectionConstants', 'gridUtil', '$compile', '$interval', 'i18nService', - function ($q, uiGridExporterConstants, uiGridSelectionConstants, gridUtil, $compile, $interval, i18nService) { + module.service('uiGridExporterService', ['$q', 'uiGridExporterConstants', 'gridUtil', '$compile', '$interval', 'i18nService', + function ($q, uiGridExporterConstants, gridUtil, $compile, $interval, i18nService) { var service = { From f806c9362d2fa3d553da9e9795b985f0be0f20eb Mon Sep 17 00:00:00 2001 From: Paul Lambert Date: Mon, 13 Apr 2015 08:13:28 +1200 Subject: [PATCH 114/173] doco(columnDef): fix #3236 clarity on columnDef types --- src/js/core/factories/Grid.js | 10 ++++++++-- src/js/core/services/ui-grid-util.js | 10 +++++----- 2 files changed, 13 insertions(+), 7 deletions(-) diff --git a/src/js/core/factories/Grid.js b/src/js/core/factories/Grid.js index 44c4526252..08e1cada9f 100644 --- a/src/js/core/factories/Grid.js +++ b/src/js/core/factories/Grid.js @@ -672,8 +672,14 @@ angular.module('ui.grid') * @propertyOf ui.grid.class:GridOptions.columnDef * @description the type of the column, used in sorting. If not provided then the * grid will guess the type. Add this only if the grid guessing is not to your - * satisfaction. Refer to {@link ui.grid.service:GridUtil.guessType gridUtil.guessType} for - * a list of values the grid knows about. + * satisfaction. One of: + * - 'string' + * - 'boolean' + * - 'number' + * - 'date' + * - 'object' + * - 'numberStr' + * Note that if you choose date, your dates should be in a javascript date type * */ Grid.prototype.assignTypes = function(){ diff --git a/src/js/core/services/ui-grid-util.js b/src/js/core/services/ui-grid-util.js index 2cdfb02d5a..d75d767968 100644 --- a/src/js/core/services/ui-grid-util.js +++ b/src/js/core/services/ui-grid-util.js @@ -403,11 +403,11 @@ module.service('gridUtil', ['$log', '$window', '$document', '$http', '$templateC * * @param {string/number/bool/object} item variable to examine * @returns {string} one of the following - * 'string' - * 'boolean' - * 'number' - * 'date' - * 'object' + * - 'string' + * - 'boolean' + * - 'number' + * - 'date' + * - 'object' */ guessType : function (item) { var itemType = typeof(item); From df377d9b2904eb1784ca91d886b7fb3b98bbceba Mon Sep 17 00:00:00 2001 From: Paul Lambert Date: Mon, 13 Apr 2015 09:12:02 +1200 Subject: [PATCH 115/173] Fix(footer): aggregation wasn't running on first load, fix e2e tests --- src/js/core/factories/GridColumn.js | 2 +- src/js/core/factories/GridOptions.js | 4 ++-- src/js/core/services/ui-grid-util.js | 10 ++++++---- test/unit/core/factories/GridOptions.spec.js | 2 +- test/unit/core/services/ui-grid-util.spec.js | 8 ++++---- 5 files changed, 14 insertions(+), 12 deletions(-) diff --git a/src/js/core/factories/GridColumn.js b/src/js/core/factories/GridColumn.js index 76e71dae9e..b4f04b3871 100644 --- a/src/js/core/factories/GridColumn.js +++ b/src/js/core/factories/GridColumn.js @@ -173,7 +173,7 @@ angular.module('ui.grid') } }; - var throttledUpdateAggregationValue = gridUtil.throttle(updateAggregationValue, self.grid.options.aggregationCalcThrottle); + var throttledUpdateAggregationValue = gridUtil.throttle(updateAggregationValue, self.grid.options.aggregationCalcThrottle, { trailing: true }); diff --git a/src/js/core/factories/GridOptions.js b/src/js/core/factories/GridOptions.js index d96c83e7c4..2d435080ef 100644 --- a/src/js/core/factories/GridOptions.js +++ b/src/js/core/factories/GridOptions.js @@ -303,9 +303,9 @@ angular.module('ui.grid') * @ngdoc property * @name aggregationCalcThrottle * @propertyOf ui.grid.class:GridOptions - * @description Default time in milliseconds to throttle aggregation calcuations, defaults to 1000ms + * @description Default time in milliseconds to throttle aggregation calcuations, defaults to 500ms */ - baseOptions.aggregationCalcThrottle = typeof(baseOptions.aggregationCalcThrottle) !== "undefined" ? baseOptions.aggregationCalcThrottle : 1000; + baseOptions.aggregationCalcThrottle = typeof(baseOptions.aggregationCalcThrottle) !== "undefined" ? baseOptions.aggregationCalcThrottle : 500; /** * @ngdoc property diff --git a/src/js/core/services/ui-grid-util.js b/src/js/core/services/ui-grid-util.js index d75d767968..f1b3d32c1e 100644 --- a/src/js/core/services/ui-grid-util.js +++ b/src/js/core/services/ui-grid-util.js @@ -169,8 +169,8 @@ var uidPrefix = 'uiGrid-'; * * @description Grid utility functions */ -module.service('gridUtil', ['$log', '$window', '$document', '$http', '$templateCache', '$timeout', '$injector', '$q', '$interpolate', 'uiGridConstants', - function ($log, $window, $document, $http, $templateCache, $timeout, $injector, $q, $interpolate, uiGridConstants) { +module.service('gridUtil', ['$log', '$window', '$document', '$http', '$templateCache', '$timeout', '$interval', '$injector', '$q', '$interpolate', 'uiGridConstants', + function ($log, $window, $document, $http, $templateCache, $timeout, $interval, $injector, $q, $interpolate, uiGridConstants) { var s = { getStyles: getStyles, @@ -1078,6 +1078,8 @@ module.service('gridUtil', ['$log', '$window', '$document', '$http', '$templateC * Adapted from debounce function (above) * Potential keys for Params Object are: * trailing (bool) - whether to trigger after throttle time ends if called multiple times + * Updated to use $interval rather than $timeout, as protractor (e2e tests) is able to work with $interval, + * but not with $timeout * @example *
        * var throttledFunc =  gridUtil.throttle(function(){console.log('throttled');}, 500, {trailing: true});
    @@ -1093,7 +1095,7 @@ module.service('gridUtil', ['$log', '$window', '$document', '$http', '$templateC
         function runFunc(endDate){
           lastCall = +new Date();
           func.apply(context, args);
    -      $timeout(function(){ queued = null; }, 0);
    +      $interval(function(){ queued = null; }, 0, 1);
         }
     
         return function(){
    @@ -1106,7 +1108,7 @@ module.service('gridUtil', ['$log', '$window', '$document', '$http', '$templateC
               runFunc();
             }
             else if (options.trailing){
    -          queued = $timeout(runFunc, wait - sinceLast);
    +          queued = $interval(runFunc, wait - sinceLast, 1);
             }
           }
         };
    diff --git a/test/unit/core/factories/GridOptions.spec.js b/test/unit/core/factories/GridOptions.spec.js
    index 5453d63f5c..dfa2177642 100644
    --- a/test/unit/core/factories/GridOptions.spec.js
    +++ b/test/unit/core/factories/GridOptions.spec.js
    @@ -36,7 +36,7 @@ describe('GridOptions factory', function () {
             scrollThreshold: 4,
             excessColumns: 4,
             horizontalScrollThreshold: 2,
    -        aggregationCalcThrottle: 1000,
    +        aggregationCalcThrottle: 500,
             wheelScrollThrottle: 70,
             scrollDebounce: 300,
             enableSorting: true,
    diff --git a/test/unit/core/services/ui-grid-util.spec.js b/test/unit/core/services/ui-grid-util.spec.js
    index 2f3cdf5086..6b6b6e162b 100644
    --- a/test/unit/core/services/ui-grid-util.spec.js
    +++ b/test/unit/core/services/ui-grid-util.spec.js
    @@ -53,7 +53,7 @@ describe('ui.grid.utilService', function() {
           x++;
         };
     
    -    it('prevents multiple function calls', inject(function ($timeout) {
    +    it('prevents multiple function calls', inject(function ($interval) {
           x = 0;
     
           var throttledFunc = gridUtil.throttle(func, 10);
    @@ -61,11 +61,11 @@ describe('ui.grid.utilService', function() {
           throttledFunc();
           throttledFunc();
           expect(x).toEqual(1);
    -      $timeout.flush();
    +      $interval.flush(15);
           expect(x).toEqual(1);
         }));
     
    -    it('queues a final event if trailing param is truthy', inject(function ($timeout) {
    +    it('queues a final event if trailing param is truthy', inject(function ($interval) {
           x = 0;
     
           var throttledFunc = gridUtil.throttle(func, 10, {trailing: true});
    @@ -73,7 +73,7 @@ describe('ui.grid.utilService', function() {
           throttledFunc();
           throttledFunc();
           expect(x).toEqual(1);
    -      $timeout.flush();
    +      $interval.flush(15);
           expect(x).toEqual(2);
         }));
     
    
    From d478e88391b028b8e282c3f9b2dfc4189eccaeb1 Mon Sep 17 00:00:00 2001
    From: Paul Lambert 
    Date: Mon, 13 Apr 2015 09:12:32 +1200
    Subject: [PATCH 116/173] Enh(e2e): add check for filter dropdown
    
    ---
     test/e2e/gridTestUtils.spec.js | 23 +++++++++++++++++++++++
     1 file changed, 23 insertions(+)
    
    diff --git a/test/e2e/gridTestUtils.spec.js b/test/e2e/gridTestUtils.spec.js
    index 7bafeee35f..86de772f32 100644
    --- a/test/e2e/gridTestUtils.spec.js
    +++ b/test/e2e/gridTestUtils.spec.js
    @@ -534,6 +534,29 @@ module.exports = {
         expect( headerCell.all( by.css( '.ui-grid-filter-input' ) ).count() ).toEqual(count);
       },
     
    +  /**
    +  * @ngdoc method
    +  * @methodOf ui.grid.e2eTestLibrary.api:gridTest
    +  * @name expectFilterSelectInColumn
    +  * @description Checks that a filter select dropdown exists in the specified column
    +  * @param {string} gridId the id of the grid that you want to inspect
    +  * @param {integer} colNumber the number of the column (within the visible columns)
    +  * that you want to verify the filter select is in
    +  * @param {integer} count the number filter selects you expect - 0 meaning none, 1 meaning
    +  * a standard filter, 2 meaning a numerical filter with greater than / less than.
    +  *
    +  * @example
    +  * 
    +  *   gridTestUtils.expectFilterSelectInColumn('myGrid', 0, 0);
    +  * 
    + * + */ + expectFilterSelectInColumn: function( gridId, colNumber, count ) { + var headerCell = this.headerCell( gridId, colNumber); + + expect( headerCell.all( by.css( '.ui-grid-filter-select' ) ).count() ).toEqual(count); + }, + /** * @ngdoc method * @methodOf ui.grid.e2eTestLibrary.api:gridTest From cb26b6a7907070bd6c7138c786dfa0134476c1e8 Mon Sep 17 00:00:00 2001 From: Paul Lambert Date: Mon, 13 Apr 2015 09:22:07 +1200 Subject: [PATCH 117/173] Fix(e2e): fix all broken e2e tests --- misc/tutorial/103_filtering.ngdoc | 6 +++--- misc/tutorial/121_grid_menu.ngdoc | 1 + misc/tutorial/306_custom_filters.ngdoc | 1 - misc/tutorial/307_external_sorting.ngdoc | 8 ++++---- misc/tutorial/401_AllFeatures.ngdoc | 2 +- test/e2e/gridTestUtils.spec.js | 24 ++++++++++++++++++++++-- 6 files changed, 31 insertions(+), 11 deletions(-) diff --git a/misc/tutorial/103_filtering.ngdoc b/misc/tutorial/103_filtering.ngdoc index 60d06af952..6b40d75e35 100644 --- a/misc/tutorial/103_filtering.ngdoc +++ b/misc/tutorial/103_filtering.ngdoc @@ -187,13 +187,13 @@ this button. var gridTestUtils = require('../../test/e2e/gridTestUtils.spec.js'); describe('first grid on the page, filtered by male by default', function() { - it('grid should have six visible columns', function () { - gridTestUtils.expectHeaderColumnCount( 'grid1', 6 ); + it('grid should have seven visible columns', function () { + gridTestUtils.expectHeaderColumnCount( 'grid1', 7 ); }); it('filter on 4 columns, filter with greater than/less than on one, one with no filter, then one with one filter', function () { gridTestUtils.expectFilterBoxInColumn( 'grid1', 0, 1 ); - gridTestUtils.expectFilterBoxInColumn( 'grid1', 1, 1 ); + gridTestUtils.expectFilterSelectInColumn( 'grid1', 1, 1 ); gridTestUtils.expectFilterBoxInColumn( 'grid1', 2, 0 ); gridTestUtils.expectFilterBoxInColumn( 'grid1', 3, 1 ); gridTestUtils.expectFilterBoxInColumn( 'grid1', 4, 1 ); diff --git a/misc/tutorial/121_grid_menu.ngdoc b/misc/tutorial/121_grid_menu.ngdoc index e88e180be6..57517020ee 100644 --- a/misc/tutorial/121_grid_menu.ngdoc +++ b/misc/tutorial/121_grid_menu.ngdoc @@ -139,6 +139,7 @@ you can suppress the ability to hide individual columns by setting `enableHiding gridTestUtils.expectHeaderCellValueMatch( 'grid1', 0, 'Name' ); gridTestUtils.expectHeaderCellValueMatch( 'grid1', 1, 'Gender' ); + gridTestUtils.unclickGridMenu( 'grid1'); // menu stays open if change columns gridTestUtils.clickGridMenuItem( 'grid1', 12 ); // there are some hidden menu items, this is company_show gridTestUtils.expectHeaderColumnCount( 'grid1', 3 ); gridTestUtils.expectHeaderCellValueMatch( 'grid1', 0, 'Name' ); diff --git a/misc/tutorial/306_custom_filters.ngdoc b/misc/tutorial/306_custom_filters.ngdoc index ffe8f2a466..a581647e87 100644 --- a/misc/tutorial/306_custom_filters.ngdoc +++ b/misc/tutorial/306_custom_filters.ngdoc @@ -179,6 +179,5 @@ a bootstrap dropdown. var gridTestUtils = require('../../test/e2e/gridTestUtils.spec.js'); - }); \ No newline at end of file diff --git a/misc/tutorial/307_external_sorting.ngdoc b/misc/tutorial/307_external_sorting.ngdoc index a06e6e5583..0d3ac0ad99 100644 --- a/misc/tutorial/307_external_sorting.ngdoc +++ b/misc/tutorial/307_external_sorting.ngdoc @@ -110,15 +110,15 @@ column however, so sorting by it has no effect. it('sort by name by clicking header', function () { gridTestUtils.clickHeaderCell( 'grid1', 0 ); - gridTestUtils.expectCellValueMatch( 'grid1', 0, 0, 'Beryl Rice' ); - gridTestUtils.expectCellValueMatch( 'grid1', 1, 0, 'Bruce Strong' ); + gridTestUtils.expectCellValueMatch( 'grid1', 0, 0, 'Alexander Foley' ); + gridTestUtils.expectCellValueMatch( 'grid1', 1, 0, 'Alisha Myers' ); }); it('reverse sort by name by clicking header', function () { gridTestUtils.clickHeaderCell( 'grid1', 0 ); gridTestUtils.clickHeaderCell( 'grid1', 0 ); - gridTestUtils.expectCellValueMatch( 'grid1', 0, 0, 'Wilder Gonzales' ); - gridTestUtils.expectCellValueMatch( 'grid1', 1, 0, 'Valarie Atkinson' ); + gridTestUtils.expectCellValueMatch( 'grid1', 0, 0, 'Yvonne Parsons' ); + gridTestUtils.expectCellValueMatch( 'grid1', 1, 0, 'Woods Key' ); }); it('return to original sort by name by clicking header', function () { diff --git a/misc/tutorial/401_AllFeatures.ngdoc b/misc/tutorial/401_AllFeatures.ngdoc index 52af778933..5f22e3c897 100644 --- a/misc/tutorial/401_AllFeatures.ngdoc +++ b/misc/tutorial/401_AllFeatures.ngdoc @@ -109,7 +109,7 @@ All features are enabled to get an idea of performance it('should not duplicate the menu options for pinning when resizing a column', function () { element( by.id('refreshButton') ).click(); gridTestUtils.resizeHeaderCell( 'grid1', 1 ); - gridTestUtils.expectVisibleColumnMenuItems( 'grid1', 1, 5) + gridTestUtils.expectVisibleColumnMenuItems( 'grid1', 1, 5); }); }); diff --git a/test/e2e/gridTestUtils.spec.js b/test/e2e/gridTestUtils.spec.js index 86de772f32..26a698f302 100644 --- a/test/e2e/gridTestUtils.spec.js +++ b/test/e2e/gridTestUtils.spec.js @@ -684,7 +684,7 @@ module.exports = { * * @example *
    -  *   gridTestUtils.clickVisibleGridMenuItem('myGrid', 9);
    +  *   gridTestUtils.clickGridMenuItem('myGrid', 9);
       * 
    * */ @@ -693,5 +693,25 @@ module.exports = { gridMenuButton.click(); gridMenuButton.element( by.repeater('item in menuItems').row( itemNumber) ).click(); - } + }, + + /** + * @ngdoc method + * @methodOf ui.grid.e2eTestLibrary.api:gridTest + * @name unclickGridMenu + * @description Closes the grid menu if it's open (opens it if it's closed). + * The grid menu stays open when you change column visibility, it sometimes needs + * to be closed again. + * @param {string} gridId the id of the grid that you want to inspect + * + * @example + *
    +  *   gridTestUtils.unclickGridMenu('myGrid');
    +  * 
    + * + */ + unclickGridMenu: function( gridId ) { + var gridMenuButton = this.getGrid( gridId ).element( by.css ( '.ui-grid-menu-button' ) ); + gridMenuButton.click(); + } }; \ No newline at end of file From 69117353218241d32f6997d285cf6d77941f53df Mon Sep 17 00:00:00 2001 From: Paul Lambert Date: Mon, 13 Apr 2015 10:45:09 +1200 Subject: [PATCH 118/173] Enh(gridMenu): support sort order on the menu items --- misc/tutorial/121_grid_menu.ngdoc | 15 +++++++++++++-- src/features/exporter/js/exporter.js | 18 ++++++++++++------ src/features/importer/js/importer.js | 3 ++- src/js/core/directives/ui-grid-menu-button.js | 13 ++++++++++--- src/less/menu.less | 6 +++--- src/templates/ui-grid/ui-grid-header.html | 1 - 6 files changed, 40 insertions(+), 16 deletions(-) diff --git a/misc/tutorial/121_grid_menu.ngdoc b/misc/tutorial/121_grid_menu.ngdoc index 57517020ee..b0572bfb36 100644 --- a/misc/tutorial/121_grid_menu.ngdoc +++ b/misc/tutorial/121_grid_menu.ngdoc @@ -17,6 +17,8 @@ use i18n on this through the `gridMenuTitleFilter` setting) property `grid` (accessible through `this.grid`. You can pass in your own context by supplying the `context` property to your menu item. It will be accessible through `this.context`. - `leaveOpen`: by default false, if set to true the menu will be left open after the action +- `order`: the order in the menu that you wish your item to be. Columns are 300 -> 300 + numColumns * 2, + importer and exporter are 150 and 200 respectively The exporter feature also adds menu items to this menu. The `exporterMenuCsv` option is set to false, which suppresses csv export. The 'export selected rows' option is only available @@ -31,6 +33,14 @@ internationalization function that waits 1 second then prefixes each column with You can suppress the ability to show and hide columns by setting the gridOption `gridMenuShowHideColumns: false`, you can suppress the ability to hide individual columns by setting `enableHiding` on that columnDef to false. +The gridMenu button is still a bit ugly. If you have the skills to do so we'd welcome a PR that makes it pretty. +In the meantime, you can override the height to fit with your application in your css: +
    +  .ui-grid-menu-button {
    +    height: 31px;
    +  }
    +
    + @example @@ -59,7 +69,8 @@ you can suppress the ability to hide individual columns by setting `enableHiding title: 'Rotate Grid', action: function ($event) { this.grid.element.toggleClass('rotated'); - } + }, + order: 210 } ], onRegisterApi: function( gridApi ){ @@ -67,7 +78,7 @@ you can suppress the ability to hide individual columns by setting `enableHiding // interval of zero just to allow the directive to have initialized $interval( function() { - gridApi.core.addToGridMenu( gridApi.grid, [{ title: 'Dynamic item'}]); + gridApi.core.addToGridMenu( gridApi.grid, [{ title: 'Dynamic item', order: 100}]); }, 0, 1); gridApi.core.on.columnVisibilityChanged( $scope, function( changedColumn ){ diff --git a/src/features/exporter/js/exporter.js b/src/features/exporter/js/exporter.js index 91d65af8d2..66dc3e8c0b 100644 --- a/src/features/exporter/js/exporter.js +++ b/src/features/exporter/js/exporter.js @@ -479,7 +479,8 @@ }, shown: function() { return this.grid.options.exporterMenuCsv && this.grid.options.exporterMenuAllData; - } + }, + order: 200 }, { title: i18nService.getSafeText('gridMenu.exporterVisibleAsCsv'), @@ -488,7 +489,8 @@ }, shown: function() { return this.grid.options.exporterMenuCsv; - } + }, + order: 201 }, { title: i18nService.getSafeText('gridMenu.exporterSelectedAsCsv'), @@ -498,7 +500,8 @@ shown: function() { return this.grid.options.exporterMenuCsv && ( this.grid.api.selection && this.grid.api.selection.getSelectedRows().length > 0 ); - } + }, + order: 202 }, { title: i18nService.getSafeText('gridMenu.exporterAllAsPdf'), @@ -507,7 +510,8 @@ }, shown: function() { return this.grid.options.exporterMenuPdf && this.grid.options.exporterMenuAllData; - } + }, + order: 203 }, { title: i18nService.getSafeText('gridMenu.exporterVisibleAsPdf'), @@ -516,7 +520,8 @@ }, shown: function() { return this.grid.options.exporterMenuPdf; - } + }, + order: 204 }, { title: i18nService.getSafeText('gridMenu.exporterSelectedAsPdf'), @@ -526,7 +531,8 @@ shown: function() { return this.grid.options.exporterMenuPdf && ( this.grid.api.selection && this.grid.api.selection.getSelectedRows().length > 0 ); - } + }, + order: 205 } ]); }, diff --git a/src/features/importer/js/importer.js b/src/features/importer/js/importer.js index c622a32a0d..a41b7b6003 100644 --- a/src/features/importer/js/importer.js +++ b/src/features/importer/js/importer.js @@ -331,7 +331,8 @@ templateUrl: 'ui-grid/importerMenuItemContainer', action: function ($event) { this.grid.api.importer.importAFile( grid ); - } + }, + order: 150 } ]); }, diff --git a/src/js/core/directives/ui-grid-menu-button.js b/src/js/core/directives/ui-grid-menu-button.js index 7837dab955..8bcafa08bd 100644 --- a/src/js/core/directives/ui-grid-menu-button.js +++ b/src/js/core/directives/ui-grid-menu-button.js @@ -196,6 +196,10 @@ angular.module('ui.grid') menuItems = menuItems.concat( service.showHideColumns( $scope ) ); } + menuItems.sort(function(a, b){ + return a.order - b.order; + }); + return menuItems; }, @@ -236,7 +240,8 @@ angular.module('ui.grid') // add header for columns showHideColumns.push({ - title: i18nService.getSafeText('gridMenu.columns') + title: i18nService.getSafeText('gridMenu.columns'), + order: 300 }); $scope.grid.options.gridMenuTitleFilter = $scope.grid.options.gridMenuTitleFilter ? $scope.grid.options.gridMenuTitleFilter : function( title ) { return title; }; @@ -254,7 +259,8 @@ angular.module('ui.grid') return this.context.gridCol.colDef.visible === true || this.context.gridCol.colDef.visible === undefined; }, context: { gridCol: $scope.grid.getColumn(colDef.name || colDef.field) }, - leaveOpen: true + leaveOpen: true, + order: 300 + index * 2 }; service.setMenuItemTitle( menuItem, colDef, $scope.grid ); showHideColumns.push( menuItem ); @@ -270,7 +276,8 @@ angular.module('ui.grid') return !(this.context.gridCol.colDef.visible === true || this.context.gridCol.colDef.visible === undefined); }, context: { gridCol: $scope.grid.getColumn(colDef.name || colDef.field) }, - leaveOpen: true + leaveOpen: true, + order: 300 + index * 2 + 1 }; service.setMenuItemTitle( menuItem, colDef, $scope.grid ); showHideColumns.push( menuItem ); diff --git a/src/less/menu.less b/src/less/menu.less index ce68d2df38..d01a6f997b 100644 --- a/src/less/menu.less +++ b/src/less/menu.less @@ -2,10 +2,11 @@ z-index: 2; position: absolute; right: 0; + top: 0; background: @headerBackgroundColor; border: @gridBorderWidth solid @borderColor; cursor: pointer; - min-height: 27px; + height: 31px; font-weight: normal; } @@ -16,7 +17,7 @@ .ui-grid-menu-button .ui-grid-menu { right: 0; .ui-grid-menu-mid { - overflow-y: scroll; + overflow: scroll; max-height: 300px; border: @gridBorderWidth solid @borderColor; } @@ -25,7 +26,6 @@ .ui-grid-menu { z-index: 2; // So it shows up over grid canvas position: absolute; - overflow: hidden; padding: 0 10px 20px 10px; cursor: pointer; box-sizing: content-box; diff --git a/src/templates/ui-grid/ui-grid-header.html b/src/templates/ui-grid/ui-grid-header.html index 1f07fda565..78b80b4d03 100644 --- a/src/templates/ui-grid/ui-grid-header.html +++ b/src/templates/ui-grid/ui-grid-header.html @@ -10,6 +10,5 @@
    -
    \ No newline at end of file From 3a54616255ec7a8dfbcb483eee4e20d99f5158de Mon Sep 17 00:00:00 2001 From: Paul Lambert Date: Mon, 13 Apr 2015 11:01:31 +1200 Subject: [PATCH 119/173] Enh(saveState): add treeView to save state, fix #3190 --- src/features/saveState/js/saveState.js | 59 +++++++++++++++++-- src/features/saveState/test/saveState.spec.js | 27 ++++++++- 2 files changed, 79 insertions(+), 7 deletions(-) diff --git a/src/features/saveState/js/saveState.js b/src/features/saveState/js/saveState.js index 2c3b7eb1af..570e3cb25a 100644 --- a/src/features/saveState/js/saveState.js +++ b/src/features/saveState/js/saveState.js @@ -20,7 +20,7 @@ *
    */ - var module = angular.module('ui.grid.saveState', ['ui.grid', 'ui.grid.selection', 'ui.grid.cellNav', 'ui.grid.grouping', 'ui.grid.pinning']); + var module = angular.module('ui.grid.saveState', ['ui.grid', 'ui.grid.selection', 'ui.grid.cellNav', 'ui.grid.grouping', 'ui.grid.pinning', 'ui.grid.treeView']); /** * @ngdoc object @@ -253,6 +253,16 @@ *
    Defaults to true */ gridOptions.savePinning = gridOptions.savePinning !== false; + /** + * @ngdoc object + * @name saveTreeView + * @propertyOf ui.grid.saveState.api:GridOptions + * @description Save the treeView configuration. If set to true and the + * treeView feature is not enabled then does nothing. + * + *
    Defaults to true + */ + gridOptions.saveTreeView = gridOptions.saveTreeView !== false; }, @@ -273,6 +283,7 @@ savedState.scrollFocus = service.saveScrollFocus( grid ); savedState.selection = service.saveSelection( grid ); savedState.grouping = service.saveGrouping( grid ); + savedState.treeView = service.saveTreeView( grid ); return savedState; }, @@ -305,9 +316,11 @@ service.restoreGrouping( grid, state.grouping ); } - // refresh twice due to rendering issue. - // specific case: save with column pinned left, hide that column, then restore - grid.refresh().then(function(){ grid.refresh(); }); + if ( state.treeView ){ + service.restoreTreeView( grid, state.treeView ); + } + + grid.refresh(); }, @@ -417,11 +430,11 @@ * @methodOf ui.grid.saveState.service:uiGridSaveStateService * @description Saves the currently selected rows, if the selection feature is enabled * @param {Grid} grid the grid whose state we'd like to save - * @returns {object} the selection state ready to be saved + * @returns {array} the selection state ready to be saved */ saveSelection: function( grid ){ if ( !grid.api.selection || !grid.options.saveSelection ){ - return {}; + return []; } var selection = grid.api.selection.getSelectedGridRows().map( function( gridRow ) { @@ -449,6 +462,23 @@ }, + /** + * @ngdoc function + * @name saveTreeView + * @methodOf ui.grid.saveState.service:uiGridSaveStateService + * @description Saves the tree view state, if the tree feature is enabled + * @param {Grid} grid the grid whose state we'd like to save + * @returns {object} the tree view state ready to be saved + */ + saveTreeView: function( grid ){ + if ( !grid.api.treeView || !grid.options.saveTreeView ){ + return {}; + } + + return grid.api.treeView.getTreeView(); + }, + + /** * @ngdoc function * @name getRowVal @@ -626,6 +656,23 @@ grid.api.grouping.setGrouping( groupingState ); }, + /** + * @ngdoc function + * @name restoreTreeView + * @methodOf ui.grid.saveState.service:uiGridSaveStateService + * @description Restores the tree view configuration, if the tree view feature + * is enabled. + * @param {Grid} grid the grid whose state we'd like to restore + * @param {object} treeViewState the tree view state ready to be restored + */ + restoreTreeView: function( grid, treeViewState ){ + if ( !grid.api.treeView || typeof(treeViewState) === 'undefined' || treeViewState === null || angular.equals(treeViewState, {}) ){ + return; + } + + grid.api.treeView.setTreeView( treeViewState ); + }, + /** * @ngdoc function * @name findRowByIdentity diff --git a/src/features/saveState/test/saveState.spec.js b/src/features/saveState/test/saveState.spec.js index f0299a6de8..fe4f1dddb3 100644 --- a/src/features/saveState/test/saveState.spec.js +++ b/src/features/saveState/test/saveState.spec.js @@ -4,6 +4,7 @@ describe('ui.grid.saveState uiGridSaveStateService', function () { var uiGridSelectionService; var uiGridCellNavService; var uiGridGroupingService; + var uiGridTreeViewService; var uiGridPinningService; var gridClassFactory; var grid; @@ -16,12 +17,14 @@ describe('ui.grid.saveState uiGridSaveStateService', function () { beforeEach(inject(function (_uiGridSaveStateService_, _gridClassFactory_, _uiGridSaveStateConstants_, _$compile_, _$rootScope_, _$document_, _uiGridSelectionService_, - _uiGridCellNavService_, _uiGridGroupingService_, _uiGridPinningService_ ) { + _uiGridCellNavService_, _uiGridGroupingService_, _uiGridTreeViewService_, + _uiGridPinningService_ ) { uiGridSaveStateService = _uiGridSaveStateService_; uiGridSaveStateConstants = _uiGridSaveStateConstants_; uiGridSelectionService = _uiGridSelectionService_; uiGridCellNavService = _uiGridCellNavService_; uiGridGroupingService = _uiGridGroupingService_; + uiGridTreeViewService = _uiGridTreeViewService_; uiGridPinningService = _uiGridPinningService_; gridClassFactory = _gridClassFactory_; $compile = _$compile_; @@ -78,6 +81,7 @@ describe('ui.grid.saveState uiGridSaveStateService', function () { saveSelection: true, saveGrouping: true, saveGroupingExpandedStates: false, + saveTreeView: true, savePinning: true }); }); @@ -95,6 +99,7 @@ describe('ui.grid.saveState uiGridSaveStateService', function () { saveSelection: false, saveGrouping: false, saveGroupingExpandedStates: true, + saveTreeView: false, savePinning: false }; uiGridSaveStateService.defaultGridOptions(options); @@ -109,6 +114,7 @@ describe('ui.grid.saveState uiGridSaveStateService', function () { saveSelection: false, saveGrouping: false, saveGroupingExpandedStates: true, + saveTreeView: false, savePinning: false }); }); @@ -652,6 +658,25 @@ describe('ui.grid.saveState uiGridSaveStateService', function () { }); }); + describe('restoreTreeView', function() { + beforeEach( function() { + grid.api.treeView = { setTreeView: function() {}}; + spyOn( grid.api.treeView, 'setTreeView' ).andCallFake(function() {}); + }); + + it( 'calls setTreeView with config', function() { + uiGridSaveStateService.restoreTreeView( grid, { test: 'test' }); + + expect(grid.api.treeView.setTreeView).toHaveBeenCalledWith( { test: 'test' }); + }); + + it( 'doesn\'t call setTreeView when config missing', function() { + uiGridSaveStateService.restoreTreeView( grid, undefined); + + expect(grid.api.treeView.setTreeView).not.toHaveBeenCalled(); + }); + }); + describe('findRowByIdentity', function() { it('no row identity', function() { From fd6caa323cd2c2aa2242285b6709ede25ba4a9b2 Mon Sep 17 00:00:00 2001 From: Paul Lambert Date: Mon, 13 Apr 2015 11:31:29 +1200 Subject: [PATCH 120/173] Fix(importer): missing grid menu order --- src/features/importer/js/importer.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/features/importer/js/importer.js b/src/features/importer/js/importer.js index a41b7b6003..fdcff340d2 100644 --- a/src/features/importer/js/importer.js +++ b/src/features/importer/js/importer.js @@ -325,14 +325,15 @@ addToMenu: function ( grid ) { grid.api.core.addToGridMenu( grid, [ { - title: i18nService.getSafeText('gridMenu.importerTitle') + title: i18nService.getSafeText('gridMenu.importerTitle'), + order: 150 }, { templateUrl: 'ui-grid/importerMenuItemContainer', action: function ($event) { this.grid.api.importer.importAFile( grid ); }, - order: 150 + order: 151 } ]); }, From 612edccf7659e294c32c23c93c5531c64431daba Mon Sep 17 00:00:00 2001 From: valepu Date: Mon, 13 Apr 2015 16:05:20 +0200 Subject: [PATCH 121/173] Update it.js - add grouping text --- src/js/i18n/it.js | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/src/js/i18n/it.js b/src/js/i18n/it.js index 76baed0703..2cdc07f553 100644 --- a/src/js/i18n/it.js +++ b/src/js/i18n/it.js @@ -61,9 +61,19 @@ invalidCsv: 'Impossibile elaborare il file, sicuro che sia un CSV?', invalidJson: 'Impossibile elaborare il file, sicuro che sia un JSON valido?', jsonNotArray: 'Errore! Il file JSON da importare deve contenere un array.' + }, + grouping: { + group: 'Raggruppa', + ungroup: 'Separa', + aggregate_count: 'Agg: N. Elem.', + aggregate_sum: 'Agg: Somma', + aggregate_max: 'Agg: Massimo', + aggregate_min: 'Agg: Minimo', + aggregate_avg: 'Agg: Media', + aggregate_remove: 'Agg: Rimuovi' } }); return $delegate; }]); }]); -})(); \ No newline at end of file +})(); From 64b6a927d600e799faeaf46a186684fb321c2b00 Mon Sep 17 00:00:00 2001 From: Brian Hann Date: Mon, 13 Apr 2015 09:23:09 -0500 Subject: [PATCH 122/173] chore(Dev): Add .editorconfig Closes #3269 [skip ci] --- .editorconfig | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100644 .editorconfig diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000000..5ed5ba34b0 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,21 @@ +# EditorConfig helps developers define and maintain consistent +# coding styles between different editors and IDEs +# editorconfig.org + +root = true + + +[*] + +# Change these settings to your own preference +indent_style = space +indent_size = 2 + +# We recommend you to keep these unchanged +end_of_line = lf +charset = utf-8 +trim_trailing_whitespace = true +insert_final_newline = true + +[*.md] +trim_trailing_whitespace = false \ No newline at end of file From 56d9b2e61c614f9b14cab41afbe5e8e3ebb9a6b5 Mon Sep 17 00:00:00 2001 From: Brian Hann Date: Mon, 13 Apr 2015 09:24:11 -0500 Subject: [PATCH 123/173] Fix trailing whitespace [skip ci] --- .editorconfig | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.editorconfig b/.editorconfig index 5ed5ba34b0..c2cdfb8ada 100644 --- a/.editorconfig +++ b/.editorconfig @@ -18,4 +18,4 @@ trim_trailing_whitespace = true insert_final_newline = true [*.md] -trim_trailing_whitespace = false \ No newline at end of file +trim_trailing_whitespace = false From ec2c11c97d98209075444c5029b361d78a975d18 Mon Sep 17 00:00:00 2001 From: valepu Date: Mon, 13 Apr 2015 16:37:55 +0200 Subject: [PATCH 124/173] Update it.js Mixed spaces and tabs --- src/js/i18n/it.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/js/i18n/it.js b/src/js/i18n/it.js index 2cdc07f553..bc86c40642 100644 --- a/src/js/i18n/it.js +++ b/src/js/i18n/it.js @@ -62,7 +62,7 @@ invalidJson: 'Impossibile elaborare il file, sicuro che sia un JSON valido?', jsonNotArray: 'Errore! Il file JSON da importare deve contenere un array.' }, - grouping: { + grouping: { group: 'Raggruppa', ungroup: 'Separa', aggregate_count: 'Agg: N. Elem.', From e47e2737790d5af01e1ab479082a63b1ad13e69a Mon Sep 17 00:00:00 2001 From: Paul Lambert Date: Tue, 14 Apr 2015 06:57:19 +1200 Subject: [PATCH 125/173] Fix(gridMenu): unit tests breaking on sauce, suspect unstable sort --- .../unit/core/directives/ui-grid-menu-button.spec.js | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/test/unit/core/directives/ui-grid-menu-button.spec.js b/test/unit/core/directives/ui-grid-menu-button.spec.js index 9ee256c520..dc3324794b 100644 --- a/test/unit/core/directives/ui-grid-menu-button.spec.js +++ b/test/unit/core/directives/ui-grid-menu-button.spec.js @@ -108,9 +108,9 @@ describe('ui-grid-menu-button uiGridGridMenuService', function () { }); it('grab bag of stuff', function () { - grid.options.gridMenuCustomItems = [ { title: 'z' }, { title: 'a' }]; + grid.options.gridMenuCustomItems = [ { title: 'z', order: 11 }, { title: 'a', order: 12 }]; grid.options.gridMenuTitleFilter = function( title ) {return 'fn_' + title;}; - var registeredMenuItems = [ { id: 'customItem1', title: 'x' }, { id: 'customItem2', title: 'y' } ]; + var registeredMenuItems = [ { id: 'customItem1', title: 'x', order: 1 }, { id: 'customItem2', title: 'y', order: 2 } ]; grid.options.columnDefs[1].enableHiding = false; uiGridGridMenuService.addToGridMenu( grid, registeredMenuItems ); @@ -118,10 +118,10 @@ describe('ui-grid-menu-button uiGridGridMenuService', function () { var menuItems = uiGridGridMenuService.getMenuItems( $scope ); expect( menuItems.length ).toEqual(11, 'Should be 11 items, 2 from customItems, 2 from registered, 1 columns header, and 2x3 columns that allow hiding'); - expect( menuItems[0].title ).toEqual('z', 'Menu item 0 should be from customItem'); - expect( menuItems[1].title ).toEqual('a', 'Menu item 1 should be from customItem'); - expect( menuItems[2].title ).toEqual('x', 'Menu item 2 should be from register'); - expect( menuItems[3].title ).toEqual('y', 'Menu item 3 should be from register'); + expect( menuItems[0].title ).toEqual('x', 'Menu item 0 should be from register'); + expect( menuItems[1].title ).toEqual('y', 'Menu item 1 should be from register'); + expect( menuItems[2].title ).toEqual('z', 'Menu item 2 should be from customItem'); + expect( menuItems[3].title ).toEqual('a', 'Menu item 3 should be from customItem'); expect( menuItems[4].title ).toEqual('Columns:', 'Menu item 4 should be header'); expect( menuItems[5].title ).toEqual('fn_col1', 'Menu item 5 should be col1'); From a54871fab69591cd0b1fb2e839e0e0dd380a97a8 Mon Sep 17 00:00:00 2001 From: Paul Lambert Date: Tue, 14 Apr 2015 07:45:57 +1200 Subject: [PATCH 126/173] Fix(resize): fix #3183, resize module without the directive I can't recreate the situation, but this change should give a clear error message if you hit the resizer api and it's not registered --- src/features/resize-columns/js/ui-grid-column-resizer.js | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/features/resize-columns/js/ui-grid-column-resizer.js b/src/features/resize-columns/js/ui-grid-column-resizer.js index bd6211d12f..7322c8b7b9 100644 --- a/src/features/resize-columns/js/ui-grid-column-resizer.js +++ b/src/features/resize-columns/js/ui-grid-column-resizer.js @@ -93,7 +93,11 @@ fireColumnSizeChanged: function (grid, colDef, deltaChange) { $timeout(function () { - grid.api.colResizable.raise.columnSizeChanged(colDef, deltaChange); + if ( grid.api.colResizable ){ + grid.api.colResizable.raise.columnSizeChanged(colDef, deltaChange); + } else { + gridUtil.logError("The resizeable api is not registered, this may indicate that you've included the module but not added the 'ui-grid-column-resize' directive to your grid definition. Cannot raise any events."); + } }); }, From 5a9d409269120a060e08d8c4880b22fffeb7f23e Mon Sep 17 00:00:00 2001 From: Paul Lambert Date: Tue, 14 Apr 2015 07:53:01 +1200 Subject: [PATCH 127/173] Fix(footer): fix #3195 sizing calculation typo --- src/js/core/directives/ui-grid-render-container.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/js/core/directives/ui-grid-render-container.js b/src/js/core/directives/ui-grid-render-container.js index 266a4be3ce..5195aca73a 100644 --- a/src/js/core/directives/ui-grid-render-container.js +++ b/src/js/core/directives/ui-grid-render-container.js @@ -150,7 +150,7 @@ ret += '\n .grid' + uiGridCtrl.grid.id + ' .ui-grid-render-container-' + $scope.containerId + ' .ui-grid-viewport { width: ' + viewportWidth + 'px; height: ' + viewportHeight + 'px; }'; ret += '\n .grid' + uiGridCtrl.grid.id + ' .ui-grid-render-container-' + $scope.containerId + ' .ui-grid-header-viewport { width: ' + headerViewportWidth + 'px; }'; - ret += '\n .grid' + uiGridCtrl.grid.id + ' .ui-grid-render-container-' + $scope.containerId + ' .ui-grid-footer-canvas { width: ' + canvasWidth + grid.scrollbarWidth + 'px; }'; + ret += '\n .grid' + uiGridCtrl.grid.id + ' .ui-grid-render-container-' + $scope.containerId + ' .ui-grid-footer-canvas { width: ' + (canvasWidth + grid.scrollbarWidth) + 'px; }'; ret += '\n .grid' + uiGridCtrl.grid.id + ' .ui-grid-render-container-' + $scope.containerId + ' .ui-grid-footer-viewport { width: ' + footerViewportWidth + 'px; }'; return ret; From 9d9180520d8d6fd16b897ba4b9fbfc4bb4860ea9 Mon Sep 17 00:00:00 2001 From: Brian Hann Date: Mon, 13 Apr 2015 15:33:27 -0500 Subject: [PATCH 128/173] fix(uiGridColumnMenu): Position relatively Positioning absolutely worked in all browsers but IE9. It was positioning the column menu at the top of the page... Changing to relative seems to have fixed this in IE9 and kept all other browsers the same. Fixes #2319 --- src/less/header.less | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/less/header.less b/src/less/header.less index 222fc8652d..072db2cfda 100644 --- a/src/less/header.less +++ b/src/less/header.less @@ -1,6 +1,6 @@ .ui-grid-top-panel-background { - .gradient(@headerBackgroundColor, @headerGradientStart, @headerGradientStop); + .gradient(@headerBackgroundColor, @headerGradientStart, @headerGradientStop); } @topPanelRadius: @gridBorderRadius - @gridBorderWidth; @@ -17,7 +17,7 @@ overflow: hidden; // Disable so menus show up font-weight: bold; - // .gradient(@headerBackgroundColor, @headerGradientStart, @headerGradientStop); + // .gradient(@headerBackgroundColor, @headerGradientStart, @headerGradientStop); .ui-grid-top-panel-background; .border-radius(@topPanelRadius, 0, 0, @topPanelRadius); @@ -103,7 +103,7 @@ } .ui-grid-column-menu { - position: absolute; + position: relative; } /* Slide up/down animations */ @@ -112,12 +112,12 @@ .transition(all, 0.05s, linear); display: block !important; } - + &.ng-hide-add.ng-hide-add-active, &.ng-hide-remove { .transform(translateY(-100%)); } - + &.ng-hide-add, &.ng-hide-remove.ng-hide-remove-active { .transform(translateY(0)); @@ -130,12 +130,12 @@ .transition(all, 0.05s, linear); display: block !important; } - + &.ng-hide-add.ng-hide-add-active, &.ng-hide-remove { .transform(translateY(-100%)); } - + &.ng-hide-add, &.ng-hide-remove.ng-hide-remove-active { .transform(translateY(0)); From 897dd27afd2bad7e730b15c5fb51e97ef5da3c10 Mon Sep 17 00:00:00 2001 From: Paul Lambert Date: Tue, 14 Apr 2015 08:48:13 +1200 Subject: [PATCH 129/173] Fix(cellNav): click executes outside digest fix #2308 --- src/features/cellnav/js/cellnav.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/features/cellnav/js/cellnav.js b/src/features/cellnav/js/cellnav.js index 196f73608c..a0038b0d23 100644 --- a/src/features/cellnav/js/cellnav.js +++ b/src/features/cellnav/js/cellnav.js @@ -874,6 +874,7 @@ uiGridCtrl.cellNav.broadcastCellNav(new RowCol($scope.row, $scope.col), evt.ctrlKey || evt.metaKey); evt.stopPropagation(); + $scope.$apply(); }); $elm.find('div').on('focus', function (evt) { From 46d8543c4efcb7766312274221a226cf24ff3022 Mon Sep 17 00:00:00 2001 From: ravishivt Date: Thu, 16 Apr 2015 12:19:04 -0700 Subject: [PATCH 130/173] exporterOlderExcelCompatibility fix MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The exporterOlderExcelCompatibility option wasn’t being used to determine whether to prefix the CSV file with "\uFEFF" or not. --- src/features/exporter/js/exporter.js | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/src/features/exporter/js/exporter.js b/src/features/exporter/js/exporter.js index 66dc3e8c0b..a0e48566ca 100644 --- a/src/features/exporter/js/exporter.js +++ b/src/features/exporter/js/exporter.js @@ -811,12 +811,20 @@ // IE10+ if (navigator.msSaveBlob) { - return navigator.msSaveBlob(new Blob(["\uFEFF", csvContent], { type: strMimeType } ), fileName); + return navigator.msSaveBlob( + new Blob( + [exporterOlderExcelCompatibility ? "\uFEFF" : '', csvContent], + { type: strMimeType } ), + fileName + ); } //html5 A[download] if ('download' in a) { - var blob = new Blob(["\uFEFF", csvContent], { type: strMimeType } ); + var blob = new Blob( + [exporterOlderExcelCompatibility ? "\uFEFF" : '', csvContent], + { type: strMimeType } + ); rawFile = URL.createObjectURL(blob); a.setAttribute('download', fileName); } else { From 1f9100defb1489bed46515fb859aed9c9a090e73 Mon Sep 17 00:00:00 2001 From: Brian Hann Date: Thu, 16 Apr 2015 17:28:20 -0500 Subject: [PATCH 131/173] fix(uiGridFooter): Watch for col change Update cell class on column change. This prevents the columns from losing the right CSS settings when the columnDefs are swapped out. Fixes #2686 --- src/js/core/directives/ui-grid-footer-cell.js | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/src/js/core/directives/ui-grid-footer-cell.js b/src/js/core/directives/ui-grid-footer-cell.js index 7df6315a41..2a39eb21d0 100644 --- a/src/js/core/directives/ui-grid-footer-cell.js +++ b/src/js/core/directives/ui-grid-footer-cell.js @@ -22,7 +22,8 @@ //$elm.addClass($scope.col.getColClass(false)); $scope.grid = uiGridCtrl.grid; - $elm.addClass($scope.col.getColClass(false)); + var initColClass = $scope.col.getColClass(false); + $elm.addClass(initColClass); // apply any footerCellClass var classAdded; @@ -46,6 +47,19 @@ updateClass(); } + // Watch for column changes so we can alter the col cell class properly + $scope.$watch('col', function (n, o) { + if (n !== o) { + // See if the column's internal class has changed + var newColClass = $scope.col.getColClass(false); + if (newColClass !== initColClass) { + $elm.removeClass(initColClass); + $elm.addClass(newColClass); + initColClass = newColClass; + } + } + }); + // Register a data change watch that would get triggered whenever someone edits a cell or modifies column defs var dataChangeDereg = $scope.grid.registerDataChangeCallback( updateClass, [uiGridConstants.dataChange.COLUMN]); From 3bf6a7bca3a6fe570470137f57ff18fc092ee7b1 Mon Sep 17 00:00:00 2001 From: Paul Lambert Date: Wed, 15 Apr 2015 10:49:18 +1200 Subject: [PATCH 132/173] Fix(footer): fix #2916 add doco for footerTemplate, standardise template handling --- src/js/core/directives/ui-grid-footer.js | 3 +-- src/js/core/directives/ui-grid-grid-footer.js | 5 +++-- src/js/core/factories/GridOptions.js | 16 +++++++++++++--- test/unit/core/factories/GridOptions.spec.js | 7 ++++++- 4 files changed, 23 insertions(+), 8 deletions(-) diff --git a/src/js/core/directives/ui-grid-footer.js b/src/js/core/directives/ui-grid-footer.js index d6db5297c8..c3a42dad2a 100644 --- a/src/js/core/directives/ui-grid-footer.js +++ b/src/js/core/directives/ui-grid-footer.js @@ -2,7 +2,6 @@ 'use strict'; angular.module('ui.grid').directive('uiGridFooter', ['$templateCache', '$compile', 'uiGridConstants', 'gridUtil', '$timeout', function ($templateCache, $compile, uiGridConstants, gridUtil, $timeout) { - var defaultTemplate = 'ui-grid/ui-grid-footer'; return { restrict: 'EA', @@ -21,7 +20,7 @@ containerCtrl.footer = $elm; - var footerTemplate = ($scope.grid.options.footerTemplate) ? $scope.grid.options.footerTemplate : defaultTemplate; + var footerTemplate = $scope.grid.options.footerTemplate; gridUtil.getTemplate(footerTemplate) .then(function (contents) { var template = angular.element(contents); diff --git a/src/js/core/directives/ui-grid-grid-footer.js b/src/js/core/directives/ui-grid-grid-footer.js index b38e3366c4..85dd89b875 100644 --- a/src/js/core/directives/ui-grid-grid-footer.js +++ b/src/js/core/directives/ui-grid-grid-footer.js @@ -2,7 +2,6 @@ 'use strict'; angular.module('ui.grid').directive('uiGridGridFooter', ['$templateCache', '$compile', 'uiGridConstants', 'gridUtil', '$timeout', function ($templateCache, $compile, uiGridConstants, gridUtil, $timeout) { - var defaultTemplate = 'ui-grid/ui-grid-grid-footer'; return { restrict: 'EA', @@ -16,7 +15,9 @@ $scope.grid = uiGridCtrl.grid; - var footerTemplate = ($scope.grid.options.gridFooterTemplate) ? $scope.grid.options.gridFooterTemplate : defaultTemplate; + + + var footerTemplate = $scope.grid.options.gridFooterTemplate; gridUtil.getTemplate(footerTemplate) .then(function (contents) { var template = angular.element(contents); diff --git a/src/js/core/factories/GridOptions.js b/src/js/core/factories/GridOptions.js index 2d435080ef..8244914ea4 100644 --- a/src/js/core/factories/GridOptions.js +++ b/src/js/core/factories/GridOptions.js @@ -427,12 +427,22 @@ angular.module('ui.grid') * @ngdoc string * @name footerTemplate * @propertyOf ui.grid.class:GridOptions - * @description (optional) Null by default. When provided, this setting uses a custom footer - * template. Can be set to either the name of a template file 'footer_template.html', inline html + * @description (optional) ui-grid/ui-grid-footer by default. This footer shows the per-column + * aggregation totals. + * When provided, this setting uses a custom footer template. Can be set to either the name of a template file 'footer_template.html', inline html *
    '
    I am a Custom Grid Footer
    '
    , or the id * of a precompiled template (TBD how to use this). Refer to the custom footer tutorial for more information. */ - baseOptions.footerTemplate = baseOptions.footerTemplate || null; + baseOptions.footerTemplate = baseOptions.footerTemplate || 'ui-grid/ui-grid-footer'; + + /** + * @ngdoc string + * @name gridFooterTemplate + * @propertyOf ui.grid.class:GridOptions + * @description (optional) ui-grid/ui-grid-grid-footer by default. This template by default shows the + * total items at the bottom of the grid, and the selected items if selection is enabled. + */ + baseOptions.gridFooterTemplate = baseOptions.gridFooterTemplate || 'ui-grid/ui-grid-grid-footer'; /** * @ngdoc string diff --git a/test/unit/core/factories/GridOptions.spec.js b/test/unit/core/factories/GridOptions.spec.js index dfa2177642..f2de324a93 100644 --- a/test/unit/core/factories/GridOptions.spec.js +++ b/test/unit/core/factories/GridOptions.spec.js @@ -48,7 +48,8 @@ describe('GridOptions factory', function () { minimumColumnSize: 10, rowEquality: jasmine.any(Function), headerTemplate: null, - footerTemplate: null, + footerTemplate: 'ui-grid/ui-grid-footer', + gridFooterTemplate: 'ui-grid/ui-grid-grid-footer', rowTemplate: 'ui-grid/ui-grid-row', appScopeProvider: null }); @@ -93,6 +94,7 @@ describe('GridOptions factory', function () { rowEquality: testFunction, headerTemplate: 'testHeader', footerTemplate: 'testFooter', + gridFooterTemplate: 'testGridFooter', rowTemplate: 'testRow', extraOption: 'testExtraOption', appScopeProvider : 'anotherRef' @@ -135,6 +137,7 @@ describe('GridOptions factory', function () { rowEquality: testFunction, headerTemplate: 'testHeader', footerTemplate: 'testFooter', + gridFooterTemplate: 'testGridFooter', rowTemplate: 'testRow', extraOption: 'testExtraOption', appScopeProvider : 'anotherRef' @@ -180,6 +183,7 @@ describe('GridOptions factory', function () { rowEquality: testFunction, headerTemplate: 'testHeader', footerTemplate: 'testFooter', + gridFooterTemplate: 'testGridFooter', rowTemplate: 'testRow', extraOption: 'testExtraOption' }; @@ -221,6 +225,7 @@ describe('GridOptions factory', function () { rowEquality: testFunction, headerTemplate: 'testHeader', footerTemplate: 'testFooter', + gridFooterTemplate: 'testGridFooter', rowTemplate: 'testRow', extraOption: 'testExtraOption', appScopeProvider : null From a681ed34564e6ae43b9ad3f045ac7ff1e1b93526 Mon Sep 17 00:00:00 2001 From: Paul Lambert Date: Wed, 15 Apr 2015 11:38:15 +1200 Subject: [PATCH 133/173] Fix(header): remove duplicate watchers on filter, call notifyDataChange --- src/js/core/directives/ui-grid-header-cell.js | 54 ++++++++++--------- 1 file changed, 30 insertions(+), 24 deletions(-) diff --git a/src/js/core/directives/ui-grid-header-cell.js b/src/js/core/directives/ui-grid-header-cell.js index 2cb1325037..18d900eafb 100644 --- a/src/js/core/directives/ui-grid-header-cell.js +++ b/src/js/core/directives/ui-grid-header-cell.js @@ -50,6 +50,10 @@ // apply any headerCellClass var classAdded; + // filter watchers + var filterDeregisters = []; + + /* * Our basic approach here for event handlers is that we listen for a down event (mousedown or touchstart). * Once we have a down event, we need to work out whether we have a click, a drag, or a @@ -216,9 +220,33 @@ $scope.filterable = false; } - if ( oldFilterable !== $scope.filterable && typeof($scope.col.updateFilters) !== 'undefined'){ + if ( oldFilterable !== $scope.filterable){ + if ( typeof($scope.col.updateFilters) !== 'undefined' ){ + $scope.col.updateFilters($scope.filterable); + } + + // if column is filterable add a filter watcher + if ($scope.filterable) { + $scope.col.filters.forEach( function(filter, i) { + filterDeregisters.push($scope.$watch('col.filters[' + i + '].term', function(n, o) { + if (n !== o) { + uiGridCtrl.grid.api.core.raise.filterChanged(); + uiGridCtrl.grid.api.core.notifyDataChange( uiGridConstants.dataChange.COLUMN ); + uiGridCtrl.grid.queueGridRefresh(); + } + })); + }); + $scope.$on('$destroy', function() { + filterDeregisters.forEach( function(filterDeregister) { + filterDeregister(); + }); + }); + } else { + filterDeregisters.forEach( function(filterDeregister) { + filterDeregister(); + }); + } - $scope.col.updateFilters($scope.filterable); } // figure out whether we support column menus @@ -257,28 +285,6 @@ $scope.offAllEvents(); }); } - - // if column is filterable add a filter watcher - var filterDeregisters = []; - if ($scope.filterable) { - $scope.col.filters.forEach( function(filter, i) { - filterDeregisters.push($scope.$watch('col.filters[' + i + '].term', function(n, o) { - if (n !== o) { - uiGridCtrl.grid.api.core.raise.filterChanged(); - uiGridCtrl.grid.refresh(true); - } - })); - }); - $scope.$on('$destroy', function() { - filterDeregisters.forEach( function(filterDeregister) { - filterDeregister(); - }); - }); - } else { - filterDeregisters.forEach( function(filterDeregister) { - filterDeregister(); - }); - } }; $scope.$watch('col', function (n, o) { From 78ddfb3ef162ed00e6c05fbd50e35d6754cde31c Mon Sep 17 00:00:00 2001 From: Paul Lambert Date: Wed, 15 Apr 2015 11:38:40 +1200 Subject: [PATCH 134/173] Doco(tutorial): highlight filtered cells on filter tut --- misc/tutorial/103_filtering.ngdoc | 29 +++++++++++++++++++++++------ 1 file changed, 23 insertions(+), 6 deletions(-) diff --git a/misc/tutorial/103_filtering.ngdoc b/misc/tutorial/103_filtering.ngdoc index 6b40d75e35..f166e97b86 100644 --- a/misc/tutorial/103_filtering.ngdoc +++ b/misc/tutorial/103_filtering.ngdoc @@ -62,6 +62,10 @@ If you need to internationalize the labels you'll need to complete that before p By default the filter shows a cancel X beside the dropdown. You can set `disableCancelFilterButton: true` to suppress this button. +In this example we've provided a "toggle filters" button to allow you to turn the filter row on and off. To +still visually indicate which columns are filtered even when the filters aren't present, we've used the headerCellClass +to make any columns with a filter condition have blue text. + @example @@ -72,6 +76,14 @@ this button. var nextWeek = new Date(); nextWeek.setDate(nextWeek.getDate() + 7); + $scope.highlightFilteredHeader = function( row, rowRenderIndex, col, colRenderIndex ) { + if( col.filters[0].term ){ + return 'header-filtered'; + } else { + return ''; + } + }; + $scope.gridOptions = { enableFiltering: true, onRegisterApi: function(gridApi){ @@ -79,14 +91,14 @@ this button. }, columnDefs: [ // default - { field: 'name' }, + { field: 'name', headerCellClass: $scope.highlightFilteredHeader }, // pre-populated search field { field: 'gender', filter: { term: '1', type: uiGridConstants.filter.SELECT, selectOptions: [ { value: '1', label: 'male' }, { value: '2', label: 'female' }, { value: '3', label: 'unknown'}, { value: '4', label: 'not stated' }, { value: '5', label: 'a really long value that extends things' } ] }, - cellFilter: 'mapGender' }, + cellFilter: 'mapGender', headerCellClass: $scope.highlightFilteredHeader }, // no filter input { field: 'company', enableFiltering: false, filter: { noTerm: true, @@ -101,7 +113,7 @@ this button. filter: { condition: uiGridConstants.filter.ENDS_WITH, placeholder: 'ends with' - } + }, headerCellClass: $scope.highlightFilteredHeader }, // custom condition function { @@ -111,7 +123,7 @@ this button. var strippedValue = (cellValue + '').replace(/[^\d]/g, ''); return strippedValue.indexOf(searchTerm) >= 0; } - } + }, headerCellClass: $scope.highlightFilteredHeader }, // multiple filters { field: 'age', filters: [ @@ -123,13 +135,14 @@ this button. condition: uiGridConstants.filter.LESS_THAN, placeholder: 'less than' } - ]}, + ], headerCellClass: $scope.highlightFilteredHeader}, // date filter { field: 'mixedDate', cellFilter: 'date', width: '15%', filter: { condition: uiGridConstants.filter.LESS_THAN, placeholder: 'less than', term: nextWeek - }} + }, headerCellClass: $scope.highlightFilteredHeader + } ] }; @@ -182,6 +195,10 @@ this button. width: 650px; height: 400px; } + + .header-filtered { + color: blue; + } var gridTestUtils = require('../../test/e2e/gridTestUtils.spec.js'); From f147789ba22c7492c597e786b7bbb16123393e17 Mon Sep 17 00:00:00 2001 From: Paul Lambert Date: Fri, 17 Apr 2015 12:28:07 +1200 Subject: [PATCH 135/173] Fix(exporter): tidy up code relating to promises and export all --- .../405_exporting_all_data_complex.ngdoc | 41 +++++++--------- src/features/exporter/js/exporter.js | 49 +++++++++---------- 2 files changed, 39 insertions(+), 51 deletions(-) diff --git a/misc/tutorial/405_exporting_all_data_complex.ngdoc b/misc/tutorial/405_exporting_all_data_complex.ngdoc index 4c93181685..1bc8c721eb 100644 --- a/misc/tutorial/405_exporting_all_data_complex.ngdoc +++ b/misc/tutorial/405_exporting_all_data_complex.ngdoc @@ -2,9 +2,11 @@ @name Tutorial: 405 Exporting All Data With External Pagination @description -When using build in pagination, the data is fully loaded before export. +When using built in pagination, the data is fully loaded before export. -For external pagination, use the `exportAll` event to load all grid data and then disable external pagination. +For external pagination, use the `exportAllDataPromise` function to load all grid data. If you get all the +data (for the purposes of exporting), then it makes sense to turn off external pagination and external sorting, +as all the data is now present within AngularJS @example This shows combined external pagination and sorting. @@ -13,7 +15,7 @@ This shows combined external pagination and sorting. var app = angular.module('app', ['ngTouch', 'ui.grid', 'ui.grid.pagination', 'ui.grid.selection', 'ui.grid.exporter']); app.controller('MainCtrl', [ - '$scope', '$http', 'uiGridConstants', '$q', function($scope, $http, uiGridConstants, $q) { + '$scope', '$http', 'uiGridConstants', function($scope, $http, uiGridConstants) { var paginationOptions = { sort: null @@ -32,12 +34,11 @@ This shows combined external pagination and sorting. ], exporterAllDataPromise: function() { return getPage(1, $scope.gridOptions.totalItems, paginationOptions.sort) - .then(getPageSuccess) - .then(function() { - $scope.gridOptions.useExternalPagination = false; - $scope.gridOptions.useExternalSorting = false; - getPage = null; - }); + .then(function() { + $scope.gridOptions.useExternalPagination = false; + $scope.gridOptions.useExternalSorting = false; + getPage = null; + }); }, onRegisterApi: function(gridApi) { $scope.gridApi = gridApi; @@ -48,12 +49,12 @@ This shows combined external pagination and sorting. } else { paginationOptions.sort = null; } - getPage(grid.options.paginationCurrentPage, grid.options.paginationPageSize, paginationOptions.sort).then(getPageSuccess); + getPage(grid.options.paginationCurrentPage, grid.options.paginationPageSize, paginationOptions.sort) } }); gridApi.pagination.on.paginationChanged($scope, function (newPage, pageSize) { if(getPage) { - getPage(newPage, pageSize, paginationOptions.sort).then(getPageSuccess); + getPage(newPage, pageSize, paginationOptions.sort); } }); } @@ -73,24 +74,16 @@ This shows combined external pagination and sorting. break; } - var deferred = $q.defer(); - $http.get(url) + var _scope = $scope; + return $http.get(url) .success(function (data) { var firstRow = (curPage - 1) * pageSize; - deferred.resolve({ - totalItems: 100, - data: data.slice(firstRow, firstRow + pageSize) - }); + $scope.gridOptions.totalItems = 100; + $scope.gridOptions.data = data.slice(firstRow, firstRow + pageSize) }); - return deferred.promise; - }; - - var getPageSuccess = function(result) { - $scope.gridOptions.totalItems = result.totalItems; - $scope.gridOptions.data = result.data; }; - getPage(1, $scope.gridOptions.paginationPageSize).then(getPageSuccess); + getPage(1, $scope.gridOptions.paginationPageSize); } ]); diff --git a/src/features/exporter/js/exporter.js b/src/features/exporter/js/exporter.js index cc84570247..489b1d7373 100644 --- a/src/features/exporter/js/exporter.js +++ b/src/features/exporter/js/exporter.js @@ -473,12 +473,7 @@ * @example *
                *   gridOptions.exporterAllDataPromise = function () {
    -           *     var deferred = $q.defer();
    -           *     $http.get('/data/100.json')
    -           *       .success(function ( data ) {
    -           *         deferred.resolve( data );
    -           *       });
    -           *     return deferred.promise;
    +           *     return $http.get('/data/100.json')
                *   }
                * 
    */ @@ -577,12 +572,13 @@ * uiGridExporterConstants.SELECTED */ csvExport: function (grid, rowTypes, colTypes) { - this.loadAllDataIfNeeded(this, grid, rowTypes, colTypes, function(ref, grid, rowTypes, colTypes) { - var exportColumnHeaders = ref.getColumnHeaders(grid, colTypes); - var exportData = ref.getData(grid, rowTypes, colTypes); - var csvContent = ref.formatAsCsv(exportColumnHeaders, exportData, grid.options.exporterCsvColumnSeparator); + var self = this; + this.loadAllDataIfNeeded(grid, rowTypes, colTypes).then(function() { + var exportColumnHeaders = self.getColumnHeaders(grid, colTypes); + var exportData = self.getData(grid, rowTypes, colTypes); + var csvContent = self.formatAsCsv(exportColumnHeaders, exportData, grid.options.exporterCsvColumnSeparator); - ref.downloadFile (grid.options.exporterCsvFilename, csvContent, grid.options.exporterOlderExcelCompatibility); + self.downloadFile (grid.options.exporterCsvFilename, csvContent, grid.options.exporterOlderExcelCompatibility); }); }, @@ -590,10 +586,10 @@ * @ngdoc function * @name loadAllDataIfNeeded * @methodOf ui.grid.exporter.service:uiGridExporterService - * @description When using server side pagination, raise exportAll event to load all data, - * and call callback function till all data loaded. - * When using client side pagination, call callback function directly - * @param {object} ref reference used by callback + * @description When using server side pagination, use exportAllDataPromise to + * load all data before continuing processing. + * When using client side pagination, return a resolved promise so processing + * continues immediately * @param {Grid} grid the grid from which data should be exported * @param {string} rowTypes which rows to export, valid values are * uiGridExporterConstants.ALL, uiGridExporterConstants.VISIBLE, @@ -601,19 +597,17 @@ * @param {string} colTypes which columns to export, valid values are * uiGridExporterConstants.ALL, uiGridExporterConstants.VISIBLE, * uiGridExporterConstants.SELECTED - * @param {object} callback callback function */ - loadAllDataIfNeeded: function (ref, grid, rowTypes, colTypes, callback) { + loadAllDataIfNeeded: function (grid, rowTypes, colTypes) { if ( rowTypes === uiGridExporterConstants.ALL && grid.rows.length !== grid.options.totalItems && grid.options.exporterAllDataPromise) { - grid.options.exporterAllDataPromise() + return grid.options.exporterAllDataPromise() .then(function() { grid.modifyRows(grid.options.data); - }) - .then(function() { - callback(ref, grid, rowTypes, colTypes); }); } else { - callback(ref, grid, rowTypes, colTypes); + var deferred = $q.defer(); + deferred.resolve(); + return $q.promise; } }, @@ -923,12 +917,13 @@ * uiGridExporterConstants.SELECTED */ pdfExport: function (grid, rowTypes, colTypes) { - this.loadAllDataIfNeeded(this, grid, rowTypes, colTypes, function(ref, grid, rowTypes, colTypes) { - var exportColumnHeaders = ref.getColumnHeaders(grid, colTypes); - var exportData = ref.getData(grid, rowTypes, colTypes); - var docDefinition = ref.prepareAsPdf(grid, exportColumnHeaders, exportData); + var self = this; + this.loadAllDataIfNeeded(grid, rowTypes, colTypes).then(function () { + var exportColumnHeaders = self.getColumnHeaders(grid, colTypes); + var exportData = self.getData(grid, rowTypes, colTypes); + var docDefinition = self.prepareAsPdf(grid, exportColumnHeaders, exportData); - if (ref.isIE()) { + if (self.isIE()) { var pdf = pdfMake.createPdf(docDefinition).download(); } else { pdfMake.createPdf(docDefinition).open(); From af647ad7849447b11be0e153cfe620b160228ae6 Mon Sep 17 00:00:00 2001 From: Paul Lambert Date: Fri, 17 Apr 2015 12:34:23 +1200 Subject: [PATCH 136/173] Fix(gridMenu): fix #3291 first grid menu column s/be 301 sort --- src/js/core/directives/ui-grid-menu-button.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/js/core/directives/ui-grid-menu-button.js b/src/js/core/directives/ui-grid-menu-button.js index 8bcafa08bd..e8617874c8 100644 --- a/src/js/core/directives/ui-grid-menu-button.js +++ b/src/js/core/directives/ui-grid-menu-button.js @@ -260,7 +260,7 @@ angular.module('ui.grid') }, context: { gridCol: $scope.grid.getColumn(colDef.name || colDef.field) }, leaveOpen: true, - order: 300 + index * 2 + order: 301 + index * 2 }; service.setMenuItemTitle( menuItem, colDef, $scope.grid ); showHideColumns.push( menuItem ); From 0926c70d3e981ea8c72964939ca7954356cd4eec Mon Sep 17 00:00:00 2001 From: Shane Walters Date: Thu, 16 Apr 2015 20:26:06 -0500 Subject: [PATCH 137/173] perf(scrolling): eliminate call to getStyles in normalizeLeft functions and instead use grid.isRTL() call Should improve performance on horizontal scrolling but no benchmark so can't ascertain how much --- src/js/core/directives/ui-grid-header.js | 2 +- .../directives/ui-grid-render-container.js | 2 +- src/js/core/directives/ui-grid-viewport.js | 8 ++-- src/js/core/factories/ScrollEvent.js | 2 +- src/js/core/services/ui-grid-util.js | 38 +++++++++---------- 5 files changed, 25 insertions(+), 27 deletions(-) diff --git a/src/js/core/directives/ui-grid-header.js b/src/js/core/directives/ui-grid-header.js index b773e9c6be..b72fad95e4 100644 --- a/src/js/core/directives/ui-grid-header.js +++ b/src/js/core/directives/ui-grid-header.js @@ -78,7 +78,7 @@ if (uiGridCtrl.grid.isScrollingHorizontally) { return; } - var newScrollLeft = gridUtil.normalizeScrollLeft(containerCtrl.headerViewport); + var newScrollLeft = gridUtil.normalizeScrollLeft(containerCtrl.headerViewport, uiGridCtrl.grid); var horizScrollPercentage = containerCtrl.colContainer.scrollHorizontal(newScrollLeft); var scrollEvent = new ScrollEvent(uiGridCtrl.grid, null, containerCtrl.colContainer, ScrollEvent.Sources.ViewPortScroll); diff --git a/src/js/core/directives/ui-grid-render-container.js b/src/js/core/directives/ui-grid-render-container.js index 5195aca73a..f3d882b399 100644 --- a/src/js/core/directives/ui-grid-render-container.js +++ b/src/js/core/directives/ui-grid-render-container.js @@ -87,7 +87,7 @@ var scrollXAmount = event.deltaX * event.deltaFactor; // Get the scroll percentage - scrollLeft = gridUtil.normalizeScrollLeft(containerCtrl.viewport); + scrollLeft = gridUtil.normalizeScrollLeft(containerCtrl.viewport, grid); var scrollXPercentage = (scrollLeft + scrollXAmount) / (colContainer.getCanvasWidth() - colContainer.getViewportWidth()); // Keep scrollPercentage within the range 0-1. diff --git a/src/js/core/directives/ui-grid-viewport.js b/src/js/core/directives/ui-grid-viewport.js index 0a8a83d6f9..04b4ba8c71 100644 --- a/src/js/core/directives/ui-grid-viewport.js +++ b/src/js/core/directives/ui-grid-viewport.js @@ -49,7 +49,7 @@ //ignoreScroll = true; var newScrollTop = $elm[0].scrollTop; - var newScrollLeft = gridUtil.normalizeScrollLeft($elm); + var newScrollLeft = gridUtil.normalizeScrollLeft($elm, grid); var vertScrollPercentage = rowContainer.scrollVertical(newScrollTop); var horizScrollPercentage = colContainer.scrollHorizontal(newScrollLeft); @@ -88,20 +88,20 @@ function syncHorizontalScroll(scrollEvent){ containerCtrl.prevScrollArgs = scrollEvent; var newScrollLeft = scrollEvent.getNewScrollLeft(colContainer, containerCtrl.viewport); - $elm[0].scrollLeft = gridUtil.denormalizeScrollLeft(containerCtrl.viewport,newScrollLeft); + $elm[0].scrollLeft = gridUtil.denormalizeScrollLeft(containerCtrl.viewport,newScrollLeft, grid); } function syncHorizontalHeader(scrollEvent){ var newScrollLeft = scrollEvent.getNewScrollLeft(colContainer, containerCtrl.viewport); if (containerCtrl.headerViewport) { - containerCtrl.headerViewport.scrollLeft = gridUtil.denormalizeScrollLeft(containerCtrl.viewport,newScrollLeft); + containerCtrl.headerViewport.scrollLeft = gridUtil.denormalizeScrollLeft(containerCtrl.viewport,newScrollLeft, grid); } } function syncHorizontalFooter(scrollEvent){ var newScrollLeft = scrollEvent.getNewScrollLeft(colContainer, containerCtrl.viewport); if (containerCtrl.footerViewport) { - containerCtrl.footerViewport.scrollLeft = gridUtil.denormalizeScrollLeft(containerCtrl.viewport,newScrollLeft); + containerCtrl.footerViewport.scrollLeft = gridUtil.denormalizeScrollLeft(containerCtrl.viewport,newScrollLeft, grid); } } diff --git a/src/js/core/factories/ScrollEvent.js b/src/js/core/factories/ScrollEvent.js index 68d31e1f16..6d84ec5374 100644 --- a/src/js/core/factories/ScrollEvent.js +++ b/src/js/core/factories/ScrollEvent.js @@ -79,7 +79,7 @@ if (!self.newScrollLeft){ var scrollWidth = (colContainer.getCanvasWidth() - colContainer.getViewportWidth()); - var oldScrollLeft = gridUtil.normalizeScrollLeft(viewport); + var oldScrollLeft = gridUtil.normalizeScrollLeft(viewport, self.grid); var scrollXPercentage; if (typeof(self.x.percentage) !== 'undefined' && self.x.percentage !== undefined) { diff --git a/src/js/core/services/ui-grid-util.js b/src/js/core/services/ui-grid-util.js index dc9ca077e7..9c0f3e6737 100644 --- a/src/js/core/services/ui-grid-util.js +++ b/src/js/core/services/ui-grid-util.js @@ -909,28 +909,27 @@ module.service('gridUtil', ['$log', '$window', '$document', '$http', '$templateC return type; }; - /** - * @ngdoc method - * @name normalizeScrollLeft - * @methodOf ui.grid.service:GridUtil - * - * @param {element} element The element to get the `scrollLeft` from. - * - * @returns {int} A normalized scrollLeft value for the current browser. - * - * @description - * Browsers currently handle RTL in different ways, resulting in inconsistent scrollLeft values. This method normalizes them - */ - s.normalizeScrollLeft = function normalizeScrollLeft(element) { + /** + * @ngdoc method + * @name normalizeScrollLeft + * @methodOf ui.grid.service:GridUtil + * + * @param {element} element The element to get the `scrollLeft` from. + * @param {boolean} grid - grid used to normalize (uses the rtl property) + * + * @returns {int} A normalized scrollLeft value for the current browser. + * + * @description + * Browsers currently handle RTL in different ways, resulting in inconsistent scrollLeft values. This method normalizes them + */ + s.normalizeScrollLeft = function normalizeScrollLeft(element, grid) { if (typeof(element.length) !== 'undefined' && element.length) { element = element[0]; } var scrollLeft = element.scrollLeft; - - var dir = s.getStyles(element).direction; - if (dir === 'rtl') { + if (grid.isRTL()) { switch (s.rtlScrollType()) { case 'default': return element.scrollWidth - scrollLeft - element.clientWidth; @@ -951,20 +950,19 @@ module.service('gridUtil', ['$log', '$window', '$document', '$http', '$templateC * * @param {element} element The element to normalize the `scrollLeft` value for * @param {int} scrollLeft The `scrollLeft` value to denormalize. + * @param {boolean} grid The grid that owns the scroll event. * * @returns {int} A normalized scrollLeft value for the current browser. * * @description * Browsers currently handle RTL in different ways, resulting in inconsistent scrollLeft values. This method denormalizes a value for the current browser. */ - s.denormalizeScrollLeft = function denormalizeScrollLeft(element, scrollLeft) { + s.denormalizeScrollLeft = function denormalizeScrollLeft(element, scrollLeft, grid) { if (typeof(element.length) !== 'undefined' && element.length) { element = element[0]; } - var dir = s.getStyles(element).direction; - - if (dir === 'rtl') { + if (grid.isRTL()) { switch (s.rtlScrollType()) { case 'default': // Get the max scroll for the element From ac8fffdb4f41ec9d6119ba077b88c0c0701ec651 Mon Sep 17 00:00:00 2001 From: Paul Lambert Date: Fri, 17 Apr 2015 15:30:56 +1200 Subject: [PATCH 138/173] Fix(gridMenu): other part of fix, failed to commit it --- src/js/core/directives/ui-grid-menu-button.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/js/core/directives/ui-grid-menu-button.js b/src/js/core/directives/ui-grid-menu-button.js index e8617874c8..f5c5a7a04b 100644 --- a/src/js/core/directives/ui-grid-menu-button.js +++ b/src/js/core/directives/ui-grid-menu-button.js @@ -277,7 +277,7 @@ angular.module('ui.grid') }, context: { gridCol: $scope.grid.getColumn(colDef.name || colDef.field) }, leaveOpen: true, - order: 300 + index * 2 + 1 + order: 301 + index * 2 + 1 }; service.setMenuItemTitle( menuItem, colDef, $scope.grid ); showHideColumns.push( menuItem ); From 1bdef74620a516d24ce9cf44957c74e70abbfbcd Mon Sep 17 00:00:00 2001 From: Wendy-Joris Date: Fri, 17 Apr 2015 08:37:26 +0200 Subject: [PATCH 139/173] Added dutch translation for pagination --- src/js/i18n/nl.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/js/i18n/nl.js b/src/js/i18n/nl.js index 727049c3ea..c9bb511624 100644 --- a/src/js/i18n/nl.js +++ b/src/js/i18n/nl.js @@ -59,6 +59,10 @@ invalidJson: 'Het bestand kan niet verwerkt worden. Is het valide json?', jsonNotArray: 'Het json bestand moet een array bevatten. De actie wordt geannuleerd.' }, + pagination: { + sizes: 'items per pagina', + totalItems: 'items' + }, grouping: { group: 'Groepeer', ungroup: 'Groepering opheffen', From a04ce65cdb44bd2313f6e3ef75d6dc0a417deac7 Mon Sep 17 00:00:00 2001 From: Paul Lambert Date: Fri, 17 Apr 2015 19:22:52 +1200 Subject: [PATCH 140/173] Enh(treeView): fix #3294 add option to suppress treeViewRowHeader --- src/features/tree-view/js/tree-view.js | 41 +++++++++++++++++--------- 1 file changed, 27 insertions(+), 14 deletions(-) diff --git a/src/features/tree-view/js/tree-view.js b/src/features/tree-view/js/tree-view.js index 34a5d68f65..17849d8875 100644 --- a/src/features/tree-view/js/tree-view.js +++ b/src/features/tree-view/js/tree-view.js @@ -348,6 +348,16 @@ *
    Defaults to 10 */ gridOptions.treeViewIndent = gridOptions.treeViewIndent || 10; + + /** + * @ngdoc object + * @name showTreeViewRowHeader + * @propertyOf ui.grid.treeView.api:GridOptions + * @description If set to false, don't create the row header. Youll need to programatically control the expand + * states + *
    Defaults to true + */ + gridOptions.showTreeViewRowHeader = gridOptions.showTreeViewRowHeader !== false; }, @@ -705,20 +715,23 @@ pre: function ($scope, $elm, $attrs, uiGridCtrl) { if (uiGridCtrl.grid.options.enableTreeView !== false){ uiGridTreeViewService.initializeGrid(uiGridCtrl.grid, $scope); - var treeViewRowHeaderDef = { - name: uiGridTreeViewConstants.treeViewRowHeaderColName, - displayName: '', - width: uiGridCtrl.grid.options.treeViewRowHeaderBaseWidth, - minWidth: 10, - cellTemplate: 'ui-grid/treeViewRowHeader', - headerCellTemplate: 'ui-grid/treeViewHeaderCell', - enableColumnResizing: false, - enableColumnMenu: false, - exporterSuppressExport: true, - allowCellFocus: true - }; - - uiGridCtrl.grid.addRowHeaderColumn(treeViewRowHeaderDef); + + if ( uiGridCtrl.grid.options.showTreeViewRowHeader ){ + var treeViewRowHeaderDef = { + name: uiGridTreeViewConstants.treeViewRowHeaderColName, + displayName: '', + width: uiGridCtrl.grid.options.treeViewRowHeaderBaseWidth, + minWidth: 10, + cellTemplate: 'ui-grid/treeViewRowHeader', + headerCellTemplate: 'ui-grid/treeViewHeaderCell', + enableColumnResizing: false, + enableColumnMenu: false, + exporterSuppressExport: true, + allowCellFocus: true + }; + + uiGridCtrl.grid.addRowHeaderColumn(treeViewRowHeaderDef); + } } }, post: function ($scope, $elm, $attrs, uiGridCtrl) { From dd06cd7657642ad301fdca03c4ac378ad121e661 Mon Sep 17 00:00:00 2001 From: "Alexander M. Turek" Date: Thu, 16 Apr 2015 11:15:35 +0200 Subject: [PATCH 141/173] Pagination tests working again, fixes #3173. --- .../pagination/test/pagination.spec.js | 52 ++++++++++--------- 1 file changed, 28 insertions(+), 24 deletions(-) diff --git a/src/features/pagination/test/pagination.spec.js b/src/features/pagination/test/pagination.spec.js index 6fd2ce0b3d..0ad6952511 100644 --- a/src/features/pagination/test/pagination.spec.js +++ b/src/features/pagination/test/pagination.spec.js @@ -1,15 +1,17 @@ -xdescribe('ui.grid.pagination uiGridPaginationService', function () { +describe('ui.grid.pagination uiGridPaginationService', function () { 'use strict'; var gridApi; var gridElement; var $rootScope; + var $timeout; beforeEach(module('ui.grid')); beforeEach(module('ui.grid.pagination')); - beforeEach(inject(function (_$rootScope_, $compile) { + beforeEach(inject(function (_$rootScope_, _$timeout_, $compile) { $rootScope = _$rootScope_; + $timeout = _$timeout_; $rootScope.gridOptions = { columnDefs: [ @@ -90,6 +92,7 @@ xdescribe('ui.grid.pagination uiGridPaginationService', function () { it('displays page 2 if I call nextPage()', function () { gridApi.pagination.nextPage(); $rootScope.$digest(); + $timeout.flush(); var gridRows = gridElement.find('div.ui-grid-row'); @@ -106,6 +109,7 @@ xdescribe('ui.grid.pagination uiGridPaginationService', function () { it('displays only 6 rows on page 3', function () { gridApi.pagination.seek(3); $rootScope.$digest(); + $timeout.flush(); var gridRows = gridElement.find('div.ui-grid-row'); @@ -145,29 +149,29 @@ xdescribe('ui.grid.pagination uiGridPaginationService', function () { it('paginates correctly on a sorted grid', function() { gridApi.grid.sortColumn(gridApi.grid.columns[1]).then(function () { - gridApi.grid.refresh(); + $rootScope.$digest(); + $timeout.flush(); + + var gridRows = gridElement.find('div.ui-grid-row'); + expect(gridApi.pagination.getPage()).toBe(1); + expect(gridRows.eq(0).find('div.ui-grid-cell').eq(1).text()).toBe('A'); + expect(gridRows.eq(1).find('div.ui-grid-cell').eq(1).text()).toBe('B'); + expect(gridRows.eq(2).find('div.ui-grid-cell').eq(1).text()).toBe('C'); + expect(gridRows.eq(3).find('div.ui-grid-cell').eq(1).text()).toBe('D'); + expect(gridRows.eq(4).find('div.ui-grid-cell').eq(1).text()).toBe('E'); + expect(gridRows.eq(5).find('div.ui-grid-cell').eq(1).text()).toBe('F'); + expect(gridRows.eq(6).find('div.ui-grid-cell').eq(1).text()).toBe('G'); + expect(gridRows.eq(7).find('div.ui-grid-cell').eq(1).text()).toBe('H'); + expect(gridRows.eq(8).find('div.ui-grid-cell').eq(1).text()).toBe('I'); + expect(gridRows.eq(9).find('div.ui-grid-cell').eq(1).text()).toBe('J'); + + gridApi.pagination.nextPage(); + $rootScope.$digest(); + + gridRows = gridElement.find('div.ui-grid-row'); + expect(gridApi.pagination.getPage()).toBe(2); + expect(gridRows.eq(0).find('div.ui-grid-cell').eq(1).text()).toBe('K'); }); - $rootScope.$digest(); - - var gridRows = gridElement.find('div.ui-grid-row'); - expect(gridApi.pagination.getPage()).toBe(1); - expect(gridRows.eq(0).find('div.ui-grid-cell').eq(1).text()).toBe('A'); - expect(gridRows.eq(1).find('div.ui-grid-cell').eq(1).text()).toBe('B'); - expect(gridRows.eq(2).find('div.ui-grid-cell').eq(1).text()).toBe('C'); - expect(gridRows.eq(3).find('div.ui-grid-cell').eq(1).text()).toBe('D'); - expect(gridRows.eq(4).find('div.ui-grid-cell').eq(1).text()).toBe('E'); - expect(gridRows.eq(5).find('div.ui-grid-cell').eq(1).text()).toBe('F'); - expect(gridRows.eq(6).find('div.ui-grid-cell').eq(1).text()).toBe('G'); - expect(gridRows.eq(7).find('div.ui-grid-cell').eq(1).text()).toBe('H'); - expect(gridRows.eq(8).find('div.ui-grid-cell').eq(1).text()).toBe('I'); - expect(gridRows.eq(9).find('div.ui-grid-cell').eq(1).text()).toBe('J'); - - gridApi.pagination.nextPage(); - $rootScope.$digest(); - - gridRows = gridElement.find('div.ui-grid-row'); - expect(gridApi.pagination.getPage()).toBe(2); - expect(gridRows.eq(0).find('div.ui-grid-cell').eq(1).text()).toBe('K'); }); }); }); From c24c51dcc962fe7eb45559f45237d58bdb88a866 Mon Sep 17 00:00:00 2001 From: Paul Lambert Date: Sat, 18 Apr 2015 06:57:51 +1200 Subject: [PATCH 142/173] Fix(filtering): annotate directive --- src/js/core/directives/ui-grid-filter.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/js/core/directives/ui-grid-filter.js b/src/js/core/directives/ui-grid-filter.js index e9a642f70c..4389f56e6c 100644 --- a/src/js/core/directives/ui-grid-filter.js +++ b/src/js/core/directives/ui-grid-filter.js @@ -1,7 +1,7 @@ (function(){ 'use strict'; - angular.module('ui.grid').directive('uiGridFilter', function ($compile, $templateCache) { + angular.module('ui.grid').directive('uiGridFilter', ['$compile', '$templateCache', function ($compile, $templateCache) { return { compile: function() { @@ -23,5 +23,5 @@ }; } }; - }); + }]); })(); From 5c474aadc4032c7cd6eb68e7fc787a4141d69c6f Mon Sep 17 00:00:00 2001 From: Paul Lambert Date: Sat, 18 Apr 2015 07:00:35 +1200 Subject: [PATCH 143/173] Fix(exporter): return promise --- src/features/exporter/js/exporter.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/features/exporter/js/exporter.js b/src/features/exporter/js/exporter.js index 489b1d7373..da51df470c 100644 --- a/src/features/exporter/js/exporter.js +++ b/src/features/exporter/js/exporter.js @@ -607,7 +607,7 @@ } else { var deferred = $q.defer(); deferred.resolve(); - return $q.promise; + return deferred.promise; } }, From 45eadd0f2a453995826013cabe5a60aeec0d40e7 Mon Sep 17 00:00:00 2001 From: Paul Lambert Date: Sun, 19 Apr 2015 07:22:11 +1200 Subject: [PATCH 144/173] Enh(edit): add file chooser option, provide tutorial --- misc/tutorial/201_editable.ngdoc | 33 +++++- src/features/edit/js/gridEdit.js | 107 +++++++++++++++++- .../edit/templates/fileChooserEditor.html | 5 + 3 files changed, 139 insertions(+), 6 deletions(-) create mode 100644 src/features/edit/templates/fileChooserEditor.html diff --git a/misc/tutorial/201_editable.ngdoc b/misc/tutorial/201_editable.ngdoc index 63029c5e41..ae9392b67c 100644 --- a/misc/tutorial/201_editable.ngdoc +++ b/misc/tutorial/201_editable.ngdoc @@ -19,6 +19,11 @@ When using a dropdown editor you need to provide an options array through the `e This array by default should be an array of `{id: xxx, value: xxx}`, although the field tags can be changed through using the `editDropdownIdLabel` and `editDropdownValueLabel` options. +A file chooser is available, through seting the 'editableCellTemplate` on the `columnDef` to `'ui-grid/fileChooserEditor'`. This +file chooser will open the file chosen by the user and assign the value of that file to the model element. In the example below +we use the file chooser to load a file, and we display the filename in the cell. The file is stored against the row in a hidden +column, which we can save to our server or otherwise process. + Custom edit templates should be used for any editor other than the default editors, but be aware that you will likely also need to provide a custom directive similar to the uiGridEditor directive so as to provide `BEGIN_CELL_EDIT, CANCEL_CELL_EDIT and END_CELL_EDIT` events. @@ -82,6 +87,23 @@ $scope.gridOptions.columnDefs = [ app.controller('MainCtrl', ['$scope', '$http', function ($scope, $http) { $scope.gridOptions = { }; + $scope.storeFile = function( gridRow, gridCol, files ) { + // ignore all but the first file, it can only select one anyway + // set the filename into this column + gridRow.entity.filename = files[0].name; + + // read the file and set it into a hidden column, which we may do stuff with later + var setFile = function(fileContent){ + gridRow.entity.file = fileContent.currentTarget.result; + // put it on scope so we can display it - you'd probably do something else with it + $scope.lastFile = fileContent.currentTarget.result; + $scope.$apply(); + }; + var reader = new FileReader(); + reader.onload = setFile; + reader.readAsText( files[0] ); + }; + $scope.gridOptions.columnDefs = [ { name: 'id', enableCellEdit: false, width: '10%' }, { name: 'name', displayName: 'Name (editable)', width: '20%' }, @@ -101,11 +123,11 @@ $scope.gridOptions.columnDefs = [ { name: 'isActive', displayName: 'Active', type: 'boolean', width: '10%' }, { name: 'pet', displayName: 'Pet', width: '20%', editableCellTemplate: 'ui-grid/dropdownEditor', editDropdownRowEntityOptionsArrayPath: 'foo.bar[0].options', editDropdownIdLabel: 'value' - } + }, + { name: 'filename', displayName: 'File', width: '20%', editableCellTemplate: 'ui-grid/fileChooserEditor', + editFileChooserCallback: $scope.storeFile } ]; - - - + $scope.msg = {}; $scope.gridOptions.onRegisterApi = function(gridApi){ @@ -158,6 +180,9 @@ $scope.gridOptions.columnDefs = [ Last Cell Edited: {{msg.lastCellEdited}}
    +
    +
    Last file uploaded:
    +
    {{lastFile}}
    diff --git a/src/features/edit/js/gridEdit.js b/src/features/edit/js/gridEdit.js index e71b3a7925..4a31b0e2d2 100644 --- a/src/features/edit/js/gridEdit.js +++ b/src/features/edit/js/gridEdit.js @@ -806,9 +806,9 @@ }); - $scope.deepEdit = false; + $scope.deepEdit = false; - $scope.stopEdit = function (evt) { + $scope.stopEdit = function (evt) { if ($scope.inputForm && !$scope.inputForm.$valid) { evt.stopPropagation(); $scope.$emit(uiGridEditConstants.events.CANCEL_CELL_EDIT); @@ -1007,4 +1007,107 @@ }; }]); + /** + * @ngdoc directive + * @name ui.grid.edit.directive:uiGridEditor + * @element div + * @restrict A + * + * @description input editor directive for editable fields. + * Provides EndEdit and CancelEdit events + * + * Events that end editing: + * blur and enter keydown + * + * Events that cancel editing: + * - Esc keydown + * + */ + module.directive('uiGridEditFileChooser', + ['gridUtil', 'uiGridConstants', 'uiGridEditConstants','$timeout', + function (gridUtil, uiGridConstants, uiGridEditConstants, $timeout) { + return { + scope: true, + require: ['?^uiGrid', '?^uiGridRenderContainer'], + compile: function () { + return { + pre: function ($scope, $elm, $attrs) { + + }, + post: function ($scope, $elm, $attrs, controllers) { + var uiGridCtrl, renderContainerCtrl; + if (controllers[0]) { uiGridCtrl = controllers[0]; } + if (controllers[1]) { renderContainerCtrl = controllers[1]; } + var grid = uiGridCtrl.grid; + + var handleFileSelect = function( event ){ + var target = event.srcElement || event.target; + + if (target && target.files && target.files.length > 0) { + /** + * @ngdoc property + * @name editFileChooserCallback + * @propertyOf ui.grid.edit.api:ColumnDef + * @description A function that should be called when any files have been chosen + * by the user. You should use this to process the files appropriately for your + * application. + * + * It passes the gridCol, the gridRow (from which you can get gridRow.entity), + * and the files. The files are in the format as returned from the file chooser, + * an array of files, with each having useful information such as: + * - `files[0].lastModifiedDate` + * - `files[0].name` + * - `files[0].size` (appears to be in bytes) + * - `files[0].type` (MIME type by the looks) + * + * Typically you would do something with these files - most commonly you would + * use the filename or read the file itself in. The example function does both. + * + * @example + *
    +                     *  editFileChooserCallBack: function(gridRow, gridCol, files ){
    +                     *    // ignore all but the first file, it can only choose one anyway
    +                     *    // set the filename into this column
    +                     *    gridRow.entity.filename = file[0].name;
    +                     *    
    +                     *    // read the file and set it into a hidden column, which we may do stuff with later
    +                     *    var setFile = function(fileContent){
    +                     *      gridRow.entity.file = fileContent.currentTarget.result;
    +                     *    };
    +                     *    var reader = new FileReader();
    +                     *    reader.onload = setFile;
    +                     *    reader.readAsText( files[0] );
    +                     *  }
    +                     *  
    + */ + if ( typeof($scope.col.colDef.editFileChooserCallback) === 'function' ) { + $scope.col.colDef.editFileChooserCallback($scope.row, $scope.col, target.files); + } else { + gridUtil.logError('You need to set colDef.editFileChooserCallback to use the file chooser'); + } + + target.form.reset(); + $scope.$emit(uiGridEditConstants.events.END_CELL_EDIT); + } else { + $scope.$emit(uiGridEditConstants.events.CANCEL_CELL_EDIT); + } + }; + + $elm[0].addEventListener('change', handleFileSelect, false); // TODO: why the false on the end? Google + + $scope.$on(uiGridEditConstants.events.BEGIN_CELL_EDIT, function () { + $elm[0].focus(); + $elm[0].select(); + + $elm.on('blur', function (evt) { + $scope.$emit(uiGridEditConstants.events.END_CELL_EDIT); + }); + }); + } + }; + } + }; + }]); + + })(); diff --git a/src/features/edit/templates/fileChooserEditor.html b/src/features/edit/templates/fileChooserEditor.html new file mode 100644 index 0000000000..c719529fbb --- /dev/null +++ b/src/features/edit/templates/fileChooserEditor.html @@ -0,0 +1,5 @@ +
    +
    + +
    +
    From 79551ba040fc6eaca0852752c5f17e40bd3fc187 Mon Sep 17 00:00:00 2001 From: Dan Date: Sat, 18 Apr 2015 16:35:52 -0700 Subject: [PATCH 145/173] Fix(less): removed importants in favor of higher priority selector #3281 --- src/features/selection/less/selection.less | 4 ++-- src/less/cell.less | 4 ++-- src/less/variables.less | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/features/selection/less/selection.less b/src/features/selection/less/selection.less index 1aaba18d65..aff0043d66 100644 --- a/src/features/selection/less/selection.less +++ b/src/features/selection/less/selection.less @@ -1,7 +1,7 @@ @import '../../../less/variables'; -.ui-grid-row-selected > [ui-grid-row] > .ui-grid-cell { - background-color: @rowSelected !important; +.ui-grid-row.ui-grid-row-selected > [ui-grid-row] > .ui-grid-cell { + background-color: @rowSelected; } .ui-grid-disable-selection { diff --git a/src/less/cell.less b/src/less/cell.less index a62d6eaf54..0c84911243 100644 --- a/src/less/cell.less +++ b/src/less/cell.less @@ -36,8 +36,8 @@ display: none; } -.ui-grid-row-header-cell { - background-color: #F0F0EE !important; +.ui-grid-row .ui-grid-cell.ui-grid-row-header-cell { + background-color: @rowHeaderCell; border-bottom: solid @gridBorderWidth @borderColor; } diff --git a/src/less/variables.less b/src/less/variables.less index b2bdb539c9..60dd18b69f 100644 --- a/src/less/variables.less +++ b/src/less/variables.less @@ -41,7 +41,7 @@ // TODO: color for menu background - +@rowHeaderCell: #F0F0EE; @rowSelected: #C9DDE1; @rowSavingForeground: #848484; @rowErrorForeground: #FF0000; @@ -74,4 +74,4 @@ */ @font-path: ''; -/*-- END VARIABLES (DO NOT REMOVE THESE COMMENTS) --*/ \ No newline at end of file +/*-- END VARIABLES (DO NOT REMOVE THESE COMMENTS) --*/ From 450e5e39f7ec3321586b6e27eb6409021a4eb79f Mon Sep 17 00:00:00 2001 From: Paul Lambert Date: Sun, 19 Apr 2015 14:12:14 +1200 Subject: [PATCH 146/173] Fix(gridApi): fix #3311, passing null instead of scope to .on is legit --- src/js/core/factories/GridApi.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/js/core/factories/GridApi.js b/src/js/core/factories/GridApi.js index b4015e5272..1ababea331 100644 --- a/src/js/core/factories/GridApi.js +++ b/src/js/core/factories/GridApi.js @@ -242,8 +242,8 @@ // gridUtil.logDebug('Creating on event method ' + featureName + '.on.' + eventName); feature.on[eventName] = function (scope, handler, _this) { - if ( !scope || typeof(scope.$on) === 'undefined' ){ - gridUtil.logError('asked to listen on ' + featureName + '.on.' + eventName + ' but scope wasn\'t passed in the input parameters, you probably forgot to provide it, not registering'); + if ( scope !== null && typeof(scope.$on) === 'undefined' ){ + gridUtil.logError('asked to listen on ' + featureName + '.on.' + eventName + ' but scope wasn\'t passed in the input parameters. It is legitimate to pass null, but you\'ve passed something else, so you probably forgot to provide scope rather than did it deliberately, not registering'); return; } var deregAngularOn = registerEventWithAngular(eventId, handler, self.grid, _this); From fa2e5c9304e0972e9d0e6f10c6208fce8d3feb36 Mon Sep 17 00:00:00 2001 From: Oliv Date: Sun, 19 Apr 2015 19:16:24 +0200 Subject: [PATCH 147/173] Fix(i18nService) : Minor fr translation fixes --- src/js/i18n/fr.js | 34 ++++++++++++++++++++++------------ 1 file changed, 22 insertions(+), 12 deletions(-) diff --git a/src/js/i18n/fr.js b/src/js/i18n/fr.js index c93710e4b1..cc7d0afced 100644 --- a/src/js/i18n/fr.js +++ b/src/js/i18n/fr.js @@ -3,16 +3,16 @@ $provide.decorator('i18nService', ['$delegate', function($delegate) { $delegate.add('fr', { aggregate: { - label: 'articles' + label: 'éléments' }, groupPanel: { - description: 'Faites glisser un en-tête de colonne ici et déposez-le vers un groupe par cette colonne.' + description: 'Faites glisser une en-tête de colonne ici pour créer un groupe de colonnes.' }, search: { placeholder: 'Recherche...', - showingItems: 'Articles Affichage des:', - selectedItems: 'Éléments Articles:', - totalItems: 'Nombre total d\'articles:', + showingItems: 'Affichage des éléments :', + selectedItems: 'Éléments sélectionnés :', + totalItems: 'Nombre total d\'éléments:', size: 'Taille de page:', first: 'Première page', next: 'Page Suivante', @@ -20,7 +20,7 @@ last: 'Dernière page' }, menu: { - text: 'Choisir des colonnes:' + text: 'Choisir des colonnes :' }, sort: { ascending: 'Trier par ordre croissant', @@ -31,7 +31,7 @@ hide: 'Cacher la colonne' }, aggregation: { - count: 'total lignes: ', + count: 'lignes totales: ', sum: 'total: ', avg: 'moy: ', min: 'min: ', @@ -53,18 +53,28 @@ exporterSelectedAsPdf: 'Exporter les données sélectionnées en PDF' }, importer: { - noHeaders: 'Impossible de déterminer le nom des colonnes, le fichier possède-t-il un en-tête ?', + noHeaders: 'Impossible de déterminer le nom des colonnes, le fichier possède-t-il une en-tête ?', noObjects: 'Aucun objet trouvé, le fichier possède-t-il des données autres que l\'en-tête ?', invalidCsv: 'Le fichier n\'a pas pu être traité, le CSV est-il valide ?', invalidJson: 'Le fichier n\'a pas pu être traité, le JSON est-il valide ?', - jsonNotArray: 'Le fichier JSON importé doit contenir un tableau. Abandon.' + jsonNotArray: 'Le fichier JSON importé doit contenir un tableau, abandon.' }, pagination: { - sizes: 'articles par page', - totalItems: 'articles' + sizes: 'éléments par page', + totalItems: 'éléments' + }, + grouping: { + group: 'Grouper', + ungroup: 'Dégrouper', + aggregate_count: 'Agg: Compte', + aggregate_sum: 'Agg: Somme', + aggregate_max: 'Agg: Max', + aggregate_min: 'Agg: Min', + aggregate_avg: 'Agg: Moy', + aggregate_remove: 'Agg: Retirer' } }); return $delegate; }]); -}]); + }]); })(); \ No newline at end of file From 9c80ac907c4244b146df06273518ba8b0727a3c7 Mon Sep 17 00:00:00 2001 From: Paul Lambert Date: Tue, 21 Apr 2015 08:35:23 +1200 Subject: [PATCH 148/173] doc(tut): add example code to grouping --- misc/tutorial/209_grouping.ngdoc | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/misc/tutorial/209_grouping.ngdoc b/misc/tutorial/209_grouping.ngdoc index 75e1b70658..7444fabd70 100644 --- a/misc/tutorial/209_grouping.ngdoc +++ b/misc/tutorial/209_grouping.ngdoc @@ -79,6 +79,9 @@ counting the number of rows), we find the max age for each grouping, and we calc suppress the aggregation text on the balance column because we want to format as currency...but that means that we can't easily see that it's an average. +We write a function that extracts the aggregated data for states and genders (if you change the grouping then this +function will stop working), and writes them to the console. + var app = angular.module('app', ['ngAnimate', 'ngTouch', 'ui.grid', 'ui.grid.grouping' ]); @@ -122,6 +125,27 @@ we can't easily see that it's an average. $scope.gridApi.grouping.groupColumn('age'); $scope.gridApi.grouping.aggregateColumn('state', uiGridGroupingConstants.aggregation.COUNT); }; + + $scope.getAggregates = function() { + var aggregateInfo = {}; + var lastState; + $scope.gridApi.grid.renderContainers.body.visibleRowCache.forEach( function(row) { + if( row.groupHeader ) { + if( row.groupLevel === 0 ){ + // in the format "xxxxx (10)", we want the xxxx and the 10 + if( match = row.entity.state.match(/(.+) \((\d+)\)/) ){ + aggregateInfo[ match[1] ] = { stateTotal: match[2] }; + lastState = match[1]; + } + } else if (row.groupLevel === 1){ + if( match = row.entity.gender.match(/(.+) \((\d+)\)/) ){ + aggregateInfo[ lastState ][ match[1] ] = match[2]; + } + } + } + }); + console.log(aggregateInfo); + }; }]); @@ -131,6 +155,7 @@ we can't easily see that it's an average. +
    From b2273518234e10213d0c976207cf7fa7e927dfd0 Mon Sep 17 00:00:00 2001 From: Amy Boyd Date: Mon, 20 Apr 2015 17:51:56 +0100 Subject: [PATCH 149/173] Add Grid.prototype.clearAllFilters() method. --- src/js/core/factories/Grid.js | 55 +++++++++++++++++++- test/unit/core/factories/Grid.spec.js | 72 +++++++++++++++++++++++++++ 2 files changed, 126 insertions(+), 1 deletion(-) diff --git a/src/js/core/factories/Grid.js b/src/js/core/factories/Grid.js index 08e1cada9f..4471d7ce62 100644 --- a/src/js/core/factories/Grid.js +++ b/src/js/core/factories/Grid.js @@ -417,7 +417,19 @@ angular.module('ui.grid') * */ self.api.registerMethod( 'core', 'notifyDataChange', this.notifyDataChange ); - + + /** + * @ngdoc method + * @name clearAllFilters + * @methodOf ui.grid.core.api:PublicApi + * @description Clears all filters and optionally refreshes the visible rows. + * @params {object} refreshRows Defaults to true. + * @params {object} clearConditions Defaults to false. + * @params {object} clearFlags Defaults to false. + * @returns {promise} If `refreshRows` is true, returns a promise of the rows refreshing. + */ + self.api.registerMethod('core', 'clearAllFilters', this.clearAllFilters); + self.registerDataChangeCallback( self.columnRefreshCallback, [uiGridConstants.dataChange.COLUMN]); self.registerDataChangeCallback( self.processRowsCallback, [uiGridConstants.dataChange.EDIT]); @@ -2382,6 +2394,47 @@ angular.module('ui.grid') return this.scrollToIfNecessary(gridRow, gridCol); }; + /** + * @ngdoc function + * @name clearAllFilters + * @methodOf ui.grid.class:Grid + * @description Clears all filters and optionally refreshes the visible rows. + * @params {object} refreshRows Defaults to true. + * @params {object} clearConditions Defaults to false. + * @params {object} clearFlags Defaults to false. + * @returns {promise} If `refreshRows` is true, returns a promise of the rows refreshing. + */ + Grid.prototype.clearAllFilters = function clearAllFilters(refreshRows, clearConditions, clearFlags) { + // Default `refreshRows` to true because it will be the most commonly desired behaviour. + if (refreshRows === undefined) { + refreshRows = true; + } + if (clearConditions === undefined) { + clearConditions = false; + } + if (clearFlags === undefined) { + clearFlags = false; + } + + this.columns.forEach(function(column) { + column.filters.forEach(function(filter) { + filter.term = undefined; + + if (clearConditions) { + filter.condition = undefined; + } + + if (clearFlags) { + filter.flags = undefined; + } + }); + }); + + if (refreshRows) { + return this.refreshRows(); + } + }; + // Blatantly stolen from Angular as it isn't exposed (yet? 2.0?) function RowHashMap() {} diff --git a/test/unit/core/factories/Grid.spec.js b/test/unit/core/factories/Grid.spec.js index 3fe02f3665..0387667db1 100644 --- a/test/unit/core/factories/Grid.spec.js +++ b/test/unit/core/factories/Grid.spec.js @@ -831,4 +831,76 @@ describe('Grid factory', function () { }); }); }); + + describe('clearAllFilters', function() { + it('should clear all filter terms from all columns', function() { + grid.columns = [ + {filters: [{term: 'A'}, {term: 'B'}]}, + {filters: [{term: 'C'}]}, + {filters: []} + ]; + + grid.clearAllFilters(); + + expect(grid.columns[0].filters).toEqual([{}, {}]); + expect(grid.columns[1].filters).toEqual([{}]); + expect(grid.columns[2].filters).toEqual([]); + }); + + it('should call grid.refreshRows() if the refreshRows parameter is true', function() { + spyOn(grid, 'refreshRows'); + + grid.clearAllFilters(true); + + expect(grid.refreshRows).toHaveBeenCalled(); + }); + + it('should not call grid.refreshRows() if the refreshRows parameter is false', function() { + spyOn(grid, 'refreshRows'); + + grid.clearAllFilters(false); + + expect(grid.refreshRows).not.toHaveBeenCalled(); + }); + + it('should clear filter conditions from all columns if the clearConditions parameter is true', function() { + grid.columns = [ + {filters: [{condition: 'a value'}]} + ]; + + grid.clearAllFilters(undefined, true, undefined); + + expect(grid.columns[0].filters[0].condition).toBeUndefined(); + }); + + it('should not clear filter conditions from any column if the clearConditions parameter is false', function() { + grid.columns = [ + {filters: [{condition: 'a value'}]} + ]; + + grid.clearAllFilters(undefined, false, undefined); + + expect(grid.columns[0].filters[0].condition).toBe('a value'); + }); + + it('should clear filter flags from all columns if the clearFlags parameter is true', function() { + grid.columns = [ + {filters: [{flags: 'a value'}]} + ]; + + grid.clearAllFilters(undefined, undefined, true); + + expect(grid.columns[0].filters[0].flags).toBeUndefined(); + }); + + it('should not clear filter flags from any column if the clearFlags parameter is false', function() { + grid.columns = [ + {filters: [{flags: 'a value'}]} + ]; + + grid.clearAllFilters(undefined, undefined, false); + + expect(grid.columns[0].filters[0].flags).toBe('a value'); + }); + }); }); From 10dc29be2296693caabe1de0446b78f25261d3e3 Mon Sep 17 00:00:00 2001 From: Paul Lambert Date: Tue, 21 Apr 2015 10:37:48 +1200 Subject: [PATCH 150/173] Fix(scrolling): dreadful iPad performance, badly formed A handful of issues, mostly comes down to poor use of $interval in the throttle method (my change), and also fixing a defect where the aggregations weren't calculated by turning the throttle into a trailing throttle. This resulted in the grid waiting 0.5s before running the aggregation, but the aggregation triggered a $digest since a bound value had changed, and in turn that called aggregation again. Moved the aggregation call to be triggered by the rowsRendered event. This event is emitted whenever visible rows change, which should suit aggregation. This meant I had to move the call to aggregation into the gridFooterCell, as I needed a destroy event in order to destroy the registered api listener. --- Gruntfile.js | 2 +- misc/tutorial/401_AllFeatures.ngdoc | 1 + src/js/core/directives/ui-grid-footer-cell.js | 3 ++ src/js/core/factories/Grid.js | 3 -- src/js/core/factories/GridColumn.js | 34 ++++++++----- src/js/core/services/ui-grid-util.js | 9 +++- test/unit/core/factories/GridColumn.spec.js | 48 +++++++++++++++++++ 7 files changed, 81 insertions(+), 19 deletions(-) diff --git a/Gruntfile.js b/Gruntfile.js index 5754ae661a..0b0adaaf34 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -37,7 +37,7 @@ module.exports = function(grunt) { version: util.getVersion(), stable_version: util.getStableVersion(), dist: 'dist', - site: process.env.TRAVIS ? 'ui-grid.info' : '127.0.0.1:<%= connect.docs.options.port %>', + site: process.env.TRAVIS ? 'ui-grid.info' : '192.168.1.3:<%= connect.docs.options.port %>', banner: '/*!\n' + ' * <%= pkg.title || pkg.name %> - v<%= version %> - ' + '<%= grunt.template.today("yyyy-mm-dd") %>\n' + diff --git a/misc/tutorial/401_AllFeatures.ngdoc b/misc/tutorial/401_AllFeatures.ngdoc index 5f22e3c897..454beeda9a 100644 --- a/misc/tutorial/401_AllFeatures.ngdoc +++ b/misc/tutorial/401_AllFeatures.ngdoc @@ -23,6 +23,7 @@ All features are enabled to get an idea of performance $scope.gridOptions.enableGridMenu = true; $scope.gridOptions.showGridFooter = true; $scope.gridOptions.showColumnFooter = true; + $scope.gridOptions.aggregationCalcThrottle = 10000; $scope.gridOptions.rowIdentity = function(row) { return row.id; diff --git a/src/js/core/directives/ui-grid-footer-cell.js b/src/js/core/directives/ui-grid-footer-cell.js index 2a39eb21d0..f4c8137fb1 100644 --- a/src/js/core/directives/ui-grid-footer-cell.js +++ b/src/js/core/directives/ui-grid-footer-cell.js @@ -60,8 +60,11 @@ } }); + // Register a data change watch that would get triggered whenever someone edits a cell or modifies column defs var dataChangeDereg = $scope.grid.registerDataChangeCallback( updateClass, [uiGridConstants.dataChange.COLUMN]); + // listen for visible rows change and update aggregation values + $scope.grid.api.core.on.rowsRendered( $scope, $scope.col.updateAggregationValue ); $scope.$on( '$destroy', dataChangeDereg ); } diff --git a/src/js/core/factories/Grid.js b/src/js/core/factories/Grid.js index 08e1cada9f..603371e9c9 100644 --- a/src/js/core/factories/Grid.js +++ b/src/js/core/factories/Grid.js @@ -1302,9 +1302,6 @@ angular.module('ui.grid') }; Grid.prototype.setVisibleRows = function setVisibleRows(rows) { - // this is also setting processedRows, providing a cache of the rows as they - // came out of the rowProcessors - in particular they are sorted so we - // can process them for grouping var self = this; // Reset all the render container row caches diff --git a/src/js/core/factories/GridColumn.js b/src/js/core/factories/GridColumn.js index b4f04b3871..4fe6456079 100644 --- a/src/js/core/factories/GridColumn.js +++ b/src/js/core/factories/GridColumn.js @@ -119,10 +119,24 @@ angular.module('ui.grid') self.aggregationValue = undefined; - var updateAggregationValue = function() { + // The footer cell registers to listen for the rowsRendered event, and calls this. Needed to be + // in something with a scope so that the dereg would get called + self.updateAggregationValue = function() { // gridUtil.logDebug('getAggregationValue for Column ' + self.colDef.name); + /** + * @ngdoc property + * @name aggregationType + * @propertyOf ui.grid.class:GridOptions.columnDef + * @description The aggregation that you'd like to show in the columnFooter for this + * column. Valid values are in uiGridConstants, and currently include `uiGridConstants.aggregationTypes.count`, + * `uiGridConstants.aggregationTypes.sum`, `uiGridConstants.aggregationTypes.avg`, `uiGridConstants.aggregationTypes.min`, + * `uiGridConstants.aggregationTypes.max`. + * + * You can also provide a function as the aggregation type, in this case your function needs to accept the full + * set of visible rows, and return a value that should be shown + */ if (!self.aggregationType) { self.aggregationValue = undefined; return; @@ -173,10 +187,7 @@ angular.module('ui.grid') } }; - var throttledUpdateAggregationValue = gridUtil.throttle(updateAggregationValue, self.grid.options.aggregationCalcThrottle, { trailing: true }); - - - +// var throttledUpdateAggregationValue = gridUtil.throttle(updateAggregationValue, self.grid.options.aggregationCalcThrottle, { trailing: true, context: self.name }); /** * @ngdoc function @@ -186,15 +197,12 @@ angular.module('ui.grid') * Debounced using scrollDebounce option setting */ this.getAggregationValue = function() { - if (!self.grid.isScrollingVertically && !self.grid.isScrollingHorizontally) { - throttledUpdateAggregationValue(); - } +// if (!self.grid.isScrollingVertically && !self.grid.isScrollingHorizontally) { +// throttledUpdateAggregationValue(); +// } return self.aggregationValue; - } ; - - - + }; } @@ -627,7 +635,7 @@ angular.module('ui.grid') defaultFilters.push({}); } -/** + /** * @ngdoc property * @name filter * @propertyOf ui.grid.class:GridOptions.columnDef diff --git a/src/js/core/services/ui-grid-util.js b/src/js/core/services/ui-grid-util.js index dc9ca077e7..a881084ef6 100644 --- a/src/js/core/services/ui-grid-util.js +++ b/src/js/core/services/ui-grid-util.js @@ -1082,6 +1082,11 @@ module.service('gridUtil', ['$log', '$window', '$document', '$http', '$templateC * trailing (bool) - whether to trigger after throttle time ends if called multiple times * Updated to use $interval rather than $timeout, as protractor (e2e tests) is able to work with $interval, * but not with $timeout + * + * Note that when using throttle, you need to use throttle to create a new function upfront, then use the function + * return from that call each time you need to call throttle. If you call throttle itself repeatedly, the lastCall + * variable will get overwritten and the throttling won't work + * * @example *
        * var throttledFunc =  gridUtil.throttle(function(){console.log('throttled');}, 500, {trailing: true});
    @@ -1092,12 +1097,12 @@ module.service('gridUtil', ['$log', '$window', '$document', '$http', '$templateC
        */
       s.throttle = function(func, wait, options){
         options = options || {};
    -    var lastCall = 0, queued = null, context, args;
    +    var lastCall = 0, queued = null, context, args, rndFunctionTag = Math.floor(Math.random() * (10000));
     
         function runFunc(endDate){
           lastCall = +new Date();
           func.apply(context, args);
    -      $interval(function(){ queued = null; }, 0, 1);
    +      $timeout(function(){ queued = null; }, 0);
         }
     
         return function(){
    diff --git a/test/unit/core/factories/GridColumn.spec.js b/test/unit/core/factories/GridColumn.spec.js
    index c17391f9bc..3be9e79b9d 100644
    --- a/test/unit/core/factories/GridColumn.spec.js
    +++ b/test/unit/core/factories/GridColumn.spec.js
    @@ -191,10 +191,14 @@ describe('GridColumn factory', function () {
           
           buildCols();
           grid.modifyRows(grid.options.data);
    +
    +      // this would be called by the footer cell if it were rendered 
    +      var deregFn = grid.api.core.on.rowsRendered(null, grid.columns[0].updateAggregationValue);
           grid.setVisibleRows(grid.rows);
           
           expect(grid.columns[0].getAggregationValue()).toEqual(5);
           expect(grid.columns[0].getAggregationText()).toEqual('total rows: ');
    +      deregFn();
         });
     
         it('count, without label', function() {
    @@ -203,10 +207,14 @@ describe('GridColumn factory', function () {
           
           buildCols();
           grid.modifyRows(grid.options.data);
    +
    +      // this would be called by the footer cell if it were rendered 
    +      var deregFn = grid.api.core.on.rowsRendered(null, grid.columns[0].updateAggregationValue);
           grid.setVisibleRows(grid.rows);
           
           expect(grid.columns[0].getAggregationValue()).toEqual(5);
           expect(grid.columns[0].getAggregationText()).toEqual('');
    +      deregFn();
         });
     
         it('sum, with label', function() {
    @@ -214,10 +222,14 @@ describe('GridColumn factory', function () {
           
           buildCols();
           grid.modifyRows(grid.options.data);
    +
    +      // this would be called by the footer cell if it were rendered 
    +      var deregFn = grid.api.core.on.rowsRendered(null, grid.columns[1].updateAggregationValue);
           grid.setVisibleRows(grid.rows);
           
           expect(grid.columns[1].getAggregationValue()).toEqual(15);
           expect(grid.columns[1].getAggregationText()).toEqual('total: ');
    +      deregFn();
         });
     
         it('sum, without label', function() {
    @@ -226,10 +238,14 @@ describe('GridColumn factory', function () {
           
           buildCols();
           grid.modifyRows(grid.options.data);
    +
    +      // this would be called by the footer cell if it were rendered 
    +      var deregFn = grid.api.core.on.rowsRendered(null, grid.columns[1].updateAggregationValue);
           grid.setVisibleRows(grid.rows);
           
           expect(grid.columns[1].getAggregationValue()).toEqual(15);
           expect(grid.columns[1].getAggregationText()).toEqual('');
    +      deregFn();
         });
     
         it('avg, with label', function() {
    @@ -237,10 +253,14 @@ describe('GridColumn factory', function () {
           
           buildCols();
           grid.modifyRows(grid.options.data);
    +
    +      // this would be called by the footer cell if it were rendered 
    +      var deregFn = grid.api.core.on.rowsRendered(null, grid.columns[1].updateAggregationValue);
           grid.setVisibleRows(grid.rows);
           
           expect(grid.columns[1].getAggregationValue()).toEqual(3);
           expect(grid.columns[1].getAggregationText()).toEqual('avg: ');
    +      deregFn();
         });
     
         it('avg, without label', function() {
    @@ -249,10 +269,14 @@ describe('GridColumn factory', function () {
           
           buildCols();
           grid.modifyRows(grid.options.data);
    +
    +      // this would be called by the footer cell if it were rendered 
    +      var deregFn = grid.api.core.on.rowsRendered(null, grid.columns[1].updateAggregationValue);
           grid.setVisibleRows(grid.rows);
           
           expect(grid.columns[1].getAggregationValue()).toEqual(3);
           expect(grid.columns[1].getAggregationText()).toEqual('');
    +      deregFn();
         });    
     
         it('min, with label', function() {
    @@ -260,10 +284,14 @@ describe('GridColumn factory', function () {
           
           buildCols();
           grid.modifyRows(grid.options.data);
    +
    +      // this would be called by the footer cell if it were rendered 
    +      var deregFn = grid.api.core.on.rowsRendered(null, grid.columns[1].updateAggregationValue);
           grid.setVisibleRows(grid.rows);
           
           expect(grid.columns[1].getAggregationValue()).toEqual(1);
           expect(grid.columns[1].getAggregationText()).toEqual('min: ');
    +      deregFn();
         });
     
         it('min, without label', function() {
    @@ -272,10 +300,14 @@ describe('GridColumn factory', function () {
           
           buildCols();
           grid.modifyRows(grid.options.data);
    +
    +      // this would be called by the footer cell if it were rendered 
    +      var deregFn = grid.api.core.on.rowsRendered(null, grid.columns[1].updateAggregationValue);
           grid.setVisibleRows(grid.rows);
           
           expect(grid.columns[1].getAggregationValue()).toEqual(1);
           expect(grid.columns[1].getAggregationText()).toEqual('');
    +      deregFn();
         });
     
         it('max, with label', function() {
    @@ -283,10 +315,14 @@ describe('GridColumn factory', function () {
           
           buildCols();
           grid.modifyRows(grid.options.data);
    +
    +      // this would be called by the footer cell if it were rendered 
    +      var deregFn = grid.api.core.on.rowsRendered(null, grid.columns[1].updateAggregationValue);
           grid.setVisibleRows(grid.rows);
           
           expect(grid.columns[1].getAggregationValue()).toEqual(5);
           expect(grid.columns[1].getAggregationText()).toEqual('max: ');
    +      deregFn();
         });
     
         it('max, without label', function() {
    @@ -295,10 +331,14 @@ describe('GridColumn factory', function () {
           
           buildCols();
           grid.modifyRows(grid.options.data);
    +
    +      // this would be called by the footer cell if it were rendered 
    +      var deregFn = grid.api.core.on.rowsRendered(null, grid.columns[1].updateAggregationValue);
           grid.setVisibleRows(grid.rows);
           
           expect(grid.columns[1].getAggregationValue()).toEqual(5);
           expect(grid.columns[1].getAggregationText()).toEqual('');
    +      deregFn();
         });
     
         it('max, with custom label', function() {
    @@ -310,10 +350,14 @@ describe('GridColumn factory', function () {
     
           buildCols();
           grid.modifyRows(grid.options.data);
    +
    +      // this would be called by the footer cell if it were rendered 
    +      var deregFn = grid.api.core.on.rowsRendered(null, grid.columns[1].updateAggregationValue);
           grid.setVisibleRows(grid.rows);
     
           expect(grid.columns[1].getAggregationValue()).toEqual(5);
           expect(grid.columns[1].getAggregationText()).toEqual(customLabel);
    +      deregFn();
         });
     
         it('max, with custom label while also being hidden', function() {
    @@ -325,10 +369,14 @@ describe('GridColumn factory', function () {
     
           buildCols();
           grid.modifyRows(grid.options.data);
    +
    +      // this would be called by the footer cell if it were rendered 
    +      var deregFn = grid.api.core.on.rowsRendered(null, grid.columns[1].updateAggregationValue);
           grid.setVisibleRows(grid.rows);
     
           expect(grid.columns[1].getAggregationValue()).toEqual(5);
           expect(grid.columns[1].getAggregationText()).toEqual('');
    +      deregFn();
         });
       });
     
    
    From 4ffaaf26774bae7f52bf4956f45243f6c7dd53a3 Mon Sep 17 00:00:00 2001
    From: Jonathan Stevens 
    Date: Mon, 20 Apr 2015 22:01:43 -0400
    Subject: [PATCH 151/173] fix(pinning): restore correct width state
    
    closes: #3324
    ---
     src/features/pinning/js/pinning.js | 1 -
     1 file changed, 1 deletion(-)
    
    diff --git a/src/features/pinning/js/pinning.js b/src/features/pinning/js/pinning.js
    index 80fde3dd7a..bcd5b5c1ce 100644
    --- a/src/features/pinning/js/pinning.js
    +++ b/src/features/pinning/js/pinning.js
    @@ -205,7 +205,6 @@
             }
             else {
               col.renderContainer = container;
    -          col.width = col.drawnWidth;
               if (container === uiGridPinningConstants.container.LEFT) {
                 grid.createLeftContainer();
               }
    
    From 8c7e0bc1a591e95eb593371da5a2b903f749d84b Mon Sep 17 00:00:00 2001
    From: Paul Lambert 
    Date: Tue, 21 Apr 2015 14:11:52 +1200
    Subject: [PATCH 152/173] Fix(grunt): fix untidiness introduced in last commit
    
    ---
     Gruntfile.js                         | 2 +-
     misc/tutorial/401_AllFeatures.ngdoc  | 1 -
     src/js/core/services/ui-grid-util.js | 4 ++--
     3 files changed, 3 insertions(+), 4 deletions(-)
    
    diff --git a/Gruntfile.js b/Gruntfile.js
    index 0b0adaaf34..5754ae661a 100644
    --- a/Gruntfile.js
    +++ b/Gruntfile.js
    @@ -37,7 +37,7 @@ module.exports = function(grunt) {
           version: util.getVersion(),
           stable_version: util.getStableVersion(),
           dist: 'dist',
    -      site: process.env.TRAVIS ? 'ui-grid.info' : '192.168.1.3:<%= connect.docs.options.port %>',
    +      site: process.env.TRAVIS ? 'ui-grid.info' : '127.0.0.1:<%= connect.docs.options.port %>',
           banner: '/*!\n' +
             ' * <%= pkg.title || pkg.name %> - v<%= version %> - ' +
             '<%= grunt.template.today("yyyy-mm-dd") %>\n' +
    diff --git a/misc/tutorial/401_AllFeatures.ngdoc b/misc/tutorial/401_AllFeatures.ngdoc
    index 454beeda9a..5f22e3c897 100644
    --- a/misc/tutorial/401_AllFeatures.ngdoc
    +++ b/misc/tutorial/401_AllFeatures.ngdoc
    @@ -23,7 +23,6 @@ All features are enabled to get an idea of performance
           $scope.gridOptions.enableGridMenu = true;
           $scope.gridOptions.showGridFooter = true;
           $scope.gridOptions.showColumnFooter = true;
    -      $scope.gridOptions.aggregationCalcThrottle = 10000;
     
           $scope.gridOptions.rowIdentity = function(row) {
             return row.id;
    diff --git a/src/js/core/services/ui-grid-util.js b/src/js/core/services/ui-grid-util.js
    index a881084ef6..01989fce43 100644
    --- a/src/js/core/services/ui-grid-util.js
    +++ b/src/js/core/services/ui-grid-util.js
    @@ -1097,12 +1097,12 @@ module.service('gridUtil', ['$log', '$window', '$document', '$http', '$templateC
        */
       s.throttle = function(func, wait, options){
         options = options || {};
    -    var lastCall = 0, queued = null, context, args, rndFunctionTag = Math.floor(Math.random() * (10000));
    +    var lastCall = 0, queued = null, context, args;
     
         function runFunc(endDate){
           lastCall = +new Date();
           func.apply(context, args);
    -      $timeout(function(){ queued = null; }, 0);
    +      $interval(function(){ queued = null; }, 0, 1);
         }
     
         return function(){
    
    From 2739e02ab6bcfbfade23981890c1a3f50e5780e6 Mon Sep 17 00:00:00 2001
    From: Paul Lambert 
    Date: Tue, 21 Apr 2015 13:15:33 +1200
    Subject: [PATCH 153/173] Fix(watchers): remove watchers on column from header,
     cell, footer
    
    We believe that the track by col.name means these aren't needed, and
    things seem to run.
    ---
     src/js/core/directives/ui-grid-cell.js        | 4 +++-
     src/js/core/directives/ui-grid-footer-cell.js | 2 ++
     src/js/core/directives/ui-grid-header-cell.js | 3 ++-
     test/unit/core/directives/uiGridCell.spec.js  | 4 ++++
     4 files changed, 11 insertions(+), 2 deletions(-)
    
    diff --git a/src/js/core/directives/ui-grid-cell.js b/src/js/core/directives/ui-grid-cell.js
    index 5e3de7bc57..9b9af47f48 100644
    --- a/src/js/core/directives/ui-grid-cell.js
    +++ b/src/js/core/directives/ui-grid-cell.js
    @@ -87,13 +87,15 @@ angular.module('ui.grid').directive('uiGridCell', ['$compile', '$parse', 'gridUt
               };
     
               // TODO(c0bra): Turn this into a deep array watch
    +/*        shouldn't be needed any more given track by col.name
               var colWatchDereg = $scope.$watch( 'col', cellChangeFunction );
    +*/
               var rowWatchDereg = $scope.$watch( 'row', cellChangeFunction );
               
               
               var deregisterFunction = function() {
                 dataChangeDereg();
    -            colWatchDereg();
    +//            colWatchDereg();
                 rowWatchDereg(); 
               };
               
    diff --git a/src/js/core/directives/ui-grid-footer-cell.js b/src/js/core/directives/ui-grid-footer-cell.js
    index f4c8137fb1..9bc2438725 100644
    --- a/src/js/core/directives/ui-grid-footer-cell.js
    +++ b/src/js/core/directives/ui-grid-footer-cell.js
    @@ -48,6 +48,7 @@
                 }
     
                 // Watch for column changes so we can alter the col cell class properly
    +/* shouldn't be needed any more, given track by col.name
                 $scope.$watch('col', function (n, o) {
                   if (n !== o) {
                     // See if the column's internal class has changed
    @@ -59,6 +60,7 @@
                     }
                   }
                 });
    +*/
     
     
                 // Register a data change watch that would get triggered whenever someone edits a cell or modifies column defs
    diff --git a/src/js/core/directives/ui-grid-header-cell.js b/src/js/core/directives/ui-grid-header-cell.js
    index 18d900eafb..d1877f8594 100644
    --- a/src/js/core/directives/ui-grid-header-cell.js
    +++ b/src/js/core/directives/ui-grid-header-cell.js
    @@ -287,6 +287,7 @@
                   } 
                 };
     
    +/*
                 $scope.$watch('col', function (n, o) {
                   if (n !== o) {
                     // See if the column's internal class has changed
    @@ -298,7 +299,7 @@
                     }
                   }
                 });
    -  
    +*/
                 updateHeaderOptions();
                 
                 // Register a data change watch that would get triggered whenever someone edits a cell or modifies column defs
    diff --git a/test/unit/core/directives/uiGridCell.spec.js b/test/unit/core/directives/uiGridCell.spec.js
    index de0e47e7de..4d8d10f2f0 100644
    --- a/test/unit/core/directives/uiGridCell.spec.js
    +++ b/test/unit/core/directives/uiGridCell.spec.js
    @@ -66,6 +66,7 @@ describe('uiGridCell', function () {
           expect(gridCell.hasClass('funcCellClass')).toBe(false);
         }));
     
    +/* Not a valid test - track by col.name
         it('should notice col changes and update cellClass', inject(function () {
           $scope.col.cellClass = function (grid, row, col, rowRenderIndex, colRenderIndex) {
             if (rowRenderIndex === 2 && colRenderIndex === 2) {
    @@ -84,9 +85,11 @@ describe('uiGridCell', function () {
           $scope.$digest();
           expect(gridCell.hasClass('funcCellClass')).toBe(false);
         }));
    +*/
       });
       
       // Don't run this on IE9. The behavior looks correct when testing interactively but these tests fail
    +/* not a valid test.....we think
       if (!navigator.userAgent.match(/MSIE\s+9\.0/)) {
         it("should change a column's class when its uid changes", inject(function (gridUtil, $compile, uiGridConstants) {
           // Reset the UIDs (used by columns) so they're fresh and clean
    @@ -143,4 +146,5 @@ describe('uiGridCell', function () {
           angular.element(gridElm).remove();
         }));
       }
    +*/
     });
    \ No newline at end of file
    
    From f7d42039513932b38e14d63f767eb3965caa9691 Mon Sep 17 00:00:00 2001
    From: Paul Lambert 
    Date: Tue, 21 Apr 2015 18:12:32 +1200
    Subject: [PATCH 154/173] Enh(grid): add fastWatch option that only watches
     columnDef and data lengths
    
    ---
     misc/tutorial/401_AllFeatures.ngdoc           |  5 +-
     .../404_large_data_sets_and_performance.ngdoc |  9 +++-
     src/js/core/directives/ui-grid.js             | 46 +++++++++++++++----
     src/js/core/factories/GridRenderContainer.js  |  1 -
     test/unit/core/row-filtering.spec.js          |  2 +-
     5 files changed, 48 insertions(+), 15 deletions(-)
    
    diff --git a/misc/tutorial/401_AllFeatures.ngdoc b/misc/tutorial/401_AllFeatures.ngdoc
    index 5f22e3c897..f57edc8a4f 100644
    --- a/misc/tutorial/401_AllFeatures.ngdoc
    +++ b/misc/tutorial/401_AllFeatures.ngdoc
    @@ -11,7 +11,7 @@ All features are enabled to get an idea of performance
     @example
     
       
    -    var app = angular.module('app', ['ngTouch', 'ui.grid', 'ui.grid.cellNav', 'ui.grid.edit', 'ui.grid.resizeColumns', 'ui.grid.pinning', 'ui.grid.selection', 'ui.grid.moveColumns']);
    +    var app = angular.module('app', ['ngTouch', 'ui.grid', 'ui.grid.cellNav', 'ui.grid.edit', 'ui.grid.resizeColumns', 'ui.grid.pinning', 'ui.grid.selection', 'ui.grid.moveColumns', 'ui.grid.exporter', 'ui.grid.importer', 'ui.grid.grouping']);
     
         app.controller('MainCtrl',  ['$scope', '$http', '$timeout', '$interval', 'uiGridConstants',
          function ($scope, $http, $timeout, $interval, uiGridConstants) {
    @@ -23,6 +23,7 @@ All features are enabled to get an idea of performance
           $scope.gridOptions.enableGridMenu = true;
           $scope.gridOptions.showGridFooter = true;
           $scope.gridOptions.showColumnFooter = true;
    +      $scope.gridOptions.fastWatch = true;
     
           $scope.gridOptions.rowIdentity = function(row) {
             return row.id;
    @@ -93,7 +94,7 @@ All features are enabled to get an idea of performance
           
    {{ myData.length }} rows
    -
    +
    diff --git a/misc/tutorial/404_large_data_sets_and_performance.ngdoc b/misc/tutorial/404_large_data_sets_and_performance.ngdoc index 2cc21af434..4d0128fda0 100644 --- a/misc/tutorial/404_large_data_sets_and_performance.ngdoc +++ b/misc/tutorial/404_large_data_sets_and_performance.ngdoc @@ -32,6 +32,12 @@ and the grid will run faster. The option for this is `flatEntityAccess`. The below grid provides a toggle on this value so you can see the difference it makes with a data set of 640,000 rows. +Further, the grid watches the data and the column defs to determine if anything has changed. Watches get called very frequently in +angularJS (on every digest cycle, which can be many times per second), and watching each row of a large data collection can be expensive. +The `fastWatch` gridOption watches only the data reference and it's length - in theory this means we might not notice if you replaced +a row in the data, or replaced a column in the columnDefs. If using fastWatch you should always do deletes and adds separately, never +a swap. Fastwatch cannot be dynamically toggled - you need to set it upon grid creation. + @example @@ -41,7 +47,8 @@ with a data set of 640,000 rows. $scope.gridOptions = { enableFiltering: true, flatEntityAccess: true, - showGridFooter: true + showGridFooter: true, + fastWatch: true }; $scope.gridOptions.columnDefs = [ diff --git a/src/js/core/directives/ui-grid.js b/src/js/core/directives/ui-grid.js index 1cbddbe89f..c235316f5e 100644 --- a/src/js/core/directives/ui-grid.js +++ b/src/js/core/directives/ui-grid.js @@ -36,15 +36,34 @@ } - var dataWatchCollectionDereg; - if (angular.isString($scope.uiGrid.data)) { - dataWatchCollectionDereg = $scope.$parent.$watchCollection($scope.uiGrid.data, dataWatchFunction); - } - else { - dataWatchCollectionDereg = $scope.$parent.$watchCollection(function() { return $scope.uiGrid.data; }, dataWatchFunction); + // if fastWatch is set we watch only the length and the reference, not every individual object + var deregFunctions = []; + if (self.grid.options.fastWatch) { + self.uiGrid = $scope.uiGrid; + if (angular.isString($scope.uiGrid.data)) { + deregFunctions.push( $scope.$parent.$watch($scope.uiGrid.data, dataWatchFunction) ); + deregFunctions.push( $scope.$parent.$watch(function() { + if ( self.grid.appScope[$scope.uiGrid.data] ){ + return self.grid.appScope[$scope.uiGrid.data].length; + } else { + return undefined; + } + }, dataWatchFunction) ); + } else { + deregFunctions.push( $scope.$parent.$watch(function() { return $scope.uiGrid.data; }, dataWatchFunction) ); + deregFunctions.push( $scope.$parent.$watch(function() { return $scope.uiGrid.data.length; }, dataWatchFunction) ); + } + deregFunctions.push( $scope.$parent.$watch(function() { return $scope.uiGrid.columnDefs; }, columnDefsWatchFunction) ); + deregFunctions.push( $scope.$parent.$watch(function() { return $scope.uiGrid.columnDefs.length; }, columnDefsWatchFunction) ); + } else { + if (angular.isString($scope.uiGrid.data)) { + deregFunctions.push( $scope.$parent.$watchCollection($scope.uiGrid.data, dataWatchFunction) ); + } else { + deregFunctions.push( $scope.$parent.$watchCollection(function() { return $scope.uiGrid.data; }, dataWatchFunction) ); + } + deregFunctions.push( $scope.$parent.$watchCollection(function() { return $scope.uiGrid.columnDefs; }, columnDefsWatchFunction) ); } - - var columnDefWatchCollectionDereg = $scope.$parent.$watchCollection(function() { return $scope.uiGrid.columnDefs; }, columnDefsWatchFunction); + function columnDefsWatchFunction(n, o) { if (n && n !== o) { @@ -63,6 +82,14 @@ // gridUtil.logDebug('dataWatch fired'); var promises = []; + if ( self.grid.options.fastWatch ){ + if (angular.isString($scope.uiGrid.data)) { + newData = self.grid.appScope[$scope.uiGrid.data]; + } else { + newData = $scope.uiGrid.data; + } + } + if (newData) { if ( // If we have no columns (i.e. columns length is either 0 or equal to the number of row header columns, which don't count because they're created automatically) @@ -108,8 +135,7 @@ }); $scope.$on('$destroy', function() { - dataWatchCollectionDereg(); - columnDefWatchCollectionDereg(); + deregFunctions.forEach( function( deregFn ){ deregFn(); }); styleWatchDereg(); }); diff --git a/src/js/core/factories/GridRenderContainer.js b/src/js/core/factories/GridRenderContainer.js index 3df93b2e12..2fc1ab3447 100644 --- a/src/js/core/factories/GridRenderContainer.js +++ b/src/js/core/factories/GridRenderContainer.js @@ -492,7 +492,6 @@ angular.module('ui.grid') // Define the top-most rendered row this.currentTopRow = renderedRange[0]; - // TODO(c0bra): make this method! this.setRenderedRows(rowArr); }; diff --git a/test/unit/core/row-filtering.spec.js b/test/unit/core/row-filtering.spec.js index eb00c78f25..4808592f29 100644 --- a/test/unit/core/row-filtering.spec.js +++ b/test/unit/core/row-filtering.spec.js @@ -51,7 +51,7 @@ describe('rowSearcher', function() { }); describe('guessCondition', function () { - it('should create a RegExp when term ends with a *', function() { + iit('should create a RegExp when term ends with a *', function() { var filter = { term: 'blah*' }; var re = new RegExp(/^blah[\s\S]*?$/i); From 89461bcbcfdfc527655c398df19555738fa9bd63 Mon Sep 17 00:00:00 2001 From: swalters Date: Tue, 21 Apr 2015 14:04:51 -0500 Subject: [PATCH 155/173] fix(scrolling): Fix for #3260 atTop/Bottom/Left/Right needed tweaking --- .../directives/ui-grid-render-container.js | 20 +++++++++---------- src/js/core/factories/ScrollEvent.js | 6 +++--- 2 files changed, 12 insertions(+), 14 deletions(-) diff --git a/src/js/core/directives/ui-grid-render-container.js b/src/js/core/directives/ui-grid-render-container.js index f3d882b399..55851e9ade 100644 --- a/src/js/core/directives/ui-grid-render-container.js +++ b/src/js/core/directives/ui-grid-render-container.js @@ -2,7 +2,7 @@ 'use strict'; var module = angular.module('ui.grid'); - + module.directive('uiGridRenderContainer', ['$timeout', '$document', 'uiGridConstants', 'gridUtil', 'ScrollEvent', function($timeout, $document, uiGridConstants, gridUtil, ScrollEvent) { return { @@ -45,7 +45,7 @@ var rowContainer = $scope.rowContainer = grid.renderContainers[$scope.rowContainerName]; var colContainer = $scope.colContainer = grid.renderContainers[$scope.colContainerName]; - + containerCtrl.containerId = $scope.containerId; containerCtrl.rowContainer = rowContainer; containerCtrl.colContainer = colContainer; @@ -98,10 +98,8 @@ } // Let the parent container scroll if the grid is already at the top/bottom - if (scrollEvent.atTop(scrollTop) || - scrollEvent.atBottom(scrollTop) || - scrollEvent.atLeft(scrollLeft) || - scrollEvent.atRight(scrollLeft)) { + if ((event.deltaY !== 0 && (scrollEvent.atTop(scrollTop) || scrollEvent.atBottom(scrollTop))) || + (event.deltaX !== 0 && (scrollEvent.atLeft(scrollLeft) || scrollEvent.atRight(scrollLeft)))) { //parent controller scrolls } else { @@ -118,7 +116,7 @@ $elm.unbind(eventName); }); }); - + // TODO(c0bra): Handle resizing the inner canvas based on the number of elements function update() { var ret = ''; @@ -137,7 +135,7 @@ var headerViewportWidth = colContainer.getHeaderViewportWidth(); var footerViewportWidth = colContainer.getHeaderViewportWidth(); - + // Set canvas dimensions ret += '\n .grid' + uiGridCtrl.grid.id + ' .ui-grid-render-container-' + $scope.containerId + ' .ui-grid-canvas { width: ' + canvasWidth + 'px; height: ' + canvasHeight + 'px; }'; @@ -146,7 +144,7 @@ if (renderContainer.explicitHeaderCanvasHeight) { ret += '\n .grid' + uiGridCtrl.grid.id + ' .ui-grid-render-container-' + $scope.containerId + ' .ui-grid-header-canvas { height: ' + renderContainer.explicitHeaderCanvasHeight + 'px; }'; } - + ret += '\n .grid' + uiGridCtrl.grid.id + ' .ui-grid-render-container-' + $scope.containerId + ' .ui-grid-viewport { width: ' + viewportWidth + 'px; height: ' + viewportHeight + 'px; }'; ret += '\n .grid' + uiGridCtrl.grid.id + ' .ui-grid-render-container-' + $scope.containerId + ' .ui-grid-header-viewport { width: ' + headerViewportWidth + 'px; }'; @@ -155,7 +153,7 @@ return ret; } - + uiGridCtrl.grid.registerStyleComputation({ priority: 6, func: update @@ -168,7 +166,7 @@ }]); module.controller('uiGridRenderContainer', ['$scope', 'gridUtil', function ($scope, gridUtil) { - + }]); })(); diff --git a/src/js/core/factories/ScrollEvent.js b/src/js/core/factories/ScrollEvent.js index 6d84ec5374..68f692aa73 100644 --- a/src/js/core/factories/ScrollEvent.js +++ b/src/js/core/factories/ScrollEvent.js @@ -132,7 +132,7 @@ }; ScrollEvent.prototype.atTop = function(scrollTop) { - return (this.y && this.y.percentage < 1 && scrollTop === 0); + return (this.y && this.y.percentage === 0 && scrollTop === 0); }; ScrollEvent.prototype.atBottom = function(scrollTop) { @@ -140,7 +140,7 @@ }; ScrollEvent.prototype.atLeft = function(scrollLeft) { - return (this.x && this.x.percentage < 1 && scrollLeft === 0); + return (this.x && this.x.percentage === 0 && scrollLeft === 0); }; ScrollEvent.prototype.atRight = function(scrollLeft) { @@ -160,4 +160,4 @@ -})(); \ No newline at end of file +})(); From 02b05cae6d5385e01d00f812662f16009130c647 Mon Sep 17 00:00:00 2001 From: swalters Date: Tue, 21 Apr 2015 16:25:17 -0500 Subject: [PATCH 156/173] fix(cellNav): fix null ref issue in navigate event for oldRowColumn scrollTo should not setFocus unless there is an active row and column. Was getting error when using restoreState --- src/features/cellnav/js/cellnav.js | 4 +++- test/unit/core/row-filtering.spec.js | 6 +++--- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/src/features/cellnav/js/cellnav.js b/src/features/cellnav/js/cellnav.js index a0038b0d23..9d0047ce1d 100644 --- a/src/features/cellnav/js/cellnav.js +++ b/src/features/cellnav/js/cellnav.js @@ -515,7 +515,9 @@ var rowCol = { row: gridRow, col: gridCol }; // Broadcast the navigation - grid.cellNav.broadcastCellNav(rowCol); + if (gridRow !== null && gridCol !== null) { + grid.cellNav.broadcastCellNav(rowCol); + } }); diff --git a/test/unit/core/row-filtering.spec.js b/test/unit/core/row-filtering.spec.js index 4808592f29..30b47876a9 100644 --- a/test/unit/core/row-filtering.spec.js +++ b/test/unit/core/row-filtering.spec.js @@ -51,7 +51,7 @@ describe('rowSearcher', function() { }); describe('guessCondition', function () { - iit('should create a RegExp when term ends with a *', function() { + it('should create a RegExp when term ends with a *', function() { var filter = { term: 'blah*' }; var re = new RegExp(/^blah[\s\S]*?$/i); @@ -236,7 +236,7 @@ describe('rowSearcher', function() { expect(ret.length).toEqual(3); }); }); - + describe('with a custom filter function', function() { var custom, ret; beforeEach(function() { @@ -250,7 +250,7 @@ describe('rowSearcher', function() { var orEqualTo = secondChar === '='; var trimBy = orEqualTo ? 2 : 1 ; var compareTo; - + if (firstChar === '>') { compareTo = searchTerm.substr(trimBy) * 1; return orEqualTo ? rowValue >= compareTo : rowValue > compareTo; From 56c723bed0b5d151109f900f3907d4ace39af1e4 Mon Sep 17 00:00:00 2001 From: Paul Lambert Date: Thu, 23 Apr 2015 06:39:22 +1200 Subject: [PATCH 157/173] Fix(tooltip): fix #3336 apply cellFilter to tooltip --- misc/tutorial/117_tooltips.ngdoc | 25 +++++++++++++++++++++++-- src/js/core/factories/Grid.js | 6 +++++- 2 files changed, 28 insertions(+), 3 deletions(-) diff --git a/misc/tutorial/117_tooltips.ngdoc b/misc/tutorial/117_tooltips.ngdoc index bc900e8d5a..f1cc651457 100644 --- a/misc/tutorial/117_tooltips.ngdoc +++ b/misc/tutorial/117_tooltips.ngdoc @@ -17,6 +17,8 @@ Note that turning on tooltips will create an extra watcher per cell, so it has a performance, it is not recommended to turn them on for every column, rather only for the columns likely to have data that won't be displayable within the grid row (e.g. long description fields). +Tooltips respect the cellFilter, so if you define a cellFilter it will also be used in the tooltip. + var app = angular.module('app', ['ngAnimate', 'ngTouch', 'ui.grid']); @@ -30,7 +32,8 @@ data that won't be displayable within the grid row (e.g. long description fields function( row, col ) { return 'Name: ' + row.entity.name + ' Company: ' + row.entity.company; } - } + }, + { field: 'gender', cellTooltip: true, cellFilter: 'mapGender' }, ], onRegisterApi: function( gridApi ) { $scope.gridApi = gridApi; @@ -42,9 +45,27 @@ data that won't be displayable within the grid row (e.g. long description fields $http.get('/data/100.json') .success(function(data) { + data.forEach( function setGender( row, index ){ + row.gender = row.gender==='male' ? '1' : '2'; + }); + $scope.gridOptions.data = data; }); - }]); + }]) + .filter('mapGender', function() { + var genderHash = { + 1: 'male', + 2: 'female' + }; + + return function(input) { + if (!input){ + return ''; + } else { + return genderHash[input]; + } + }; + });
    diff --git a/src/js/core/factories/Grid.js b/src/js/core/factories/Grid.js index 3cd2c30be0..d6e693adc2 100644 --- a/src/js/core/factories/Grid.js +++ b/src/js/core/factories/Grid.js @@ -864,7 +864,11 @@ angular.module('ui.grid') html = html.replace(uiGridConstants.TOOLTIP, ''); } else { // gridColumn will have made sure that the col either has false or a function for this value - html = html.replace(uiGridConstants.TOOLTIP, 'title="{{col.cellTooltip(row, col)}}"'); + if (col.cellFilter){ + html = html.replace(uiGridConstants.TOOLTIP, 'title="{{col.cellTooltip(row, col) | ' + col.cellFilter + '}}"'); + } else { + html = html.replace(uiGridConstants.TOOLTIP, 'title="{{col.cellTooltip(row, col)}}"'); + } } var compiledElementFn = $compile(html); From 2cd5655ec696b8d303c1c193ec39692dbf56fbce Mon Sep 17 00:00:00 2001 From: A Coleman Date: Fri, 24 Apr 2015 15:18:47 +0100 Subject: [PATCH 158/173] Update index.html Make "Tutorial" icon book Make consistent with; http://ui-grid.info/docs/#/api http://ui-grid.info/docs/#/tutorial Originally ga-gears which communicates "settings", link is served better by "book" --- misc/site/index.html | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/misc/site/index.html b/misc/site/index.html index 0f656bfd0b..1d12059b3c 100644 --- a/misc/site/index.html +++ b/misc/site/index.html @@ -69,7 +69,7 @@

    Angular UI Grid

    - + Tutorial

    @@ -411,4 +411,4 @@

    Complex Example

    }); - \ No newline at end of file + From 8a331696009184c5a8ecfe8648c363982d7f8618 Mon Sep 17 00:00:00 2001 From: Paul Lambert Date: Sat, 25 Apr 2015 08:43:40 +1200 Subject: [PATCH 159/173] Doco(tut): update tuts with some more info and examples --- misc/tutorial/103_filtering.ngdoc | 8 +++++++- misc/tutorial/209_grouping.ngdoc | 34 +++++++++++++++++++++++++------ 2 files changed, 35 insertions(+), 7 deletions(-) diff --git a/misc/tutorial/103_filtering.ngdoc b/misc/tutorial/103_filtering.ngdoc index f166e97b86..1aa4e6b713 100644 --- a/misc/tutorial/103_filtering.ngdoc +++ b/misc/tutorial/103_filtering.ngdoc @@ -8,7 +8,10 @@ UI-Grid allows you to filter rows. Just set the `enableFiltering` flag in your g Filtering can be disabled at the column level by setting `enableFiltering: false` in the column def. See the "company" column below for an example. -The filter field can be pre-populated by setting `filter: { term: 'xxx' }` in the column def. See the "gender" column below. +The filter field can be pre-populated by setting `filter: { term: 'xxx' }` in the column def. See the "gender" column below. Once +the grid has rendered changes to the columnDef don't reflect in the grid - if they did then users would have their changes to filters +overwritten every time the grid refreshed. If you want to programatically modify filters after initial render then modify +grid.column[i].filters[0] directly. ### Conditon @@ -62,6 +65,9 @@ If you need to internationalize the labels you'll need to complete that before p By default the filter shows a cancel X beside the dropdown. You can set `disableCancelFilterButton: true` to suppress this button. +### Programmatic setting of filters +You can set filters + In this example we've provided a "toggle filters" button to allow you to turn the filter row on and off. To still visually indicate which columns are filtered even when the filters aren't present, we've used the headerCellClass to make any columns with a filter condition have blue text. diff --git a/misc/tutorial/209_grouping.ngdoc b/misc/tutorial/209_grouping.ngdoc index 7444fabd70..9b5666dfc0 100644 --- a/misc/tutorial/209_grouping.ngdoc +++ b/misc/tutorial/209_grouping.ngdoc @@ -60,10 +60,11 @@ Options to watch out for include: - `groupingSuppressAggregationText`: if your column has a cellFilter, the insertion of text (e.g. 'min: xxxx') usually breaks the cellFilter. So you can suppress the aggregation text, but then you don't get a clear visual indication of what sort of aggregation is going on. Refer the example below, the balance column with - an average -- `groupingShowCounts`: set to false if you don't like the counts against the groupHeaders - - + an average +- `groupingShowCounts`: set to false if you don't like the counts against the groupHeaders + +If you have data in your entity that is 1:1 with a group row (so, for example, you have + If you would like to suppress the data in a grouped column (so it only shows in the groupHeader rows) this can be done by overriding the cellTemplate for any of the columns you allow grouping on as follows: @@ -91,7 +92,7 @@ function will stop working), and writes them to the console. enableFiltering: true, columnDefs: [ { name: 'name', width: '30%' }, - { name: 'gender', grouping: { groupPriority: 1 }, sort: { priority: 1, direction: 'asc' }, width: '20%' }, + { name: 'gender', grouping: { groupPriority: 1 }, sort: { priority: 1, direction: 'asc' }, width: '20%', cellFilter: 'mapGender' }, { name: 'age', grouping: { aggregation: uiGridGroupingConstants.aggregation.MAX }, width: '20%' }, { name: 'company', width: '25%' }, { name: 'state', grouping: { groupPriority: 0 }, sort: { priority: 0, direction: 'desc' }, width: '35%', cellTemplate: '
    {{COL_FIELD CUSTOM_FILTERS}}
    ' }, @@ -106,6 +107,7 @@ function will stop working), and writes them to the console. .success(function(data) { for ( var i = 0; i < data.length; i++ ){ data[i].state = data[i].address.state; + data[i].gender = data[i].gender === 'male' ? 1: 2; data[i].balance = Number( data[i].balance.slice(1).replace(/,/,'') ); } delete data[2].age; @@ -146,7 +148,27 @@ function will stop working), and writes them to the console. }); console.log(aggregateInfo); }; - }]); + }]) + .filter('mapGender', function() { + var genderHash = { + 1: 'male', + 2: 'female' + }; + + return function(input) { + var result; + var match; + if (!input){ + return ''; + } else if (result = genderHash[input]) { + return result; + } else if ( ( match = input.match(/(.+)( \(\d+\))/) ) && ( result = genderHash[match[1]] ) ) { + return result + match[2]; + } else { + return input; + } + }; + }); From 1a83269a4b4e17f8593e50a3cae47825e69f2f64 Mon Sep 17 00:00:00 2001 From: Brian Hann Date: Mon, 27 Apr 2015 12:29:36 -0500 Subject: [PATCH 160/173] fix(Column Moving): Abs instead of fixed position Fixed positioning in IE wasn't displaying correctly. This change uses relative positioning on the header row container, and absolute positioning on the cloned column header element. This also prevents having to check for IE when doing the positioning. Fixes #2894 --- .../move-columns/js/column-movable.js | 24 ++++++++----------- .../move-columns/less/colMovable.less | 6 ++--- src/less/header.less | 1 + 3 files changed, 14 insertions(+), 17 deletions(-) diff --git a/src/features/move-columns/js/column-movable.js b/src/features/move-columns/js/column-movable.js index c8563e639b..fd9391e957 100644 --- a/src/features/move-columns/js/column-movable.js +++ b/src/features/move-columns/js/column-movable.js @@ -253,7 +253,7 @@ * - if we are a touchstart then we listen for a touchmove, if we are a mousedown we listen for * a mousemove (i.e. a drag) before we decide that there's a move underway. If there's never a move, * and we instead get a mouseup or a touchend, then we just drop out again and do nothing. - * + * */ var $contentsElm = angular.element( $elm[0].querySelectorAll('.ui-grid-cell-contents') ); @@ -281,11 +281,11 @@ $document.on('touchend', upFn); } }; - + var moveFn = function( event ) { //Disable text selection in Chrome during column move document.onselectstart = function() { return false; }; - + moveOccurred = true; var changeValue = event.pageX - previousMouseX; @@ -297,7 +297,7 @@ previousMouseX = event.pageX; } }; - + var upFn = function( event ){ //Re-enable text selection after column move document.onselectstart = null; @@ -307,7 +307,7 @@ movingElm.remove(); elmCloned = false; } - + offAllEvents(); onDownEvents(); @@ -366,12 +366,12 @@ } } }; - + var onDownEvents = function(){ $contentsElm.on('touchstart', downFn); $contentsElm.on('mousedown', downFn); }; - + var offAllEvents = function() { $contentsElm.off('touchstart', downFn); $contentsElm.off('mousedown', downFn); @@ -382,7 +382,7 @@ $document.off('mouseup', upFn); $document.off('touchend', upFn); }; - + onDownEvents(); @@ -429,12 +429,8 @@ var currentElmLeft = movingElm[0].getBoundingClientRect().left - 1; var currentElmRight = movingElm[0].getBoundingClientRect().right; var newElementLeft; - if (gridUtil.detectBrowser() === 'ie') { - newElementLeft = currentElmLeft + changeValue; - } - else { - newElementLeft = currentElmLeft - gridLeft + changeValue; - } + + newElementLeft = currentElmLeft - gridLeft + changeValue; newElementLeft = newElementLeft < rightMoveLimit ? newElementLeft : rightMoveLimit; //Update css of moving column to adjust to new left value or fire scroll in case column has reached edge of grid diff --git a/src/features/move-columns/less/colMovable.less b/src/features/move-columns/less/colMovable.less index 54ed21f3bd..8b0efc5b16 100644 --- a/src/features/move-columns/less/colMovable.less +++ b/src/features/move-columns/less/colMovable.less @@ -1,12 +1,12 @@ @import '../../../less/variables'; .movingColumn { - - position: fixed; + position: absolute; + top: 0; border: 1px solid @borderColor; box-shadow: inset 0 0 14px rgba(0, 0, 0, 0.2); .ui-grid-icon-angle-down { display: none; } -} \ No newline at end of file +} diff --git a/src/less/header.less b/src/less/header.less index 072db2cfda..69b1069612 100644 --- a/src/less/header.less +++ b/src/less/header.less @@ -53,6 +53,7 @@ .ui-grid-header-cell-row { display: table-row; + position: relative } .ui-grid-header-cell { From 1373b99e1e1680184270d61bca88124efd7a4c14 Mon Sep 17 00:00:00 2001 From: Brian Hann Date: Fri, 24 Apr 2015 10:36:07 -0500 Subject: [PATCH 161/173] fix(uiGrid): Use margins rather than floats for pinning Pinned containers were wrapping in certain scenarios where grids were being resi zed. It looked to the end-user like a poorly-rendered flash, and could possibly happen over and over during resizing. This fix uses margins to offset the body render container from the the left and right render containers Fixes #2997, Fixes #1957 --- misc/demo/pinning.html | 74 +++++++++++++++ src/features/pinning/less/pinning.less | 27 ++++-- src/js/core/directives/ui-pinned-container.js | 30 +++++- src/js/core/factories/GridRenderContainer.js | 93 ++++++++++++------- src/less/header.less | 2 +- src/templates/ui-grid/ui-grid.html | 13 ++- .../ui-grid/uiGridRenderContainer.html | 4 +- 7 files changed, 191 insertions(+), 52 deletions(-) create mode 100644 misc/demo/pinning.html diff --git a/misc/demo/pinning.html b/misc/demo/pinning.html new file mode 100644 index 0000000000..f1db42c7e9 --- /dev/null +++ b/misc/demo/pinning.html @@ -0,0 +1,74 @@ + + + + + + + + + + + + + + + + + + + + + +

    Grid

    +
    + + + +
    +
    + + + + + diff --git a/src/features/pinning/less/pinning.less b/src/features/pinning/less/pinning.less index 9081e6eb99..d1340ce7ad 100644 --- a/src/features/pinning/less/pinning.less +++ b/src/features/pinning/less/pinning.less @@ -1,21 +1,32 @@ @import '../../../less/variables'; .ui-grid-pinned-container { - // position: absolute; - float: left; + position: absolute; + display: inline; + top: 0; + + &.ui-grid-pinned-container-left { + float: left; + left: 0; + } + + &.ui-grid-pinned-container-right { + float: right; + right: 0; + } &.ui-grid-pinned-container-left .ui-grid-header-cell:last-child { box-sizing: border-box; border-right: @gridBorderWidth solid; border-width: @gridBorderWidth; - border-color: darken(@headerVerticalBarColor, 15%); + border-right-color: darken(@headerVerticalBarColor, 15%); } &.ui-grid-pinned-container-left .ui-grid-cell:last-child { box-sizing: border-box; border-right: @gridBorderWidth solid; border-width: @gridBorderWidth; - border-color: darken(@verticalBarColor, 15%); + border-right-color: darken(@verticalBarColor, 15%); } &.ui-grid-pinned-container-left .ui-grid-header-cell:not(:last-child) .ui-grid-vertical-bar, .ui-grid-cell:not(:last-child) .ui-grid-vertical-bar { @@ -41,14 +52,14 @@ box-sizing: border-box; border-left: @gridBorderWidth solid; border-width: @gridBorderWidth; - border-color: darken(@headerVerticalBarColor, 15%); + border-left-color: darken(@headerVerticalBarColor, 15%); } &.ui-grid-pinned-container-right .ui-grid-cell:first-child { box-sizing: border-box; border-left: @gridBorderWidth solid; border-width: @gridBorderWidth; - border-color: darken(@verticalBarColor, 15%); + border-left-color: darken(@verticalBarColor, 15%); } &.ui-grid-pinned-container-right .ui-grid-header-cell:not(:first-child) .ui-grid-vertical-bar, .ui-grid-cell:not(:first-child) .ui-grid-vertical-bar { @@ -71,5 +82,5 @@ } .ui-grid-render-container-body { - float: left; -} \ No newline at end of file + // float: left; +} diff --git a/src/js/core/directives/ui-pinned-container.js b/src/js/core/directives/ui-pinned-container.js index 343ea7ab3b..f500791911 100644 --- a/src/js/core/directives/ui-pinned-container.js +++ b/src/js/core/directives/ui-pinned-container.js @@ -23,6 +23,27 @@ $elm.addClass('ui-grid-pinned-container-' + $scope.side); + // Monkey-patch the viewport width function + if ($scope.side === 'left' || $scope.side === 'right') { + grid.renderContainers[$scope.side].getViewportWidth = monkeyPatchedGetViewportWidth; + } + + function monkeyPatchedGetViewportWidth() { + /*jshint validthis: true */ + var self = this; + + var viewportWidth = 0; + self.visibleColumnCache.forEach(function (column) { + viewportWidth += column.drawnWidth; + }); + + var adjustment = self.getViewportAdjustment(); + + viewportWidth = viewportWidth + adjustment.width; + + return viewportWidth; + } + function updateContainerWidth() { if ($scope.side === 'left' || $scope.side === 'right') { var cols = grid.renderContainers[$scope.side].visibleColumnCache; @@ -35,10 +56,10 @@ return width; } } - + function updateContainerDimensions() { var ret = ''; - + // Column containers if ($scope.side === 'left' || $scope.side === 'right') { myWidth = updateContainerWidth(); @@ -49,7 +70,7 @@ $elm.attr('style', null); var myHeight = grid.renderContainers.body.getViewportHeight(); // + grid.horizontalScrollbarHeight; - + ret += '.grid' + grid.id + ' .ui-grid-pinned-container-' + $scope.side + ', .grid' + grid.id + ' .ui-grid-pinned-container-' + $scope.side + ' .ui-grid-render-container-' + $scope.side + ' .ui-grid-viewport { width: ' + myWidth + 'px; height: ' + myHeight + 'px; } '; } @@ -61,6 +82,7 @@ // Subtract our own width adjustment.width -= myWidth; + adjustment.side = $scope.side; return adjustment; }); @@ -75,4 +97,4 @@ } }; }]); -})(); \ No newline at end of file +})(); diff --git a/src/js/core/factories/GridRenderContainer.js b/src/js/core/factories/GridRenderContainer.js index 2fc1ab3447..1a33f90615 100644 --- a/src/js/core/factories/GridRenderContainer.js +++ b/src/js/core/factories/GridRenderContainer.js @@ -1,12 +1,12 @@ (function(){ angular.module('ui.grid') - + /** * @ngdoc function * @name ui.grid.class:GridRenderContainer * @description The grid has render containers, allowing the ability to have pinned columns. If the grid - * is right-to-left then there may be a right render container, if left-to-right then there may + * is right-to-left then there may be a right render container, if left-to-right then there may * be a left render container. There is always a body render container. * @param {string} name The name of the render container ('body', 'left', or 'right') * @param {Grid} grid the grid the render container is in @@ -23,7 +23,7 @@ angular.module('ui.grid') self.name = name; self.grid = grid; - + // self.rowCache = []; // self.columnCache = []; @@ -143,7 +143,7 @@ angular.module('ui.grid') * @methodOf ui.grid.class:GridRenderContainer * @description Registers an adjuster to the render container's available width or height. Adjusters are used * to tell the render container that there is something else consuming space, and to adjust it's size - * appropriately. + * appropriately. * @param {function} func the adjuster function we want to register */ @@ -170,7 +170,7 @@ angular.module('ui.grid') * @ngdoc function * @name getViewportAdjustment * @methodOf ui.grid.class:GridRenderContainer - * @description Gets the adjustment based on the viewportAdjusters. + * @description Gets the adjustment based on the viewportAdjusters. * @returns {object} a hash of { height: x, width: y }. Usually the values will be negative */ GridRenderContainer.prototype.getViewportAdjustment = function getViewportAdjustment() { @@ -185,6 +185,22 @@ angular.module('ui.grid') return adjustment; }; + GridRenderContainer.prototype.getMargin = function getMargin(side) { + var self = this; + + var amount = 0; + + self.viewportAdjusters.forEach(function (func) { + var adjustment = func.call(this, { height: 0, width: 0 }); + + if (adjustment.side && adjustment.side === side) { + amount += adjustment.width * -1; + } + }); + + return amount; + }; + GridRenderContainer.prototype.getViewportHeight = function getViewportHeight() { var self = this; @@ -194,7 +210,7 @@ angular.module('ui.grid') var adjustment = self.getViewportAdjustment(); - + viewPortHeight = viewPortHeight + adjustment.height; return viewPortHeight; @@ -203,23 +219,28 @@ angular.module('ui.grid') GridRenderContainer.prototype.getViewportWidth = function getViewportWidth() { var self = this; - var viewPortWidth = self.grid.gridWidth; + var viewportWidth = self.grid.gridWidth; //if (typeof(self.grid.verticalScrollbarWidth) !== 'undefined' && self.grid.verticalScrollbarWidth !== undefined && self.grid.verticalScrollbarWidth > 0) { // viewPortWidth = viewPortWidth - self.grid.verticalScrollbarWidth; //} + // var viewportWidth = 0;\ + // self.visibleColumnCache.forEach(function (column) { + // viewportWidth += column.drawnWidth; + // }); + var adjustment = self.getViewportAdjustment(); - - viewPortWidth = viewPortWidth + adjustment.width; - return viewPortWidth; + viewportWidth = viewportWidth + adjustment.width; + + return viewportWidth; }; GridRenderContainer.prototype.getHeaderViewportWidth = function getHeaderViewportWidth() { var self = this; - var viewPortWidth = this.getViewportWidth(); + var viewportWidth = this.getViewportWidth(); //if (typeof(self.grid.verticalScrollbarWidth) !== 'undefined' && self.grid.verticalScrollbarWidth !== undefined && self.grid.verticalScrollbarWidth > 0) { // viewPortWidth = viewPortWidth + self.grid.verticalScrollbarWidth; @@ -228,7 +249,7 @@ angular.module('ui.grid') // var adjustment = self.getViewportAdjustment(); // viewPortWidth = viewPortWidth + adjustment.width; - return viewPortWidth; + return viewportWidth; }; @@ -289,7 +310,7 @@ angular.module('ui.grid') for (var i = 0; i < newColumns.length; i++) { this.renderedColumns[i] = newColumns[i]; } - + this.updateColumnOffset(); }; @@ -396,7 +417,7 @@ angular.module('ui.grid') if ((typeof(scrollPercentage) === 'undefined' || scrollPercentage === null) && scrollTop) { scrollPercentage = scrollTop / self.getVerticalScrollLength(); } - + var rowIndex = Math.ceil(Math.min(maxRowIndex, maxRowIndex * scrollPercentage)); // Define a max row index that we can't scroll past @@ -453,7 +474,7 @@ angular.module('ui.grid') if (colIndex > maxColumnIndex) { colIndex = maxColumnIndex; } - + var newRange = []; if (columnCache.length > self.grid.options.columnVirtualizationThreshold && self.getCanvasWidth() > self.getViewportWidth()) { /* Commented the following lines because otherwise the moved column wasn't visible immediately on the new position @@ -477,7 +498,7 @@ angular.module('ui.grid') newRange = [0, Math.max(maxLen, minCols + self.grid.options.excessColumns)]; } - + self.updateViewableColumnRange(newRange); self.prevColumnScrollIndex = colIndex; @@ -509,7 +530,7 @@ angular.module('ui.grid') GridRenderContainer.prototype.headerCellWrapperStyle = function () { var self = this; - + if (self.currentFirstColumn !== 0) { var offset = self.columnOffset; @@ -529,19 +550,19 @@ angular.module('ui.grid') * @name updateColumnWidths * @propertyOf ui.grid.class:GridRenderContainer * @description Determine the appropriate column width of each column across all render containers. - * - * Column width is easy when each column has a specified width. When columns are variable width (i.e. + * + * Column width is easy when each column has a specified width. When columns are variable width (i.e. * have an * or % of the viewport) then we try to calculate so that things fit in. The problem is that * we have multiple render containers, and we don't want one render container to just take the whole viewport * when it doesn't need to - we want things to balance out across the render containers. - * + * * To do this, we use this method to calculate all the renderContainers, recognising that in a given render * cycle it'll get called once per render container, so it needs to return the same values each time. - * + * * The constraints on this method are therefore: * - must return the same value when called multiple times, to do this it needs to rely on properties of the * columns, but not properties that change when this is called (so it shouldn't rely on drawnWidth) - * + * * The general logic of this method is: * - calculate our total available width * - look at all the columns across all render containers, and work out which have widths and which have @@ -559,7 +580,7 @@ angular.module('ui.grid') var self = this; var asterisksArray = [], - asteriskNum = 0, + asteriskNum = 0, usedWidthSum = 0, ret = ''; @@ -584,22 +605,22 @@ angular.module('ui.grid') width = parseInt(column.width, 10); usedWidthSum = usedWidthSum + width; column.drawnWidth = width; - + } else if (gridUtil.endsWith(column.width, "%")) { // percentage width, set to percentage of the viewport width = parseInt(parseInt(column.width.replace(/%/g, ''), 10) / 100 * availableWidth); - + if ( width > column.maxWidth ){ width = column.maxWidth; } - + if ( width < column.minWidth ){ width = column.minWidth; } - + usedWidthSum = usedWidthSum + width; column.drawnWidth = width; - } else if (angular.isString(column.width) && column.width.indexOf('*') !== -1) { + } else if (angular.isString(column.width) && column.width.indexOf('*') !== -1) { // is an asterisk column, the gridColumn already checked the string consists only of '****' asteriskNum = asteriskNum + column.width.length; asterisksArray.push(column); @@ -621,18 +642,18 @@ angular.module('ui.grid') if ( width > column.maxWidth ){ width = column.maxWidth; } - + if ( width < column.minWidth ){ width = column.minWidth; } - + usedWidthSum = usedWidthSum + width; column.drawnWidth = width; }); } - // If the grid width didn't divide evenly into the column widths and we have pixels left over, or our - // calculated widths would have the grid narrower than the available space, + // If the grid width didn't divide evenly into the column widths and we have pixels left over, or our + // calculated widths would have the grid narrower than the available space, // dole the remainder out one by one to make everything fit var processColumnUpwards = function(column){ if ( column.drawnWidth < column.maxWidth && leftoverWidth > 0) { @@ -642,9 +663,9 @@ angular.module('ui.grid') columnsToChange = true; } }; - + var leftoverWidth = availableWidth - usedWidthSum; - var columnsToChange = true; + var columnsToChange = true; while (leftoverWidth > 0 && columnsToChange) { columnsToChange = false; @@ -661,9 +682,9 @@ angular.module('ui.grid') columnsToChange = true; } }; - + var excessWidth = usedWidthSum - availableWidth; - columnsToChange = true; + columnsToChange = true; while (excessWidth > 0 && columnsToChange) { columnsToChange = false; diff --git a/src/less/header.less b/src/less/header.less index 69b1069612..94e85589c1 100644 --- a/src/less/header.less +++ b/src/less/header.less @@ -104,7 +104,7 @@ } .ui-grid-column-menu { - position: relative; + position: absolute; } /* Slide up/down animations */ diff --git a/src/templates/ui-grid/ui-grid.html b/src/templates/ui-grid/ui-grid.html index a0c5587a0c..6a99780a25 100644 --- a/src/templates/ui-grid/ui-grid.html +++ b/src/templates/ui-grid/ui-grid.html @@ -28,9 +28,20 @@
    -
    + +
    +
    +
    +
    diff --git a/src/templates/ui-grid/uiGridRenderContainer.html b/src/templates/ui-grid/uiGridRenderContainer.html index 0f62cf910f..578ffd629d 100644 --- a/src/templates/ui-grid/uiGridRenderContainer.html +++ b/src/templates/ui-grid/uiGridRenderContainer.html @@ -1,5 +1,5 @@ -
    +
    -
    \ No newline at end of file +
    From 2cd2266bc580866dfbf36c1eddfaaa3d0b81732b Mon Sep 17 00:00:00 2001 From: Brian Hann Date: Tue, 28 Apr 2015 16:14:46 -0500 Subject: [PATCH 162/173] Release v3.0.0-rc.21 --- CHANGELOG.md | 61 ++++++++++++++++++++++++++++++++++++++++++++++++++++ bower.json | 2 +- package.json | 2 +- 3 files changed, 63 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2a162c3356..69202ff08c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,64 @@ + +### v3.0.0-rc.21 (2015-04-28) + + +#### Bug Fixes + +* **Expandable:** Run with lower priority than ngIf ([949013c3](http://github.com/angular-ui/ng-grid/commit/949013c332c5af1b3e37b1d3fa515dfd96c8acb2), closes [#2804](http://github.com/angular-ui/ng-grid/issues/2804)) +* **RTL:** + * Use Math.abs for normalizing negatives ([4acbdc1a](http://github.com/angular-ui/ng-grid/commit/4acbdc1a58d8043d60e3a62d1126b0f69bc6ee86)) + * Use feature detection to determine RTL ([fbb36319](http://github.com/angular-ui/ng-grid/commit/fbb363197ab3975411589dfa0904495f861795c0), closes [#1689](http://github.com/angular-ui/ng-grid/issues/1689)) +* **cellNav:** fix null ref issue in navigate event for oldRowColumn scrollTo should not setF ([02b05cae](http://github.com/angular-ui/ng-grid/commit/02b05cae6d5385e01d00f812662f16009130c647)) +* **pinning:** restore correct width state ([4ffaaf26](http://github.com/angular-ui/ng-grid/commit/4ffaaf26774bae7f52bf4956f45243f6c7dd53a3)) +* **scrolling:** Fix for #3260 atTop/Bottom/Left/Right needed tweaking ([89461bcb](http://github.com/angular-ui/ng-grid/commit/89461bcbcfdfc527655c398df19555738fa9bd63)) +* **selection:** + * allow rowSelection to be navigable if using cellNav; allow rowSelection via the ([95ce7b1b](http://github.com/angular-ui/ng-grid/commit/95ce7b1b694b23f1a7506cf4f6a32d0ae384697c)) + * allow rowSelection to be navigable if using cellNav; allow rowSelection via the ([3d5d6031](http://github.com/angular-ui/ng-grid/commit/3d5d603178f0fcb4cc2abab6ce637c1dd6face8d)) +* **uiGrid:** + * Use margins rather than floats for pinning ([1373b99e](http://github.com/angular-ui/ng-grid/commit/1373b99e1e1680184270d61bca88124efd7a4c14), closes [#2997](http://github.com/angular-ui/ng-grid/issues/2997), [#NaN](http://github.com/angular-ui/ng-grid/issues/NaN)) + * Wait for grid to get dimensions ([e7dfb8c2](http://github.com/angular-ui/ng-grid/commit/e7dfb8c2dfac69bb3a38f7253062367671fec56d)) +* **uiGridColumnMenu:** Position relatively ([9d918052](http://github.com/angular-ui/ng-grid/commit/9d9180520d8d6fd16b897ba4b9fbfc4bb4860ea9), closes [#2319](http://github.com/angular-ui/ng-grid/issues/2319)) +* **uiGridFooter:** Watch for col change ([1f9100de](http://github.com/angular-ui/ng-grid/commit/1f9100defb1489bed46515fb859aed9c9a090e73), closes [#2686](http://github.com/angular-ui/ng-grid/issues/2686)) +* **uiGridHeader:** + * Use parseInt on header heights ([98ed0104](http://github.com/angular-ui/ng-grid/commit/98ed01049015b22caddb651b1884f6e383fc58aa)) + * Allow header to shrink in size ([7c5cdca1](http://github.com/angular-ui/ng-grid/commit/7c5cdca1f471a0a3c1ef340fe65af268df68cae3), closes [#3138](http://github.com/angular-ui/ng-grid/issues/3138)) + + +#### Features + +* **saveState:** add pinning to save state ([b0d943a8](http://github.com/angular-ui/ng-grid/commit/b0d943a82a1d5c64808b759c8b96833e66380b02)) + + +#### Breaking Changes + +* gridUtil will no longer calculate dimensions of hidden +elements + ([e7dfb8c2](http://github.com/angular-ui/ng-grid/commit/e7dfb8c2dfac69bb3a38f7253062367671fec56d)) +* Two events are now emitted on scroll: + + grid.api.core.ScrollBegin + grid.api.core.ScrollEnd + + Before: + grid.api.core.ScrollEvent + After: +grid.api.core.ScrollBegin + +ScrollToIfNecessary and ScrollTo moved from cellNav to core and grid removed from arguments +Before: +grid.api.cellNav.ScrollToIfNecessary(grid, gridRow, gridCol) +grid.api.cellNav.ScrollTo(grid, rowEntity, colDef) + +After: +grid.api.core.ScrollToIfNecessary(gridRow, gridCol) +grid.api.core.ScrollTo(rowEntity, colDef) + +GridEdit/cellNav +When using cellNav, a cell no longer receives focus. Instead the viewport always receives focus. This eliminated many bugs associated with scrolling and focus. + +If you have a custom editor, you will no longer receive keyDown/Up events from the readonly cell. Use the cellNav api viewPortKeyDown to capture any needed keydown events. see GridEdit.js for an example + ([052c2321](http://github.com/angular-ui/ng-grid/commit/052c2321f97b37f860c769dcbd2e8d9094cf2bbf)) + ### v3.0.0-rc.20 (2015-02-24) diff --git a/bower.json b/bower.json index 185d4c0afb..8ee4fd2a0b 100644 --- a/bower.json +++ b/bower.json @@ -1,6 +1,6 @@ { "name": "angular-ui-grid", - "version": "3.0.0-rc.20", + "version": "3.0.0-rc.21", "homepage": "http://ui-grid.info", "repository": { "type": "git", diff --git a/package.json b/package.json index 8b2742f0c0..bd492aaa93 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "ui-grid", - "version": "3.0.0-rc.20", + "version": "3.0.0-rc.21", "description": "__Contributors:__", "directories": { "test": "test" From 43f63ac9f62a133b51d7af2f4a966318eae8e9ac Mon Sep 17 00:00:00 2001 From: Brian Hann Date: Tue, 28 Apr 2015 16:55:26 -0500 Subject: [PATCH 163/173] fix(uiGridHeader): Recalc all explicit heights Any time we are refreshing the canvas and headers have explicit heights, we need to remove them and then recalculate as altered settings may change the necessary height. Fixes #3136 --- .../directives/ui-grid-render-container.js | 8 +- src/js/core/factories/Grid.js | 260 +++++++++--------- 2 files changed, 140 insertions(+), 128 deletions(-) diff --git a/src/js/core/directives/ui-grid-render-container.js b/src/js/core/directives/ui-grid-render-container.js index 55851e9ade..b8ee5ed299 100644 --- a/src/js/core/directives/ui-grid-render-container.js +++ b/src/js/core/directives/ui-grid-render-container.js @@ -133,8 +133,9 @@ var viewportHeight = rowContainer.getViewportHeight(); - var headerViewportWidth = colContainer.getHeaderViewportWidth(); - var footerViewportWidth = colContainer.getHeaderViewportWidth(); + var headerViewportWidth, + footerViewportWidth; + headerViewportWidth = footerViewportWidth = colContainer.getHeaderViewportWidth(); // Set canvas dimensions ret += '\n .grid' + uiGridCtrl.grid.id + ' .ui-grid-render-container-' + $scope.containerId + ' .ui-grid-canvas { width: ' + canvasWidth + 'px; height: ' + canvasHeight + 'px; }'; @@ -144,6 +145,9 @@ if (renderContainer.explicitHeaderCanvasHeight) { ret += '\n .grid' + uiGridCtrl.grid.id + ' .ui-grid-render-container-' + $scope.containerId + ' .ui-grid-header-canvas { height: ' + renderContainer.explicitHeaderCanvasHeight + 'px; }'; } + else { + ret += '\n .grid' + uiGridCtrl.grid.id + ' .ui-grid-render-container-' + $scope.containerId + ' .ui-grid-header-canvas { height: inherit; }'; + } ret += '\n .grid' + uiGridCtrl.grid.id + ' .ui-grid-render-container-' + $scope.containerId + ' .ui-grid-viewport { width: ' + viewportWidth + 'px; height: ' + viewportHeight + 'px; }'; ret += '\n .grid' + uiGridCtrl.grid.id + ' .ui-grid-render-container-' + $scope.containerId + ' .ui-grid-header-viewport { width: ' + headerViewportWidth + 'px; }'; diff --git a/src/js/core/factories/Grid.js b/src/js/core/factories/Grid.js index d6e693adc2..df5bac16fd 100644 --- a/src/js/core/factories/Grid.js +++ b/src/js/core/factories/Grid.js @@ -10,7 +10,7 @@ angular.module('ui.grid') * @description Public Api for the core grid features * */ - + /** * @ngdoc function * @name ui.grid.class:Grid @@ -29,10 +29,10 @@ angular.module('ui.grid') else { throw new Error('No ID provided. An ID must be given when creating a grid.'); } - + self.id = options.id; delete options.id; - + // Get default options self.options = GridOptions.initialize( options ); @@ -45,7 +45,7 @@ angular.module('ui.grid') * use gridOptions.appScopeProvider to override the default assignment of $scope.$parent with any reference */ self.appScope = self.options.appScopeProvider; - + self.headerHeight = self.options.headerRowHeight; @@ -81,26 +81,26 @@ angular.module('ui.grid') self.horizontalScrollSyncCallBackFns = {}; // self.visibleRowCache = []; - + // Set of 'render' containers for self grid, which can render sets of rows self.renderContainers = {}; - + // Create a self.renderContainers.body = new GridRenderContainer('body', self); - + self.cellValueGetterCache = {}; - + // Cached function to use with custom row templates self.getRowTemplateFn = null; - - + + //representation of the rows on the grid. //these are wrapped references to the actual data rows (options.data) self.rows = []; - + //represents the columns on the grid self.columns = []; - + /** * @ngdoc boolean * @name isScrollingVertically @@ -108,7 +108,7 @@ angular.module('ui.grid') * @description set to true when Grid is scrolling vertically. Set to false via debounced method */ self.isScrollingVertically = false; - + /** * @ngdoc boolean * @name isScrollingHorizontally @@ -144,7 +144,7 @@ angular.module('ui.grid') var debouncedHorizontal = gridUtil.debounce(horizontal, self.options.scrollDebounce); var debouncedHorizontalMinDelay = gridUtil.debounce(horizontal, 0); - + /** * @ngdoc function * @name flagScrollingVertically @@ -163,7 +163,7 @@ angular.module('ui.grid') debouncedVertical(scrollEvent); } }; - + /** * @ngdoc function * @name flagScrollingHorizontally @@ -192,11 +192,11 @@ angular.module('ui.grid') if (self.options.enableVerticalScrollbar === uiGridConstants.scrollbars.ALWAYS) { self.scrollbarWidth = gridUtil.getScrollbarWidth(); } - - - + + + self.api = new GridApi(self); - + /** * @ngdoc function * @name refresh @@ -206,13 +206,13 @@ angular.module('ui.grid') * rowProcessors, as well as calling refreshCanvas to update all * the grid sizing. In general you should prefer to use queueGridRefresh * instead, which is basically a debounced version of refresh. - * + * * If you only want to resize the grid, not regenerate all the rows * and columns, you should consider directly calling refreshCanvas instead. - * + * */ self.api.registerMethod( 'core', 'refresh', this.refresh ); - + /** * @ngdoc function * @name queueGridRefresh @@ -223,10 +223,10 @@ angular.module('ui.grid') * rowProcessors, as well as calling refreshCanvas to update all * the grid sizing. In general you should prefer to use queueGridRefresh * instead, which is basically a debounced version of refresh. - * + * */ self.api.registerMethod( 'core', 'queueGridRefresh', this.queueGridRefresh ); - + /** * @ngdoc function * @name refreshRows @@ -234,10 +234,10 @@ angular.module('ui.grid') * @description Runs only the rowProcessors, columns remain as they were. * It then calls redrawInPlace and refreshCanvas, which adjust the grid sizing. * @returns {promise} promise that is resolved when render completes? - * + * */ self.api.registerMethod( 'core', 'refreshRows', this.refreshRows ); - + /** * @ngdoc function * @name queueRefresh @@ -245,10 +245,10 @@ angular.module('ui.grid') * @description Requests execution of refreshCanvas, if multiple requests are made * during a digest cycle only one will run. RefreshCanvas updates the grid sizing. * @returns {promise} promise that is resolved when render completes? - * + * */ self.api.registerMethod( 'core', 'refreshRows', this.queueRefresh ); - + /** * @ngdoc function * @name handleWindowResize @@ -257,18 +257,18 @@ angular.module('ui.grid') * up by a watch on window size, but in some circumstances it is necessary * to call this manually * @returns {promise} promise that is resolved when render completes? - * + * */ self.api.registerMethod( 'core', 'handleWindowResize', this.handleWindowResize ); - - + + /** * @ngdoc function * @name addRowHeaderColumn * @methodOf ui.grid.core.api:PublicApi * @description adds a row header column to the grid * @param {object} column def - * + * */ self.api.registerMethod( 'core', 'addRowHeaderColumn', this.addRowHeaderColumn ); @@ -306,13 +306,13 @@ angular.module('ui.grid') * the grid calls each registered "rows processor", which has a chance * to alter the set of rows (sorting, etc) as long as the count is not * modified. - * - * @param {function(renderedRowsToProcess, columns )} processorFunction rows processor function, which + * + * @param {function(renderedRowsToProcess, columns )} processorFunction rows processor function, which * is run in the context of the grid (i.e. this for the function will be the grid), and must * return the updated rows list, which is passed to the next processor in the chain * @param {number} priority the priority of this processor. In general we try to do them in 100s to leave room * for other people to inject rows processors at intermediate priorities. Lower priority rowsProcessors run earlier. - * + * * At present allRowsVisible is running at 50, filter is running at 100, sort is at 200, grouping at 400, selectable rows at 500, pagination at 900 (pagination will generally want to be last) */ self.api.registerMethod( 'core', 'registerRowsProcessor', this.registerRowsProcessor ); @@ -359,11 +359,11 @@ angular.module('ui.grid') * @param {object} b sort value b * @returns {number} null if there were no nulls/undefineds, otherwise returns * a sort value that should be passed back from the sort function - * + * */ self.api.registerMethod( 'core', 'sortHandleNulls', rowSorter.handleNulls ); - - + + /** * @ngdoc function * @name sortChanged @@ -371,28 +371,28 @@ angular.module('ui.grid') * @description The sort criteria on one or more columns has * changed. Provides as parameters the grid and the output of * getColumnSorting, which is an array of gridColumns - * that have sorting on them, sorted in priority order. - * + * that have sorting on them, sorted in priority order. + * * @param {Grid} grid the grid - * @param {array} sortColumns an array of columns with + * @param {array} sortColumns an array of columns with * sorts on them, in priority order - * + * * @example *
          *      gridApi.core.on.sortChanged( grid, sortColumns );
          * 
    */ self.api.registerEvent( 'core', 'sortChanged' ); - + /** * @ngdoc function * @name columnVisibilityChanged * @methodOf ui.grid.core.api:PublicApi * @description The visibility of a column has changed, - * the column itself is passed out as a parameter of the event. - * + * the column itself is passed out as a parameter of the event. + * * @param {GridCol} column the column that changed - * + * * @example *
          *      gridApi.core.on.columnVisibilityChanged( $scope, function (column) {
    @@ -401,20 +401,20 @@ angular.module('ui.grid')
          * 
    */ self.api.registerEvent( 'core', 'columnVisibilityChanged' ); - + /** * @ngdoc method * @name notifyDataChange * @methodOf ui.grid.core.api:PublicApi * @description Notify the grid that a data or config change has occurred, - * where that change isn't something the grid was otherwise noticing. This + * where that change isn't something the grid was otherwise noticing. This * might be particularly relevant where you've changed values within the data - * and you'd like cell classes to be re-evaluated, or changed config within + * and you'd like cell classes to be re-evaluated, or changed config within * the columnDef and you'd like headerCellClasses to be re-evaluated. - * @param {string} type one of the + * @param {string} type one of the * uiGridConstants.dataChange values (ALL, ROW, EDIT, COLUMN), which tells * us which refreshes to fire. - * + * */ self.api.registerMethod( 'core', 'notifyDataChange', this.notifyDataChange ); @@ -527,25 +527,25 @@ angular.module('ui.grid') * @methodOf ui.grid.class:Grid * @description When a data change occurs, the data change callbacks of the specified type * will be called. The rules are: - * + * * - when the data watch fires, that is considered a ROW change (the data watch only notices * added or removed rows) * - when the api is called to inform us of a change, the declared type of that change is used * - when a cell edit completes, the EDIT callbacks are triggered * - when the columnDef watch fires, the COLUMN callbacks are triggered * - when the options watch fires, the OPTIONS callbacks are triggered - * + * * For a given event: * - ALL calls ROW, EDIT, COLUMN, OPTIONS and ALL callbacks * - ROW calls ROW and ALL callbacks * - EDIT calls EDIT and ALL callbacks * - COLUMN calls COLUMN and ALL callbacks * - OPTIONS calls OPTIONS and ALL callbacks - * + * * @param {function(grid)} callback function to be called - * @param {array} types the types of data change you want to be informed of. Values from + * @param {array} types the types of data change you want to be informed of. Values from * the uiGridConstants.dataChange values ( ALL, EDIT, ROW, COLUMN, OPTIONS ). Optional and defaults to - * ALL + * ALL * @returns {function} deregister function - a function that can be called to deregister this callback */ Grid.prototype.registerDataChangeCallback = function registerDataChangeCallback(callback, types, _this) { @@ -557,7 +557,7 @@ angular.module('ui.grid') gridUtil.logError("Expected types to be an array or null in registerDataChangeCallback, value passed was: " + types ); } this.dataChangeCallbacks[uid] = { callback: callback, types: types, _this:_this }; - + var self = this; var deregisterFunction = function() { delete self.dataChangeCallbacks[uid]; @@ -570,9 +570,9 @@ angular.module('ui.grid') * @name callDataChangeCallbacks * @methodOf ui.grid.class:Grid * @description Calls the callbacks based on the type of data change that - * has occurred. Always calls the ALL callbacks, calls the ROW, EDIT, COLUMN and OPTIONS callbacks if the + * has occurred. Always calls the ALL callbacks, calls the ROW, EDIT, COLUMN and OPTIONS callbacks if the * event type is matching, or if the type is ALL. - * @param {number} type the type of event that occurred - one of the + * @param {number} type the type of event that occurred - one of the * uiGridConstants.dataChange values (ALL, ROW, EDIT, COLUMN, OPTIONS) */ Grid.prototype.callDataChangeCallbacks = function callDataChangeCallbacks(type, options) { @@ -589,20 +589,20 @@ angular.module('ui.grid') } }, this); }; - + /** * @ngdoc function * @name notifyDataChange * @methodOf ui.grid.class:Grid * @description Notifies us that a data change has occurred, used in the public - * api for users to tell us when they've changed data or some other event that + * api for users to tell us when they've changed data or some other event that * our watches cannot pick up - * @param {string} type the type of event that occurred - one of the + * @param {string} type the type of event that occurred - one of the * uiGridConstants.dataChange values (ALL, ROW, EDIT, COLUMN) */ Grid.prototype.notifyDataChange = function notifyDataChange(type) { var constants = uiGridConstants.dataChange; - if ( type === constants.ALL || + if ( type === constants.ALL || type === constants.COLUMN || type === constants.EDIT || type === constants.ROW || @@ -612,15 +612,15 @@ angular.module('ui.grid') gridUtil.logError("Notified of a data change, but the type was not recognised, so no action taken, type was: " + type); } }; - - + + /** * @ngdoc function * @name columnRefreshCallback * @methodOf ui.grid.class:Grid * @description refreshes the grid when a column refresh - * is notified, which triggers handling of the visible flag. - * This is called on uiGridConstants.dataChange.COLUMN, and is + * is notified, which triggers handling of the visible flag. + * This is called on uiGridConstants.dataChange.COLUMN, and is * registered as a dataChangeCallback in grid.js * @param {string} name column name */ @@ -642,7 +642,7 @@ angular.module('ui.grid') Grid.prototype.processRowsCallback = function processRowsCallback( grid ){ grid.queueGridRefresh(); }; - + /** * @ngdoc function @@ -682,7 +682,7 @@ angular.module('ui.grid') * @ngdoc property * @name type * @propertyOf ui.grid.class:GridOptions.columnDef - * @description the type of the column, used in sorting. If not provided then the + * @description the type of the column, used in sorting. If not provided then the * grid will guess the type. Add this only if the grid guessing is not to your * satisfaction. One of: * - 'string' @@ -819,7 +819,7 @@ angular.module('ui.grid') // We need to allow for the "row headers" when mapping from the column defs array to the columns array // If we have a row header in columns[0] and don't account for it we'll overwrite it with the column in columnDefs[0] - // Go through all the column defs, use the shorter of columns length and colDefs.length because if a user has given two columns the same name then + // Go through all the column defs, use the shorter of columns length and colDefs.length because if a user has given two columns the same name then // columns will be shorter than columnDefs. In this situation we'll avoid an error, but the user will still get an unexpected result var len = Math.min(self.options.columnDefs.length, self.columns.length); for (i = 0; i < len; i++) { @@ -859,7 +859,7 @@ angular.module('ui.grid') this.columns.forEach(function (col) { var html = col.cellTemplate.replace(uiGridConstants.MODEL_COL_FIELD, self.getQualifiedColField(col)); html = html.replace(uiGridConstants.COL_FIELD, 'grid.getCellValue(row, col)'); - + if (col.cellTooltip === false){ html = html.replace(uiGridConstants.TOOLTIP, ''); } else { @@ -989,7 +989,7 @@ angular.module('ui.grid') // Make sure to parse to an int lastNum = parseInt(lastNum, 10); - // Add 1 to the number from the last column and tack it on to the field to be the name for this new column + // Add 1 to the number from the last column and tack it on to the field to be the name for this new column colDef.name = colDef.field + (lastNum + 1); } } @@ -1008,7 +1008,7 @@ angular.module('ui.grid') var t = []; for (var i = 0; i < n.length; i++) { var nV = nAccessor ? n[i][nAccessor] : n[i]; - + var found = false; for (var j = 0; j < o.length; j++) { var oV = oAccessor ? o[j][oAccessor] : o[j]; @@ -1021,7 +1021,7 @@ angular.module('ui.grid') t.push(nV); } } - + return t; }; @@ -1036,9 +1036,9 @@ angular.module('ui.grid') */ Grid.prototype.getRow = function getRow(rowEntity, lookInRows) { var self = this; - + lookInRows = typeof(lookInRows) === 'undefined' ? self.rows : lookInRows; - + var rows = lookInRows.filter(function (row) { return self.options.rowEquality(row.entity, rowEntity); }); @@ -1058,10 +1058,10 @@ angular.module('ui.grid') * rowsProcessors immediately after to sort the data anyway * 2. if we have row hashing available, we try to use the rowHash to find the row * 3. no memory leaks - rows that are no longer in newRawData need to be garbage collected - * + * * The basic logic flow makes use of the newRawData, oldRows and oldHash, and creates * the newRows and newHash - * + * * ``` * newRawData.forEach newEntity * if (hashing enabled) @@ -1072,7 +1072,7 @@ angular.module('ui.grid') * create newRow * append to the newRows and add to newHash * run the processors - * + * * Rows are identified using the hashKey if configured. If not configured, then rows * are identified using the gridOptions.rowEquality function */ @@ -1082,7 +1082,7 @@ angular.module('ui.grid') var oldRowHash = self.rowHashMap || self.createRowHashMap(); self.rowHashMap = self.createRowHashMap(); self.rows.length = 0; - + newRawData.forEach( function( newEntity, i ) { var newRow; if ( self.options.enableRowHashing ){ @@ -1101,7 +1101,7 @@ angular.module('ui.grid') self.rows.push( newRow ); self.rowHashMap.put( newEntity, newRow ); }); - + self.assignTypes(); var p1 = $q.when(self.processRowsProcessors(self.rows)) @@ -1166,7 +1166,7 @@ angular.module('ui.grid') * @name registerStyleComputation * @methodOf ui.grid.class:Grid * @description registered a styleComputation function - * + * * If the function returns a value it will be appended into the grid's `