From ce330978fe566e9695c30acef04e55221e520960 Mon Sep 17 00:00:00 2001 From: Brian Hann Date: Wed, 16 Apr 2014 13:23:54 -0500 Subject: [PATCH] feat(rowSort): Added row sorting Also refactored quite a lot of Grid functionality; added rows processors, menus, etc. --- Gruntfile.js | 62 +- TODO.md | 78 +- misc/tutorial/5.1_sorting.ngdoc | 50 ++ .../5.2_customizing_column_menu.ngdoc | 84 +++ misc/tutorial/5.3_custom_grid_menu.ngdoc | 96 +++ misc/tutorial/92_horizontal_scrolling.ngdoc | 4 +- misc/tutorial/93_column_resizing.ngdoc | 1 + package.json | 3 +- .../js/ui-grid-column-resizer.js | 8 +- src/font/config.json | 100 +++ .../{arrow.svg => arrow.colors-default.svg} | 2 +- src/js/core/constants.js | 11 +- src/js/core/directives/ui-grid-body.js | 21 +- src/js/core/directives/ui-grid-column-menu.js | 237 +++++++ src/js/core/directives/ui-grid-header-cell.js | 112 ++- src/js/core/directives/ui-grid-header.js | 3 + src/js/core/directives/ui-grid-menu.js | 174 +++++ src/js/core/directives/ui-grid-scrollbar.js | 1 - src/js/core/directives/ui-grid.js | 77 +- src/js/core/factories/Grid.js | 664 ++++++++++++++++++ src/js/core/factories/GridColumn.js | 26 + src/js/core/factories/GridOptions.js | 94 +++ src/js/core/factories/GridRows.js | 42 ++ src/js/core/services/gridClassFactory.js | 486 +------------ src/js/core/services/rowSorter.js | 283 ++++++++ src/js/core/services/ui-grid-util.js | 21 +- src/less/grid.less | 15 +- src/less/header.less | 43 +- src/less/icons.less | 51 ++ src/less/main.less | 3 + src/less/menu.less | 43 ++ src/less/sorting.less | 39 +- src/less/variables.less | 3 + src/templates/ui-grid/ui-grid.html | 4 + src/templates/ui-grid/uiGridColumnMenu.html | 15 + src/templates/ui-grid/uiGridHeaderCell.html | 14 +- src/templates/ui-grid/uiGridMenu.html | 7 + src/templates/ui-grid/uiGridMenuItem.html | 1 + .../directives/ui-grid-header-cell.spec.js | 118 ++++ .../unit/core/directives/ui-grid-menu.spec.js | 182 +++++ test/unit/core/directives/ui-grid.spec.js | 117 +-- test/unit/core/factories/Grid.spec.js | 162 +++++ test/unit/core/factories/GridColumn.spec.js | 48 ++ test/unit/core/row-filtering.spec.js | 43 ++ test/unit/core/row-sorting.spec.js | 269 +++++++ .../core/services/GridClassFactory.spec.js | 5 - 46 files changed, 3297 insertions(+), 625 deletions(-) create mode 100644 misc/tutorial/5.1_sorting.ngdoc create mode 100644 misc/tutorial/5.2_customizing_column_menu.ngdoc create mode 100644 misc/tutorial/5.3_custom_grid_menu.ngdoc create mode 100644 src/font/config.json rename src/img/{arrow.svg => arrow.colors-default.svg} (79%) create mode 100644 src/js/core/directives/ui-grid-column-menu.js create mode 100644 src/js/core/directives/ui-grid-menu.js create mode 100644 src/js/core/factories/Grid.js create mode 100644 src/js/core/factories/GridOptions.js create mode 100644 src/js/core/factories/GridRows.js create mode 100644 src/js/core/services/rowSorter.js create mode 100644 src/less/icons.less create mode 100644 src/less/menu.less create mode 100644 src/templates/ui-grid/uiGridColumnMenu.html create mode 100644 src/templates/ui-grid/uiGridMenu.html create mode 100644 src/templates/ui-grid/uiGridMenuItem.html create mode 100644 test/unit/core/directives/ui-grid-header-cell.spec.js create mode 100644 test/unit/core/directives/ui-grid-menu.spec.js create mode 100644 test/unit/core/factories/Grid.spec.js create mode 100644 test/unit/core/factories/GridColumn.spec.js create mode 100644 test/unit/core/row-filtering.spec.js create mode 100644 test/unit/core/row-sorting.spec.js diff --git a/Gruntfile.js b/Gruntfile.js index 66ef486c47..ac2cff85b2 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -110,12 +110,14 @@ module.exports = function(grunt) { dist: { // paths: ['/bower_components/bootstrap'], files: { - 'dist/release/<%= pkg.name %>.css': ['src/less/main.less', 'src/features/*/less/**/*.less'], + // 'dist/release/<%= pkg.name %>.css': ['src/less/main.less', 'src/features/*/less/**/*.less', '.tmp/icon/icons.data.svg.css'], + 'dist/release/<%= pkg.name %>.css': ['src/less/main.less', 'src/features/*/less/**/*.less', '.tmp/font/ui-grid-codes.css'] } }, min: { files: { - 'dist/release/<%= pkg.name %>.min.css': ['src/less/main.less', 'src/features/*/less/**/*.less'] + // 'dist/release/<%= pkg.name %>.min.css': ['src/less/main.less', 'src/features/*/less/**/*.less', '.tmp/icon/icons.data.svg.css'] + 'dist/release/<%= pkg.name %>.min.css': ['src/less/main.less', 'src/features/*/less/**/*.less', '.tmp/font/ui-grid-codes.css'] }, options: { compress: true @@ -123,6 +125,38 @@ module.exports = function(grunt) { } }, + // grunticon: { + // icons: { + // files: [{ + // expand: true, + // cwd: 'src/img', + // src: ['*.svg'], + // dest: '.tmp/icon' + // }], + // options: { + // cssprefix: '.ui-grid-icon-', + // colors: { + // 'default': '#2c3e50' + // } + // } + // } + // }, + + fontello: { + options: { + sass: false + }, + dist: { + options: { + config : 'src/font/config.json', + fonts : 'dist/release', + styles : '.tmp/font', + scss : false + // force : true + } + } + }, + uglify: { options: { banner: '<%= banner %>' @@ -234,6 +268,7 @@ module.exports = function(grunt) { require: false, /* Jasmine */ + jasmine: false, after: false, afterEach: false, before: false, @@ -252,7 +287,8 @@ module.exports = function(grunt) { waits: false, waitsFor: false, xit: false, - xdescribe: false + xdescribe: false, + spyOn: false } }, gruntfile: { @@ -296,6 +332,15 @@ module.exports = function(grunt) { tasks: ['less', 'ngdocs', 'concat:customizer_less'] }, + // grunticon: { + // files: 'src/img/**/*.svg', + // tasks: ['grunticon', 'less'] + // }, + fontello: { + files: 'src/font/config.json', + tasks: ['fontello', 'less'] + }, + docs: { files: ['misc/tutorial/**/*.ngdoc', 'misc/doc/**'], tasks: 'ngdocs' @@ -373,11 +418,12 @@ module.exports = function(grunt) { }, scripts: [ '//ajax.googleapis.com/ajax/libs/jquery/2.0.3/jquery.min.js', // TODO(c0bra): REMOVE! - '//ajax.googleapis.com/ajax/libs/angularjs/1.2.11/angular.js', - '//ajax.googleapis.com/ajax/libs/angularjs/1.2.11/angular-touch.js', + '//ajax.googleapis.com/ajax/libs/angularjs/1.2.15/angular.js', + '//ajax.googleapis.com/ajax/libs/angularjs/1.2.15/angular-touch.js', + '//ajax.googleapis.com/ajax/libs/angularjs/1.2.15/angular-animate.js', ], hiddenScripts: [ - '//ajax.googleapis.com/ajax/libs/angularjs/1.2.11/angular-animate.js', + '//ajax.googleapis.com/ajax/libs/angularjs/1.2.15/angular-animate.js', 'bower_components/google-code-prettify/src/prettify.js', 'node_modules/marked/lib/marked.js' ], @@ -463,6 +509,8 @@ module.exports = function(grunt) { grunt.loadNpmTasks('grunt-conventional-changelog'); grunt.loadNpmTasks('grunt-gh-pages'); grunt.loadNpmTasks('grunt-shell-spawn'); + // grunt.loadNpmTasks('grunt-grunticon'); + grunt.loadNpmTasks('grunt-fontello'); // grunt.renameTask('protractor', 'protractor-old'); grunt.registerTask('protractor-watch', function () { @@ -493,7 +541,7 @@ module.exports = function(grunt) { grunt.registerTask('default', ['before-test', 'test', 'after-test']); // Build with no testing - grunt.registerTask('build', ['concat', 'uglify', 'less', 'ngdocs', 'copy']); + grunt.registerTask('build', ['concat', 'uglify', 'fontello', 'less', 'ngdocs', 'copy']); // Auto-test tasks for development grunt.registerTask('autotest:unit', ['karmangular:start']); diff --git a/TODO.md b/TODO.md index 59a20a8397..62db689ad7 100644 --- a/TODO.md +++ b/TODO.md @@ -2,14 +2,55 @@ # CURRENT +1. [BUG] - Do we need to validate passed in grid 'id' property to make sure it can be in a CSS rule? +1. [IDEA] - Hook the column menu button into the menu it activates so it can show/hide depending on the number of items it will show. Can we do that? + 1. If sorting is enabled or the user / extension has supplied extra menu items, show the menu button. Otherwise don't show it. + 1. We'll need a way to separate extension menu items from user menu items so the user doesn't override them. +1. [IDEA] - Add an showColumnMenu option? Maybe you don't want it on mobile? +1. [TODO] - Make HOME and END keys scroll to top/bottom if grid has focus... +1. [IDEA] - Can we deselect any selected text when the grid is scrolled? +1. [TODO] - Make row builders async with $q +1. [TODO] - Make plnkr/jsfiddle ngdocs buttons work +1. [TODO] - Remove IE11 cell selected weird green color... +1. [IDEA] - Add gridOptions.options for all opts, and deep watch it then rebuild +1. [IDEA] - Add version number to uiGrid module. + +1. [IDEA] - Might need to make dragging and reordering columns watch for a minimum pixel delta before starting drag, so it doesn't always cancel long-clicks +1. [BUG] - Grid not redrawing properly when switching between tutorials. It still has the grid body height from the previous tutorial. + 1. This is due to a combination of grunt-ngdocs and ngAnimate. ngAnimate is leaving two "page" (or whatever) elements on the page at the + same time. Both have a main.css which include styles for the grid. Having the old one on there at the same time as the new one makes + it use the height from the old one when calculating the grid height... *** Can we switch to Dgeni? *** +1. [BUG] - Menu icon overlays menu text when column name is too long... + 1. [IDEA] - Can we shrink the size of the header-cell-contents div and make it text-overflow: ellipsis? +1. [TOFIX] - Menu icon vertical alignment off in IE11 (how does it look in FF?) +1. [NOTE] - Use "-webkit-text-stroke: 0.3px" on icon font to fix jaggies in Chrome on Windows +1. [TODO] - Add a failing test for the IE9-11 column sorting hack (columnSorter.js, line 229) +1. [TODO] - Kendo Grid shows the column menu positioned OUTSIDE the grid for the final column, but it doesn't flow outside the window. + + 1. [TODO] - Add row filtering 1. [TODO] - Add notes about browser version support and Angular version support to README.md +1. [TODO] - Add handling for sorting null values with columnDef sortingAlgorithm (PR #940) + +# Cleanup + +1. [TODO] - Rename gridUtil to uiGridUtil +1. [TODO] - Rename GridUtil in uiGridBody to gridUtil or the above +1. [TODO] - Move uiGridCell to its own file + +# Extras + +1. Add iit and ddescribe checks as commit hooks # Native scrolling -1. [BUG] - Touch event deceleration goes backwards when scrolling up, but only with small amounts +1. [BUG] - Touch event deceleration goes backwards when scrolling up, but only with small amounts. + 1. [BUG] - Horizontal scrolling when emulating a touch device is weird too, scroll between grid canvas and header canvas is offset. 1. [TODO] - Take a look at Hamster.js for normalizing mouse wheel events, test on MacAir. +# Memory Issues +1. [LEAKS] - Make sure stylesheets are being removed on $destroy, and anywhere that we might be doing manual appendChild, or other appending. +1. [LEAKS] - Null out all references to DOM elements in $destroy handler # MORE @@ -50,38 +91,3 @@ # Done! -1. [DONE] - [BUG] - When column resizing and you've scrolled to the end of the grid, the scrollbar extends beyond the viewport... -1. [DONE] Figure out how to run e2e tests on docs (look at angularjs source / protractor?) -1. [DONE] Add --browsers option for testing on saucelabs with specific browser(s) -1. [DONE] Make karmangular run in `watch` mode and in singlerun too. -1. [DONE] Make sure failing saucelabs tests don't cause the build to fail. Only if the normal test run fails -1. [DONE] Add grunt task that will use ngdoc to build test specs from docs into .tmp/e2e/doc.spec.js - - It will need to run after ngdocs does. Maybe make a `gendocs` task that runs both serially. -1. [DONE] Docs ref for ui-grid.js is pointing to localhost:9999 on travis. -1. [DONE] Sometimes scrollbar snaps back to the top??? - 1. I think it's getting mousewheel events when the element doesn't have focus. - 1. To reproduce, use mousewheel to scroll to bottom of grid, then move outside grid and scroll page to top. Use window scrollbar to move back down to show grid, then click on scrollbar. It will snap to the top. -1. [DONE] elementHeight() (AND jQuery.height()) isn't working on the .ui-grid element. It's not accounting for the border when figuring out the canvas drawing space. - 1. [NOTE] - I just had to subtract "1" from the canvas height. Not sure why. After that, any borders of any size on the grid element are accounted for correctly. - 1 [NOTE] - It was because of the top-panel bottom border, which is 1px by default -1. [DONE] Looks like the canvas needs to be the height of all the elements (rowheight * data length) in order for the scroll to work right -1. [DONE] Add 'track by $index' to ng-repeats? -1. [DONE] Add virtual repeat functionality -1. [DONE] Scrollbar should only show up when there's elements to scroll. - 1. i.e. add ng-show based on canvasHeight > gridbody height -1. [DONE] Copy angular-animate, prettify.js and marked.js into the docs/js dir separately from grunt-ngdocs. It's causing them to show up in ` + +
+
+
+ + + it('should apply the right class to the element', function () { + element(by.css('.blah')).getCssValue('border') + .then(function(c) { + expect(c).toContain('1px solid'); + }); + }); + + + */ +angular.module('ui.grid') + +.directive('uiGridMenu', ['$log', '$timeout', '$window', '$document', 'gridUtil', function ($log, $timeout, $window, $document, gridUtil) { + var uiGridMenu = { + priority: 0, + scope: { + // shown: '&', + menuItems: '=', + autoHide: '=?' + }, + require: '?^uiGrid', + templateUrl: 'ui-grid/uiGridMenu', + replace: false, + link: function ($scope, $elm, $attrs, uiGridCtrl) { + gridUtil.enableAnimations($elm); + + if (typeof($scope.autoHide) === 'undefined' || $scope.autoHide === undefined) { + $scope.autoHide = true; + } + + if ($scope.autoHide) { + angular.element($window).on('resize', $scope.hideMenu); + } + + $scope.$on('hide-menu', function () { + $scope.shown = false; + }); + + $scope.$on('show-menu', function () { + $scope.shown = true; + }); + + $scope.$on('$destroy', function() { + angular.element($window).off('resize', $scope.hideMenu); + }); + }, + controller: function ($scope, $element, $attrs) { + var self = this; + + self.hideMenu = $scope.hideMenu = function() { + $scope.shown = false; + }; + + function documentClick() { + $scope.$apply(function () { + self.hideMenu(); + angular.element(document).off('click', documentClick); + }); + } + + self.showMenu = $scope.showMenu = function() { + $scope.shown = true; + + // Turn off an existing dpcument click handler + angular.element(document).off('click', documentClick); + + // 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('click', documentClick); + }); + }; + + $scope.$on('$destroy', function () { + angular.element(document).off('click', documentClick); + }); + } + }; + + return uiGridMenu; +}]) + +.directive('uiGridMenuItem', ['$log', function ($log) { + var uiGridMenuItem = { + priority: 0, + scope: { + title: '=', + active: '=', + action: '=', + icon: '=', + shown: '=', + context: '=' + }, + require: ['?^uiGrid', '^uiGridMenu'], + templateUrl: 'ui-grid/uiGridMenuItem', + replace: true, + link: function ($scope, $elm, $attrs, controllers) { + var uiGridCtrl = controllers[0], + uiGridMenuCtrl = controllers[1]; + + // TODO(c0bra): validate that shown and active are function if they're defined. An exception is already thrown above this though + // if (typeof($scope.shown) !== 'undefined' && $scope.shown && typeof($scope.shown) !== 'function') { + // throw new TypeError("$scope.shown is defined but not a function"); + // } + + if (typeof($scope.shown) === 'undefined' || $scope.shown === null) { + $scope.shown = function() { return true; }; + } + + $scope.itemShown = function () { + var context = {}; + if ($scope.context) { + context.context = $scope.context; + } + + if (typeof(uiGridCtrl) !== 'undefined' && uiGridCtrl) { + context.grid = uiGridCtrl.grid; + } + + return $scope.shown.call(context); + }; + + $scope.itemAction = function($event) { + $event.stopPropagation(); + + if (typeof($scope.action) === 'function') { + var context = {}; + + if ($scope.context) { + context.context = $scope.context; + } + + // Add the grid to the function call context if the uiGrid controller is present + if (typeof(uiGridCtrl) !== 'undefined' && uiGridCtrl) { + context.grid = uiGridCtrl.grid; + } + + $scope.action.call(context, $event); + + uiGridMenuCtrl.hideMenu(); + } + }; + } + }; + + return uiGridMenuItem; +}]); + +})(); \ No newline at end of file diff --git a/src/js/core/directives/ui-grid-scrollbar.js b/src/js/core/directives/ui-grid-scrollbar.js index 7d71d03ea9..45b4defcd3 100644 --- a/src/js/core/directives/ui-grid-scrollbar.js +++ b/src/js/core/directives/ui-grid-scrollbar.js @@ -102,7 +102,6 @@ // Only show the scrollbar when the canvas height is less than the viewport height $scope.showScrollbar = function() { - // TODO: handle type if ($scope.type === 'vertical') { return uiGridCtrl.grid.getCanvasHeight() > uiGridCtrl.grid.getViewportHeight(); } diff --git a/src/js/core/directives/ui-grid.js b/src/js/core/directives/ui-grid.js index 50438f6b38..43c9bf6095 100644 --- a/src/js/core/directives/ui-grid.js +++ b/src/js/core/directives/ui-grid.js @@ -9,10 +9,10 @@ var self = this; - self.grid = gridClassFactory.createGrid(); - // Extend options with ui-grid attribute reference - angular.extend(self.grid.options, $scope.uiGrid); + self.grid = gridClassFactory.createGrid($scope.uiGrid); + + // angular.extend(self.grid.options, ); //all properties of grid are available on scope $scope.grid = self.grid; @@ -91,19 +91,20 @@ //wrap data in a gridRow $log.debug('Modifying rows'); - self.grid.modifyRows(n); - - //todo: move this to the ui-body-directive and define how we handle ordered event registration - if (self.viewport) { - var scrollTop = self.viewport[0].scrollTop; - var scrollLeft = self.viewport[0].scrollLeft; - self.adjustScrollVertical(scrollTop, 0, true); - self.adjustScrollHorizontal(scrollLeft, 0, true); - } - - $scope.$evalAsync(function() { - self.refreshCanvas(true); - }); + self.grid.modifyRows(n) + .then(function () { + //todo: move this to the ui-body-directive and define how we handle ordered event registration + if (self.viewport) { + var scrollTop = self.viewport[0].scrollTop; + var scrollLeft = self.viewport[0].scrollLeft; + self.adjustScrollVertical(scrollTop, 0, true); + self.adjustScrollHorizontal(scrollLeft, 0, true); + } + + $scope.$evalAsync(function() { + self.refreshCanvas(true); + }); + }); }); } } @@ -114,7 +115,7 @@ columnDefWatchDereg(); }); - + // TODO(c0bra): Do we need to destroy this watch on $destroy? $scope.$watch(function () { return self.grid.styleComputations; }, function() { self.refreshCanvas(true); }); @@ -144,19 +145,44 @@ return p.promise; }; - var cellValueGetterCache = {}; - self.getCellValue = function(row,col){ - if(!cellValueGetterCache[col.colDef.name]){ - cellValueGetterCache[col.colDef.name] = $parse(row.getEntityQualifiedColField(col)); - } - return cellValueGetterCache[col.colDef.name](row); + self.getCellValue = function(row, col) { + return $scope.grid.getCellValue(row, col); + }; + + $scope.grid.refreshRows = self.refreshRows = function () { + self.grid.processRowsProcessors(self.grid.rows) + .then(function (renderableRows) { + self.grid.setVisibleRows(renderableRows); + + self.redrawRows(); + + self.refreshCanvas(); + }); }; + /* Sorting Methods */ + + + /* Event Methods */ + //todo: throttle this event? self.fireScrollingEvent = function(args) { $scope.$broadcast(uiGridConstants.events.GRID_SCROLL, args); }; + self.fireEvent = function(eventName, args) { + // Add the grid to the event arguments if it's not there + if (typeof(args) === 'undefined' || args === undefined) { + args = {}; + } + + if (typeof(args.grid) === 'undefined' || args.grid === undefined) { + args.grid = self.grid; + } + + $scope.$broadcast(eventName, args); + }; + }]); /** @@ -205,6 +231,7 @@ angular.module('ui.grid').directive('uiGrid', uiGrid: '=' }, replace: true, + transclude: true, controller: 'uiGridController', compile: function () { return { @@ -231,7 +258,7 @@ angular.module('ui.grid').directive('uiGrid', ]); //todo: move to separate file once Brian has finished committed work in progress - angular.module('ui.grid').directive('uiGridCell', ['$compile', 'uiGridConstants', '$log', '$parse', function ($compile, uiGridConstants, $log, $parse) { + angular.module('ui.grid').directive('uiGridCell', ['$compile', '$log', '$parse', 'gridUtil', 'uiGridConstants', function ($compile, $log, $parse, gridUtil, uiGridConstants) { var uiGridCell = { priority: 0, scope: false, @@ -252,7 +279,7 @@ angular.module('ui.grid').directive('uiGrid', // No controller, compile the element manually else { var html = $scope.col.cellTemplate - .replace(uiGridConstants.COL_FIELD, 'getCellValue(row,col)'); + .replace(uiGridConstants.COL_FIELD, 'getCellValue(row, col)'); var cellElement = $compile(html)($scope); $elm.append(cellElement); } diff --git a/src/js/core/factories/Grid.js b/src/js/core/factories/Grid.js new file mode 100644 index 0000000000..6c08982f01 --- /dev/null +++ b/src/js/core/factories/Grid.js @@ -0,0 +1,664 @@ +(function(){ + +angular.module('ui.grid') +.factory('Grid', ['$log', '$q', '$parse', 'gridUtil', 'uiGridConstants', 'GridOptions', 'GridColumn', 'GridRow', 'rowSorter', function($log, $q, $parse, gridUtil, uiGridConstants, GridOptions, GridColumn, GridRow, rowSorter) { + +/** + * @ngdoc function + * @name ui.grid.class:Grid + * @description Grid defines a logical grid. Any non-dom properties and elements needed by the grid should + * be defined in this class + * @param {string} id id to assign to grid + */ + var Grid = function (options) { + // Get the id out of the options, then remove it + this.id = options.id; + delete options.id; + + // Get default options + this.options = new GridOptions(); + + // Extend the default options with what we were passed in + angular.extend(this.options, options); + + this.headerHeight = this.options.headerRowHeight; + this.gridHeight = 0; + this.gridWidth = 0; + this.columnBuilders = []; + this.rowBuilders = []; + this.rowsProcessors = []; + this.styleComputations = []; + this.visibleRowCache = []; + + this.cellValueGetterCache = {}; + + // Validate options + if (!this.options.enableNativeScrolling && !this.options.enableVirtualScrolling) { + throw "Either native or virtual scrolling must be enabled."; + } + + + //representation of the rows on the grid. + //these are wrapped references to the actual data rows (options.data) + this.rows = []; + + //represents the columns on the grid + this.columns = []; + + //current rows that are rendered on the DOM + this.renderedRows = []; + this.renderedColumns = []; + }; + + /** + * @ngdoc function + * @name registerColumnBuilder + * @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 + */ + Grid.prototype.registerColumnBuilder = function (columnsProcessor) { + this.columnBuilders.push(columnsProcessor); + }; + + /** + * @ngdoc function + * @name registerRowBuilder + * @methodOf ui.grid.class:Grid + * @description When the build creates rows from gridOptions.data, the rowBuilders will be called to add + * additional properties to the row. + * @param {function(colDef, col, gridOptions)} columnsProcessor function to be called + */ + Grid.prototype.registerRowBuilder = function (rowBuilder) { + this.rowBuilders.push(rowBuilder); + }; + + /** + * @ngdoc function + * @name getColumn + * @methodOf ui.grid.class:Grid + * @description returns a grid column for the column name + * @param {string} name column name + */ + Grid.prototype.getColumn = function (name) { + var columns = this.columns.filter(function (column) { + return column.colDef.name === name; + }); + return columns.length > 0 ? columns[0] : null; + }; + + /** + * @ngdoc function + * @name buildColumns + * @methodOf ui.grid.class:Grid + * @description creates GridColumn objects from the columnDefinition. Calls each registered + * columnBuilder to further process the column + * @returns {Promise} a promise to load any needed column resources + */ + Grid.prototype.buildColumns = function () { + $log.debug('buildColumns'); + var self = this; + var builderPromises = []; + + self.options.columnDefs.forEach(function (colDef, index) { + self.preprocessColDef(colDef); + var col = self.getColumn(colDef.name); + + if (!col) { + col = new GridColumn(colDef, index); + self.columns.push(col); + } + else { + col.updateColumnDef(colDef, col.index); + } + + self.columnBuilders.forEach(function (builder) { + builderPromises.push(builder.call(self, colDef, col, self.options)); + }); + + }); + + return $q.all(builderPromises); + }; + + /** + * undocumented function + * @name preprocessColDef + * @methodOf ui.grid.class:Grid + * @description defaults the name property from field to maintain backwards compatibility with 2.x + * validates that name or field is present + */ + Grid.prototype.preprocessColDef = function (colDef) { + if (!colDef.field && !colDef.name) { + throw new Error('colDef.name or colDef.field property is required'); + } + + //maintain backwards compatibility with 2.x + //field was required in 2.x. now name is required + if (colDef.name === undefined && colDef.field !== undefined) { + colDef.name = colDef.field; + } + }; + + /** + * @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 + */ + Grid.prototype.modifyRows = function(newRawData) { + var self = this; + + if (self.rows.length === 0 && newRawData.length > 0) { + self.addRows(newRawData); + } + else { + //look for new rows + var newRows = newRawData.filter(function (newItem) { + return !self.rows.some(function(oldRow) { + return self.options.rowEquality(oldRow.entity, newItem); + }); + }); + + for (i = 0; i < newRows.length; i++) { + self.addRows([newRows[i]]); + } + + //look for deleted rows + var deletedRows = self.rows.filter(function (oldRow) { + return !newRawData.some(function (newItem) { + return self.options.rowEquality(newItem, oldRow.entity); + }); + }); + + for (var i = 0; i < deletedRows.length; i++) { + self.rows.splice( self.rows.indexOf(deletedRows[i] ), 1 ); + } + } + + // Make a reference copy that we can alter (sort, etc) + // var renderableRows = self.processRowsProcessors(self.rows); + return $q.when(self.processRowsProcessors(self.rows)) + .then(function (renderableRows) { + return self.setVisibleRows(renderableRows); + }); + + // self.setVisibleRows(renderableRows); + }; + + /** + * Private Undocumented Method + * @name addRows + * @methodOf ui.grid.class:Grid + * @description adds the newRawData array of rows to the grid and calls all registered + * rowBuilders. this keyword will reference the grid + */ + Grid.prototype.addRows = function(newRawData) { + var self = this; + + for (var i=0; i < newRawData.length; i++) { + self.rows.push( self.processRowBuilders(new GridRow(newRawData[i], i)) ); + } + }; + + /** + * @ngdoc function + * @name processRowBuilders + * @methodOf ui.grid.class:Grid + * @description processes all RowBuilders for the gridRow + * @param {GridRow} gridRow reference to gridRow + * @returns {GridRow} the gridRow with all additional behavior added + */ + Grid.prototype.processRowBuilders = function(gridRow) { + var self = this; + + self.rowBuilders.forEach(function (builder) { + builder.call(self, gridRow, self.gridOptions); + }); + + return gridRow; + }; + + /** + * @ngdoc function + * @name registerStyleComputation + * @methodOf ui.grid.class:Grid + * @description registered a styleComputation function + * @param {function($scope)} styleComputation function + */ + Grid.prototype.registerStyleComputation = function (styleComputationInfo) { + this.styleComputations.push(styleComputationInfo); + }; + + + // NOTE (c0bra): We already have rowBuilders. I think these do exactly the same thing... + // Grid.prototype.registerRowFilter = function(filter) { + // // TODO(c0bra): validate filter? + + // this.rowFilters.push(filter); + // }; + + // Grid.prototype.removeRowFilter = function(filter) { + // var idx = this.rowFilters.indexOf(filter); + + // if (typeof(idx) !== 'undefined' && idx !== undefined) { + // this.rowFilters.slice(idx, 1); + // } + // }; + + // Grid.prototype.processRowFilters = function(rows) { + // var self = this; + // self.rowFilters.forEach(function (filter) { + // filter.call(self, rows); + // }); + // }; + + + /** + * @ngdoc function + * @name registerRowsProcessor + * @methodOf ui.grid.class:Grid + * @param {function(renderableRows)} rows processor function + * @returns {Array[GridRow]} Updated renderable rows + * @description + + Register a "rows processor" function. When the rows are updated, + the grid calls eached registered "rows processor", which has a chance + to alter the set of rows (sorting, etc) as long as the count is not + modified. + */ + Grid.prototype.registerRowsProcessor = function(processor) { + if (! angular.isFunction(processor)) { + throw 'Attempt to register non-function rows processor: ' + processor; + } + + this.rowsProcessors.push(processor); + }; + + /** + * @ngdoc function + * @name removeRowsProcessor + * @methodOf ui.grid.class:Grid + * @param {function(renderableRows)} rows processor function + * @description Remove a registered rows processor + */ + Grid.prototype.removeRowsProcessor = function(processor) { + var idx = this.rowsProcessors.indexOf(processor); + + if (typeof(idx) !== 'undefined' && idx !== undefined) { + this.rowsProcessors.splice(idx, 1); + } + }; + + /** + * Private Undocumented Method + * @name processRowsProcessors + * @methodOf ui.grid.class:Grid + * @param {Array[GridRow]} The array of "renderable" rows + * @param {Array[GridColumn]} The array of columns + * @description Run all the registered rows processors on the array of renderable rows + */ + Grid.prototype.processRowsProcessors = function(renderableRows) { + var self = this; + + // Create a shallow copy of the rows so that we can safely sort them without altering the original grid.rows sort order + var myRenderableRows = renderableRows.slice(0); + + // self.rowsProcessors.forEach(function (processor) { + // myRenderableRows = processor.call(self, myRenderableRows, self.columns); + + // if (! renderableRows) { + // throw "Processor at index " + i + " did not return a set of renderable rows"; + // } + + // if (!angular.isArray(renderableRows)) { + // throw "Processor at index " + i + " did not return an array"; + // } + + // i++; + // }); + + // Return myRenderableRows with no processing if we have no rows processors + if (self.rowsProcessors.length === 0) { + return $q.when(myRenderableRows); + } + + // Counter for iterating through rows processors + var i = 0; + + // Promise for when we're done with all the processors + var finished = $q.defer(); + + // This function will call the processor in self.rowsProcessors at index 'i', and then + // when done will call the next processor in the list, using the output from the processor + // at i as the argument for 'renderedRowsToProcess' on the next iteration. + // + // If we're at the end of the list of processors, we resolve our 'finished' callback with + // the result. + function startProcessor(i, renderedRowsToProcess) { + // Get the processor at 'i' + var processor = self.rowsProcessors[i]; + + // 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) + return $q.when( processor.call(self, renderedRowsToProcess, self.columns) ) + .then(function(processedRows) { + // Check for errors + if (! processedRows) { + throw "Processor at index " + i + " did not return a set of renderable rows"; + } + + if (!angular.isArray(processedRows)) { + throw "Processor at index " + i + " did not return an array"; + } + + // Processor is done, increment the counter + i++; + + // If we're not done with the processors, call the next one + if (i <= self.rowsProcessors.length - 1) { + return startProcessor(i, processedRows); + } + // We're done! Resolve the 'finished' promise + else { + finished.resolve(processedRows); + } + }); + } + + // Start on the first processor + startProcessor(0, myRenderableRows); + + return finished.promise; + }; + + Grid.prototype.setVisibleRows = function(rows) { + var newVisibleRowCache = []; + + rows.forEach(function (row) { + if (row.visible) { + newVisibleRowCache.push(row); + } + }); + + this.visibleRowCache = newVisibleRowCache; + }; + + + Grid.prototype.setRenderedRows = function (newRows) { + this.renderedRows.length = newRows.length; + for (var i = 0; i < newRows.length; i++) { + this.renderedRows[i] = newRows[i]; + } + }; + + Grid.prototype.setRenderedColumns = function (newColumns) { + this.renderedColumns.length = newColumns.length; + for (var i = 0; i < newColumns.length; i++) { + this.renderedColumns[i] = newColumns[i]; + } + }; + + /** + * @ngdoc function + * @name buildStyles + * @methodOf ui.grid.class:Grid + * @description calls each styleComputation function + */ + Grid.prototype.buildStyles = function ($scope) { + var self = this; + self.styleComputations + .sort(function(a, b) { + if (a.priority === null) { return 1; } + if (b.priority === null) { return -1; } + if (a.priority === null && b.priority === null) { return 0; } + return a.priority - b.priority; + }) + .forEach(function (compInfo) { + compInfo.func.call(self, $scope); + }); + }; + + Grid.prototype.minRowsToRender = function () { + return Math.ceil(this.getViewportHeight() / this.options.rowHeight); + }; + + Grid.prototype.minColumnsToRender = function () { + var self = this; + var viewport = this.getViewportWidth(); + + var min = 0; + var totalWidth = 0; + self.columns.forEach(function(col, i) { + if (totalWidth < viewport) { + totalWidth += col.drawnWidth; + min++; + } + else { + var currWidth = 0; + for (var j = i; j >= i - min; j--) { + currWidth += self.columns[j].drawnWidth; + } + if (currWidth < viewport) { + min++; + } + } + }); + + return min; + }; + + Grid.prototype.getBodyHeight = function () { + // Start with the viewportHeight + var bodyHeight = this.getViewportHeight(); + + // Add the horizontal scrollbar height if there is one + if (typeof(this.horizontalScrollbarHeight) !== 'undefined' && this.horizontalScrollbarHeight !== undefined && this.horizontalScrollbarHeight > 0) { + bodyHeight = bodyHeight + this.horizontalScrollbarHeight; + } + + return bodyHeight; + }; + + // NOTE: viewport drawable height is the height of the grid minus the header row height (including any border) + // TODO(c0bra): account for footer height + Grid.prototype.getViewportHeight = function () { + var viewPortHeight = this.gridHeight - this.headerHeight; + + // Account for native horizontal scrollbar, if present + if (typeof(this.horizontalScrollbarHeight) !== 'undefined' && this.horizontalScrollbarHeight !== undefined && this.horizontalScrollbarHeight > 0) { + viewPortHeight = viewPortHeight - this.horizontalScrollbarHeight; + } + + return viewPortHeight; + }; + + Grid.prototype.getViewportWidth = function () { + var viewPortWidth = this.gridWidth; + + if (typeof(this.verticalScrollbarWidth) !== 'undefined' && this.verticalScrollbarWidth !== undefined && this.verticalScrollbarWidth > 0) { + viewPortWidth = viewPortWidth - this.verticalScrollbarWidth; + } + + return viewPortWidth; + }; + + Grid.prototype.getHeaderViewportWidth = function () { + var viewPortWidth = this.getViewportWidth(); + + if (typeof(this.verticalScrollbarWidth) !== 'undefined' && this.verticalScrollbarWidth !== undefined && this.verticalScrollbarWidth > 0) { + viewPortWidth = viewPortWidth + this.verticalScrollbarWidth; + } + + return viewPortWidth; + }; + + Grid.prototype.getCanvasHeight = function () { + var ret = this.options.rowHeight * this.rows.length; + + if (typeof(this.horizontalScrollbarHeight) !== 'undefined' && this.horizontalScrollbarHeight !== undefined && this.horizontalScrollbarHeight > 0) { + ret = ret - this.horizontalScrollbarHeight; + } + + return ret; + }; + + Grid.prototype.getCanvasWidth = function () { + var ret = this.canvasWidth; + + if (typeof(this.verticalScrollbarWidth) !== 'undefined' && this.verticalScrollbarWidth !== undefined && this.verticalScrollbarWidth > 0) { + ret = ret - this.verticalScrollbarWidth; + } + + return ret; + }; + + Grid.prototype.getTotalRowHeight = function () { + return this.options.rowHeight * this.rows.length; + }; + + // Is the grid currently scrolling? + Grid.prototype.isScrolling = function() { + return this.scrolling ? true : false; + }; + + Grid.prototype.setScrolling = function(scrolling) { + this.scrolling = scrolling; + }; + + Grid.prototype.rowSearcher = function rowSearcher(rows) { + var grid = this; + }; + + Grid.prototype.sortByColumn = function sortByColumn(renderableRows) { + return rowSorter.sort(this, renderableRows, this.columns); + }; + + 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)); + } + + return self.cellValueGetterCache[col.colDef.name](row); + }; + + // Reset all sorting on the grid + Grid.prototype.getNextColumnSortPriority = function () { + var self = this, + p = 0; + + self.columns.forEach(function (col) { + if (col.sort && col.sort.priority && col.sort.priority > p) { + p = col.sort.priority; + } + }); + + return p + 1; + }; + + /** + * @ngdoc function + * @name resetColumnSorting + * @methodOf ui.grid.class:Grid + * @description Return the columns that the grid is currently being sorted by + * @param {GridColumn} [excludedColumn] Optional GridColumn to exclude from having its sorting reset + */ + Grid.prototype.resetColumnSorting = function (excludeCol) { + var self = this; + + self.columns.forEach(function (col) { + if (col !== excludeCol) { + col.sort = {}; + } + }); + }; + + /** + * @ngdoc function + * @name getColumnSorting + * @methodOf ui.grid.class:Grid + * @description Return the columns that the grid is currently being sorted by + * @returns {Array[GridColumn]} An array of GridColumn objects + */ + Grid.prototype.getColumnSorting = function() { + var self = this; + + var sortedCols = []; + + // Iterate through all the columns, sorted by priority + self.columns.sort(rowSorter.prioritySort).forEach(function (col) { + if (col.sort && typeof(col.sort.direction) !== 'undefined' && col.sort.direction && (col.sort.direction === uiGridConstants.ASC || col.sort.direction === uiGridConstants.DESC)) { + sortedCols.push(col); + } + }); + + return sortedCols; + }; + + /** + * @ngdoc function + * @name sortColumn + * @methodOf ui.grid.class:Grid + * @description Set the sorting on a given column, optionally resetting any existing sorting on the Grid. + * @param {GridColumn} column Column to set the sorting on + * @param {uiGridConstants.ASC|uiGridConstants.DESC} [direction] Direction to sort by, either descending or ascending. + * If not provided, the column will iterate through the sort directions: ascending, descending, unsorted. + * @param {boolean} [add] Add this column to the sorting. If not provided or set to `false`, the Grid will reset any existing sorting and sort + * by this column only + * @returns {Promise} A resolved promise that supplies the column. + */ + Grid.prototype.sortColumn = function (column, directionOrAdd, add) { + var self = this, + direction = null; + + if (typeof(column) === 'undefined' || !column) { + throw new Error('No column parameter provided'); + } + + // Second argument can either be a direction or whether to add this column to the existing sort. + // If it's a boolean, it's an add, otherwise, it's a direction + if (typeof(directionOrAdd) === 'boolean') { + add = directionOrAdd; + } + else { + direction = directionOrAdd; + } + + if (!add) { + self.resetColumnSorting(column); + column.sort.priority = 0; + } + else { + column.sort.priority = self.getNextColumnSortPriority(); + } + + if (!direction) { + // Figure out the sort direction + if (column.sort.direction && column.sort.direction === uiGridConstants.ASC) { + column.sort.direction = uiGridConstants.DESC; + } + else if (column.sort.direction && column.sort.direction === uiGridConstants.DESC) { + column.sort.direction = null; + } + else { + column.sort.direction = uiGridConstants.ASC; + } + } + else { + column.sort.direction = direction; + } + + return $q.when(column); + }; + + return Grid; + +}]); + +})(); \ No newline at end of file diff --git a/src/js/core/factories/GridColumn.js b/src/js/core/factories/GridColumn.js index 4cd02c3e14..7b8eeb88e5 100644 --- a/src/js/core/factories/GridColumn.js +++ b/src/js/core/factories/GridColumn.js @@ -25,6 +25,7 @@ angular.module('ui.grid')
see angular docs on binding expressions
  • displayName - column name when displayed on screen. defaults to name
  • +
  • sortingAlgorithm - Algorithm to use for sorting this column. Takes 'a' and 'b' parameters like any normal sorting function.
  • todo: add other optional fields as implementation matures
  • * @@ -84,6 +85,11 @@ angular.module('ui.grid') } } + // Remove this column from the grid sorting + GridColumn.prototype.unsort = function () { + this.sort = {}; + }; + self.minWidth = !colDef.minWidth ? 50 : colDef.minWidth; self.maxWidth = !colDef.maxWidth ? 9000 : colDef.maxWidth; @@ -104,6 +110,26 @@ angular.module('ui.grid') //self.cursor = self.sortable ? 'pointer' : 'default'; self.visible = true; + + // Turn on sorting by default + self.enableSorting = typeof(colDef.enableSorting) !== 'undefined' ? colDef.enableSorting : true; + + self.sortingAlgorithm = colDef.sortingAlgorithm; + + self.menuItems = colDef.menuItems; + + // Use the column definition sort if we were passed it + if (typeof(colDef.sort) !== 'undefined' && colDef.sort) { + self.sort = colDef.sort; + } + // Otherwise use our own if it's set + else if (typeof(self.sort) !== 'undefined') { + self.sort = self.sort; + } + // Default to empty object for the sort + else { + self.sort = {}; + } }; return GridColumn; diff --git a/src/js/core/factories/GridOptions.js b/src/js/core/factories/GridOptions.js new file mode 100644 index 0000000000..80cf684e42 --- /dev/null +++ b/src/js/core/factories/GridOptions.js @@ -0,0 +1,94 @@ +(function(){ + +angular.module('ui.grid') +.factory('GridOptions', [function() { + + /** + * @ngdoc function + * @name ui.grid.class:GridOptions + * @description Default GridOptions class. GridOptions are defined by the application developer and overlaid + * over this object. + * @param {string} id id to assign to grid + */ + function GridOptions() { + /** + * @ngdoc object + * @name data + * @propertyOf ui.grid.class:GridOptions + * @description Array of data to be rendered to grid. Array can contain complex objects + */ + this.data = []; + + /** + * @ngdoc object + * @name columnDefs + * @propertyOf ui.grid.class:GridOptions + * @description (optional) Array of columnDef objects. Only required property is name. + * _field property can be used in place of name for backwards compatibilty with 2.x_ + * @example + + var columnDefs = [{name:'field1'}, {name:'field2'}]; + + */ + this.columnDefs = []; + + this.headerRowHeight = 30; + this.rowHeight = 30; + this.maxVisibleRowCount = 200; + + this.columnWidth = 50; + this.maxVisibleColumnCount = 200; + + // Turn virtualization on when number of data elements goes over this number + this.virtualizationThreshold = 20; + + this.columnVirtualizationThreshold = 10; + + // Extra rows to to render outside of the viewport + this.excessRows = 4; + this.scrollThreshold = 4; + + // Extra columns to to render outside of the viewport + this.excessColumns = 4; + this.horizontalScrollThreshold = 2; + + // Sorting on by default + this.enableSorting = true; + + // Column menu can be used by default + this.enableColumnMenu = true; + + // Native scrolling on by default + this.enableNativeScrolling = true; + + // Virtual scrolling off by default, overrides enableNativeScrolling if set + this.enableVirtualScrolling = false; + + // Resizing columns, off by default + this.enableColumnResizing = false; + + // Columns can't be smaller than 10 pixels + this.minimumColumnSize = 10; + + /** + * @ngdoc function + * @name rowEquality + * @methodOf ui.grid.class:GridOptions + * @description By default, rows are compared using object equality. This option can be overridden + * to compare on any data item property or function + * @param {object} entityA First Data Item to compare + * @param {object} entityB Second Data Item to compare + */ + this.rowEquality = function(entityA, entityB) { + return entityA === entityB; + }; + + // Custom template for header row + this.headerTemplate = null; + } + + return GridOptions; + +}]); + +})(); \ No newline at end of file diff --git a/src/js/core/factories/GridRows.js b/src/js/core/factories/GridRows.js new file mode 100644 index 0000000000..08e66e4612 --- /dev/null +++ b/src/js/core/factories/GridRows.js @@ -0,0 +1,42 @@ +(function(){ + +angular.module('ui.grid') +.factory('GridRow', ['gridUtil', function(gridUtil) { + + /** + * @ngdoc function + * @name ui.grid.class:GridRow + * @description Wrapper for the GridOptions.data rows. Allows for needed properties and functions + * to be assigned to a grid row + * @param {object} entity the array item from GridOptions.data + * @param {number} index the current position of the row in the array + */ + function GridRow(entity, index) { + this.entity = entity; + this.index = index; + + // Default to true + this.visible = true; + } + + /** + * @ngdoc function + * @name getQualifiedColField + * @methodOf ui.grid.class:GridRow + * @description returns the qualified field name as it exists on scope + * ie: row.entity.fieldA + * @param {GridCol} col column instance + * @returns {string} resulting name that can be evaluated on scope + */ + GridRow.prototype.getQualifiedColField = function(col) { + return 'row.entity.' + col.field; + }; + + GridRow.prototype.getEntityQualifiedColField = function(col) { + return 'entity.' + col.field; + }; + + return GridRow; +}]); + +})(); \ No newline at end of file diff --git a/src/js/core/services/gridClassFactory.js b/src/js/core/services/gridClassFactory.js index c4b3cf3763..b9de6d2409 100644 --- a/src/js/core/services/gridClassFactory.js +++ b/src/js/core/services/gridClassFactory.js @@ -7,8 +7,8 @@ * @description factory to return dom specific instances of a grid * */ - angular.module('ui.grid').service('gridClassFactory', ['gridUtil', '$q', '$templateCache', 'uiGridConstants', '$log', 'GridColumn', - function (gridUtil, $q, $templateCache, uiGridConstants, $log, GridColumn) { + angular.module('ui.grid').service('gridClassFactory', ['gridUtil', '$q', '$templateCache', 'uiGridConstants', '$log', 'Grid', 'GridColumn', 'GridRow', + function (gridUtil, $q, $templateCache, uiGridConstants, $log, Grid, GridColumn, GridRow) { var service = { /** @@ -18,9 +18,23 @@ * @description Creates a new grid instance. Each instance will have a unique id * @returns {Grid} grid */ - createGrid : function() { - var grid = new Grid(gridUtil.newId()); + createGrid : function(options) { + options = (typeof(options) !== 'undefined') ? options: {}; + options.id = gridUtil.newId(); + var grid = new Grid(options); + grid.registerColumnBuilder(service.defaultColumnBuilder); + + grid.registerRowBuilder(grid.rowSearcher); + + // Register the default row processor, it sorts rows by selected columns + if (!grid.options.externalSort && angular.isFunction) { + grid.registerRowsProcessor(grid.sortByColumn); + } + else { + grid.registerRowsProcessor(grid.options.externalSort); + } + return grid; }, @@ -67,469 +81,7 @@ }; - //class definitions - - - /** - * @ngdoc function - * @name ui.grid.class:Grid - * @description Grid defines a logical grid. Any non-dom properties and elements needed by the grid should - * be defined in this class - * @param {string} id id to assign to grid - */ - var Grid = function (id) { - this.id = id; - this.options = new GridOptions(); - this.headerHeight = this.options.headerRowHeight; - this.gridHeight = 0; - this.gridWidth = 0; - this.columnBuilders = []; - this.rowBuilders = []; - this.styleComputations = []; - - // Validate options - if (!this.options.enableNativeScrolling && !this.options.enableVirtualScrolling) { - throw "Either native or virtual scrolling must be enabled."; - } - - - //representation of the rows on the grid. - //these are wrapped references to the actual data rows (options.data) - this.rows = []; - - //represents the columns on the grid - this.columns = []; - - //current rows that are rendered on the DOM - this.renderedRows = []; - this.renderedColumns = []; - }; - - /** - * @ngdoc function - * @name registerColumnBuilder - * @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 - */ - Grid.prototype.registerColumnBuilder = function (columnsProcessor) { - this.columnBuilders.push(columnsProcessor); - }; - - /** - * @ngdoc function - * @name registerRowBuilder - * @methodOf ui.grid.class:Grid - * @description When the build creates rows from gridOptions.data, the rowBuilders will be called to add - * additional properties to the row. - * @param {function(colDef, col, gridOptions)} columnsProcessor function to be called - */ - Grid.prototype.registerRowBuilder = function (rowBuilder) { - this.rowBuilders.push(rowBuilder); - }; - - /** - * @ngdoc function - * @name getColumn - * @methodOf ui.grid.class:Grid - * @description returns a grid column for the column name - * @param {string} name column name - */ - Grid.prototype.getColumn = function (name) { - var columns = this.columns.filter(function (column) { - return column.colDef.name === name; - }); - return columns.length > 0 ? columns[0] : null; - }; - - /** - * @ngdoc function - * @name buildColumns - * @methodOf ui.grid.class:Grid - * @description creates GridColumn objects from the columnDefinition. Calls each registered - * columnBuilder to further process the column - * @returns {Promise} a promise to load any needed column resources - */ - Grid.prototype.buildColumns = function () { - $log.debug('buildColumns'); - var self = this; - var builderPromises = []; - - self.options.columnDefs.forEach(function (colDef, index) { - self.preprocessColDef(colDef); - var col = self.getColumn(colDef.name); - - if (!col) { - col = new GridColumn(colDef, index); - self.columns.push(col); - } - else { - col.updateColumnDef(colDef, col.index); - } - - self.columnBuilders.forEach(function (builder) { - builderPromises.push(builder.call(self, colDef, col, self.options)); - }); - - }); - - return $q.all(builderPromises); - }; - - /** - * undocumented function - * @name preprocessColDef - * @methodOf ui.grid.class:Grid - * @description defaults the name property from field to maintain backwards compatibility with 2.x - * validates that name or field is present - */ - Grid.prototype.preprocessColDef = function (colDef) { - if (!colDef.field && !colDef.name) { - throw new Error('colDef.name or colDef.field property is required'); - } - - //maintain backwards compatibility with 2.x - //field was required in 2.x. now name is required - if (colDef.name === undefined && colDef.field !== undefined) { - colDef.name = colDef.field; - } - }; - - /** - * @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 - */ - Grid.prototype.modifyRows = function(newRawData) { - var self = this; - - if (self.rows.length === 0 && newRawData.length > 0) { - self.addRows(newRawData); - return; - } - - //look for new rows - var newRows = newRawData.filter(function (newItem) { - return !self.rows.some(function(oldRow) { - return self.options.rowEquality(oldRow.entity, newItem); - }); - }); - - for (i = 0; i < newRows.length; i++) { - self.addRows([newRows[i]]); - } - - //look for deleted rows - var deletedRows = self.rows.filter(function (oldRow) { - return !newRawData.some(function (newItem) { - return self.options.rowEquality(newItem, oldRow.entity); - }); - }); - - for (var i = 0; i < deletedRows.length; i++) { - self.rows.splice( self.rows.indexOf(deletedRows[i] ), 1 ); - } - - }; - - /** - * Private Undocumented Method - * @name addRows - * @methodOf ui.grid.class:Grid - * @description adds the newRawData array of rows to the grid and calls all registered - * rowBuilders. this keyword will reference the grid - */ - Grid.prototype.addRows = function(newRawData) { - var self = this; - - for (var i=0; i < newRawData.length; i++) { - self.rows.push( self.processRowBuilders(new GridRow(newRawData[i], i)) ); - } - }; - - /** - * @ngdoc function - * @name processRowBuilders - * @methodOf ui.grid.class:Grid - * @description processes all RowBuilders for the gridRow - * @param {GridRow} gridRow reference to gridRow - * @returns {GridRow} the gridRow with all additional behavior added - */ - Grid.prototype.processRowBuilders = function(gridRow) { - var self = this; - - self.rowBuilders.forEach(function (builder) { - builder.call(self,gridRow, self.gridOptions); - }); - - return gridRow; - }; - - /** - * @ngdoc function - * @name registerStyleComputation - * @methodOf ui.grid.class:Grid - * @description registered a styleComputation function - * @param {function($scope)} styleComputation function - */ - Grid.prototype.registerStyleComputation = function (styleComputationInfo) { - this.styleComputations.push(styleComputationInfo); - }; - - Grid.prototype.setRenderedRows = function (newRows) { - this.renderedRows.length = newRows.length; - for (var i = 0; i < newRows.length; i++) { - this.renderedRows[i] = newRows[i]; - } - }; - - Grid.prototype.setRenderedColumns = function (newColumns) { - this.renderedColumns.length = newColumns.length; - for (var i = 0; i < newColumns.length; i++) { - this.renderedColumns[i] = newColumns[i]; - } - }; - - /** - * @ngdoc function - * @name buildStyles - * @methodOf ui.grid.class:Grid - * @description calls each styleComputation function - */ - Grid.prototype.buildStyles = function ($scope) { - var self = this; - self.styleComputations - .sort(function(a, b) { - if (a.priority === null) { return 1; } - if (b.priority === null) { return -1; } - if (a.priority === null && b.priority === null) { return 0; } - return a.priority - b.priority; - }) - .forEach(function (compInfo) { - compInfo.func.call(self, $scope); - }); - }; - - Grid.prototype.minRowsToRender = function () { - return Math.ceil(this.getViewportHeight() / this.options.rowHeight); - }; - - Grid.prototype.minColumnsToRender = function () { - var self = this; - var viewport = this.getViewportWidth(); - - var min = 0; - var totalWidth = 0; - self.columns.forEach(function(col, i) { - if (totalWidth < viewport) { - totalWidth += col.drawnWidth; - min++; - } - else { - var currWidth = 0; - for (var j = i; j >= i - min; j--) { - currWidth += self.columns[j].drawnWidth; - } - if (currWidth < viewport) { - min++; - } - } - }); - - return min; - }; - - Grid.prototype.getBodyHeight = function () { - // Start with the viewportHeight - var bodyHeight = this.getViewportHeight(); - - // Add the horizontal scrollbar height if there is one - if (typeof(this.horizontalScrollbarHeight) !== 'undefined' && this.horizontalScrollbarHeight !== undefined && this.horizontalScrollbarHeight > 0) { - bodyHeight = bodyHeight + this.horizontalScrollbarHeight; - } - - return bodyHeight; - }; - - // NOTE: viewport drawable height is the height of the grid minus the header row height (including any border) - // TODO(c0bra): account for footer height - Grid.prototype.getViewportHeight = function () { - var viewPortHeight = this.gridHeight - this.headerHeight; - - // Account for native horizontal scrollbar, if present - if (typeof(this.horizontalScrollbarHeight) !== 'undefined' && this.horizontalScrollbarHeight !== undefined && this.horizontalScrollbarHeight > 0) { - viewPortHeight = viewPortHeight - this.horizontalScrollbarHeight; - } - - return viewPortHeight; - }; - - Grid.prototype.getViewportWidth = function () { - var viewPortWidth = this.gridWidth; - - if (typeof(this.verticalScrollbarWidth) !== 'undefined' && this.verticalScrollbarWidth !== undefined && this.verticalScrollbarWidth > 0) { - viewPortWidth = viewPortWidth - this.verticalScrollbarWidth; - } - - return viewPortWidth; - }; - - Grid.prototype.getHeaderViewportWidth = function () { - var viewPortWidth = this.getViewportWidth(); - - if (typeof(this.verticalScrollbarWidth) !== 'undefined' && this.verticalScrollbarWidth !== undefined && this.verticalScrollbarWidth > 0) { - viewPortWidth = viewPortWidth + this.verticalScrollbarWidth; - } - - return viewPortWidth; - }; - - Grid.prototype.getCanvasHeight = function () { - var ret = this.options.rowHeight * this.rows.length; - - if (typeof(this.horizontalScrollbarHeight) !== 'undefined' && this.horizontalScrollbarHeight !== undefined && this.horizontalScrollbarHeight > 0) { - ret = ret - this.horizontalScrollbarHeight; - } - - return ret; - }; - - Grid.prototype.getCanvasWidth = function () { - var ret = this.canvasWidth; - - if (typeof(this.verticalScrollbarWidth) !== 'undefined' && this.verticalScrollbarWidth !== undefined && this.verticalScrollbarWidth > 0) { - ret = ret - this.verticalScrollbarWidth; - } - - return ret; - }; - - Grid.prototype.getTotalRowHeight = function () { - return this.options.rowHeight * this.rows.length; - }; - - // Is the grid currently scrolling? - Grid.prototype.isScrolling = function() { - return this.scrolling ? true : false; - }; - - Grid.prototype.setScrolling = function(scrolling) { - this.scrolling = scrolling; - }; - - - /** - * @ngdoc function - * @name ui.grid.class:GridOptions - * @description Default GridOptions class. GridOptions are defined by the application developer and overlaid - * over this object. - * @param {string} id id to assign to grid - */ - function GridOptions() { - /** - * @ngdoc object - * @name data - * @propertyOf ui.grid.class:GridOptions - * @description Array of data to be rendered to grid. Array can contain complex objects - */ - this.data = []; - - /** - * @ngdoc object - * @name columnDefs - * @propertyOf ui.grid.class:GridOptions - * @description (optional) Array of columnDef objects. Only required property is name. - * _field property can be used in place of name for backwards compatibilty with 2.x_ - * @example - - var columnDefs = [{name:'field1'}, {name:'field2'}]; - - */ - this.columnDefs = []; - - this.headerRowHeight = 30; - this.rowHeight = 30; - this.maxVisibleRowCount = 200; - - this.columnWidth = 50; - this.maxVisibleColumnCount = 200; - - // Turn virtualization on when number of data elements goes over this number - this.virtualizationThreshold = 20; - - this.columnVirtualizationThreshold = 10; - - // Extra rows to to render outside of the viewport - this.excessRows = 4; - this.scrollThreshold = 4; - - // Extra columns to to render outside of the viewport - this.excessColumns = 4; - this.horizontalScrollThreshold = 2; - - // Native scrolling on by default - this.enableNativeScrolling = true; - - // Virtual scrolling off by default, overrides enableNativeScrolling if set - this.enableVirtualScrolling = false; - - // Resizing columns, off by default - this.enableColumnResizing = false; - - // Columns can't be smaller than 10 pixels - this.minimumColumnSize = 10; - - /** - * @ngdoc function - * @name rowEquality - * @methodOf ui.grid.class:GridOptions - * @description By default, rows are compared using object equality. This option can be overridden - * to compare on any data item property or function - * @param {object} entityA First Data Item to compare - * @param {object} entityB Second Data Item to compare - */ - this.rowEquality = function(entityA, entityB) { - return entityA === entityB; - }; - - // Custom template for header row - this.headerTemplate = null; - } - - /** - * @ngdoc function - * @name ui.grid.class:GridRow - * @description Wrapper for the GridOptions.data rows. Allows for needed properties and functions - * to be assigned to a grid row - * @param {object} entity the array item from GridOptions.data - * @param {number} index the current position of the row in the array - */ - function GridRow(entity, index) { - this.entity = entity; - this.index = index; - } - - /** - * @ngdoc function - * @name getQualifiedColField - * @methodOf ui.grid.class:GridRow - * @description returns the qualified field name as it exists on scope - * ie: row.entity.fieldA - * @param {GridCol} col column instance - * @returns {string} resulting name that can be evaluated on scope - */ - GridRow.prototype.getQualifiedColField = function(col) { - return 'row.entity.' + col.field; - }; - - GridRow.prototype.getEntityQualifiedColField = function(col) { - return 'entity.' + col.field; - }; + //class definitions (moved to separate factories) return service; }]); diff --git a/src/js/core/services/rowSorter.js b/src/js/core/services/rowSorter.js new file mode 100644 index 0000000000..29b3d005cc --- /dev/null +++ b/src/js/core/services/rowSorter.js @@ -0,0 +1,283 @@ +(function() { + +var module = angular.module('ui.grid'); + +module.service('rowSorter', ['$parse', 'uiGridConstants', function ($parse, uiGridConstants) { + var currencyRegexStr = + '(' + + uiGridConstants.CURRENCY_SYMBOLS + .map(function (a) { return '\\' + a; }) // Escape all the currency symbols ($ at least will jack up this regex) + .join('|') + // Join all the symbols together with |s + ')?'; + + // /^[-+]?[£$¤¥]?[\d,.]+%?$/ + var numberStrRegex = new RegExp('^[-+]?' + currencyRegexStr + '[\\d,.]+' + currencyRegexStr + '%?$'); + + var rowSorter = { + // 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: [] + }; + + // Guess which sort function to use on this item + rowSorter.guessSortFn = function guessSortFn(item) { + var itemType = typeof(item); + + // Check for numbers and booleans + switch (itemType) { + case "number": + return rowSorter.sortNumber; + case "boolean": + return rowSorter.sortBool; + case "string": + // if number string return number string sort fn. else return the str + return item.match(numberStrRegex) ? rowSorter.sortNumberStr : rowSorter.sortAlpha; + default: + // Check if the item is a valid Date TODO(c0bra): Can we use angular.isDate() ? + if (Object.prototype.toString.call(item) === '[object Date]') { + return rowSorter.sortDate; + } + else { + //finally just sort the basic sort... + return rowSorter.basicSort; + } + } + }; + + // Basic sorting function + rowSorter.basicSort = function basicSort(a, b) { + if (a === b) { + return 0; + } + if (a < b) { + return -1; + } + return 1; + }; + + // Number sorting function + rowSorter.sortNumber = function sortNumber(a, b) { + return a - b; + }; + + rowSorter.sortNumberStr = function sortNumberStr(a, b) { + var numA, // The parsed number form of 'a' + numB, // The parsed number form of 'b' + badA = false, + badB = false; + + // Try to parse 'a' to a float + numA = parseFloat(a.replace(/[^0-9.-]/g, '')); + + // If 'a' couldn't be parsed to float, flag it as bad + if (isNaN(numA)) { + badA = true; + } + + // Try to parse 'b' to a float + numB = parseFloat(b.replace(/[^0-9.-]/g, '')); + + // If 'b' couldn't be parsed to float, flag it as bad + if (isNaN(numB)) { + badB = true; + } + + // We want bad ones to get pushed to the bottom... which effectively is "greater than" + if (badA && badB) { + return 0; + } + + if (badA) { + return 1; + } + + if (badB) { + return -1; + } + + return numA - numB; + }; + + // String sorting function + rowSorter.sortAlpha = function sortAlpha(a, b) { + var strA = a.toLowerCase(), + strB = b.toLowerCase(); + + return strA === strB ? 0 : (strA < strB ? -1 : 1); + }; + + // Date sorting function + rowSorter.sortDate = function sortDate(a, b) { + var timeA = a.getTime(), + timeB = b.getTime(); + + return timeA === timeB ? 0 : (timeA < timeB ? -1 : 1); + }; + + // Boolean sorting function + rowSorter.sortBool = function sortBool(a, b) { + if (a && b) { + return 0; + } + + if (!a && !b) { + return 0; + } + else { + return a ? 1 : -1; + } + }; + + rowSorter.getSortFn = function getSortFn(grid, col, rows) { + var sortFn, item; + + // See if we already figured out what to use to sort the column and have it in the cache + if (rowSorter.colSortFnCache[col.field]) { + sortFn = rowSorter.colSortFnCache[col.field]; + } + // If the column has its OWN sorting algorithm, use that + else if (col.sortingAlgorithm !== undefined) { + sortFn = col.sortingAlgorithm; + rowSorter.colSortFnCache[col.field] = col.sortingAlgorithm; + } + // Try and guess what sort function to use + else { + // Get the first row + var row = rows[0]; + + // No first row, can't guess so return null + if (!row) { + return null; + } + + // TODO(c0bra): need to use that function from the grid class here + // Get the value of this column for the row + var fieldValue = grid.getCellValue(row, col); // $parse(col.field)(row); + + // Guess the sort function + sortFn = rowSorter.guessSortFn(fieldValue); + + // If we found a sort function, cache it + if (sortFn) { + rowSorter.colSortFnCache[col.field] = sortFn; + } + else { + // We assign the alpha sort because anything that is null/undefined will never get passed to + // the actual sorting function. It will get caught in our null check and returned to be sorted + // down to the bottom + sortFn = rowSorter.sortAlpha; + } + } + + return sortFn; + }; + + rowSorter.prioritySort = function (a, b) { + // Both columns have a sort priority + if (a.sort.priority !== undefined && b.sort.priority !== undefined) { + // A is higher priority + if (a.sort.priority < b.sort.priority) { + return -1; + } + // Equal + else if (a.sort.priority === b.sort.priority) { + return 0; + } + // B is higher + else { + return 1; + } + } + // Only A has a priority + else if (a.sort.priority) { + return -1; + } + // Only B has a priority + else if (b.sort.priority) { + return 1; + } + // Neither has a priority + else { + return 0; + } + }; + + rowSorter.sort = function rowSorterSort(grid, rows, columns) { + // first make sure we are even supposed to do work + if (!rows) { + return; + } + + // Build the list of columns to sort by + var sortCols = []; + columns.forEach(function (col) { + if (col.sort && col.sort.direction && (col.sort.direction === uiGridConstants.ASC || col.sort.direction === uiGridConstants.DESC)) { + sortCols.push(col); + } + }); + + // Sort the "sort columns" by their sort priority + sortCols = sortCols.sort(rowSorter.prioritySort); + + // Now rows to sort by, maintain original order + if (sortCols.length === 0) { + return rows; + } + + // 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); + + // Now actually sort the data + return rows.sort(function rowSortFn(rowA, rowB) { + var tem = 0, + idx = 0, + sortFn; + + while (tem === 0 && idx < sortCols.length) { + // grab the metadata for the rest of the logic + col = sortCols[idx]; + direction = sortCols[idx].sort.direction; + + sortFn = rowSorter.getSortFn(grid, col, r); + + var propA = grid.getCellValue(rowA, col); // $parse(col.field)(rowA); + var propB = grid.getCellValue(rowB, col); // $parse(col.field)(rowB); + + // We want to allow zero values to be evaluated in the sort function + if ((!propA && propA !== 0) || (!propB && propB !== 0)) { + // We want to force nulls and such to the bottom when we sort... which effectively is "greater than" + if (!propB && !propA) { + tem = 0; + } + else if (!propA) { + tem = 1; + } + else if (!propB) { + tem = -1; + } + } + else { + tem = sortFn(propA, propB); + } + + idx++; + } + + // Made it this far, we don't have to worry about null & undefined + if (direction === uiGridConstants.ASC) { + return tem; + } else { + return 0 - tem; + } + }); + }; + + return rowSorter; +}]); + +})(); \ No newline at end of file diff --git a/src/js/core/services/ui-grid-util.js b/src/js/core/services/ui-grid-util.js index 3ec2d3306e..8028fe5bab 100644 --- a/src/js/core/services/ui-grid-util.js +++ b/src/js/core/services/ui-grid-util.js @@ -131,7 +131,7 @@ function getWidthOrHeight( elem, name, extra ) { * * @description Grid utility functions */ -module.service('gridUtil', ['$window', '$document', '$http', '$templateCache', '$timeout', function ($window, $document, $http, $templateCache, $timeout) { +module.service('gridUtil', ['$window', '$document', '$http', '$templateCache', '$timeout', '$injector', function ($window, $document, $http, $templateCache, $timeout, $injector) { var s = { /** @@ -497,6 +497,25 @@ module.service('gridUtil', ['$window', '$document', '$http', '$templateCache', ' if (b === null) { return -1; } if (a === null && b === null) { return 0; } return a - b; + }, + + // Disable ngAnimate animations on an element + disableAnimations: function (element) { + var $animate; + try { + $animate = $injector.get('$animate'); + $animate.enabled(false, element); + } + catch (e) {} + }, + + enableAnimations: function (element) { + var $animate; + try { + $animate = $injector.get('$animate'); + $animate.enabled(true, element); + } + catch (e) {} } }; diff --git a/src/less/grid.less b/src/less/grid.less index cc215af0f4..d656f2dcd6 100644 --- a/src/less/grid.less +++ b/src/less/grid.less @@ -27,7 +27,7 @@ } .ui-grid-header-cell:last-child .ui-grid-vertical-bar { - right: -1px; + right: -1px; // TODO(c0bra): Should this be grid width? Test column resizing with custom grid border width width: @gridBorderWidth; background-color: @headerVerticalBarColor; } @@ -35,4 +35,15 @@ // .ui-grid-vertical-bar-visible { // width: 1px; // background-color: @borderColor; -// } \ No newline at end of file +// } + +.ui-grid-clearfix { + &:before, &:after { + content: ""; + display: table; + } + + &:after { + clear:both; + } +} \ No newline at end of file diff --git a/src/less/header.less b/src/less/header.less index bd4a368f6a..8c4b98ddec 100644 --- a/src/less/header.less +++ b/src/less/header.less @@ -6,7 +6,7 @@ // background-color: @darkGray; // #EAEAEA border-bottom: 1px solid @borderColor; // #D4D4D4 - overflow: hidden; + overflow: hidden; // Disable so menus show up font-weight: bold; .gradient(@headerBackgroundColor, @headerGradientStart, @headerGradientStop); @@ -25,7 +25,7 @@ } .ui-grid-header-viewport { - overflow: hidden; + overflow: hidden; // Disable so menus show up } .ui-grid-header-canvas { @@ -52,15 +52,54 @@ bottom: 0; background-color: inherit; + .user-select(none); + // Default to width 0 so header height can calculate right. Otherwise // the header cells will flow onto the next line of the header container // and cause the header height to be calculated as twice the height // it should be. The column widths are calculated dynamically width: 0; + + &.sortable { + cursor: pointer; + } } // Make vertical bar in header row fill the height of the cell completely .ui-grid-header .ui-grid-vertical-bar { top: 0; bottom: 0; +} + +.ui-grid-column-menu-button { + position: absolute; + right: @gridBorderWidth; // So it doesn't overlay the vertical bar + top: 0; + bottom: 0; + + .ui-grid-icon-angle-down { + vertical-align: sub; + } +} + +.ui-grid-column-menu { + position: absolute; +} + +/* Slide up/down animations */ +.ui-grid-column-menu .ui-grid-menu .ui-grid-menu-inner { + &.ng-hide-add, &.ng-hide-remove { + .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)); + } } \ No newline at end of file diff --git a/src/less/icons.less b/src/less/icons.less new file mode 100644 index 0000000000..646d541f48 --- /dev/null +++ b/src/less/icons.less @@ -0,0 +1,51 @@ +@font-face { + font-family: 'ui-grid'; + src: url('ui-grid.eot'); + src: url('ui-grid.eot#iefix') format('embedded-opentype'), + url('ui-grid.woff') format('woff'), + url('ui-grid.ttf?') format('truetype'), + url('ui-grid.svg?#ui-grid') format('svg'); + font-weight: normal; + font-style: normal; +} +/* Chrome hack: SVG is rendered more smooth in Windozze. 100% magic, uncomment if you need it. */ +/* Note, that will break hinting! In other OS-es font will be not as sharp as it could be */ +/* +@media screen and (-webkit-min-device-pixel-ratio:0) { + @font-face { + font-family: 'ui-grid'; + src: url('../font/ui-grid.svg?12312827#ui-grid') format('svg'); + } +} +*/ + + [class^="ui-grid-icon"]:before, [class*=" ui-grid-icon"]:before { + font-family: "ui-grid"; + font-style: normal; + font-weight: normal; + speak: none; + + display: inline-block; + text-decoration: inherit; + width: 1em; + margin-right: .2em; + text-align: center; + /* opacity: .8; */ + + /* For safety - reset parent styles, that can break glyph codes*/ + font-variant: normal; + text-transform: none; + + /* fix buttons height, for twitter bootstrap */ + line-height: 1em; + + /* Animation center compensation - margins should be symmetric */ + /* remove if not needed */ + margin-left: .2em; + + /* you can be more comfortable with increased icons size */ + /* font-size: 120%; */ + + /* Uncomment for 3D effect */ + /* text-shadow: 1px 1px 1px rgba(127, 127, 127, 0.3); */ +} \ No newline at end of file diff --git a/src/less/main.less b/src/less/main.less index f9fd0cb721..21777b64cb 100644 --- a/src/less/main.less +++ b/src/less/main.less @@ -6,7 +6,10 @@ @import 'scrollbar'; @import 'native-scrollbar'; @import 'footer'; +@import 'menu'; +@import 'sorting'; @import 'pinning'; +@import 'icons'; @import 'rtl'; @import 'elements'; @import 'variables'; \ No newline at end of file diff --git a/src/less/menu.less b/src/less/menu.less new file mode 100644 index 0000000000..adb135f0da --- /dev/null +++ b/src/less/menu.less @@ -0,0 +1,43 @@ +.ui-grid-menu { + z-index: 2; // So it shows up over grid canvas + position: absolute; + overflow: hidden; + padding: 0 10px 20px 10px; + cursor: default; // Parent element can have pointer cursor +} + +.ui-grid-menu .ui-grid-menu-inner { + background: @headerBackgroundColor; + border: @gridBorderWidth solid @borderColor; + position: relative; + white-space: nowrap; + + .rounded(@gridBorderRadius); + .box-shadow(e("0 10px 20px rgba(0, 0, 0, 0.2), inset 0 12px 12px -14px rgba(0, 0, 0, 0.2)")); +} + +.ui-grid-menu .ui-grid-menu-inner ul { + margin: 0; + list-style-type: none; + + li { + padding: 8px; + cursor: pointer; + + // Show a shadow when hovering over a menu item + &:hover { + // background-color: negation(@headerBackgroundColor, #fff); + .inner-shadow(@vertical: 0, @blur: 14px, @alpha: 0.2); + } + + &.ui-grid-menu-item-active { + .inner-shadow(@vertical: 0, @blur: 14px, @alpha: 0.2); + background-color: @selectedColor; + } + } + + // Show a bottom border on all but the last menu item + li:not(:last-child) { + border-bottom: @gridBorderWidth solid @borderColor; + } +} \ No newline at end of file diff --git a/src/less/sorting.less b/src/less/sorting.less index 9d7b04a4c0..bcc79c4857 100644 --- a/src/less/sorting.less +++ b/src/less/sorting.less @@ -1,12 +1,29 @@ +// .ui-grid-sortarrow { +// fill: @sortArrowBackgroundColor; +// stroke: @sortArrowBorderColor; +// stroke-linejoin:miter; +// } + +// .ui-grid-sortarrow.down { +// -webkit-transform: rotate(180deg); +// -moz-transform: rotate(180deg); +// -ms-transform: rotate(180deg); +// -o-transform: rotate(180deg); +// transform: rotate(180deg); +// } + + +@sortArrowWidth: 20px; + .ui-grid-sortarrow { - fill: @sortArrowBackgroundColor; - stroke: @sortArrowBorderColor; - stroke-linejoin:miter; -} -.ui-grid-sortarrow.down { - -webkit-transform: rotate(180deg); - -moz-transform: rotate(180deg); - -ms-transform: rotate(180deg); - -o-transform: rotate(180deg); - transform: rotate(180deg); -} + right: 5px; + position: absolute; + width: @sortArrowWidth; + top: 0; + bottom: 0; + background-position: center; + + &.down { + .transform(rotate(180deg)); + } +} \ No newline at end of file diff --git a/src/less/variables.less b/src/less/variables.less index 72d8cf7e49..e1dbeba703 100644 --- a/src/less/variables.less +++ b/src/less/variables.less @@ -47,6 +47,9 @@ // TODO: color for cell selections @focusedCell: #b3c4c7; +// Color to use for enabled or selected settings/items/cells, etc. Should probably override the one above +@selectedColor: #cecece; + /** * @section Scrollbar styles */ diff --git a/src/templates/ui-grid/ui-grid.html b/src/templates/ui-grid/ui-grid.html index db168170f9..eb055b1749 100644 --- a/src/templates/ui-grid/ui-grid.html +++ b/src/templates/ui-grid/ui-grid.html @@ -59,4 +59,8 @@
    + +
    + +
    \ No newline at end of file diff --git a/src/templates/ui-grid/uiGridColumnMenu.html b/src/templates/ui-grid/uiGridColumnMenu.html new file mode 100644 index 0000000000..370f8ef7a7 --- /dev/null +++ b/src/templates/ui-grid/uiGridColumnMenu.html @@ -0,0 +1,15 @@ +
    +
    + +
    +
    \ No newline at end of file diff --git a/src/templates/ui-grid/uiGridHeaderCell.html b/src/templates/ui-grid/uiGridHeaderCell.html index 2886f9f26b..a21f4dc7fa 100644 --- a/src/templates/ui-grid/uiGridHeaderCell.html +++ b/src/templates/ui-grid/uiGridHeaderCell.html @@ -1,4 +1,12 @@ -
    -
     
    -
    {{ col.displayName }}
    +
    +
     
    +
    + {{ col.displayName }} + + +
    + +
    +   +
    \ No newline at end of file diff --git a/src/templates/ui-grid/uiGridMenu.html b/src/templates/ui-grid/uiGridMenu.html new file mode 100644 index 0000000000..5246509793 --- /dev/null +++ b/src/templates/ui-grid/uiGridMenu.html @@ -0,0 +1,7 @@ +
    +
    +
      +
    • +
    +
    +
    \ No newline at end of file diff --git a/src/templates/ui-grid/uiGridMenuItem.html b/src/templates/ui-grid/uiGridMenuItem.html new file mode 100644 index 0000000000..16c56188fa --- /dev/null +++ b/src/templates/ui-grid/uiGridMenuItem.html @@ -0,0 +1 @@ +
  • {{ title }}
  • \ No newline at end of file diff --git a/test/unit/core/directives/ui-grid-header-cell.spec.js b/test/unit/core/directives/ui-grid-header-cell.spec.js new file mode 100644 index 0000000000..a80455c163 --- /dev/null +++ b/test/unit/core/directives/ui-grid-header-cell.spec.js @@ -0,0 +1,118 @@ +describe('uiGridHeaderCell', function () { + var grid, $scope, $compile, $document, $timeout, $window, recompile; + + 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" } + ]; + + beforeEach(module('ui.grid')); + + beforeEach(inject(function (_$compile_, $rootScope, _$document_, _$timeout_, _$window_) { + $scope = $rootScope; + $compile = _$compile_; + $document = _$document_; + $timeout = _$timeout_; + $window = _$window_; + + $scope.gridOpts = { + enableSorting: true, + data: data + }; + + recompile = function () { + grid = angular.element('
    '); + + $compile(grid)($scope); + $document[0].body.appendChild(grid[0]); + + $scope.$digest(); + }; + + recompile(); + })); + + afterEach(function() { + grid.remove(); + }); + + describe('column menu', function (){ + var headerCell1, + headerCell2, + menu; + + beforeEach(function () { + headerCell1 = $(grid).find('.ui-grid-header-cell:nth(0)'); + headerCell2 = $(grid).find('.ui-grid-header-cell:nth(1)'); + + menu = $(grid).find('.ui-grid-column-menu .ui-grid-menu-inner'); + }); + + function openMenu() { + headerCell1.trigger('mousedown'); + $scope.$digest(); + $timeout.flush(); + $scope.$digest(); + } + + describe('showing a menu with long-click', function () { + it('should open the menu', inject(function () { + openMenu(); + expect(menu.hasClass('ng-hide')).toBe(false, 'column menu is visible (does not have ng-hide class)'); + })); + }); + + describe('right click', function () { + it('should do nothing', inject(function() { + expect(menu.hasClass('ng-hide')).toBe(true, 'column menu is not initially visible'); + + headerCell1.trigger({ type: 'mousedown', button: 3 }); + $scope.$digest(); + $timeout.flush(); + $scope.$digest(); + + expect(menu.hasClass('ng-hide')).toBe(true, 'column menu is not visible'); + })); + }); + + describe('clicking outside visible menu', function () { + it('should close the menu', inject(function() { + openMenu(); + expect(menu.hasClass('ng-hide')).toBe(false, 'column menu is visible'); + + $document.trigger('click'); + $scope.$digest(); + + expect(menu.hasClass('ng-hide')).toBe(true, 'column menu is hidden'); + })); + }); + + describe('with enableColumnMenu off', function() { + it('should not be present', function () { + $scope.gridOpts.enableColumnMenu = false; + recompile(); + + menu = $(grid).find('.ui-grid-column-menu .ui-grid-menu-inner'); + + expect(menu[0]).toBeUndefined('menu is undefined'); + }); + }); + + describe('when window is resized', function () { + it('should hide an open menu', function () { + openMenu(); + expect(menu.hasClass('ng-hide')).toBe(false, 'column menu is visible'); + + $(window).trigger('resize'); + // NOTE: don't have to $digest() here, the menu needs to handle running it on its own in the resize handler + + expect(menu.hasClass('ng-hide')).toBe(true, 'column menu is hidden'); + }); + }); + + // TODO(c0bra): Allow extra items to be added to a column menu through columnDefs + }); + +}); \ No newline at end of file diff --git a/test/unit/core/directives/ui-grid-menu.spec.js b/test/unit/core/directives/ui-grid-menu.spec.js new file mode 100644 index 0000000000..1a9a5122dc --- /dev/null +++ b/test/unit/core/directives/ui-grid-menu.spec.js @@ -0,0 +1,182 @@ +describe('ui-grid-menu', function() { + var $scope, $compile, menu, inner, recompile; + + beforeEach(module('ui.grid')); + + beforeEach(inject(function (_$compile_, $rootScope) { + $scope = $rootScope; + $compile = _$compile_; + + $scope.foo = null; + + $scope.items = [ + { + title: 'Blah 1', + action: jasmine.createSpy('item-action'), + icon: 'ui-grid-icon-close', + active: function () { return true; } + }, + { + title: 'Blah 2', + action: function () { + $scope.foo = 'blah'; + } + }, + { + title: 'Blah 3' + }, + { + title: 'Blah 4', + shown: function () { return false; } + } + ]; + + // $scope.isShown = true; + + recompile = function () { + menu = angular.element('
    '); + $compile(menu)($scope); + $scope.$digest(); + inner = $(menu).find('.ui-grid-menu-inner').first(); + }; + + recompile(); + })); + + it('should hide the menu by default', function () { + expect(inner.hasClass('ng-hide')).toBe(true); + }); + + // TODO(c0bra): Change to test hide-menu & show-menu events + // it('should be shown when the shown property is a true boolean', function () { + // $scope.isShown = true; + // $scope.$digest(); + + // expect(inner.hasClass('ng-hide')).toBe(false); + // }); + + // it('should be shown when the shown property is a function that returns true', function () { + // $scope.isShown = function() { return true; }; + // $scope.$digest(); + + // expect(inner.hasClass('ng-hide')).toBe(false); + // }); + + it('should hide when hideMenu() is called', function() { + $scope.$broadcast('show-menu'); + $scope.$digest(); + + expect(inner.hasClass('ng-hide')).toBe(false); + + menu.isolateScope().hideMenu(); + $scope.$digest(); + + expect(inner.hasClass('ng-hide')).toBe(true); + }); + + it('should create a list of menu items from the menuItems attribute', function() { + var items = menu.find('.ui-grid-menu-item'); + + expect(items.length).toEqual($scope.items.length); + }); + + it("should obey menu item's 'shown' property", function() { + $scope.items[0].shown = function () { return false; }; + + recompile(); + + var item = menu.find('.ui-grid-menu-item').first(); + expect(item.hasClass('ng-hide')).toBe(true); + }); + + it("should run an item's action when it's clicked", function() { + var item = menu.find('.ui-grid-menu-item').first(); + item.trigger('click'); + $scope.$digest(); + + expect($scope.items[0].action).toHaveBeenCalled(); + + var item2 = menu.find('.ui-grid-menu-item:nth(1)').first(); + item2.trigger('click'); + $scope.$digest(); + + expect($scope.foo).toEqual('blah'); + }); + + describe('when an item has no action and is clicked', function() { + it('should do nothing', function() { + var item = menu.find('.ui-grid-menu-item:nth(2)').first(); + + expect(function(){ + item.trigger('click'); + $scope.$digest(); + }).not.toThrow(); + }); + }); + + it('should show an icon for a menu item', function() { + var icon = menu.find('.ui-grid-menu-item:nth(0) i').first(); + expect(icon.hasClass('ui-grid-icon-close')).toBe(true); + }); + + it('should add the active class if the item is active', function() { + var item = menu.find('.ui-grid-menu-item:nth(0)').first(); + + expect(item.hasClass('ui-grid-menu-item-active')).toBe(true, 'item gets active class'); + }); + + it('should add the active class if the active property is a function that returns true', function() { + var item = menu.find('.ui-grid-menu-item:nth(0)').first(); + + $scope.items[0].active = function() { return true; }; + $scope.$digest(); + + expect(item.hasClass('ui-grid-menu-item-active')).toBe(true); + }); + + it('should hide a menu item based on its shown property', function() { + var item = menu.find('.ui-grid-menu-item:nth(3)').first(); + + expect(item.hasClass('ng-hide')).toBe(true); + }); + + it("should throw an exception when an item's 'shown' property is not a function", function () { + $scope.items[0].shown = 'shown goobers'; + + expect(function() { + recompile(); + }).toThrow(); + }); + + it("should throw an exception when an item's 'active' property is not a function", function () { + $scope.items[0].active = 'active goobers'; + + expect(function() { + recompile(); + }).toThrow(); + }); + + describe("with a menu item that has no 'shown' property", function () { + beforeEach(inject(function (_$compile_, $rootScope) { + $scope = $rootScope; + $compile = _$compile_; + + $scope.items = [ + { + title: 'Blah 1' + } + ]; + + menu = angular.element('
    '); + $compile(menu)($scope); + $scope.$digest(); + inner = $(menu).find('.ui-grid-menu-inner').first(); + })); + + it("should display a menu item by default if no 'shown' property is passed", function() { + var item = menu.find('.ui-grid-menu-item').first(); + + expect(item.hasClass('ng-hide')).toBe(false); + }); + }); +}); \ No newline at end of file diff --git a/test/unit/core/directives/ui-grid.spec.js b/test/unit/core/directives/ui-grid.spec.js index 820c827567..3a3dd2aacf 100644 --- a/test/unit/core/directives/ui-grid.spec.js +++ b/test/unit/core/directives/ui-grid.spec.js @@ -1,66 +1,79 @@ describe('ui-grid', function() { - beforeEach(module('ui.grid')); - // beforeEach(module('ui.grid.body')); - // beforeEach(module('ui.grid.header')); - - /*describe('ui-grid calculated columns', function() { - var element, scope; + beforeEach(module('ui.grid')); + // beforeEach(module('ui.grid.body')); + // beforeEach(module('ui.grid.header')); + + /*describe('ui-grid calculated columns', function() { + var element, scope; - beforeEach(inject(function($compile, $rootScope) { - element = angular.element('
    '); - scope = $rootScope; - scope.data = [{ col1: 'col1', col2: 'col2' }]; - $compile(element)(scope); - scope.$digest(); - })); - - it('gets columns correctly', function() { - expect(element.isolateScope().gridOptions.columnDefs.length).toBe(2); - expect(element.isolateScope().gridOptions.columnDefs[0].name).toBe('Col1'); - expect(element.isolateScope().gridOptions.columnDefs[0].field).toBe('col1'); - }); + beforeEach(inject(function($compile, $rootScope) { + element = angular.element('
    '); + scope = $rootScope; + scope.data = [{ col1: 'col1', col2: 'col2' }]; + $compile(element)(scope); + scope.$digest(); + })); + it('gets columns correctly', function() { + expect(element.isolateScope().gridOptions.columnDefs.length).toBe(2); + expect(element.isolateScope().gridOptions.columnDefs[0].name).toBe('Col1'); + expect(element.isolateScope().gridOptions.columnDefs[0].field).toBe('col1'); }); - describe('ui-grid declarative columns', function() { - var element, scope; + }); + + describe('ui-grid declarative columns', function() { + var element, scope; + + beforeEach(inject(function($compile, $rootScope) { + element = angular.element('
    '); + scope = $rootScope; + scope.data = [{ declCol1: 'col1', declCol2: 'col2' }]; + $compile(element)(scope); + scope.$digest(); + })); + + it('gets columns correctly', function() { + expect(element.isolateScope().gridOptions.columnDefs.length).toBe(1); + expect(element.isolateScope().gridOptions.columnDefs[0].name).toBe('Decl Col 1'); + expect(element.isolateScope().gridOptions.columnDefs[0].field).toBe('declCol1'); + }); - beforeEach(inject(function($compile, $rootScope) { - element = angular.element('
    '); - scope = $rootScope; - scope.data = [{ declCol1: 'col1', declCol2: 'col2' }]; - $compile(element)(scope); - scope.$digest(); - })); + }); + + describe('ui-grid imperative columns', function () { + var element, scope; - it('gets columns correctly', function() { - expect(element.isolateScope().gridOptions.columnDefs.length).toBe(1); - expect(element.isolateScope().gridOptions.columnDefs[0].name).toBe('Decl Col 1'); - expect(element.isolateScope().gridOptions.columnDefs[0].field).toBe('declCol1'); - }); + beforeEach(inject(function ($compile, $rootScope) { + element = angular.element('
    '); + scope = $rootScope; + scope.data = [{ impCol1: 'col1', impCol2: 'col2' }]; + //specifying gridOptions on parent scope will override any attributes + scope.myGridOptions = {}; + scope.myGridOptions.columnDefs = [{ name: 'Imp Col 1', field: 'impCol1' }]; + $compile(element)(scope); + scope.$digest(); + })); + it('gets columns correctly', function () { + expect(element.isolateScope().gridOptions.columnDefs.length).toBe(1); + expect(element.isolateScope().gridOptions.columnDefs[0].name).toBe('Imp Col 1'); + expect(element.isolateScope().gridOptions.columnDefs[0].field).toBe('impCol1'); }); - - describe('ui-grid imperative columns', function () { - var element, scope; - beforeEach(inject(function ($compile, $rootScope) { - element = angular.element('
    '); - scope = $rootScope; - scope.data = [{ impCol1: 'col1', impCol2: 'col2' }]; - //specifying gridOptions on parent scope will override any attributes - scope.myGridOptions = {}; - scope.myGridOptions.columnDefs = [{ name: 'Imp Col 1', field: 'impCol1' }]; - $compile(element)(scope); - scope.$digest(); - })); + });*/ - it('gets columns correctly', function () { - expect(element.isolateScope().gridOptions.columnDefs.length).toBe(1); - expect(element.isolateScope().gridOptions.columnDefs[0].name).toBe('Imp Col 1'); - expect(element.isolateScope().gridOptions.columnDefs[0].field).toBe('impCol1'); - }); + describe('refreshRows', function() { + it('should do something', function () { + // TODO(c0bra) ... + }); + }); + + describe('minColumnsToRender', function() { + it('calculates the minimum number of columns to render, correctly', function() { + // TODO + }); + }); - });*/ }); \ No newline at end of file diff --git a/test/unit/core/factories/Grid.spec.js b/test/unit/core/factories/Grid.spec.js new file mode 100644 index 0000000000..2bfb8db810 --- /dev/null +++ b/test/unit/core/factories/Grid.spec.js @@ -0,0 +1,162 @@ +describe('Grid factory', function () { + var $q, $scope, grid, Grid, GridRow, GridColumn, rows, returnedRows, column; + + beforeEach(module('ui.grid')); + + beforeEach(inject(function (_$q_, _$rootScope_, _Grid_, _GridRow_, _GridColumn_) { + $q = _$q_; + $scope = _$rootScope_; + Grid = _Grid_; + GridRow = _GridRow_; + GridColumn = _GridColumn_; + + rows = [ + new GridRow({ a: 'one' }, 0), + new GridRow({ a: 'two' }, 1) + ]; + + column = new GridColumn({ name: 'a' }, 0); + + grid = new Grid({ id: 1 }); + grid.rows = rows; + grid.columns = [column]; + + returnedRows = null; + })); + + function runProcs () { + grid.processRowsProcessors(grid.rows) + .then(function (newRows) { + returnedRows = newRows; + }); + + $scope.$digest(); + } + + describe('row processors', function () { + var proc1, proc2, returnedRows; + + // Stub for adding function spies to + function testObj() { + + } + + /* Actual rows processors */ + proc1 = function (rows) { + rows.forEach(function (r) { + r.c = 'foo'; + }); + + return rows; + }; + + proc2 = function (rows) { + var p = $q.defer(); + + rows.forEach(function (r) { + r.d = 'bar'; + }); + + p.resolve(rows); + + return p.promise; + }; + + beforeEach(function () { + // Create function spies but also call real functions + testObj.proc1 = jasmine.createSpy('proc1').andCallFake(proc1); + testObj.proc2 = jasmine.createSpy('proc2').andCallFake(proc2); + + // Register the two spies as rows processors + grid.registerRowsProcessor(testObj.proc1); + grid.registerRowsProcessor(testObj.proc2); + }); + + it('should call both processors', function() { + runs(runProcs); + + runs(function () { + expect(testObj.proc1).toHaveBeenCalled(); + expect(testObj.proc2).toHaveBeenCalled(); + }); + }); + + it('should actually process the rows', function () { + runs(runProcs); + + runs(function () { + expect(rows[0].c).toEqual('foo'); + expect(rows[0].d).toEqual('bar'); + expect(rows[1].c).toEqual('foo'); + expect(rows[1].d).toEqual('bar'); + }); + }); + + describe(', when deregistered, ', function () { + it('should not be run', function () { + grid.removeRowsProcessor(testObj.proc1); + + runs(runProcs); + + runs(function () { + expect(testObj.proc1).not.toHaveBeenCalled(); + expect(testObj.proc2).toHaveBeenCalled(); + }); + }); + }); + + describe(', when one is broken and does not return an array, ', function () { + beforeEach(function () { + grid.removeRowsProcessor(testObj.proc1); + grid.removeRowsProcessor(testObj.proc2); + + grid.registerRowsProcessor(function (blargh) { + return "goobers!"; + }); + }); + + it('should throw an exception', function () { + expect(function () { + runProcs(); + }).toThrow(); + }); + }); + }); + + describe('with no rows processors', function () { + it('should have none registered', function () { + expect(grid.rowsProcessors.length).toEqual(0); + }); + + it('processRowsProcessors should return a shallow copy of grid.rows', function () { + runs(runProcs); + + runs(function() { + expect(returnedRows).toEqual(grid.rows); + }); + }); + }); + + describe('registering a non-function as a rows processor', function () { + it('should error', function () { + expect(function () { + grid.registerRowsProcessor('blah'); + }).toThrow(); + }); + }); + + describe('sortColumn', function() { + it('should throw an exception if no column parameter is provided', function() { + expect(function () { + grid.sortColumn(); + }).toThrow(); + + try { + grid.sortColumn(); + } + catch (e) { + expect(e.message).toContain('No column parameter provided', 'exception contains column name'); + } + }); + }); +}); diff --git a/test/unit/core/factories/GridColumn.spec.js b/test/unit/core/factories/GridColumn.spec.js new file mode 100644 index 0000000000..72877a1872 --- /dev/null +++ b/test/unit/core/factories/GridColumn.spec.js @@ -0,0 +1,48 @@ +describe('GridColumn factory', function () { + var $q, $scope, cols, grid, gridCol, Grid, GridColumn, gridClassFactory; + + beforeEach(module('ui.grid')); + + function buildCols() { + grid.buildColumns(); + } + + + beforeEach(inject(function (_$q_, _$rootScope_, _Grid_, _GridColumn_, _gridClassFactory_) { + $q = _$q_; + $scope = _$rootScope_; + Grid = _Grid_; + GridColumn = _GridColumn_; + gridClassFactory = _gridClassFactory_; + + cols = [ + { field: 'firstName' } + ]; + + grid = new Grid({ id: 1 }); + + grid.registerColumnBuilder(gridClassFactory.defaultColumnBuilder); + + grid.options.columnDefs = cols; + + buildCols(); + })); + + describe('buildColumns', function () { + it('should not remove existing sort details on a column', function () { + var sort = { priority: 0, direction: 'asc' }; + grid.columns[0].sort = sort; + + runs(buildCols); + + runs(function () { + expect(grid.columns[0].sort).toEqual(sort); + }); + }); + + it('should obey columnDef sort spec', function () { + // ... TODO(c0bra) + }); + }); + +}); \ No newline at end of file diff --git a/test/unit/core/row-filtering.spec.js b/test/unit/core/row-filtering.spec.js new file mode 100644 index 0000000000..a610175fd9 --- /dev/null +++ b/test/unit/core/row-filtering.spec.js @@ -0,0 +1,43 @@ + +describe('row filtering', function() { + var grid, $scope, $compile, recompile; + + 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" } + ]; + + beforeEach(module('ui.grid')); + + beforeEach(inject(function (_$compile_, $rootScope) { + $scope = $rootScope; + $compile = _$compile_; + + $scope.gridOpts = { + data: data + }; + + recompile = function () { + grid = angular.element('
    '); + // document.body.appendChild(grid[0]); + $compile(grid)($scope); + $scope.$digest(); + }; + + recompile(); + })); + + afterEach(function () { + // angular.element(grid).remove(); + grid = null; + }); + + describe('blarg', function () { + it('yargh!', function () { + + }); + }); + +}); \ No newline at end of file diff --git a/test/unit/core/row-sorting.spec.js b/test/unit/core/row-sorting.spec.js new file mode 100644 index 0000000000..88eada9c40 --- /dev/null +++ b/test/unit/core/row-sorting.spec.js @@ -0,0 +1,269 @@ + +describe('rowSorter', function() { + var grid, $scope, $compile, recompile, uiGridConstants, rowSorter, gridClassFactory, Grid, GridColumn, GridRow; + + 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" } + ]; + + beforeEach(module('ui.grid')); + + beforeEach(inject(function (_$compile_, $rootScope, _uiGridConstants_, _rowSorter_, _Grid_, _GridColumn_, _GridRow_, _gridClassFactory_) { + $scope = $rootScope; + $compile = _$compile_; + uiGridConstants = _uiGridConstants_; + rowSorter = _rowSorter_; + Grid = _Grid_; + GridColumn = _GridColumn_; + GridRow = _GridRow_; + gridClassFactory = _gridClassFactory_; + + // $scope.gridOpts = { + // data: data + // }; + + // recompile = function () { + // grid = angular.element('
    '); + // // document.body.appendChild(grid[0]); + // $compile(grid)($scope); + // $scope.$digest(); + // }; + + // recompile(); + })); + + afterEach(function () { + // grid = null; + }); + + // TODO(c0bra): Add test for grid sorting constants? + + describe('guessSortFn', function () { + it('should guess a number', function () { + var guessFn = rowSorter.guessSortFn(5); + expect(guessFn).toBe(rowSorter.sortNumber); + }); + + it('should guess a date', function () { + var guessFn = rowSorter.guessSortFn(new Date()); + + expect(guessFn).toBe(rowSorter.sortDate); + }); + + it('should guess a string', function () { + var guessFn = rowSorter.guessSortFn("hi there!"); + + expect(guessFn).toBe(rowSorter.sortAlpha); + }); + + it('should guess a number when a number is signed', function () { + var guessFn = rowSorter.guessSortFn(-50); + expect(guessFn).toBe(rowSorter.sortNumber, 'Negative signed number'); + + guessFn = rowSorter.guessSortFn(+50); + expect(guessFn).toBe(rowSorter.sortNumber, 'Positive signed number'); + }); + + it('should guess a number-string when the value is a numeric string', function () { + var guessFn = rowSorter.guessSortFn('500'); + expect(guessFn).toBe(rowSorter.sortNumberStr, '500'); + + guessFn = rowSorter.guessSortFn('500.00'); + expect(guessFn).toBe(rowSorter.sortNumberStr, '500.00'); + + guessFn = rowSorter.guessSortFn('-500.00'); + expect(guessFn).toBe(rowSorter.sortNumberStr, '-500.00'); + }); + + it('should guess a number-string when the value is currency', function () { + var guessFn = rowSorter.guessSortFn('$500'); + expect(guessFn).toBe(rowSorter.sortNumberStr, '$500'); + + guessFn = rowSorter.guessSortFn('¥500'); + expect(guessFn).toBe(rowSorter.sortNumberStr, '¥500'); + }); + + it('should allow a currency symbol to come after the number', function () { + var guessFn = rowSorter.guessSortFn('500$'); + expect(guessFn).toBe(rowSorter.sortNumberStr, '500$'); + }); + + it('should allow percents', function () { + var guessFn = rowSorter.guessSortFn('75.25%'); + expect(guessFn).toBe(rowSorter.sortNumberStr, '75.25%'); + }); + + it('should not allow percent signs before the number', function () { + var guessFn = rowSorter.guessSortFn('%75.25'); + expect(guessFn).toBe(rowSorter.sortAlpha, '%75.25'); + }); + + it('should allow booleans', function () { + var guessFn = rowSorter.guessSortFn(true); + expect(guessFn).toBe(rowSorter.sortBool, true); + + guessFn = rowSorter.guessSortFn(false); + expect(guessFn).toBe(rowSorter.sortBool, false); + }); + + it('should use basicSort for objects', function () { + function WeirdObject() {} + var val = new WeirdObject(); + + var guessFn = rowSorter.guessSortFn(val); + expect(guessFn).toBe(rowSorter.basicSort, 'WeirdObject'); + }); + }); + + describe('sort', function() { + var grid, rows, cols; + + beforeEach(function() { + grid = new Grid({ id: 123 }); + + var e1 = { name: 'Bob' }; + var e2 = { name: 'Jim' }; + + rows = [ + new GridRow(e1, 0), + new GridRow(e2, 1) + ]; + + cols = [ + new GridColumn({ + name: 'name', + sort: { + direction: uiGridConstants.ASC, + priority: 0 + } + }, 0) + ]; + }); + + it('should sort this ascending', function() { + var ret = rowSorter.sort(grid, rows, cols); + + expect(ret[0].entity.name).toEqual('Bob'); + }); + + it('should sort things descending', function() { + cols[0].sort.direction = uiGridConstants.DESC; + + var ret = rowSorter.sort(grid, rows, cols); + + expect(ret[0].entity.name).toEqual('Jim'); + }); + + // TODO(c0bra) ... + describe('with a custom sorting algorithm', function () { + beforeEach(function() { + + }); + + it("should use the column's specified sorting algorithm if it has one", function () { + cols[0] = new GridColumn({ + name: 'name', + sortingAlgorithm: jasmine.createSpy('sortingAlgorithm').andReturn(rows), + sort: { + direction: uiGridConstants.ASC, + priority: 0 + } + }, 0); + + rowSorter.sort(grid, rows, cols); + + expect(cols[0].sortingAlgorithm).toHaveBeenCalled(); + }); + + it('should run and use the sorting algorithm output properly', function() { + cols[0] = new GridColumn({ + name: 'name', + // Sort words containing the letter 'i' to the top + sortingAlgorithm: function (a, b) { + var r = 0; + if (/i/.test(a) && /i/.test(b)) { + r = 0; + } + else if (/i/.test(a)) { + r = -1; + } + else if (/i/.test(b)) { + r = 1; + } + + return r; + }, + sort: { + direction: uiGridConstants.ASC, + priority: 0 + } + }, 0); + + var ret = rowSorter.sort(grid, rows, cols); + + expect(ret[0].entity.name).toEqual('Jim'); + }); + }); + }); + + describe('external sort', function() { + var grid, rows, cols, column, timeoutRows, returnedRows, $timeout; + + beforeEach(inject(function(_$timeout_) { + $timeout = _$timeout_; + + timeoutRows = [new GridRow({ name: 'Frank' }, 0)]; + + grid = gridClassFactory.createGrid({ + externalSort: jasmine.createSpy('externalSort') + .andCallFake(function (r) { + return $timeout(function() { + return timeoutRows; + }, 1000); + }) + }); + + // grid.options.externalSort = function (grid, column, rows) { + // // sort stuff here + // }; + + var e1 = { name: 'Bob' }; + var e2 = { name: 'Jim' }; + + rows = grid.rows = [ + new GridRow(e1, 0), + new GridRow(e2, 1) + ]; + + column = new GridColumn({ + name: 'name' + }, 0); + + cols = grid.columns = [column]; + })); + + it('should run', function() { + grid.sortColumn(column); + + runs(function() { + grid.processRowsProcessors(grid.rows) + .then(function (newRows) { + returnedRows = newRows; + }); + + $timeout.flush(); + $scope.$digest(); + }); + + runs(function (){ + expect(grid.options.externalSort).toHaveBeenCalled(); + + expect(returnedRows).toEqual(timeoutRows); + }); + }); + }); + +}); \ No newline at end of file diff --git a/test/unit/core/services/GridClassFactory.spec.js b/test/unit/core/services/GridClassFactory.spec.js index c9e99969a2..4a7091d772 100644 --- a/test/unit/core/services/GridClassFactory.spec.js +++ b/test/unit/core/services/GridClassFactory.spec.js @@ -21,9 +21,4 @@ describe('gridClassFactory', function() { }); }); - describe('minColumnsToRender', function() { - it('calculates the minimum number of columns to render, correctly', function() { - // TODO - }); - }); }); \ No newline at end of file